4. Linux SPI 子系统架构:从硬件到驱动的分层设计
SPI 总线在嵌入式系统里太常见了——传感器、显示屏、ADC、Flash 芯片,几乎都离不开它。但很多初学者一上来就对着 SPI 控制器寄存器猛写,结果换个平台代码全废。我个人习惯是先把 Linux SPI 子系统的分层架构搞清楚,这样不管换什么芯片,驱动框架都是一套思路。
Linux SPI 子系统分为三层:SPI 核心层、SPI 控制器驱动层、SPI 设备驱动层。说白了,核心层负责搭桥,控制器驱动管硬件,设备驱动管业务逻辑。下面我带你一层层拆开看。
4.1 SPI 核心层:数据结构与注册机制
核心层是 SPI 子系统的“交通枢纽”。它定义了几个关键数据结构:spi_master、spi_device、spi_message。我刚开始学的时候,这三个结构体傻傻分不清,后来在项目里调一个 SPI Flash 驱动,才真正理解它们各自扮演的角色。
4.1.1 spi_master:控制器的抽象
spi_master 代表一个 SPI 控制器硬件。每个 SPI 控制器(比如 SoC 里的 SPI0、SPI1)在系统中对应一个 spi_master 实例。它包含总线号、时钟频率、传输模式、DMA 配置等信息。
struct spi_master {
struct device dev;
s16 bus_num; // 总线编号
u16 num_chipselect; // 片选数量
u32 min_speed_hz; // 最小时钟频率
u32 max_speed_hz; // 最大时钟频率
u16 mode_bits; // 支持的 SPI 模式位
int (*transfer_one)(struct spi_master *master,
struct spi_device *spi,
struct spi_transfer *transfer);
// ... 其他成员
};
注册一个 spi_master 的流程很简单:分配结构体 → 设置回调函数 → 调用 spi_register_master()。我在项目中遇到过一个问题:忘记设置 mode_bits 导致设备驱动无法配置 SPI 模式,排查了半天才发现是这里漏了。
4.1.2 spi_device:从设备的抽象
spi_device 代表挂在 SPI 总线上的一个从设备。它绑定到某个 spi_master,包含片选引脚、最大频率、SPI 模式(CPOL/CPHA)、字长等参数。设备驱动通过 spi_device 来发起传输。
struct spi_device {
struct device dev;
struct spi_master *master; // 所属控制器
u32 max_speed_hz;
u8 chip_select; // 片选编号
u8 mode; // SPI 模式
u8 bits_per_word;
// ... 其他成员
};
你想想看,spi_device 是怎么创建的?有两种方式:一是通过设备树静态创建,二是通过 spi_new_device() 动态创建。我个人更推荐设备树方式,后面会详细讲。
4.1.3 spi_message:传输的载体
spi_message 是 SPI 传输的基本单元。它包含一个或多个 spi_transfer 段,每个段可以独立设置发送缓冲区、接收缓冲区、长度、延时等。一次 spi_message 传输过程中,片选信号会一直保持有效——这个特性在操作 Flash 芯片时特别重要。
struct spi_message {
struct list_head transfers; // transfer 链表
struct spi_device *spi; // 目标设备
unsigned is_dma_mapped:1;
void (*complete)(void *context);
void *context;
// ... 其他成员
};
spi_message 里只放了一个 spi_transfer,而触摸屏要求连续读写。解决办法是把多个 transfer 放到同一个 message 里,片选就不会被拉高了。
4.2 SPI 控制器驱动层:master_ops 回调函数
控制器驱动层负责和硬件寄存器打交道。它通过实现 spi_master 中的回调函数来提供底层传输能力。核心回调是 transfer_one 和 transfer_one_message。
| 回调函数 | 作用 | 适用场景 |
|---|---|---|
transfer_one |
传输单个 transfer 段 | 简单控制器,每次只处理一段 |
transfer_one_message |
传输整个 message(含多个 transfer) | 需要保持片选连续的复杂设备 |
setup |
配置 SPI 设备参数(模式、频率等) | 每次设备参数变更时调用 |
cleanup |
释放设备相关资源 | 设备移除时调用 |
写控制器驱动时,我建议你先实现 transfer_one,因为逻辑简单、容易调试。等跑通了再考虑优化成 transfer_one_message 来提升性能。嗯,这里要注意:transfer_one 返回后,核心层会自动处理片选信号;而 transfer_one_message 需要你自己管理片选。
4.3 SPI 设备驱动层:spi_driver 结构
设备驱动层才是我们平时写的最多的部分。它通过 spi_driver 结构体注册到 SPI 子系统,核心层会在匹配到设备时调用你的 probe 函数。
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_spi_device",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
};
module_spi_driver(my_spi_driver);
在 probe 函数里,你通常会做这几件事:
- 从
spi_device获取设备参数(频率、模式等) - 设置 SPI 设备参数(调用
spi_setup()) - 分配私有数据结构
- 注册字符设备或输入设备等
spi_setup(),结果设备一直以默认模式工作,频率也不对。后来加上这行代码,问题就解决了。记住:spi_setup() 会触发控制器驱动的 setup 回调,把参数真正写到硬件寄存器里。
4.4 设备树中 SPI 节点配置
设备树是描述硬件拓扑的标准方式。SPI 控制器和设备都在设备树里声明,内核在启动时解析设备树,自动创建 spi_master 和 spi_device。
一个典型的 SPI 设备树节点长这样:
&spi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = &spi0_pins;
cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
spidev@0 {
compatible = "my,spi-device";
reg = <0>; // 片选编号
spi-max-frequency = <10000000>; // 10MHz
spi-cpol; // 时钟极性 = 1
spi-cpha; // 时钟相位 = 1
spi-cs-high; // 片选高电平有效(可选)
};
};
关键属性说明:
reg:片选编号,从 0 开始spi-max-frequency:最大时钟频率,单位 Hzspi-cpol/spi-cpha:设置 SPI 模式(CPOL=1, CPHA=1 对应模式 3)cs-gpios:控制器级片选 GPIO,用于非标准片选引脚
4.5 架构总览:一张图看懂 SPI 子系统
下面这张图展示了 SPI 子系统的三层架构和数据流向。你可以看到,设备驱动通过 spi_message 发起传输,核心层调度到控制器驱动,最终操作硬件寄存器。
从这张图可以清楚看到:设备驱动只和核心层的数据结构打交道,控制器驱动负责硬件细节。这种分层设计的好处是——你写一个 SPI 温度传感器驱动,不管它挂在哪个 SoC 上,驱动代码几乎不用改。我在项目中从 i.MX6 移植到 STM32MP1,设备驱动只改了设备树 compatible 字符串,其他代码一行没动。
4.6 本章小结
SPI 子系统的三层架构是理解 Linux SPI 驱动的基石。记住几个关键点:
- 核心层提供
spi_master、spi_device、spi_message三个核心结构体 - 控制器驱动实现
transfer_one等回调,操作硬件寄存器 - 设备驱动通过
spi_driver注册,在probe中初始化设备 - 设备树描述硬件拓扑,内核自动解析创建设备实例