跳到主要内容

SPI 主从通讯

在本章中,我们将学习如何配置 SPI 设备,将 RK 芯片分别作为 Master 和 Slave 端,在用户空间直接操作 SPI 接口,实现 Master 端和 Slave 端通讯。

示例程序:Code.zip

1.修改内核配置

1.1 保存文件,清除编译重新编译

cd ~/SDK目录/sysdrv/source/kernel
cp ./arch/arm/configs/luckfox_rv1106_linux_defconfig .config
make ARCH=arm menuconfig

1.2 修改配置

  1. 选择"Device Drivers"
  2. 选择"SPI support"
  3. 修改配置
    • 配置用户空间直接操作 SPI 接口
    • Master 端按"Y"使能"Rockchip SPI controller driver"
    • Slave 端按"Y"使能"SPI slave protocol handlers"

1.3 保存config,回到SDK目录下重新编译内核

make ARCH=arm savedefconfig
cp defconfig arch/arm/configs/luckfox_rv1106_linux_defconfig
cd ~/SDK目录
./build kernel

2.修改设备树

2.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. 修改设备树文件

    • Master 端

      &spi0 {
      status = "okay";
      spi_test@00 {
      compatible = "rockchip,spidev";
      reg = <0>;
      spi-cpha;
      spi-cpol;
      spi-lsb-first;
      spi-max-frequency = <49000000>;
      status = "okay";
      };
      };
    • Slave 端

      &spi0 {
      status = "okay";
      spi-slave;
      slave {
      compatible ="rockchip,spidev";
      reg = <0>;
      id = <0>;
      };
      };

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

2.3 重新烧录固件

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

3.SPI通讯(C程序)

3.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优先模式。

3.2 示例程序

通过下面两个程序,可以实现 Master 端与 Slave 端 SPI 通讯。

Master 端

#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[255];
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 = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

/* Send Data */
for (int i = 0; i < transfer.len; i++)
tx_buffer[i] = i;

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

printf("Send %d bytes of data max speed: %d Hz.\n",transfer.len,transfer.speed_hz);

/* Close the SPI device */
close(spi_file);

return 0;
}

Slave 端

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

#define SPI_DEVICE_PATH "/dev/spidev0.0"

int main() {
int spi_file,ret;
uint8_t tx_buffer[50];
uint8_t rx_buffer[255];

/* 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(rx_buffer),
.delay_usecs = 0,
.speed_hz = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

while(1)
{
/* Clear buffer */
memset(rx_buffer,0,sizeof(rx_buffer));

/* Waiting for data */
do {
ret = ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer);
} while (ret < 0);

/* Print rx_buffer*/
printf("---------Receive %d bytes of data max speed:%d Hz---------\n",ret,transfer.speed_hz);
printf("SPI RX: 0x%08X:", 0);
for (int i = 0; i < ret; i++) {
printf(" %02X",rx_buffer[i] );
if ((i + 1) % 16 == 0){
printf("\nSPI RX: 0x%08X:", i+1);
}
}
printf("\n");
}

/* Close the SPI device */
close(spi_file);

return 0;
}

3.3 文件路径

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

#define SPI_DEVICE_PATH "/dev/spidev0.0"

3.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;
}

3.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 = 49000000, // SPI speed in Hz
.bits_per_word = 8,
};

3.6 Master 端数据发送

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

/* Send Data */
for (int i = 0; i < transfer.len; i++)
tx_buffer[i] = i;

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

3.7 Slave 端数据接收

这段代码首先清空数据缓冲区,然后根据 ioctl 函数的返回值判断是否接收到数据,无则循环等待主机数据。当接收到数据后, ioctl 函数返回接收到的数据长度,接着以十六进制格式打印接收到的数据以及相关信息,然后清空数据缓冲区,继续下一轮等待。

while(1)
{
/* Clear rx_buffer */
memset(rx_buffer,0,sizeof(rx_buffer));

/* Waiting for data */
do {
ret = ioctl(spi_file, SPI_IOC_MESSAGE(1), &transfer);
} while (ret < 0);

/* Print rx_buffer */
printf("---------Receive %d bytes of data max speed:%d Hz---------\n",ret,transfer.speed_hz);
printf("SPI RX: 0x%08X:", 0);
for (int i = 0; i < ret; i++) {
printf(" %02X",rx_buffer[i] );
if ((i + 1) % 16 == 0){
printf("\nSPI RX: 0x%08X:", i+1);
}
}
printf("\n");
}

3.8 交叉编译

  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.运行程序

4.1 文件传输

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

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

adb -s 设备SN push 文件所在路径 开发板存储路径

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

adb -s d48936ed7d155xxx push spi /

4.2 硬件连接

1.连接SPI0_MISO、SPI0_MOSI、SPI0_CLK、CPI0_CS0
2.共地

4.3 运行程序

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

# chmod +x spi
# ./spi

4.4 运行效果

  1. Slave 端运行程序后,等待 Master 端发送数据
  2. Master 端运行程序,向 Slave 端发送数据
  3. Slave 端成功接收数据并输出,进入下一轮等待