编写 SPI 设备驱动主要有两种方式:
- spidev 方式:不编写专用的驱动,直接使用内核提供的通用
spidev驱动,在用户空间通过文件系统接口(如/dev/spidevX.Y)直接操作 SPI 总线。 - 专用驱动方式:编写内核模块,注册为 SPI 驱动,通过内核 API 与设备交互,通常向用户空间暴露特定的设备节点或接口(如 Input 子系统、IIO 子系统等)。
1. 方式一:使用 spidev 通用驱动(用户空间驱动)
这是最简单的方式,适用于对性能要求不是极其苛刻,或者协议比较简单的设备。
1.1 设备树配置
首先需要在设备树中启用 SPI 控制器,并添加一个兼容 spidev 的子节点。
dts
&spi0 {
status = "okay";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi0m0_cs0 &spi0m0_pins>;
pinctrl-1 = <&spi0m0_cs0 &spi0m0_pins_hs>;
spidev@0 {
compatible = "rockchip,spidev"; // 或者 "rohm,dh2228fv" 等被 spidev 驱动识别的 compatible
reg = <0>; // 片选信号索引,对应 CS0
spi-max-frequency = <10000000>; // 最大频率 10MHz
};
};1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- compatible:
spidev驱动会匹配特定的 compatible 字符串。在较新的内核中,直接使用"spidev"可能被视为不规范,通常建议使用"rohm,dh2228fv"或厂商自定义的特定字符串,并在驱动源码drivers/spi/spidev.c的spidev_dt_ids中添加匹配,或者使用内核已有的匹配项。 - reg:指定使用的片选线(Chip Select)。
1.2 内核配置
确保内核配置中启用了 User mode SPI device driver support。
config
Device Drivers --->
[*] SPI support --->
<*> User mode SPI device driver support1
2
3
2
3
1.3 用户空间编程
在用户空间,可以通过 standard file I/O (open, read, write, ioctl) 来控制 SPI 设备。
- 打开设备:
open("/dev/spidev0.0", O_RDWR) - 配置模式:
ioctl(fd, SPI_IOC_WR_MODE, &mode) - 配置位宽:
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) - 配置速度:
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) - 数据传输:
ioctl(fd, SPI_IOC_MESSAGE(N), xfer_array)
详细代码示例请参考 SPI 应用程序编程。
2. 方式二:编写专用内核驱动
对于需要中断处理、高性能传输或需要集成到其他内核子系统(如传感器、显示屏、网络设备)的 SPI 设备,需要编写专用的内核驱动。
2.1 驱动框架
SPI 驱动基于 Linux 设备驱动模型,主要包含 spi_driver 结构体和 spi_device 结构体。
c
#include <linux/module.h>
#include <linux/spi/spi.h>
// 1. 定义 probe 函数:设备匹配成功时调用
static int my_spi_probe(struct spi_device *spi)
{
dev_info(&spi->dev, "My SPI Device Probed!\n");
// 初始化设备
// 注册字符设备、Input 设备或 IIO 设备等
return 0;
}
// 2. 定义 remove 函数:设备卸载时调用
static void my_spi_remove(struct spi_device *spi)
{
dev_info(&spi->dev, "My SPI Device Removed!\n");
// 清理资源
}
// 3. 定义设备匹配表 (Device Tree 匹配)
static const struct of_device_id my_spi_dt_ids[] = {
{ .compatible = "myvendor,my-spi-device", },
{ }
};
MODULE_DEVICE_TABLE(of, my_spi_dt_ids);
// 4. 定义 spi_driver 结构体
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my-spi-driver",
.of_match_table = my_spi_dt_ids,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
};
// 5. 注册驱动
module_spi_driver(my_spi_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A Simple SPI Driver Template");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2.2 设备树配置
在设备树中描述实际的 SPI 设备。
dts
&spi0 {
status = "okay";
my_spi_dev@0 {
compatible = "myvendor,my-spi-device";
reg = <0>;
spi-max-frequency = <5000000>;
// 其他自定义属性,如中断引脚
interrupt-parent = <&gpio1>;
interrupts = <RK_PA0 IRQ_TYPE_LEVEL_LOW>;
};
};1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2.3 数据传输 API
在 probe 函数或其他逻辑中,使用内核提供的 SPI API 进行数据交互。
同步传输 (阻塞):
spi_write(struct spi_device *spi, const void *buf, size_t len)spi_read(struct spi_device *spi, void *buf, size_t len)spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx)spi_sync(struct spi_device *spi, struct spi_message *message)
异步传输 (非阻塞):
spi_async(struct spi_device *spi, struct spi_message *message):提交传输请求后立即返回,传输完成后调用回调函数。
2.4 常用 API 详解
spi_write_then_read
c
/**
* spi_write_then_read - 便捷函数,先写后读
* @spi: SPI 设备指针
* @txbuf: 发送缓冲区
* @n_tx: 发送字节数
* @rxbuf: 接收缓冲区
* @n_rx: 接收字节数
*
* 上下文:可以睡眠
*/
int spi_write_then_read(struct spi_device *spi,
const void *txbuf, unsigned n_tx,
void *rxbuf, unsigned n_rx);1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
常用于寄存器读写:先写寄存器地址,再读取寄存器值。
构建 spi_message
对于复杂的传输序列(例如:CS 拉低 -> 发送命令 -> 发送地址 -> 接收数据 -> CS 拉高),需要手动构建 spi_transfer 和 spi_message。
c
struct spi_transfer t[2];
struct spi_message m;
u8 cmd_buf[1] = { 0xREAD_CMD };
u8 data_buf[16];
// 初始化 message
spi_message_init(&m);
// 准备 transfer 1: 发送命令
memset(&t[0], 0, sizeof(t[0]));
t[0].tx_buf = cmd_buf;
t[0].len = 1;
spi_message_add_tail(&t[0], &m);
// 准备 transfer 2: 接收数据
memset(&t[1], 0, sizeof(t[1]));
t[1].rx_buf = data_buf;
t[1].len = 16;
spi_message_add_tail(&t[1], &m);
// 执行同步传输
int ret = spi_sync(spi, &m);
if (ret)
dev_err(&spi->dev, "SPI transfer failed: %d\n", ret);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2.5 驱动编写注意事项
- DMA 支持:RK3566 的 SPI 控制器支持 DMA。当传输数据量较大时(通常超过 FIFO 深度或一定阈值),SPI 核心层会自动尝试使用 DMA 传输,驱动开发者通常不需要手动处理 DMA 映射,只需提供虚拟地址(virtual address)的缓冲区即可。如果使用
spi_message且手动设置了is_dma_mapped和dma_addr,则会使用调用者提供的 DMA 地址。 - 并发控制:
spi_sync内部会使用互斥锁(mutex)保护总线,确保同一时刻只有一个设备在使用总线。但在驱动层,如果多个线程访问同一个设备的数据结构,仍需注意并发保护。 - GPIO 片选 vs 控制器片选:在设备树中,如果指定了
cs-gpios,内核会使用 GPIO 模拟片选信号,这通常比控制器原生片选更灵活(支持更多设备)。驱动中无需关心,内核 SPI 核心层会自动处理。
3. 总结
| 特性 | spidev 方式 | 专用内核驱动方式 |
|---|---|---|
| 开发难度 | 低,无需编写内核代码 | 中/高,需要了解内核 API |
| 灵活性 | 高,应用层完全控制 | 可定制性强,可集成中断、DMA 等 |
| 效率 | 较低,系统调用开销大 | 高,减少用户/内核空间切换 |
| 适用场景 | 调试、简单控制、低速设备 | 高速传感器、显示屏、网络接口 |
根据实际需求选择合适的驱动开发方式。对于测试和验证阶段,spidev 是非常好的工具;对于最终产品,特别是对时序和性能有要求的场景,建议编写专用内核驱动。