前言
在嵌入式 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 设备驱动。