跳到主要内容

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 引脚分布

开发板都默认开启了 SPI0 接口,我们可以通过接口图进行确定对应的引脚,如 LuckFox Pico 的 SPI0 接口对应的引脚编号为48、49、50、51和58,引脚58默认关闭。

  1. LuckFox Pico 引脚图:

  2. LuckFox Pico Mini A/B 引脚图:

  3. LuckFox Pico Plus 引脚图:

  4. LuckFox Pico Pro/Max 引脚图:

  5. 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

5.修改设备树

在 SDK 的设备树文件中已经默认启用了 SPI 接口,您可以直接进行使用,也可以根据下文修改 SPI 引脚配置。当连接多个 SPI 设备使用时,可以选择在程序中将 CS 引脚作为普通IO,自主控制 CS 引脚电平,例如,我们可以将 GPIO1_C0_d 配置为普通IO。

5.1 修改设备树文件

  1. 设备树文件路径

    • 开发板配置文件

      <SDK目录>/project/cfg/BoardConfig_IPC/ 目录下有不同型号开发板的配置文件,这些文件主要包含了针对 Luckfox Pico 不同型号开发板的配置参数,涵盖了目标架构、启动介质、Uboot和内核配置、分区设置等多方面的内容。SDK目录结构如下:

      ├── build.sh -> project/build.sh ---- SDK编译脚本
      ├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
      ├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
      ├── project ------------------------- 参考应用、编译配置以及脚本目录
      │ ├── cfg
      │ ├── BoardConfig_IPC
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
      │ └── ...
      ├── output -------------------------- SDK编译后镜像文件存放目录
      ├── docs ---------------------------- SDK文档目录
      └── tools --------------------------- 烧录镜像打包工具以及烧录工具

      其中,RK_KERNEL_DTS 指定了内核的设备树文件。我们以 Luckfox Pico 为例,打开 BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk 文件,可以看到 RK_KERNEL_DTS 指向的文件为 rv1103g-luckfox-pico.dts

    • 设备树文件路径

      根据RK_KERNEL_DTS,可以确定 Luckfox Pico 的设备树文件路径如下:

      <SDK目录>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. 定义GPIO

    定义一个 GPIO,通常需要添加两段代码。以下是一个示例,展示如何在设备树中添加 GPIO1_C0_d 引脚的定义。
    image
    image

    需要添加的代码片段如下:

    /{
    gpio1pc:gpio1pc0 {
    compatible = "regulator-fixed";
    pinctrl-names = "default";
    pinctrl-0 = <&gpio1_pc0>;
    regulator-name = "gpio1_pc0";
    regulator-always-on;
    };
    };

    &pinctrl {
    gpio1-pc0 {
    gpio1_pc0:gpio1-pc0 {
    rockchip,pins = <1 RK_PC0 RK_FUNC_GPIO &pcfg_pull_none>;
    };
    };
    };
  3. 注释引脚的外设功能

    根据接口图,可以看到 GPIO1_C0_d 的默认功能是 PWM 功能,在程序中,我们希望将将该引脚配置为普通IO,因此需要注释引脚的外设功能。注释引脚外设功能可以通过在设备树中将相应的外设节点注释来实现,以下是一个示例,展示如何在设备树中禁用 GPIO1_C0_d 引脚的 PWM 功能。
    image

  4. 配置SPI

    在SDK的设备树文件中已经默认启用了 SPI0 接口。这里我们不配置 GPIO1_C0_d 作为 SPI0 的 CS 引脚(请查看第167行注释)。以下是一个示例,展示如何在设备树中配置SPI:
    image

    需要添加的代码片段如下:

    &pinctrl {
    spi0 {
    /omit-if-no-ref/
    spi0m0_pins: spi0m0-pins {
    rockchip,pins =
    /* spi0_clk_m0 */
    <1 RK_PC1 4 &pcfg_pull_none>,
    /* spie_miso_m0 */
    <1 RK_PC3 6 &pcfg_pull_none>,
    /* spi_mosi_m0 */
    <1 RK_PC2 6 &pcfg_pull_none>;
    };
    };
    };

5.2 编译内核

  1. 编译选择分支,分别是指定 LuckFox Pico 、LuckFox Pico Mini A 、LuckFox Pico Mini B、LuckFox Pico Plus 和 LuckFox Pico Pro/Max

    luckfox@luckfox:~/luckfox-pico$ ./build.sh lunch
    ls: cannot access 'BoardConfig*.mk': No such file or directory

    You're building on Linux
    Lunch menu...pick a combo:

    BoardConfig-*.mk naming rules:
    BoardConfig-"启动介质"-"电源方案"-"硬件版本"-"应用场景".mk
    BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk

    ----------------------------------------------------------------
    0. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    1. BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
    boot medium(启动介质): EMMC
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_A
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    2. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Mini_B
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    3. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1103_Luckfox_Pico_Plus
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    ----------------------------------------------------------------
    4. BoardConfig_IPC/BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
    boot medium(启动介质): SPI_NAND
    power solution(电源方案): NONE
    hardware version(硬件版本): RV1106_Luckfox_Pico_Pro_Max
    applicaton(应用场景): IPC
    ----------------------------------------------------------------

    Which would you like? [0]: 0
    [build.sh:info] switching to board: /home/luckfox/luckfox-pico/project/cfg/BoardConfig_IPC/BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
    [build.sh:info] Running build_select_board succeeded.
  2. 重新编译内核

    luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel

5.3 重新烧录固件

  1. 内核编译成功后生成的文件在 <SDK目录>output/image 目录下
    image
  2. 替换原固件中的 boot.imageenv.txt 文件
    image
  3. 重新创建SD(Luckfox Pico Plus 可只修改相应的分区)
    image