4. Linux SPI 子系统架构:从硬件到驱动的分层设计

SPI 总线在嵌入式系统里太常见了——传感器、显示屏、ADC、Flash 芯片,几乎都离不开它。但很多初学者一上来就对着 SPI 控制器寄存器猛写,结果换个平台代码全废。我个人习惯是先把 Linux SPI 子系统的分层架构搞清楚,这样不管换什么芯片,驱动框架都是一套思路。

Linux SPI 子系统分为三层: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 触摸屏驱动时,发现每次传输后片选信号就释放了,导致触摸屏不响应。后来发现是 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 函数里,你通常会做这几件事:

  1. 从 spi_device 获取设备参数(频率、模式等)
  2. 设置 SPI 设备参数(调用 spi_setup())
  3. 分配私有数据结构
  4. 注册字符设备或输入设备等
避坑指南:我曾经在 probe 里忘记调用 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:最大时钟频率,单位 Hz
  • spi-cpol / spi-cpha:设置 SPI 模式(CPOL=1, CPHA=1 对应模式 3)
  • cs-gpios:控制器级片选 GPIO,用于非标准片选引脚
小技巧:如果你不确定 SPI 模式怎么配,可以先用逻辑分析仪抓一下从设备的时序图。我一般会先看空闲时时钟电平(CPOL),再看数据采样边沿(CPHA),两个一组合就出来了。

4.5 架构总览:一张图看懂 SPI 子系统

下面这张图展示了 SPI 子系统的三层架构和数据流向。你可以看到,设备驱动通过 spi_message 发起传输,核心层调度到控制器驱动,最终操作硬件寄存器。

Linux SPI 子系统三层架构 SPI 设备驱动层 spi_driver → probe() → 注册字符/输入/网络设备 调用 spi_write() / spi_read() / spi_sync() 发起传输 SPI 核心层 spi_master(控制器抽象) spi_device(从设备抽象) spi_message / spi_transfer(传输载体) SPI 控制器驱动层 实现 transfer_one / transfer_one_message 回调 操作 SPI 控制器寄存器(FIFO、DMA、中断) SPI 控制器硬件(寄存器级) 设备驱动不直接 操作硬件寄存器 通过核心层中转

从这张图可以清楚看到:设备驱动只和核心层的数据结构打交道,控制器驱动负责硬件细节。这种分层设计的好处是——你写一个 SPI 温度传感器驱动,不管它挂在哪个 SoC 上,驱动代码几乎不用改。我在项目中从 i.MX6 移植到 STM32MP1,设备驱动只改了设备树 compatible 字符串,其他代码一行没动。

4.6 本章小结

SPI 子系统的三层架构是理解 Linux SPI 驱动的基石。记住几个关键点:

  • 核心层提供 spi_master、spi_device、spi_message 三个核心结构体
  • 控制器驱动实现 transfer_one 等回调,操作硬件寄存器
  • 设备驱动通过 spi_driver 注册,在 probe 中初始化设备
  • 设备树描述硬件拓扑,内核自动解析创建设备实例