4、SPI总线驱动:控制器框架、DMA传输与设备调试
SPI这个接口,做嵌入式的没人不认识。四根线——SCLK、MOSI、MISO、CS,结构简单,速度却不慢。瑞芯微RK平台的SPI控制器,我这些年打交道下来,觉得它最大的特点就是灵活:主从模式可配、时钟极性可调、DMA能接、FIFO深度也够用。
但灵活的另一面,就是坑多。时钟相位搞反了、DMA没对齐、CS脚控制时序不对……这些我都踩过。今天咱们就把SPI驱动从控制器到设备,再到调试手段,一条线捋清楚。
4.1 SPI控制器驱动框架
RK平台的SPI控制器,本质上是一个内存映射的硬件模块。驱动框架分三层:
- 核心层:Linux SPI core,提供总线注册、设备匹配、数据传输接口
- 控制器驱动层:操作寄存器,管理FIFO、时钟、中断、DMA
- 设备驱动层:挂接到SPI总线,实现具体外设的读写协议
我个人习惯先看设备树。RK3588的SPI节点长这样:
&spi2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi2m2_cs0 &spi2m2_pins>;
cs-gpios = <&gpio3 RK_PB0 GPIO_ACTIVE_LOW>;
max-freq = <50000000>;
spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <25000000>;
};
};
这里有个细节:cs-gpios 用GPIO模拟片选,而不是硬件CS。为什么?因为硬件CS在连续传输时会有片选释放再拉低的问题,很多从设备受不了。我遇到过一款ADC,每次CS拉高再拉低就会复位,用硬件CS根本没法做连续读。换成GPIO模拟,完美解决。
控制器驱动核心流程:
- probe阶段:获取时钟、复位、DMA通道、中断号,初始化FIFO阈值
- transfer_one:单次传输的底层实现,写FIFO、触发传输、等待完成
- prepare_message:设置时钟极性、相位、字节序
- unprepare_message:传输结束后的清理
4.2 DMA传输模式
SPI用PIO模式,小数据量没问题。一旦超过64字节,CPU占用率就上去了。DMA模式才是正经做法。
RK的SPI控制器支持两种DMA触发方式:
| 模式 | 触发条件 | 适用场景 |
|---|---|---|
| FIFO半满触发 | TX FIFO空余过半 / RX FIFO数据过半 | 中等数据量,延迟敏感 |
| FIFO空/满触发 | TX FIFO全空 / RX FIFO全满 | 大数据量,吞吐优先 |
我建议用半满触发。为什么?因为空/满触发容易导致DMA请求间隔不均匀,尤其在系统负载高时,DMA响应延迟会拉长,SPI时钟上出现毛刺。我在RK3399上调试一个显示驱动时,就因为这个毛刺导致屏幕闪了一下。换成半满触发,问题消失。
DMA配置的关键代码片段:
static int spi_dma_xfer(struct spi_master *master, struct spi_transfer *xfer)
{
struct dma_async_tx_descriptor *tx_desc, *rx_desc;
// 配置TX DMA
tx_desc = dmaengine_prep_slave_sg(master->dma_tx,
xfer->tx_sg.sgl, xfer->tx_sg.nents,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx_desc)
return -EINVAL;
// 配置RX DMA
rx_desc = dmaengine_prep_slave_sg(master->dma_rx,
xfer->rx_sg.sgl, xfer->rx_sg.nents,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!rx_desc)
return -EINVAL;
// 提交并启动
dmaengine_submit(tx_desc);
dmaengine_submit(rx_desc);
dma_async_issue_pending(master->dma_tx);
dma_async_issue_pending(master->dma_rx);
return 0;
}
注意:DMA传输的buffer必须cacheline对齐。RK平台L1 cacheline是64字节。如果buffer没对齐,DMA和CPU看到的数据不一致,读出来的全是0xFF。我曾经排查这个问题花了一整天,最后发现是kmalloc没指定GFP_DMA标志。
4.3 SPI设备驱动开发
设备驱动说白了就是注册一个spi_driver,然后在probe里初始化外设。以一款常见的SPI Flash为例:
static const struct spi_device_id flash_ids[] = {
{ "w25q128", 0 },
{ }
};
static struct spi_driver flash_driver = {
.driver = {
.name = "spi-flash",
.of_match_table = flash_of_match,
},
.probe = flash_probe,
.remove = flash_remove,
.id_table = flash_ids,
};
module_spi_driver(flash_driver);
设备驱动里最核心的是数据传输。我一般封装一个函数:
static int spi_flash_transfer(struct spi_device *spi, u8 *tx, u8 *rx, int len)
{
struct spi_transfer t = {
.tx_buf = tx,
.rx_buf = rx,
.len = len,
.speed_hz = spi->max_speed_hz,
.bits_per_word = 8,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(spi, &m);
}
这里有个坑:spi_sync是同步接口,会在等待传输完成时休眠。如果在中断上下文调用,系统会panic。我早期写驱动时没注意,结果一跑就Oops。后来改成spi_async才解决。
4.4 调试技巧与避坑指南
调试SPI,我有一套固定流程:
- 看波形:逻辑分析仪是必须的。先看SCLK频率对不对,再看CS时序是否满足从设备要求
- 检查时钟极性:CPOL和CPHA搞反,数据全错。用示波器对比MISO和SCLK的边沿关系
- 验证DMA:关掉DMA用PIO试一次,如果PIO正常DMA异常,问题就在DMA配置上
- 打印寄存器:在关键路径加dev_dbg,打印SPI控制器的状态寄存器,看FIFO计数和错误标志
我曾经遇到一个奇葩问题:SPI读回来的数据,低字节总是对的,高字节全是0。查了两天,最后发现是字节序配置错了。RK的SPI控制器默认MSB first,但我的从设备要求LSB first。在设备树里加一行spi-tx-bus-width = <1>;没用,得在驱动里调用spi_setup时设置mode位。
还有一个常见问题:CS片选拉低后,第一个SCLK边沿之前需要一段建立时间。有些从设备要求至少100ns。如果时序太紧,从设备不响应。解决办法是在设备树里加spi-cs-setup-delay-ns属性。
嗯,这里要注意:RK平台的SPI控制器,在连续传输多个spi_transfer时,默认会在每个transfer之间释放CS。如果你需要保持CS一直拉低(比如Flash的连续读命令),必须设置spi_transfer->cs_change = 0。
4.5 SPI驱动知识体系总览
下面这张图,是我自己总结的SPI驱动知识结构。每次带新人,我都先让他们看这个:
这张图把SPI驱动的层次和模式都串起来了。从上到下,应用层通过设备驱动访问SPI核心,核心层调度控制器驱动,控制器驱动再选择PIO或DMA跟硬件交互。调试时,你可以从任意一层切入,但我的经验是:先确认硬件波形,再逐层向上排查。
总结一下SPI调试的黄金法则:
- 波形是真理,逻辑分析仪比任何打印都管用
- DMA出问题时,先切PIO排除硬件问题
- CS时序问题最常见,用GPIO模拟CS最稳妥
- 寄存器状态要勤打印,尤其是FIFO计数和错误标志位
好了,SPI这块的内容就这些。记住,驱动开发没有捷径,但有了这些经验和技巧,至少能让你少走弯路。