4、Linux SPI子系统架构:三层结构深度解析

SPI子系统,说白了就是Linux内核为了统一管理SPI总线而设计的一套框架。我最早接触它的时候,觉得这东西挺绕的——明明就是发几个字节数据,干嘛要搞这么复杂?后来在项目里踩了几次坑,才明白这套分层设计有多重要。

Linux SPI子系统分为三层:SPI核心层SPI控制器驱动SPI设备驱动。这三层各司其职,又互相配合。咱们一层一层来看。

Linux SPI子系统三层架构 用户空间应用程序 SPI设备驱动层 spi_driver / spi_device SPI核心层 spi.c / spi.h / 总线管理 / 数据传输 SPI控制器驱动层 spi_master / 硬件寄存器操作 SPI控制器硬件(SoC内部) 软件层 硬件层

4.1 SPI核心层——承上启下的枢纽

SPI核心层位于drivers/spi/spi.c中。它不直接操作硬件,而是提供了一套标准接口。我个人习惯把核心层看作一个「调度中心」——它负责管理总线、匹配设备和驱动、提供数据传输的通用API。

核心层主要干了这几件事:

  • 总线注册与管理:维护系统中的所有SPI总线
  • 设备与驱动的匹配:通过设备树或板级信息完成绑定
  • 数据传输框架:提供spi_sync()spi_async()等接口
  • 消息队列管理:处理多个SPI传输请求的排队

核心数据结构struct spi_controller(老版本叫spi_master)是整个子系统的灵魂。每个SPI控制器对应一个这样的结构体。里面包含了传输函数指针、时钟频率、模式设置等关键信息。

4.2 SPI控制器驱动——和硬件打交道的人

这一层负责直接操作SPI控制器的寄存器。说白了,它就是硬件和核心层之间的翻译官。

控制器驱动需要实现的核心回调函数:

回调函数 作用 我在项目中遇到的坑
transfer_one_message 传输一条完整的SPI消息 DMA传输时要注意对齐问题
transfer_one 传输单个SPI transfer 片选信号时序容易搞错
set_cs 控制片选信号电平 有些控制器片选是硬件自动控制的
prepare_message 传输前的准备工作 记得检查时钟极性设置

⚠️ 注意:我曾经在一个项目中,因为没处理好transfer_one的原子性,导致并发传输时数据错乱。后来加了自旋锁才解决。所以,如果你的控制器支持多队列,一定要考虑并发安全。

控制器驱动的注册流程大致是这样的:

// 伪代码示例
static struct spi_controller *spi;

spi = spi_alloc_master(dev, sizeof(struct my_spi_data));
spi->bus_num = 1;           // 总线编号
spi->num_chipselect = 2;    // 支持的片选数量
spi->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
spi->transfer_one = my_spi_transfer_one;
spi->set_cs = my_spi_set_cs;

spi_register_controller(spi);  // 注册到核心层

4.3 SPI设备驱动——面向外设的接口

这一层是离应用最近的一层。它不关心SPI控制器怎么工作的,只关心怎么和挂载在SPI总线上的外设通信——比如ADC、传感器、显示屏等。

设备驱动的核心是struct spi_driver结构体:

static struct spi_driver my_spi_device_driver = {
    .driver = {
        .name = "my_spi_sensor",
        .of_match_table = my_spi_of_match,
    },
    .probe = my_spi_probe,
    .remove = my_spi_remove,
    .id_table = my_spi_id_table,
};

module_spi_driver(my_spi_device_driver);

probe函数里,你会拿到一个struct spi_device指针。通过它,你可以设置通信参数:

static int my_spi_probe(struct spi_device *spi)
{
    // 设置通信参数
    spi->mode = SPI_MODE_0;        // 模式0:CPOL=0, CPHA=0
    spi->max_speed_hz = 1000000;   // 1MHz
    spi->bits_per_word = 8;        // 8位数据宽度
    spi_setup(spi);                 // 应用设置
    
    // 开始数据传输
    // ...
}

💡 经验之谈:我建议你在probe函数里先调用spi_setup(),再去做其他初始化。因为有些控制器在setup时才会真正配置硬件寄存器。顺序搞反了,数据可能就发不出去。

4.4 三层之间的数据流

这三层是怎么配合工作的?我画个流程图你就明白了:

应用层调用 write()/read()
        ↓
SPI设备驱动 (spi_driver)
  调用 spi_sync() / spi_async()
        ↓
SPI核心层 (spi.c)
  消息排队、拆分、调度
        ↓
SPI控制器驱动 (spi_master)
  操作硬件寄存器,发送/接收数据
        ↓
SPI控制器硬件
  通过MOSI/MISO/SCLK/CS与外部设备通信

嗯,这里要注意一点:spi_sync()是同步接口,会阻塞当前进程直到传输完成。spi_async()是异步接口,通过回调通知结果。我在做高速数据采集时,一般用异步方式,避免阻塞影响实时性。

4.5 设备树中的SPI节点

现在大部分平台都用设备树来描述SPI设备。一个典型的SPI设备节点长这样:

&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_pins>;
    
    sensor@0 {
        compatible = "mycompany,my-sensor";
        reg = <0>;              // 片选索引
        spi-max-frequency = <5000000>;
        spi-cpol;               // 时钟极性
        spi-cpha;               // 时钟相位
    };
    
    display@1 {
        compatible = "mycompany,my-display";
        reg = <1>;              // 第二个片选
        spi-max-frequency = <20000000>;
    };
};

设备树解析时,核心层会根据compatible属性匹配对应的驱动。匹配成功后,调用驱动的probe函数,并把spi_device传进去。

🔑 关键点reg属性对应的是片选号。比如reg = <0>表示这个设备连接在控制器的第0个片选上。我曾经见过有人把片选号写错了,结果两个设备同时被选中,总线冲突,数据全乱套了。

4.6 小结

SPI子系统的三层架构,说白了就是「分层解耦」的思想在嵌入式领域的体现。核心层做调度,控制器驱动管硬件,设备驱动管协议。每一层只关心自己的事,互不干扰。

你在移植SPI驱动时,先搞清楚你要改哪一层:

  • 换了一个新的SPI外设?→ 写设备驱动
  • 换了一个新的SoC?→ 写控制器驱动
  • 想改传输策略?→ 看核心层

这样思路就清晰多了。下一节咱们聊聊具体的移植实战,我会拿一个真实的项目案例来讲。


公众号:蓝海资料掘金营,微信deep3321