跳到主要内容

SPI 通信

在本章中,我们将学习如何在应用层中使用 SPI 总线与外部设备的通讯。

示例程序:Code.zip

1.SPI子系统

在 Linux 操作系统中,SPI 子系统是一个关键的驱动框架,用于管理和控制通过 SPI 总线连接的各种外部设备,有关 SPI 子系统的更多详细信息可以在 <Linux内核源码>/Documentation/spi 目录中找到。SPI 子系统的关键组成部分:

  1. sysfs接口:SPI子系统通过sysfs提供了一组文件和目录,用于配置和管理 SPI 总线和 SPI 设备。这些文件和目录位于 /sys/class/spi_master/sys/bus/spi/devices 目录下,允许用户查看和修改 SPI 设备的属性。
  2. 设备节点:每个已连接的 SPI 设备都在 /dev 目录下创建了一个设备节点,允许用户空间程序通过标准文件I/O操作与设备进行通信。通常,这些设备节点的名称是 /dev/spidevX.Y,其中X表示 SPI 总线编号,Y表示 SPI 设备编号。

2.查看SPI(Shell)

2.1 引脚分布

开启 SPI的方法参考 PWM 部分, LuckFox Pico Ultra/Ultra W 引脚图:

2.2 查看设备

/sys/bus/spi/devices 目录中,每个 SPI 设备都有自己的文件夹。这些文件夹的名称通常包含 spi 和设备编号,例如 /sys/bus/spi/devices/spi0.0 表示 SPI 总线编号0的0号设备。如果您想查看系统存在的 SPI 总线,可以使用如下命令:

# ls /sys/bus/spi/devices/
spi2.0 spi0.0

3.SPI通讯(Python程序)

3.1 完整代码

通过以下程序,可以实现SPI通讯。

import spidev

def main():
tx_buffer = [ord(char) for char in "hello world!"]
rx_buffer = [0] * len(tx_buffer)

try:
spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000

rx_buffer = spi.xfer2(tx_buffer[:])
print("tx_buffer:\n\r", ''.join(map(chr, tx_buffer)))
print("rx_buffer:\n\r", ''.join(map(chr, rx_buffer)))

except Exception as e:
print(f"An error occurred: {e}")

finally:
if spi:
spi.close()

if __name__ == "__main__":
main()

3.2 打开SPI设备

这段代码使用 spidev 库中的SpiDev类创建了一个SPI对象。通过调用 open 方法,指定SPI总线和设备的编号,这里是SPI总线0的设备0。设置了SPI的最大传输速率为1,000,000 Hz,即1 MHz。这一步是为了配置SPI设备的基本参数,以便进行后续的数据传输。

spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1000000

3.3 数据收发

这段代码使用 xfer2 方法进行SPI数据传输。在这个例子中, tx_buffer 是要发送的数据,而 rx_buffer 将存储接收到的数据。注意,为了确保 tx_buffer 的原始值保持不变,传递给 xfer2 的是 tx_buffer 的副本,即tx_buffer[:]。最后,通过 print 语句将发送和接收的数据以字符串形式打印出来,方便用户查看。

rx_buffer = spi.xfer2(tx_buffer[:])
print("tx_buffer:\n\r", ''.join(map(chr, tx_buffer)))
print("rx_buffer:\n\r", ''.join(map(chr, rx_buffer)))

3.4 运行程序

  1. 使用 nano 工具打开文件,粘贴并保存代码

    # nano i2c.py
  2. 运行程序

    # python3 i2c.py
  3. 实验现象

    将SPI的MOSI和MISO相接,运行程序:
    image

4.SPI通讯(C程序)

4.1 ioctl函数

在编写应用程序时需要使用ioctl函数设置spi相关配置,其函数原型如下

 #include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

当使用ioctl函数进行SPI通信时,常用的request参数主要有以下几种:

  • SPI_IOC_RD_MODE:用于读取当前SPI通信的模式设置。这个请求参数将模式信息读取到一个整数变量中,以便检查当前SPI通信的极性和相位设置。
  • SPI_IOC_WR_MODE:用于设置SPI通信的模式。您需要提供一个整数值,该值通常由两位二进制数字组成,表示SPI通信的极性和相位。
  • SPI_IOC_RD_BITS_PER_WORD:用于读取每个数据字的位数。这个请求参数将位数信息读取到一个整数变量中。
  • SPI_IOC_WR_BITS_PER_WORD:用于设置每个数据字的位数。您需要提供一个整数值,以指定要发送和接收的每个数据字的位数。
  • SPI_IOC_RD_MAX_SPEED_HZ:用于读取SPI总线的最大速度。这个请求参数将速度信息读取到一个整数变量中,以便检查当前SPI总线的最大传输速度。
  • SPI_IOC_WR_MAX_SPEED_HZ:用于设置SPI总线的最大速度。您需要提供一个整数值,以指定要使用的最大传输速度。
  • SPI_IOC_MESSAGE(N):用于执行SPI传输的读写操作。这个请求参数需要一个指向 struct spi_ioc_transfer 数组的指针,每个元素描述了一个SPI传输操作,可以执行多个操作。
  • SPI_IOC_RD_LSB_FIRST:用于读取LSB(Least Significant Bit)优先设置。这个请求参数将LSB优先信息读取到一个整数变量中。
  • SPI_IOC_WR_LSB_FIRST:用于设置LSB优先设置。您需要提供一个整数值,以指定是否要使用LSB优先模式。

4.2 示例程序

通过以下程序,可以实现SPI通讯。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>

#define SPI_DEVICE_PATH "/dev/spidev0.0"

int main() {
int spi_file;
uint8_t tx_buffer[50] = "hello world!";
uint8_t rx_buffer[50];

// Open the SPI device
if ((spi_file = open(SPI_DEVICE_PATH, O_RDWR)) < 0) {
perror("Failed to open SPI device");
return -1;
}

// Configure SPI mode and bits per word
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
if (ioctl(spi_file, SPI_IOC_WR_MODE, &mode) < 0) {
perror("Failed to set SPI mode");
close(spi_file);
return -1;
}
if (ioctl(spi_file, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
perror("Failed to set SPI bits per word");
close(spi_file);
return -1;
}

// Perform SPI transfer
struct spi_ioc_transfer transfer = {
.tx_buf = (unsigned long)tx_buffer,
.rx_buf = (unsigned long)rx_buffer,
.len = sizeof(tx_buffer),
.delay_usecs = 0,
.speed_hz = 1000000, // SPI speed in Hz
.bits_per_word = 8,
};

if (ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer) < 0) {
perror("Failed to perform SPI transfer");
close(spi_file);
return -1;
}

/* Print tx_buffer and rx_buffer*/
printf("\rtx_buffer: \n %s\n ", tx_buffer);
printf("\rrx_buffer: \n %s\n ", rx_buffer);

// Close the SPI device
close(spi_file);

return 0;
}

4.3 文件路径

这行代码定义了一个宏,用于存储 SPI 设备文件的路径。

#define SPI_DEVICE_PATH "/dev/spidev0.0"

4.4 打开SPI设备

这部分代码尝试打开指定的 SPI 设备文件。

// Open the SPI device
if ((spi_file = open(SPI_DEVICE_PATH, O_RDWR)) < 0) {
perror("Failed to open SPI device");
return -1;
}

4.5 配置SPI

这段代码用于配置 SPI 通信的模式为 SPI 模式0(时钟极性为0、时钟相位为0)以及每个字的位数为8位,以确保 SPI 通信的正确性和一致性。

// Configure SPI mode and bits per word
uint8_t mode = SPI_MODE_0;
uint8_t bits = 8;
if (ioctl(spi_file, SPI_IOC_WR_MODE, &mode) < 0) {
perror("Failed to set SPI mode");
close(spi_file);
return -1;
}
if (ioctl(spi_file, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0) {
perror("Failed to set SPI bits per word");
close(spi_file);
return -1;
}

这段代码定义了一个 spi_ioc_transfer 结构体变量 transfer,用于配置 SPI 传输的参数。它指定了传输的数据缓冲区、传输的长度、延迟时间、SPI 速度(以赫兹为单位)以及每个字的位数。这个结构体将被传递给 SPI_IOC_MESSAGE ioctl 函数,以执行 SPI 传输。

// Perform SPI transfer
struct spi_ioc_transfer transfer = {
.tx_buf = (unsigned long)tx_buffer,
.rx_buf = (unsigned long)rx_buffer,
.len = sizeof(tx_buffer),
.delay_usecs = 0,
.speed_hz = 1000000, // SPI speed in Hz
.bits_per_word = 8,
};

4.6 数据收发

这段代码使用 ioctl 函数执行 SPI 传输。它通过 SPI_IOC_MESSAGE(1) 宏指定传输的数量为1,第三个参数为上文中配置的结构体变量 transfer 的地址。如果 SPI 传输失败,将会输出错误消息并关闭 SPI 设备文件描述符。

if (ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer) < 0) {
perror("Failed to perform SPI transfer");
close(spi_file);
return -1;
}

4.7 交叉编译

  1. 指定交叉编译工具

    首先,我们要将交叉编译工具的路径添加到系统的 PATH 环境变量中,以便可以在任何地方使用交叉编译工具,您可以在shell配置文件中添加以下行(通常是 ~/.bashrc~/.bash_profile~/.zshrc,具体取决于您使用的shell),注意 PATH= 后的路径为交叉编译工具所在的目录。

    • gcc路径

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • 打开shell配置文件

      vi ~/.bashrc 
    • 将交叉编译工具的路径添加到系统的PATH环境变量中,将 <SDK Directory> 修改为自己的 SDK 路径,如 /home/luckfox/luckfox-pico/

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • 重新加载shell配置文件,使更改生效

      source ~/.bashrc  
  2. 使用交叉编译工具编译程序

    arm-rockchip830-linux-uclibcgnueabihf-gcc spi.c -o spi
  3. 交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件

    # ls
    spi spi.c

4.8 运行程序

  1. 文件传输

    先将 spi 从虚拟机传输到 Windows,再通过 TFTP 或 ADB 传输到开发板,将文件从 Windows 通过 ADB 将文件传输到开发板的步骤如下:

    adb push 文件所在路径 开发板存储路径

    eg:(将当前目录下spi文件传输到开发板的根目录)
    adb push spi /
  2. 运行程序

    修改 spi 文件的操作权限后运行程序

    # chmod +x spi
    # ./spi
  3. 实验现象

    将SPI的MOSI和MISO相接,运行程序:
    image