前言
在嵌入式 Linux 驱动开发中,SPI 是最常用的外设总线之一,SPI Flash、OLED、ADC、触摸屏、以太网 PHY 等大量外设都通过 SPI 接口与主控通信。Linux 内核为了统一管理 SPI 设备,抽象出了一套完整的 SPI 子系统框架。
很多初学者写 SPI 驱动,只会照搬例程调用spi_write、spi_read,却不理解内核 SPI 子系统的分层架构、核心数据结构之间的关系,遇到问题很难排查。本文就从架构层面入手,系统拆解 Linux SPI 子系统,再结合完整代码示例,讲解 SPI 设备驱动的标准开发流程。
一、SPI 子系统整体架构
Linux SPI 子系统采用典型的分层架构,分为两层驱动、两类设备,职责清晰,解耦硬件与协议。
1. 两层驱动模型
表格
| 驱动类型 | 别称 | 职责 | 代表 |
|---|---|---|---|
| 控制器驱动 | Master 驱动 | 抽象 SPI 主机硬件,提供时钟、片选、数据移位的底层能力,向上提供统一操作接口 | spi-imx.c(i.MX 系列)、spi-bcm2835.c(树莓派) |
| 协议驱动 | 设备驱动 | 针对具体 SPI 外设,实现业务逻辑,通过子系统 API 与外设通信 | W25Qxx Flash 驱动、ICM20608 加速度计驱动 |
简单理解:控制器驱动管硬件时序,协议驱动管业务逻辑。控制器驱动一般由芯片厂商提供,驱动开发者主要工作是编写协议驱动(设备驱动)。
2. 三大核心数据结构
SPI 子系统围绕三个核心结构体运转:
struct spi_master:代表一个 SPI 主机控制器,管理一条 SPI 总线struct spi_device:代表总线上一个具体的 SPI 从设备struct spi_driver:代表 SPI 设备驱动,对应具体外设的驱动逻辑
三者的关系:一个spi_master下可以挂载多个spi_device;spi_driver通过匹配表与spi_device绑定,绑定成功后调用 probe 函数初始化设备。
二、核心数据结构详解
1. spi_master
spi_master是 SPI 控制器的抽象,描述了总线的硬件能力。
struct spi_master { struct device dev; s16 bus_num; // 总线编号 u16 num_chipselect; // 支持的片选数量 u32 min_speed_hz; // 最低时钟频率 u32 max_speed_hz; // 最高时钟频率 // 控制器驱动实现的底层传输方法 int (*transfer)(struct spi_master *master, struct spi_message *msg); int (*setup)(struct spi_device *spi); // ... };控制器驱动的核心工作就是分配、初始化spi_master,实现transfer等回调,最后调用spi_register_master注册到内核。
2. spi_device
spi_device是 SPI 从设备的抽象,每个从设备对应一个实例,保存了设备的硬件参数。
struct spi_device { struct device dev; struct spi_master *master; // 所属的主机控制器 u32 max_speed_hz; // 设备支持的最大速率 u8 chip_select; // 片选号 u8 mode; // SPI模式(CPOL/CPHA) u8 bits_per_word; // 数据位宽 // 设备地址相关 u8 irq; // ... };spi_device不是驱动手动创建的,而是内核根据设备树(或板级信息)自动创建的。
3. spi_driver
spi_driver是设备驱动的抽象,遵循 Linux 标准驱动模型。
struct spi_driver { const struct spi_device_id *id_table; // 设备匹配表 int (*probe)(struct spi_device *spi); int (*remove)(struct spi_device *spi); struct device_driver driver; };驱动开发者的核心工作就是填充这个结构体,实现 probe、remove 等函数,通过spi_register_driver注册。
三、数据传输模型:spi_transfer 与 spi_message
SPI 子系统的数据传输不是直接发字节,而是基于消息队列模型,由spi_transfer和spi_message两个结构组成。
1. spi_transfer:单次传输
spi_transfer描述一次独立的 SPI 传输,包含发送缓冲区、接收缓冲区、长度等信息。
struct spi_transfer { const void *tx_buf; // 发送缓冲区 void *rx_buf; // 接收缓冲区 unsigned len; // 传输长度 u8 bits_per_word; u32 speed_hz; // 本次传输的时钟速率 u16 delay_usecs; // 传输后延时 bool cs_change; // 传输后是否改变片选状态 struct list_head transfer_list; // 链表节点 };全双工特性:一次 transfer 中,tx_buf 发送数据的同时,rx_buf 会同步接收数据,这是 SPI 全双工本质的体现。
2. spi_message:原子消息
spi_message是一个或多个spi_transfer的集合,代表一次原子操作。一个 message 中的所有 transfer 会连续执行,中间不会被其他消息打断。
struct spi_message { struct list_head transfers; // transfer链表头 struct spi_device *spi; // 目标设备 void (*complete)(void *context); // 完成回调 void *context; // 队列节点 struct list_head queue; // ... };3. 传输流程
- 驱动创建
spi_message - 构造一个或多个
spi_transfer,加入 message 的链表 - 调用
spi_async(异步)或spi_sync(同步)提交 message - 内核将 message 加入队列,由控制器驱动按序执行
- 执行完成后调用 complete 回调(异步)或函数返回(同步)
我们常用的
spi_write、spi_read、spi_write_then_read等 API,本质都是封装了 spi_message 的同步便捷函数。
四、SPI 设备驱动完整实战
下面以一个虚拟 SPI 传感器为例,完整演示 SPI 设备驱动的编写流程。
1. 设备树配置
首先在设备树中 SPI 控制器节点下添加设备节点:
&ecspi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi1>; sensor@0 { compatible = "myboard,spi-sensor"; reg = <0>; // 片选号0 spi-max-frequency = <10000000>; // 最大10MHz spi-cpol; // CPOL=1 spi-cpha; // CPHA=1,即模式3 interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; };2. 驱动代码实现
#include <linux/module.h> #include <linux/spi/spi.h> #include <linux/interrupt.h> /* 设备ID匹配表 */ static const struct spi_device_id spi_sensor_id[] = { {"spi-sensor", 0}, {} }; MODULE_DEVICE_TABLE(spi, spi_sensor_id); /* 设备私有数据结构 */ struct sensor_priv { struct spi_device *spi; int irq; /* 其他业务数据 */ }; /* * @brief 读取传感器寄存器 * @param priv 私有数据 * @param reg 寄存器地址 * @retval 读取到的值 */ static u8 sensor_read_reg(struct sensor_priv *priv, u8 reg) { u8 tx_buf[2]; u8 rx_buf[2]; struct spi_transfer t = { .tx_buf = tx_buf, .rx_buf = rx_buf, .len = 2, }; /* 地址最高位置1表示读 */ tx_buf[0] = reg | 0x80; tx_buf[1] = 0xFF; // 哑元 spi_sync_transfer(priv->spi, &t, 1); return rx_buf[1]; } /* * @brief 写入传感器寄存器 * @param priv 私有数据 * @param reg 寄存器地址 * @param val 待写入值 */ static void sensor_write_reg(struct sensor_priv *priv, u8 reg, u8 val) { u8 tx_buf[2] = {reg & 0x7F, val}; spi_write(priv->spi, tx_buf, sizeof(tx_buf)); } /* 中断处理函数 */ static irqreturn_t sensor_irq_handler(int irq, void *dev_id) { struct sensor_priv *priv = dev_id; /* 处理中断,读取数据 */ return IRQ_HANDLED; } /* probe函数:设备匹配成功后执行 */ static int spi_sensor_probe(struct spi_device *spi) { struct sensor_priv *priv; int ret; /* 1. 分配私有数据 */ priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->spi = spi; spi_set_drvdata(spi, priv); /* 2. 配置SPI设备参数 */ spi->mode = SPI_MODE_3; spi->bits_per_word = 8; ret = spi_setup(spi); if (ret) return ret; /* 3. 获取中断号 */ priv->irq = spi->irq; ret = devm_request_irq(&spi->dev, priv->irq, sensor_irq_handler, IRQF_TRIGGER_FALLING, "spi_sensor", priv); if (ret) return ret; /* 4. 初始化传感器硬件 */ sensor_write_reg(priv, 0x01, 0x80); // 启动测量 dev_info(&spi->dev, "SPI sensor probe success\n"); return 0; } /* remove函数:设备移除时执行 */ static int spi_sensor_remove(struct spi_device *spi) { struct sensor_priv *priv = spi_get_drvdata(spi); /* 硬件复位、释放资源 */ sensor_write_reg(priv, 0x01, 0x00); return 0; } static struct spi_driver spi_sensor_driver = { .driver = { .name = "spi_sensor", .owner = THIS_MODULE, }, .probe = spi_sensor_probe, .remove = spi_sensor_remove, .id_table = spi_sensor_id, }; module_spi_driver(spi_sensor_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Developer"); MODULE_DESCRIPTION("SPI sensor device driver");3. 代码要点说明
module_spi_driver:内核提供的宏,简化了驱动注册注销的样板代码。spi_setup:将设备的 SPI 模式、位宽、速率等参数下发给控制器,必须在通信前调用。spi_write/spi_read:便捷同步 API,内部封装了 spi_message。spi_sync_transfer:同步执行自定义 transfer 数组,适合收发一体的场景。devm_*系列函数:托管资源分配,remove 时自动释放,减少内存泄漏。
五、常用 API 速查
表格
| API 函数 | 功能 | 场景 |
|---|---|---|
spi_setup | 配置 SPI 设备参数 | probe 中初始化设备 |
spi_write | 同步写数据 | 单纯发送数据 |
spi_read | 同步读数据 | 单纯接收数据 |
spi_write_then_read | 先写后读 | 写地址 + 读数据,最常用 |
spi_sync | 同步执行 message | 复杂多段传输 |
spi_async | 异步执行 message | 高并发、驱动中断上下文 |
spi_message_init | 初始化 message | 构造自定义传输 |
spi_message_add_tail | transfer 加入 message | 构造传输序列 |
六、开发调试常见踩坑
- 设备树匹配失败
- 检查
compatible字符串与驱动 id_table 是否完全一致 - 确认 SPI 控制器节点 status 为 "okay"
- 检查片选号
reg是否在控制器支持范围内
- 检查
- 数据全 0 / 全乱码
- 优先检查 SPI 模式(CPOL/CPHA)是否与外设一致
- 确认时钟频率是否超过外设最大值
- 检查 bits_per_word 配置
- 读数据不对
- 检查读操作的地址位是否正确(通常最高位为读标志)
- 确认哑元字节长度是否正确,SPI 读数据在第二个字节开始有效
- 注意大小端和字节序
- 传输卡顿、性能低
- 优先使用 DMA 模式的控制器驱动
- 批量数据尽量合并为一个 message,减少多次提交开销
- 高频场景考虑异步 API,避免阻塞线程
结语
Linux SPI 子系统是 Linux 驱动中设计非常经典的子系统,分层架构清晰地分离了硬件与协议。作为驱动开发者,理解spi_master、spi_device、spi_driver三者的关系,掌握spi_message和spi_transfer的传输模型,就能以不变应万变,快速开发各类 SPI 设备驱动。