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 修改配置
- 选择"Device Drivers"
- 选择"SPI support"
- 修改配置
- 配置用户空间直接操作 SPI 接口
- Master 端按"Y"使能"Rockchip SPI controller driver"
- Slave 端按"Y"使能"SPI slave protocol handlers"
- 配置用户空间直接操作 SPI 接口
1.3 保存config,回到SDK目录下重新编译内核
make ARCH=arm savedefconfig
cp defconfig arch/arm/configs/luckfox_rv1106_linux_defconfig
cd ~/SDK目录
./build kernel
2.修改设备树
2.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
修改设备树文件
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 重新编译内核
编译选择分支,分别是指定 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.重新编译内核
luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel
2.3 重新烧录固件
- 内核编译成功后生成的文件在
<SDK目录>output/image
目录下 - 替换原固件中的
boot.image
与env.txt
文件 - 重新创建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 交叉编译
指定交叉编译工具
首先,我们要将交叉编译工具的路径添加到系统的
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
使用交叉编译工具编译程序
arm-rockchip830-linux-uclibcgnueabihf-gcc spi.c -o spi
交叉编译成功后,将在当前目录下生成可在开发板运行的可执行文件
# 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 运行效果
- Slave 端运行程序后,等待 Master 端发送数据
- Master 端运行程序,向 Slave 端发送数据
- Slave 端成功接收数据并输出,进入下一轮等待