6. SPI 操作指南¶
本文档说明在 CV184x 平台上使用 SPI 总线进行读写操作、环回测试以及内核态/用户态编程的流程。
平台说明 :CV184x 上的 SPI 控制器仅支持主机(Master)模式,不支持配置为从机(Slave)模式。即本平台只能作为 SPI 总线的主设备发起传输、连接 Flash/传感器等从设备,无法作为从设备被其他主控通过 SPI 访问。本平台 SPI 时钟最高支持 46.8MHz,设备树与驱动中 spi-max-frequency 仅可配置至 46800000(Hz),超出该值可能工作异常或不支持。
6.1. 操作准备¶
使用 SPI 前需满足以下条件:
使用官方发布的 SDK 编译后的内核镜像(默认已包含 SPI 相关驱动)。
6.1.1. 使能内核 SPI 与 DMA 配置¶
本步骤分为三部分:在板级内核配置中开启 SPI 与 DMA 相关选项;在设备树中为 SPI1 启用 DMA;如需使用 DMA 通道重映射,则在板级 DTS 根节点下配置 sysdma_remap。路径中的”板名”请替换为实际使用的板级目录名(如 cv1842hp_wevb_0014a_spinor),具体以 SDK 为准。
(1)内核与 DMA 相关配置
在板级内核配置文件中确保以下选项已开启。配置文件路径一般为 build/boards/cv184x/板名/linux/板名_defconfig 。
CONFIG_DMADEVICES=y
CONFIG_DW_DMAC_CVITEK=y
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_SPIDEV=y
CONFIG_SPI_DESIGNWARE=y
CONFIG_SPI_DW_DMA=y
CONFIG_SPI_DW_MMIO=y
若不需要 DMA 传输(例如调试、对比性能或外设不兼容 DMA),可将 CONFIG_SPI_DW_DMA 注释掉,使 SPI 控制器回退到 PIO(中断/轮询)模式:
CONFIG_DMADEVICES=y
CONFIG_DW_DMAC_CVITEK=y
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_SPIDEV=y
CONFIG_SPI_DESIGNWARE=y
# CONFIG_SPI_DW_DMA is not set
CONFIG_SPI_DW_MMIO=y
同时需在设备树中将对应 SPI 节点的 #if 1 改为 #if 0,关闭 DMA 属性(见下文(2)中的示例)。修改后重新编译内核与设备树并烧录至开发板即可。
(2)在设备树中为 SPI1 启用 DMA
在板级 DTS 或其所包含的 DTSI 中,找到 SPI1 节点(如 spi1@04190000),将其中的 #if 预编译开关置为 1,使 dmas、dma-names、capability 等 DMA 相关属性被编译进设备树,从而开启 SPI1 的 DMA 功能。若 #if 为 0 或未定义该块,则 SPI1 使用 PIO 模式。
spi1: spi1@04190000 {
compatible = "snps,dw-apb-ssi";
reg = <0x0 0x04190000 0x0 0x10000>;
clocks = <&clk CV184X_CLK_APB_SPI1>;
#address-cells = <1>;
#size-cells = <0>;
#if 1
dmas = <&dmac 2 1 1
&dmac 3 1 1>;
dma-names = "rx", "tx";
capability = "txrx";
#endif
status = "okay";
num-cs = <1>;
spidev@0 {
compatible = "rohm,dh2228fv";
spi-max-frequency = <1000000>; /* 最高支持 46.8MHz,可设为 46800000 */
reg = <0>;
};
};
spi-max-frequency 单位为 Hz,本平台 SPI IP 仅支持最高 46.8MHz,请勿配置超过 46800000。
(3)DMA 通道重映射
若板级需要指定 DMA 通道与外设的对应关系,可在板级 DTS 的根节点 / 下添加或合并 sysdma_remap 节点。板级 DTS 路径一般为 build/boards/cv184x/板名/dts_arm/板名.dts 。下例中 CVI_SPI1_RX、CVI_SPI1_TX 与 SPI1 设备节点中的 DMA 通道 2 和 3 对应,其余通道按实际需求配置。
/ {
sysdma_remap {
ch-remap = <CVI_I2S0_RX CVI_I2S2_TX CVI_SPI1_RX CVI_SPI1_TX
CVI_SPI_NOR_RX CVI_SPI_NOR_TX CVI_I2S2_RX CVI_I2S3_TX>;
};
};
6.1.2. 在设备树中将 SPI1 使能¶
在板级 DTS 中将 SPI1 节点设为 status = "okay",其余不使用的 SPI 节点设为 status = "disabled"。
6.1.3. SPI1 引脚切换¶
说明 :以下”需切换两次引脚功能”仅为 SPI1 的特殊情况(先切到 MUX 再指到具体功能)。其他 SPI 控制器或外设的 Pinmux 请根据实际板级原理图与引脚定义操作,可能只需一次切换或不同顺序,以 pinout文档为准。
步骤 1. 将 PAD_MIPIRX3P 切 MUX_SPI1_MOSI
cvi_pinmux -w PAD_MIPIRX3P/MUX_SPI1_MOSI
步骤 2. 将 MUX_SPI1_MOSI 切 SPI1_SDO
cvi_pinmux -w MUX_SPI1_MOSI/SPI1_SDO
步骤 3. MISO 引脚
按同样方式进行切换(先切换到 MUX_SPI1_MISO,再切到 SPI1_SDI ,具体以板级引脚定义为准)。
cvi_pinmux -w PAD_MIPIRX3N/MUX_SPI1_MISO
cvi_pinmux -w MUX_SPI1_MISO/SPI1_SDI
6.2. 操作过程¶
启动系统 SPI 相关驱动已编入内核,无需单独加载模块;启动后即可使用 SPI。
进行 SPI 读写 在串口或 SSH 终端中运行 SPI 读写命令,或在内核态/用户态编写程序,对挂载在 SPI 控制器上的从设备进行读写。
6.3. SPI 环回测试¶
环回测试用于验证 SPI 控制器与引脚是否工作正常:将 SPI1 的 MOSI 与 MISO 短接,发送数据后读回,比对是否一致。
步骤 1:短接 SPI1 的 MOSI 与 MISO 引脚
步骤 2:完成 SPI1 内核配置与设备树使能
按上文「操作准备」中的「使能内核 SPI 与 DMA 配置」和「在设备树中将 SPI1 使能」两节完成配置,重新编译内核与设备树并烧录至开发板。
步骤 3:确认 spidev 节点
ls /dev/spidev*
# 示例输出:/dev/spidev1.0,后续命令中的设备节点需与输出一致
步骤 4:执行 cvi_pinmux 切换
以下「需切换两次引脚功能」仅为 SPI1 的特殊情况(先切到 MUX 再指到具体功能)。其他 SPI 控制器或外设的 Pinmux 请根据实际板级原理图与引脚定义操作,可能只需一次切换或不同顺序,以 pinout 文档为准。
步骤 4.1 将 PAD_MIPIRX3P 切 MUX_SPI1_MOSI
cvi_pinmux -w PAD_MIPIRX3P/MUX_SPI1_MOSI
步骤 4.2 将 MUX_SPI1_MOSI 切 SPI1_SDO
cvi_pinmux -w MUX_SPI1_MOSI/SPI1_SDO
步骤 4.3 MISO 引脚
按同样方式进行切换(先切换到 MUX_SPI1_MISO,再切到 SPI1_SDI,具体以板级引脚定义为准)。
cvi_pinmux -w PAD_MIPIRX3N/MUX_SPI1_MISO
cvi_pinmux -w MUX_SPI1_MISO/SPI1_SDI
步骤 5:编译并部署 spidev_test
在 SDK 的linux_5.10/tools/spi 目录下执行 make,将生成的 spidev_test 拷贝到板端。
步骤 6:生成测试数据
dd if=/dev/urandom of=data.bin bs=2k count=1
步骤 7:执行环回测试
./spidev_test -D /dev/spidev1.0 -s 10000000 -i data.bin -o data_out.bin
# -D 为设备节点,-s 为速率(Hz),-i 为输入文件,-o 为输出文件
步骤 8:比对数据
diff data.bin data_out.bin
# 无输出表示两文件一致,环回测试通过
环回测试通过后,可连接实际 SPI 从设备进行读写操作。
6.4. 操作示例¶
6.5. 内核态 SPI 读写程序示例¶
以下说明在内核驱动中通过 SPI 子系统访问从设备的典型流程与示例代码。
流程概述:
使用
spi_busnum_to_master(bus_num)获取 SPI 控制器(spi_master),bus_num为目标设备所在的总线号。根据总线名与片选号找到
spi_device``(如通过 ``bus_find_device_by_name等)。填充
spi_transfer并加入spi_message,再调用spi_sync或spi_async执行同步或异步传输。
步骤 1:获取 SPI 控制器
master = spi_busnum_to_master(bus_num);
/* bus_num:设备所在 SPI 总线号;master:spi_master 指针 */
步骤 2:获取 SPI 从设备
snprintf(str, sizeof(str), "%s.%u", dev_name(&master->dev), cs);
dev = bus_find_device_by_name(&spi_bus_type, NULL, str);
spi = to_spi_device(dev);
/* cs:片选号;spi:spi_device 指针 */
步骤 3:组织 spi_message
spi_message_init(&m);
spi_message_add_tail(&t, &m);
/* t:spi_transfer;m:spi_message */
步骤 4:发起传输
status = spi_sync(spi, &m); /* 同步 */
/* 或 status = spi_async(spi, &m); 异步 */
完整示例代码(仅供参考,非可直接运行的完整模块):
1#include <linux/spi/spi.h>
2
3static unsigned int busnum;
4module_param(busnum, uint, 0);
5MODULE_PARM_DESC(busnum, "SPI bus number (default=0)");
6
7static unsigned int cs;
8module_param(cs, uint, 0);
9MODULE_PARM_DESC(cs, "SPI chip select (default=0)");
10
11extern struct bus_type spi_bus_type;
12static struct spi_master *master;
13static struct spi_device *spi_device;
14
15static int __init spidev_init(void)
16{
17 char *spi_name;
18 struct device *dev;
19
20 master = spi_busnum_to_master(busnum);
21 if (!master)
22 return -ENODEV;
23
24 spi_name = kzalloc(strlen(dev_name(&master->dev)) + 8, GFP_KERNEL);
25 if (!spi_name)
26 return -ENOMEM;
27
28 snprintf(spi_name, strlen(dev_name(&master->dev)) + 8,
29 "%s.%u", dev_name(&master->dev), cs);
30 dev = bus_find_device_by_name(&spi_bus_type, NULL, spi_name);
31 kfree(spi_name);
32 if (!dev)
33 return -ENODEV;
34
35 spi_device = to_spi_device(dev);
36 put_device(dev);
37 if (!spi_device)
38 return -ENODEV;
39
40 return 0;
41}
42
43static int spi_dev_write(void *buf, unsigned long len, int buswidth)
44{
45 struct spi_transfer t = {
46 .speed_hz = 2000000,
47 .tx_buf = buf,
48 .len = len,
49 };
50 struct spi_message m;
51
52 if (!spi_device)
53 return -ENODEV;
54
55 spi_device->mode = SPI_MODE_0;
56 t.bits_per_word = (buswidth == 16) ? 16 : 8;
57
58 spi_message_init(&m);
59 spi_message_add_tail(&t, &m);
60 return spi_sync(spi_device, &m);
61}
62
63static int spi_dev_read(unsigned char devaddr, unsigned char reg_addr,
64 void *buf, size_t len)
65{
66 u8 txbuf[4] = { devaddr, 0, reg_addr, 0 };
67 struct spi_transfer t = {
68 .speed_hz = 2000000,
69 .tx_buf = txbuf,
70 .rx_buf = buf,
71 .len = len,
72 };
73 struct spi_message m;
74
75 if (!spi_device)
76 return -ENODEV;
77
78 spi_device->mode = SPI_MODE_0;
79 /* txbuf 长度与位宽需根据从设备协议填写,此处为示例 */
80 spi_message_init(&m);
81 spi_message_add_tail(&t, &m);
82 return spi_sync(spi_device, &m);
83}
6.6. 用户态 SPI 读写程序示例¶
以下说明在用户空间通过 spidev 设备节点与 ioctl 进行 SPI 读写的步骤。完整实现可参考 SDK 中 tools/spi/spidev_test.c。
步骤 1:打开设备节点
static const char *device = "/dev/spidev1.0";
int fd = open(device, O_RDWR);
if (fd < 0)
/* 错误处理 */;
注解
设备节点与 SPI 控制器对应关系(以实际 ls /dev/spidev* 为准):
正常情况:SPI 控制器 0 默认为
/dev/spidev1.0,SPI 控制器 1 为/dev/spidev2.0,以此类推(控制器 N 对应 spidev(N+1).0)。若禁用了某个 SPI 控制器(如禁用控制器 0),则其后控制器节点号前移:此时 SPI 控制器 1 为
/dev/spidev1.0,控制器 2 为/dev/spidev2.0,以此类推。
其余操作与控制器 0 相同。
步骤 2:设置 SPI 模式
ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
if (ret == -1)
/* 错误处理 */;
ret = ioctl(fd, SPI_IOC_RD_MODE32, &mode);
mode 取值参见内核头文件 include/linux/spi/spi.h,例如:
#define SPI_CPHA 0x01 /* 时钟相位 */
#define SPI_CPOL 0x02 /* 时钟极性 */
#define SPI_MODE_0 (0|0)
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
/* 示例:mode = SPI_MODE_3 | SPI_LSB_FIRST; */
步骤 3:设置每字位数(bits per word)
ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
if (ret == -1)
/* 错误处理 */;
ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
步骤 4:设置传输速率
ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
if (ret == -1)
/* 错误处理 */;
ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
一般建议 speed 不超过 25MHz,具体以从设备规格为准。
步骤 5:发送/接收一帧数据
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (ret < 1)
/* 错误处理 */;
tr 为 struct spi_ioc_transfer 数组首地址,用于描述本帧传输的缓冲区与长度。
完整可运行示例
将上述步骤串联,并加上头文件、struct spi_ioc_transfer 的填充与缓冲区,即可得到可直接编译运行的程序(需内核提供 /dev/spidev* 及对应头文件):
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <errno.h>
5#include <fcntl.h>
6#include <unistd.h>
7#include <sys/ioctl.h>
8#include <linux/spi/spidev.h>
9
10#define SPI_DEV "/dev/spidev1.0"
11#define SPI_SPEED 10000000
12#define SPI_BITS 8
13
14int main(void)
15{
16 int fd;
17 int ret;
18 __u8 mode = SPI_MODE_0;
19 __u8 bits = SPI_BITS;
20 __u32 speed = SPI_SPEED;
21 __u8 tx_buf[4] = { 0x01, 0x02, 0x03, 0x04 };
22 __u8 rx_buf[4] = { 0 };
23
24 struct spi_ioc_transfer tr = {
25 .tx_buf = (unsigned long)tx_buf,
26 .rx_buf = (unsigned long)rx_buf,
27 .len = 4,
28 .speed_hz = speed,
29 .bits_per_word = bits,
30 };
31
32 fd = open(SPI_DEV, O_RDWR);
33 if (fd < 0) {
34 perror("open " SPI_DEV);
35 return EXIT_FAILURE;
36 }
37
38 ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
39 if (ret == -1) {
40 perror("SPI_IOC_WR_MODE");
41 close(fd);
42 return EXIT_FAILURE;
43 }
44
45 ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
46 if (ret == -1) {
47 perror("SPI_IOC_WR_BITS_PER_WORD");
48 close(fd);
49 return EXIT_FAILURE;
50 }
51
52 ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
53 if (ret == -1) {
54 perror("SPI_IOC_WR_MAX_SPEED_HZ");
55 close(fd);
56 return EXIT_FAILURE;
57 }
58
59 ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
60 if (ret < 1) {
61 perror("SPI_IOC_MESSAGE");
62 close(fd);
63 return EXIT_FAILURE;
64 }
65
66 printf("TX: %02x %02x %02x %02x\n", tx_buf[0], tx_buf[1], tx_buf[2], tx_buf[3]);
67 printf("RX: %02x %02x %02x %02x\n", rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3]);
68
69 close(fd);
70 return EXIT_SUCCESS;
71}
编译与运行:arm-none-linux-uclibcgnueabihf-gcc -o spidev_demo spidev_demo.c -static,在目标板上执行 ./spidev_demo。设备节点与 SPI 控制器对应关系见上文备注;速率、模式、字长可按从设备要求修改。