2. SPI协议详解与驱动框架
SPI,全称Serial Peripheral Interface,摩托罗拉在80年代搞出来的东西。说白了就是四根线搞定板级通信,简单、快速、可靠。我这些年做过的项目里,SPI出现的频率仅次于I2C,但论速度,SPI甩I2C好几条街。
不过,简单归简单,坑也不少。时序搞反了、模式配错了、设备树写漏了,板子就是死活不通。咱们这一章就把SPI从物理层到驱动层彻底捋一遍。
2.1 SPI物理层与时序基础
先看最基本的四根线:
| 信号 | 全称 | 方向 | 作用 |
|---|---|---|---|
| SCLK | Serial Clock | Master → Slave | 时钟,由主机产生 |
| MOSI | Master Out Slave In | Master → Slave | 主机输出,从机输入 |
| MISO | Master In Slave Out | Slave → Master | 从机输出,主机输入 |
| CS | Chip Select | Master → Slave | 片选,低电平有效 |
SPI是全双工的。什么意思?主机发数据的同时,从机也在往回发数据。每次传输,主从双方各移出一个bit,各移入一个bit。所以一次SPI传输,本质上是一个环形移位寄存器的交换过程。
核心要点:SPI没有应答机制,主机不知道从机是否真的收到了数据。这是SPI和I2C最大的区别之一。所以SPI从机的时序必须严格对齐,差一个时钟周期都不行。
2.2 四种模式——CPOL与CPHA
这是SPI最容易出问题的地方。我见过太多人板子调不通,最后发现是模式配错了。
两个参数决定模式:
- CPOL(Clock Polarity):时钟空闲时的电平。0=低电平空闲,1=高电平空闲。
- CPHA(Clock Phase):数据采样沿。0=第一个跳变沿采样,1=第二个跳变沿采样。
组合起来就是四种模式:
| 模式 | CPOL | CPHA | 空闲时钟电平 | 数据采样沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 |
实际项目中,Mode 0和Mode 3最常见。为什么?因为很多SPI Flash、ADC、DAC芯片默认就支持这两种模式。Mode 0更是事实上的"通用模式"。
我的经验:拿到一个新芯片,先看数据手册里的时序图。找到SCLK的波形,看空闲时是高还是低(确定CPOL),再看数据是在哪个沿被采样的(确定CPHA)。别靠猜,一定要看手册。
我曾经在一个项目里,把一颗SPI NOR Flash配成了Mode 2,结果读ID一直返回0xFF。折腾了两天,最后发现是CPOL搞反了。从那以后,我每次写设备树都会再三确认从机的模式。
2.3 SPI时序分析——以Mode 0为例
咱们拿Mode 0画个时序图看看:
看到没?在Mode 0下:
- SCLK空闲时是低电平
- 主机在上升沿采样MISO上的数据
- 从机在下降沿更新MOSI上的数据
换句话说,主机在时钟的"前半段"读数据,从机在"后半段"写数据。这样错开半个周期,保证数据稳定。
注意:SPI的时序参数不只是模式。还有SCLK频率、建立时间(tSU)、保持时间(tH)、CS到SCLK的延迟等。高速SPI(比如50MHz以上)对PCB走线长度、信号完整性都有要求。我遇到过SPI跑20MHz没问题,一提到40MHz就丢数据,最后发现是走线太长导致信号反射。
2.4 Linux SPI子系统驱动框架
Linux内核的SPI子系统分三层,我习惯这么理解:
这三层各司其职:
- 控制器驱动层:直接操作SPI控制器的寄存器。比如i.MX6的ecspi、STM32的spi、Zynq的spi。这一层跟硬件绑定,每个SoC都不一样。
- SPI核心层:提供通用的框架机制。包括spi_bus_type、spi_alloc_master()、spi_register_master()、spi_sync()等。这一层你不用改,内核已经写好了。
- 设备驱动层:你写的就是这一层。比如SPI Flash驱动、SPI ADC驱动、SPI LCD驱动。你只需要实现probe、remove和具体的读写函数。
设备驱动层的核心数据结构是spi_driver:
static struct spi_driver my_spi_driver = {
.driver = {
.name = "my_spi_device",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
};
module_spi_driver(my_spi_driver);
probe函数里你会拿到一个struct spi_device *spi指针,通过它设置模式、频率、字长:
static int my_spi_probe(struct spi_device *spi)
{
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 10 * 1000 * 1000; // 10MHz
spi->bits_per_word = 8;
spi_setup(spi);
// 然后就可以用 spi_write() / spi_read() / spi_sync() 通信了
...
}
我个人的习惯:probe函数里一定要先读一次从机的ID寄存器,确认通信正常再往下走。如果ID读不对,直接返回-ENODEV。这样能尽早暴露问题,而不是等到后面莫名其妙地挂掉。
2.5 设备树配置——让内核认识你的SPI设备
设备树是Linux下描述硬件的主要方式。SPI设备的设备树节点长这样:
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>;
status = "okay";
spiflash: flash@0 {
compatible = "jedec,spi-nor";
reg = <0>; // 片选索引,对应第0个CS
spi-max-frequency = <40000000>;
spi-cpol; // CPOL=1,如果配Mode 3
spi-cpha; // CPHA=1,如果配Mode 3
#address-cells = <1>;
#size-cells = <1>;
};
adc@1 {
compatible = "ti,ads1256";
reg = <1>; // 片选索引,对应第1个CS
spi-max-frequency = <10000000>;
// 不写spi-cpol/spi-cpha,默认Mode 0
};
};
几个关键点:
reg属性决定挂在哪个片选上。0对应CS0,1对应CS1,以此类推。spi-max-frequency是从机支持的最大频率,实际频率由控制器和从机协商决定。spi-cpol和spi-cpha是可选属性。不写的话默认Mode 0。cs-gpios可以用GPIO模拟片选,灵活性更高。
我曾经踩过的坑:设备树里配了spi-cpol和spi-cpha,但驱动里又用spi->mode重新设了一遍。结果设备树和驱动各管各的,最后模式冲突,通信失败。记住:设备树和驱动二选一,别两边都设。我个人建议在设备树里配,驱动里只做校验。
2.6 驱动开发流程——从设备树到读写
一个完整的SPI设备驱动开发流程,我总结为五步:
- 确认硬件连接:用示波器或逻辑分析仪看SCLK、MOSI、MISO、CS的波形。确保电平匹配(3.3V还是1.8V?)。
- 编写设备树节点:配好compatible、reg、spi-max-frequency、模式属性。
- 编写驱动probe函数:设置spi_device参数,注册字符设备或输入设备等。
- 实现读写函数:使用spi_write()、spi_read()或spi_sync()进行数据传输。
- 验证通信:读从机ID寄存器,确认数据正确。
一个简单的读写示例:
/* 发送命令并读取响应 */
static int spi_cmd_read(struct spi_device *spi, u8 cmd, u8 *buf, int len)
{
int ret;
struct spi_transfer t = {
.tx_buf = &cmd,
.rx_buf = buf,
.len = len + 1, // 第一个字节发命令,后面收数据
.cs_change = 0,
};
struct spi_message m;
spi_message_init(&m);
spi_message_add_tail(&t, &m);
ret = spi_sync(spi, &m);
if (ret < 0)
dev_err(&spi->dev, "spi_sync failed: %d\n", ret);
return ret;
}
这里要注意,spi_sync是同步接口,会阻塞直到传输完成。如果在中断上下文或需要高性能的场景,用spi_async。
性能提示:SPI传输的瓶颈往往不在SPI总线速度,而在CPU处理中断和DMA的开销。如果数据量大,一定要用DMA模式。在设备树里加dmas属性,驱动里用spi_async配合DMA,吞吐量能提升一个数量级。
2.7 本章小结
SPI协议本身不复杂,但细节决定成败。模式配错、时序不对、设备树写漏,任何一个环节出问题,板子就是黑的。我建议你每次调SPI,都按这个顺序排查:
- 先看波形——逻辑分析仪是最好的老师
- 再对模式——CPOL/CPHA必须和从机手册一致
- 再看频率——别超过从机规格
- 最后看驱动——spi_setup()有没有调用,返回值有没有检查
把这套流程走熟了,SPI对你来说就是小菜一碟。