4、Linux SPI子系统架构:三层结构深度解析
SPI子系统,说白了就是Linux内核为了统一管理SPI总线而设计的一套框架。我最早接触它的时候,觉得这东西挺绕的——明明就是发几个字节数据,干嘛要搞这么复杂?后来在项目里踩了几次坑,才明白这套分层设计有多重要。
Linux SPI子系统分为三层:SPI核心层、SPI控制器驱动、SPI设备驱动。这三层各司其职,又互相配合。咱们一层一层来看。
4.1 SPI核心层——承上启下的枢纽
SPI核心层位于drivers/spi/spi.c中。它不直接操作硬件,而是提供了一套标准接口。我个人习惯把核心层看作一个「调度中心」——它负责管理总线、匹配设备和驱动、提供数据传输的通用API。
核心层主要干了这几件事:
- 总线注册与管理:维护系统中的所有SPI总线
- 设备与驱动的匹配:通过设备树或板级信息完成绑定
- 数据传输框架:提供
spi_sync()、spi_async()等接口 - 消息队列管理:处理多个SPI传输请求的排队
核心数据结构:struct spi_controller(老版本叫spi_master)是整个子系统的灵魂。每个SPI控制器对应一个这样的结构体。里面包含了传输函数指针、时钟频率、模式设置等关键信息。
4.2 SPI控制器驱动——和硬件打交道的人
这一层负责直接操作SPI控制器的寄存器。说白了,它就是硬件和核心层之间的翻译官。
控制器驱动需要实现的核心回调函数:
| 回调函数 | 作用 | 我在项目中遇到的坑 |
|---|---|---|
transfer_one_message |
传输一条完整的SPI消息 | DMA传输时要注意对齐问题 |
transfer_one |
传输单个SPI transfer | 片选信号时序容易搞错 |
set_cs |
控制片选信号电平 | 有些控制器片选是硬件自动控制的 |
prepare_message |
传输前的准备工作 | 记得检查时钟极性设置 |
⚠️ 注意:我曾经在一个项目中,因为没处理好transfer_one的原子性,导致并发传输时数据错乱。后来加了自旋锁才解决。所以,如果你的控制器支持多队列,一定要考虑并发安全。
控制器驱动的注册流程大致是这样的:
// 伪代码示例
static struct spi_controller *spi;
spi = spi_alloc_master(dev, sizeof(struct my_spi_data));
spi->bus_num = 1; // 总线编号
spi->num_chipselect = 2; // 支持的片选数量
spi->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
spi->transfer_one = my_spi_transfer_one;
spi->set_cs = my_spi_set_cs;
spi_register_controller(spi); // 注册到核心层
4.3 SPI设备驱动——面向外设的接口
这一层是离应用最近的一层。它不关心SPI控制器怎么工作的,只关心怎么和挂载在SPI总线上的外设通信——比如ADC、传感器、显示屏等。
设备驱动的核心是struct spi_driver结构体:
static struct spi_driver my_spi_device_driver = {
.driver = {
.name = "my_spi_sensor",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
.id_table = my_spi_id_table,
};
module_spi_driver(my_spi_device_driver);
在probe函数里,你会拿到一个struct spi_device指针。通过它,你可以设置通信参数:
static int my_spi_probe(struct spi_device *spi)
{
// 设置通信参数
spi->mode = SPI_MODE_0; // 模式0:CPOL=0, CPHA=0
spi->max_speed_hz = 1000000; // 1MHz
spi->bits_per_word = 8; // 8位数据宽度
spi_setup(spi); // 应用设置
// 开始数据传输
// ...
}
💡 经验之谈:我建议你在probe函数里先调用spi_setup(),再去做其他初始化。因为有些控制器在setup时才会真正配置硬件寄存器。顺序搞反了,数据可能就发不出去。
4.4 三层之间的数据流
这三层是怎么配合工作的?我画个流程图你就明白了:
应用层调用 write()/read()
↓
SPI设备驱动 (spi_driver)
调用 spi_sync() / spi_async()
↓
SPI核心层 (spi.c)
消息排队、拆分、调度
↓
SPI控制器驱动 (spi_master)
操作硬件寄存器,发送/接收数据
↓
SPI控制器硬件
通过MOSI/MISO/SCLK/CS与外部设备通信
嗯,这里要注意一点:spi_sync()是同步接口,会阻塞当前进程直到传输完成。spi_async()是异步接口,通过回调通知结果。我在做高速数据采集时,一般用异步方式,避免阻塞影响实时性。
4.5 设备树中的SPI节点
现在大部分平台都用设备树来描述SPI设备。一个典型的SPI设备节点长这样:
&spi1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi1_pins>;
sensor@0 {
compatible = "mycompany,my-sensor";
reg = <0>; // 片选索引
spi-max-frequency = <5000000>;
spi-cpol; // 时钟极性
spi-cpha; // 时钟相位
};
display@1 {
compatible = "mycompany,my-display";
reg = <1>; // 第二个片选
spi-max-frequency = <20000000>;
};
};
设备树解析时,核心层会根据compatible属性匹配对应的驱动。匹配成功后,调用驱动的probe函数,并把spi_device传进去。
🔑 关键点:reg属性对应的是片选号。比如reg = <0>表示这个设备连接在控制器的第0个片选上。我曾经见过有人把片选号写错了,结果两个设备同时被选中,总线冲突,数据全乱套了。
4.6 小结
SPI子系统的三层架构,说白了就是「分层解耦」的思想在嵌入式领域的体现。核心层做调度,控制器驱动管硬件,设备驱动管协议。每一层只关心自己的事,互不干扰。
你在移植SPI驱动时,先搞清楚你要改哪一层:
- 换了一个新的SPI外设?→ 写设备驱动
- 换了一个新的SoC?→ 写控制器驱动
- 想改传输策略?→ 看核心层
这样思路就清晰多了。下一节咱们聊聊具体的移植实战,我会拿一个真实的项目案例来讲。
公众号:蓝海资料掘金营,微信deep3321