4. SPI驱动开发实战:SPI控制器驱动框架、SPI设备驱动编写、SPI数据传输流程与API
SPI,串行外设接口,在嵌入式世界里太常见了。ADC、DAC、传感器、显示屏、Flash芯片……几乎每个板子上都能找到它的身影。今天我们就来聊聊SPI驱动开发的那些事。
我个人习惯把SPI驱动分成两块来看:控制器驱动和设备驱动。前者是芯片厂商的事,后者才是我们嵌入式工程师的日常。不过,不理解控制器驱动,设备驱动也写不顺手。
4.1 SPI控制器驱动框架
Linux内核里,SPI控制器驱动遵循一套标准框架。说白了,就是内核帮你搭好了舞台,你只需要把硬件相关的部分填进去。
核心结构体是 struct spi_controller(老版本叫 spi_master)。每个SPI控制器对应一个这样的实例。你需要实现它的几个关键回调函数:
transfer_one_message:处理一条完整的SPI消息transfer_one:处理一次SPI传输setup:配置SPI模式、时钟、位宽等参数prepare_message/unprepare_message:消息传输前后的准备和清理
嗯,这里要注意:transfer_one_message 是默认的消息处理函数。如果你不实现它,内核会用默认的,但效率嘛……你懂的。
核心要点:控制器驱动的本质,就是把硬件寄存器操作封装成内核框架要求的回调函数。DMA支持、FIFO管理、中断处理,这些都是加分项。
我在项目中遇到过一个问题:某款国产MCU的SPI控制器,硬件上支持16位数据模式,但驱动里只实现了8位。结果读传感器数据时,高位总是错的。后来加上了16位模式的支持,问题就解决了。所以,硬件能力 ≠ 驱动能力,你得自己补齐。
4.2 SPI设备驱动编写
这才是我们真正要写的代码。SPI设备驱动,就是让某个具体的SPI外设工作起来。
注册一个SPI设备驱动,你需要:
- 定义一个
struct spi_driver结构体 - 实现
probe和remove函数 - 用
module_spi_driver()宏注册
来看一个典型的SPI设备驱动框架:
static int my_spi_device_probe(struct spi_device *spi)
{
// 设置SPI模式、时钟频率、位宽
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 1000000;
spi->bits_per_word = 8;
spi_setup(spi);
// 分配设备私有数据
// 注册字符设备或输入设备等
return 0;
}
static int my_spi_device_remove(struct spi_device *spi)
{
// 清理资源
return 0;
}
static const struct spi_device_id my_spi_id[] = {
{ "my_spi_sensor", 0 },
{}
};
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_spi_sensor",
.of_match_table = of_match_ptr(my_spi_of_match),
},
.probe = my_spi_device_probe,
.remove = my_spi_device_remove,
.id_table = my_spi_id,
};
module_spi_driver(my_spi_driver);
你想想看,这个框架是不是很眼熟?和I2C、Platform驱动长得差不多。内核的驱动模型就是这样,一通百通。
个人经验:probe函数里一定要检查spi_setup的返回值。我曾经因为没检查,结果设备树里配的时钟频率不对,驱动却"成功"加载了,数据读出来全是乱的。排查了两天才发现是频率超了上限。
4.3 SPI数据传输流程
SPI数据传输,核心是 struct spi_message 和 struct spi_transfer。一个message包含一个或多个transfer,每个transfer描述一次独立的收发操作。
传输流程是这样的:
- 分配并初始化
spi_message - 分配并初始化
spi_transfer,设置tx_buf、rx_buf、len等 - 用
spi_message_add_tail()把transfer加到message里 - 调用
spi_sync()或spi_async()发起传输 - 等待传输完成(同步)或处理回调(异步)
同步传输最常用,简单粗暴:
struct spi_message msg;
struct spi_transfer xfer = {
.tx_buf = tx_buffer,
.rx_buf = rx_buffer,
.len = 32,
.cs_change = 0,
.delay_usecs = 10,
};
spi_message_init(&msg);
spi_message_add_tail(&xfer, &msg);
spi_sync(spi_device, &msg);
为什么要有多个transfer?举个例子:你要读一个传感器的数据,通常需要先发一个命令字节,再读回数据。这两个步骤之间可能需要片选保持、需要延时。用两个transfer放在一个message里,就能保证片选不释放,连续完成。
避坑指南:我曾经在批量传输时,每个transfer都设了cs_change=1,结果每次传输完片选就释放了。有些外设受不了这个,数据就丢了。正确的做法是:同一个message内的transfer,只有最后一个才设cs_change=0(保持片选),或者根据外设手册来。
4.4 核心API详解
SPI子系统提供的API,常用的就这几个:
| API | 说明 | 使用场景 |
|---|---|---|
spi_sync() |
同步传输,阻塞等待完成 | 大多数设备驱动,简单可靠 |
spi_async() |
异步传输,注册回调 | 需要高吞吐、不阻塞主流程的场景 |
spi_write_then_read() |
先写后读,封装好的便捷函数 | 传感器寄存器读写,省去手动构造message |
spi_write() |
只写不读 | 配置寄存器、发送命令 |
spi_read() |
只读不写 | 读取数据缓冲区 |
我个人最常用的是 spi_write_then_read()。它内部帮你处理了message和transfer的构造,代码简洁很多。但要注意,它只适用于小数据量(通常不超过几百字节),因为内部用了栈上的缓冲区。
来看一个实际例子:读一个SPI温度传感器的温度值。
uint8_t cmd = 0x10; // 读温度命令
uint8_t rx_data[2] = {0};
struct spi_message msg;
struct spi_transfer xfer[2] = {
{
.tx_buf = &cmd,
.len = 1,
},
{
.rx_buf = rx_data,
.len = 2,
},
};
spi_message_init(&msg);
spi_message_add_tail(&xfer[0], &msg);
spi_message_add_tail(&xfer[1], &msg);
spi_sync(spi_dev, &msg);
// 解析温度值
int16_t temp = (rx_data[0] << 8) | rx_data[1];
用 spi_write_then_read() 改写,就简洁多了:
uint8_t cmd = 0x10;
uint8_t rx_data[2] = {0};
spi_write_then_read(spi_dev, &cmd, 1, rx_data, 2);
嗯,是不是清爽很多?但记住,简洁不等于万能。如果你需要精细控制片选、延时、DMA等,还是得用message/transfer的方式。
4.5 性能优化小贴士
SPI传输的性能瓶颈,通常不在协议本身,而在软件开销上。我总结了几点:
- 减少message构造次数:能一次传完的数据,别拆成多次。每次spi_sync都有上下文切换和锁的开销。
- 使用DMA:如果控制器支持DMA,尽量用。CPU不用逐字节搬运,效率提升明显。
- 预分配缓冲区:不要在传输路径上动态分配内存。用静态或预分配的DMA缓冲区。
- 批量传输:对于大量数据,考虑用更大的transfer长度,减少传输次数。
核心观点:SPI驱动开发,说白了就是三件事——配参数、发数据、收数据。但要把这三件事做好,你得理解控制器、设备和数据流。框架是死的,硬件是活的,经验是慢慢攒的。
好了,SPI驱动开发的核心内容就这些。从控制器框架到设备驱动,从数据传输流程到API使用,每一步都有坑,但也都有解法。多写、多测、多看芯片手册,慢慢就熟了。
写在最后:SPI驱动开发,说难不难,说简单也不简单。关键是理解数据流——从用户空间到内核,从内核到控制器,从控制器到外设,每一步都有它的规矩。把这些规矩摸透了,写驱动就跟搭积木一样。