3、SPI子系统详解:从协议到实战
SPI总线,说白了就是嵌入式世界里最常用的「短距离高速通道」。我做了这么多年驱动,接触过的传感器里,十有七八都走SPI。瑞芯微RK平台的SPI控制器设计得挺规整,但坑也不少。这一章咱们就把SPI子系统从头捋一遍,从协议到代码,再到调试手段,一次说透。
3.1 SPI总线协议回顾
SPI全称Serial Peripheral Interface,摩托罗拉发明的。四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。全双工,一主多从,靠片选区分设备。
为什么SPI在传感器领域这么流行?
- 速度快:轻松跑到几十MHz,比I2C快一个数量级
- 全双工:收发同时进行,适合数据流场景
- 硬件简单:没有地址仲裁,没有应答机制
- 灵活:4种模式(CPOL/CPHA组合),兼容各种奇葩芯片
这里有个关键点——SPI模式。我见过太多人栽在这个上面。模式由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 采样边沿 | 典型场景 |
|---|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 | 最常用,大部分传感器默认 |
| 模式1 | 0 | 1 | 下降沿采样 | 少数老器件 |
| 模式2 | 1 | 0 | 下降沿采样 | 某些ADI芯片 |
| 模式3 | 1 | 1 | 上升沿采样 | 部分无线芯片 |
3.2 RK平台SPI控制器驱动
瑞芯微的SPI控制器,在RK3568/RK3588这些芯片上,设计思路基本一致。核心是DesignWare SPI IP,Synopsys家的。驱动代码在drivers/spi/spi-dw-*.c里。
控制器驱动的职责很清晰:
- 初始化硬件寄存器(时钟、模式、位宽、片选)
- 管理DMA或PIO传输
- 提供
spi_controller结构体给上层 - 处理中断和错误
我个人习惯,拿到一块新板子,先看设备树里SPI节点的配置:
// RK3588设备树SPI节点示例
&spi2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi2m2_cs0 &spi2m2_pins>;
max-freq = <50000000>; // 50MHz,别超
num-cs = <1>;
sensor@0 {
compatible = "vendor,xxx-sensor";
reg = <0>; // 片选0
spi-max-frequency = <10000000>; // 10MHz
spi-cpha; // 如果模式1,加上这个
// spi-cpol; // 模式2或3需要
};
};
这里有个细节:max-freq是控制器支持的最高频率,spi-max-frequency是具体设备的工作频率。我建议先降频调试,比如从1MHz开始,通了再往上提。为什么?高频下信号完整性容易出问题,尤其是飞线连接传感器的时候。
3.3 SPI设备驱动编写模板
写一个SPI传感器驱动,核心就是填充struct spi_driver。我直接给模板,你们照着改就行:
#include <linux/spi/spi.h>
#include <linux/module.h>
static int sensor_probe(struct spi_device *spi)
{
int ret;
u8 tx_buf[2], rx_buf[2];
// 1. 设置SPI参数
spi->mode = SPI_MODE_0; // 根据芯片手册
spi->bits_per_word = 8; // 大部分传感器8位
spi->max_speed_hz = 10000000; // 10MHz
ret = spi_setup(spi);
if (ret) {
dev_err(&spi->dev, "spi_setup failed\n");
return ret;
}
// 2. 读取设备ID验证通信
tx_buf[0] = 0x0F; // 读ID寄存器命令
tx_buf[1] = 0x00; // dummy byte
ret = spi_write_then_read(spi, tx_buf, 1, rx_buf, 1);
if (ret || rx_buf[0] != 0x5A) {
dev_err(&spi->dev, "ID mismatch: got 0x%02x\n", rx_buf[0]);
return -ENODEV;
}
dev_info(&spi->dev, "sensor detected, ID=0x%02x\n", rx_buf[0]);
return 0;
}
static void sensor_remove(struct spi_device *spi)
{
// 清理资源
}
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "vendor,xxx-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
static struct spi_driver sensor_driver = {
.driver = {
.name = "xxx_sensor",
.of_match_table = sensor_of_match,
},
.probe = sensor_probe,
.remove = sensor_remove,
};
module_spi_driver(sensor_driver);
SPI数据传输有几种方式,我列个对比:
| API | 特点 | 适用场景 |
|---|---|---|
spi_write_then_read() |
先写后读,自动处理CS | 寄存器读写,最常用 |
spi_write() |
只写,不读 | 配置寄存器、写数据 |
spi_read() |
只读,需要先发dummy | 批量数据读取 |
spi_sync_transfer() |
自定义传输序列 | 复杂时序、DMA传输 |
3.4 SPI通信调试技巧
这部分是我最想分享的。调试SPI,说白了就是三板斧:
- 看波形:逻辑分析仪或示波器,抓SCLK、MOSI、MISO、CS
- 看返回值:驱动里加printk,打印每次传输的收发数据
- 降速测试:把频率降到1MHz以下,排除信号完整性问题
我遇到过最典型的故障场景:
- CS拉不下来:检查GPIO复用,确认pinctrl配置正确
- MISO一直高电平:传感器没上电,或者片选没选中
- 数据全是0xFF:MOSI没通,或者模式配反了
- 数据偶尔错位:时钟太快,或者走线太长
调试利器:spidev
如果不想写完整驱动,可以先在设备树里使能spidev:
// 设备树中
&spi2 {
status = "okay";
spidev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <10000000>;
};
};
然后用户态用spi-tools或自己写ioctl程序,直接操作SPI。我经常用这招先验证硬件通路,再写内核驱动。
还有一个技巧——环回测试。把MOSI和MISO短接,发数据看能不能收回来。如果能,说明控制器和驱动没问题;如果不能,查硬件连接。
spi_dw_prepare_transfer_hardware()里加对齐检查。所以,遇到DMA问题,先试试PIO模式,排除法定位。
最后,我画了一张SPI子系统整体架构图,方便大家理解各层关系: