4. Linux SPI子系统架构:三层架构与核心数据结构
SPI子系统在Linux内核里,说白了就是一套分层设计。我刚开始接触时也觉得抽象,但实际写过几个驱动后就明白了——这种分层其实是为了让我们各司其职,不用重复造轮子。
Linux SPI子系统分为三层:SPI核心层、控制器驱动层、设备驱动层。这三层各管一摊,通过几个关键数据结构串联起来。
4.1 三层架构总览
先看一张整体架构图,后面我再逐个拆解。
从上往下看,数据流向很清晰。核心层在中间当调度员,控制器驱动管硬件,设备驱动管协议。我当年做第一个SPI驱动时,就是没搞懂这个分层,把硬件操作和协议解析写在一起,后来维护起来简直想哭。
4.2 SPI核心层——承上启下的枢纽
核心层不直接操作硬件,它提供的是框架和工具。说白了,就是帮上下两层把脏活累活干了。
核心层主要干这几件事:
- 总线注册与注销:维护SPI总线,管理设备与驱动的匹配
- 数据结构管理:分配、初始化、销毁spi_master、spi_device等对象
- 传输调度:把上层发来的spi_message排队,交给控制器驱动执行
- 通用API:提供spi_sync()、spi_async()、spi_write_then_read()等接口
核心层的关键文件:drivers/spi/spi.c,这里面实现了大部分核心逻辑。我建议你有空翻翻这个文件,比看十篇博客都管用。
4.3 控制器驱动层——直面硬件的实干家
这一层负责和SPI控制器硬件打交道。每个SoC的SPI控制器都不一样,所以这部分代码是平台相关的。
控制器驱动要做的事:
- 初始化硬件:设置时钟极性(CPOL)、相位(CPHA)、波特率、数据位宽
- 实现传输函数:填充FIFO、触发DMA、处理中断
- 管理DMA缓冲区:分配DMA安全的缓冲区,处理cache一致性
- 报告传输完成:通过回调通知核心层传输结果
控制器驱动的核心是填充一个struct spi_master结构体,然后注册到核心层。我见过不少新手直接在内核里写死寄存器地址,其实正确的做法是通过platform_device匹配,然后从设备树拿资源。
我的经验:调试控制器驱动时,先用逻辑分析仪抓波形。我曾经花了两天查一个SPI通信失败的问题,最后发现是时钟极性配置反了。示波器一看就明白的事,代码里翻来覆去查半天。
4.4 设备驱动层——面向外设的业务逻辑
这一层才是我们大多数驱动开发者真正要写的。设备驱动不关心SPI控制器怎么操作寄存器,它只关心怎么和挂载在SPI总线上的外设通信。
设备驱动的典型工作流:
- 在probe()函数里获取spi_device,设置通信参数(模式、速度、位宽)
- 构造spi_message和spi_transfer,填充要发送/接收的数据
- 调用spi_sync()或spi_async()发起传输
- 解析返回数据,转换成上层能理解的格式
注意:设备驱动里不要直接操作SPI控制器的寄存器!我见过有人图省事,在设备驱动里ioremap了控制器的地址空间,结果内核版本升级后驱动就炸了。正确的做法永远是通过核心层提供的API来通信。
4.5 三大数据结构的关系
这三个结构体是SPI子系统的基石。我画了个关系图,方便你理解它们怎么串起来的。
这三个结构体的关系,我用一句话总结:spi_master是控制器,spi_device是外设,spi_driver是驱动代码。一个master可以挂多个device,每个device由对应的driver来驱动。
4.6 关键数据结构成员解析
光知道结构体名字不够,得知道里面放了什么。我挑几个最常用的成员说说。
| 结构体 | 关键成员 | 作用 |
|---|---|---|
| spi_master | bus_num | SPI总线编号,用于区分多个SPI控制器 |
| num_chipselect | 支持的片选数量,即最多挂几个从设备 | |
| transfer_one_message | 核心传输函数,驱动必须实现 | |
| max_speed_hz | 控制器支持的最大时钟频率 | |
| spi_device | master | 指向所属的spi_master,反向关联 |
| max_speed_hz | 该设备使用的通信速率 | |
| mode | SPI模式(CPOL/CPHA组合) | |
| chip_select | 片选引脚编号 | |
| spi_driver | probe | 设备匹配成功时调用,初始化外设 |
| remove | 设备移除时调用,释放资源 | |
| id_table | 设备匹配表,用于非设备树平台 |
避坑指南:设置spi_device的mode时,一定要确认外设数据手册的时序要求。我曾经把CPOL=0、CPHA=0配反了,结果读出来的ADC数据全是乱的。后来用示波器一对比才发现问题。
4.7 数据传输的完整流程
从设备驱动发起一次SPI传输,到底经历了什么?我梳理了一下:
- 设备驱动构造
spi_transfer,设置tx_buf、rx_buf、len - 将transfer添加到
spi_message,可以加多个transfer实现连续操作 - 调用
spi_sync()或spi_async(),把message交给核心层 - 核心层将message加入master的传输队列,调用
transfer_one_message - 控制器驱动操作硬件寄存器,启动SPI时钟,逐字节发送/接收
- 传输完成(或出错),控制器驱动调用
spi_finalize_current_message - 核心层唤醒等待的进程,返回传输结果给设备驱动
这个流程里,第5步是真正耗时的部分。如果数据量大,建议用DMA方式,别让CPU在那傻等。
核心要点:设备驱动只负责构造数据,不负责怎么发。控制器驱动只负责怎么发,不关心数据含义。核心层在中间做调度和转换。各司其职,这就是分层设计的好处。
4.8 实际开发中的选择
写SPI驱动时,你通常只需要关注设备驱动层。控制器驱动一般由SoC厂商提供,比如三星的s3c64xx_spi.c、TI的omap2_mcspi.c。除非你在做芯片移植,否则别碰控制器驱动。
我个人的建议是:
- 先确认你的SPI控制器驱动是否已经在内核里
- 设备树里配好spi节点,设置好片选、频率、模式
- 写设备驱动时,多用
spi_write_then_read()这种封装好的接口 - 调试时加
dev_dbg()打印,别用printk
嗯,这一章的内容就到这。三层架构和数据结构的关联,是理解SPI子系统的基础。搞懂了这些,后面写具体驱动时就不会迷路。