4、SPI协议栈实现:SPI总线协议与四种模式、Linux SPI子系统架构、SPI设备驱动与DMA传输
SPI,全称是Serial Peripheral Interface。说白了,就是一根时钟线,两根数据线,再加一根片选线。简单,但坑不少。我最早接触SPI是在一个传感器项目上,当时觉得这协议太简单了,结果调试了整整两天——嗯,就是模式没配对。
4.1 SPI总线协议与四种模式
SPI是主从结构。主设备负责产生时钟,从设备被动响应。数据线有两根:MOSI(主出从入)和MISO(主入从出)。片选线CS,低电平有效,谁拉低谁说话。
SPI的四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我见过不少工程师在这上面翻车,包括我自己。
| 模式 | CPOL | CPHA | 数据采样边沿 | 时钟空闲电平 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 | 低电平 |
| 模式1 | 0 | 1 | 下降沿采样 | 低电平 |
| 模式2 | 1 | 0 | 上升沿采样 | 高电平 |
| 模式3 | 1 | 1 | 下降沿采样 | 高电平 |
模式0和模式3最常用。为什么?因为很多Flash和传感器默认就支持这两种。我个人习惯先用模式0,如果通信不正常,再切模式3试试。
SPI的传输速率,理论上可以很高。但实际受限于PCB走线、从设备响应时间、以及驱动能力。我一般先看从设备数据手册里的最大时钟频率,然后留20%余量。
4.2 Linux SPI子系统架构
Linux的SPI子系统,分三层:
- SPI核心层:提供总线注册、设备枚举、数据传输的通用框架。
- SPI控制器驱动:操作具体的硬件寄存器,比如配置时钟、模式、片选。
- SPI设备驱动:面向具体外设,比如Flash、ADC、传感器。
你想想看,这三层分工很明确。核心层帮你把脏活累活都干了,你只需要写设备驱动,或者控制器驱动。
下面这张图,是我画的一个SPI子系统架构图,方便你理解层级关系。
核心层提供的关键数据结构是 spi_device 和 spi_message。你写驱动时,主要就是填充这两个结构体。
4.3 SPI设备驱动与DMA传输
写一个SPI设备驱动,其实不复杂。核心步骤就这几步:
- 在设备树里描述SPI从设备:片选引脚、模式、最大频率。
- 注册一个spi_driver,实现probe和remove回调。
- 在probe里,用spi_setup配置模式、速率。
- 用spi_write_then_read或spi_sync_transfer收发数据。
我贴一段伪代码,你看一眼就明白了:
static int my_spi_probe(struct spi_device *spi)
{
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 10000000;
spi_setup(spi);
// 注册字符设备或输入设备
return 0;
}
static int my_spi_transfer(struct spi_device *spi, u8 *tx, u8 *rx, int len)
{
struct spi_transfer t = {
.tx_buf = tx,
.rx_buf = rx,
.len = len,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
这段代码,说白了就是最基础的PIO模式。CPU一个一个字节地搬数据。如果数据量大,比如操作一个几兆字节的Flash,CPU就被占死了。
这时候,DMA就该上场了。
4.3.1 DMA传输的优势
DMA,直接存储器访问。数据从外设到内存,或者从内存到外设,不经过CPU。CPU只需要告诉DMA控制器:从哪搬,搬到哪,搬多少。然后就可以去干别的事了。
我做过一个项目,用SPI读取摄像头数据,每帧图像几百KB。用PIO模式,CPU占用率飙到80%。换成DMA后,直接降到5%。效果立竿见影。
| 传输方式 | CPU占用率 | 吞吐量 | 适用场景 |
|---|---|---|---|
| PIO模式 | 高(60-90%) | 受限于CPU频率 | 小数据量、低频通信 |
| DMA模式 | 低(5-20%) | 接近硬件极限 | 大数据量、高速通信 |
4.3.2 Linux SPI DMA的实现
在Linux里,启用SPI DMA通常不需要你改驱动代码。控制器驱动如果支持DMA,核心层会自动帮你做。你只需要在设备树里配好DMA通道。
设备树里大概是这样:
&spi1 {
status = "okay";
dmas = <&dma 0 1>, <&dma 1 1>;
dma-names = "tx", "rx";
my_device@0 {
compatible = "my,spi-device";
reg = <0>;
spi-max-frequency = <20000000>;
};
};
这里 dmas 指定了发送和接收的DMA通道。内核在传输时,如果检测到有DMA通道,会自动使用DMA,否则回退到PIO。
dmatest 模块测试。我曾经因为DMA中断没配好,数据一直传不出去,查了两天才发现是中断号写错了。
4.3.3 DMA传输的注意事项
DMA虽好,但有几个坑要注意:
- Cache一致性:DMA直接操作内存,如果CPU有Cache,数据可能不一致。需要调用
dma_map_single或dma_sync_single_for_device。 - 缓冲区对齐:DMA通常要求缓冲区地址对齐到Cache Line大小(一般是32或64字节)。不对齐的话,性能会下降,甚至出错。
- 中断处理:DMA传输完成后,会触发中断。中断处理函数要尽量短,别在里面做复杂操作。
4.4 小结
SPI协议,模式选对,速率别超,基本就稳了。Linux SPI子系统,三层架构清晰,设备驱动写起来不复杂。DMA传输,能大幅降低CPU占用,但要注意Cache一致性和缓冲区对齐。
嗯,这一章就到这里。记住,调试SPI时,示波器是你最好的朋友。先看时钟,再看数据,最后看片选。波形对了,代码基本没问题。