ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DAYS 56【Linux 驱动深度】SPI 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战

DAYS 56【Linux 驱动深度】SPI 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战

前言

在嵌入式 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. 传输流程

  1. 驱动创建spi_message
  2. 构造一个或多个spi_transfer,加入 message 的链表
  3. 调用spi_async(异步)或spi_sync(同步)提交 message
  4. 内核将 message 加入队列,由控制器驱动按序执行
  5. 执行完成后调用 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. 代码要点说明

  1. module_spi_driver:内核提供的宏,简化了驱动注册注销的样板代码。
  2. spi_setup:将设备的 SPI 模式、位宽、速率等参数下发给控制器,必须在通信前调用。
  3. spi_write/spi_read:便捷同步 API,内部封装了 spi_message。
  4. spi_sync_transfer:同步执行自定义 transfer 数组,适合收发一体的场景。
  5. 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_tailtransfer 加入 message构造传输序列

六、开发调试常见踩坑

  1. 设备树匹配失败
    • 检查compatible字符串与驱动 id_table 是否完全一致
    • 确认 SPI 控制器节点 status 为 "okay"
    • 检查片选号reg是否在控制器支持范围内
  2. 数据全 0 / 全乱码
    • 优先检查 SPI 模式(CPOL/CPHA)是否与外设一致
    • 确认时钟频率是否超过外设最大值
    • 检查 bits_per_word 配置
  3. 读数据不对
    • 检查读操作的地址位是否正确(通常最高位为读标志)
    • 确认哑元字节长度是否正确,SPI 读数据在第二个字节开始有效
    • 注意大小端和字节序
  4. 传输卡顿、性能低
    • 优先使用 DMA 模式的控制器驱动
    • 批量数据尽量合并为一个 message,减少多次提交开销
    • 高频场景考虑异步 API,避免阻塞线程

结语

Linux SPI 子系统是 Linux 驱动中设计非常经典的子系统,分层架构清晰地分离了硬件与协议。作为驱动开发者,理解spi_master、spi_device、spi_driver三者的关系,掌握spi_message和spi_transfer的传输模型,就能以不变应万变,快速开发各类 SPI 设备驱动。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进