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_tail transfer 加入 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 设备驱动。

相关推荐
听雨笑1 小时前
终端窗口关掉了,正在跑的活会不会死:三种起法各自的下场
linux
m0_587383001 小时前
无人自助拼豆系统开发实战指南 核心功能架构搭建与落地经验分享
java·经验分享·spring·小程序·架构·需求分析
水圈模拟1 小时前
从"数字花瓶"到"数字实验场"
人工智能·架构
星云低代码开发平台1 小时前
企业 AI 连接架构:iPaaS、网关、RPA、MCP 与连接器边界
人工智能·架构·rpa·企业ai
nagualky1231 小时前
GPT-4 系列退役在即:一套可回滚的模型迁移验收法
java·开发语言·ai·架构
程序员-Benothing1 小时前
Linux CPU性能分析:top、mpstat、pidstat、perf实战
linux·运维·服务器
做个文艺程序员1 小时前
MQ第10篇:RAG与消息队列实战:Java构建AI客服系统的检索增强生成完整架构
人工智能·架构·rocketmq·向量数据库·rag·ai客服·spring ai
小诗懂技术2 小时前
Linux设备驱动并发控制总结_CSDN
linux·运维·服务器
cheems95272 小时前
从统一响应到 Swagger:一篇讲清微服务接口文档设计未命名
java·微服务·架构·状态模式