3、SPI高级驱动设计:多从机片选管理、双工模式切换、高速模式下的时钟相位调整
好,咱们今天聊点硬核的。SPI这个总线,很多人觉得简单——不就是四根线嘛,MISO、MOSI、SCK、CS,搞定。但真到了实际项目里,尤其是ECU这种对可靠性要求极高的环境,你会发现坑一个接一个。我个人习惯把SPI驱动分成三个层次:能跑、跑稳、跑快。今天讲的就是后两层。
3.1 多从机片选管理:别让CS线坑了你
先说说多从机片选。很多工程师喜欢用GPIO直接拉CS,简单粗暴。但你想过没有,当总线上挂了四五个从机,每个从机的片选极性还不一样,有的高有效,有的低有效,这时候怎么办?
我在项目中遇到过一件事:某次调试一个六轴传感器,SPI总线上同时挂了三个传感器和一个Flash。结果发现,每次读取传感器数据时,Flash的数据莫名其妙被篡改了。查了两天,最后发现是片选信号毛刺导致的——CS线在切换时产生了短暂的抖动,Flash误以为被选中了。
多从机片选管理,我建议遵循这几个原则:
- 片选信号独立控制:每个从机用独立的GPIO,别想着用译码器省引脚。ECU上GPIO又不值钱,省那三五个引脚,换来的是调试时的噩梦。
- 片选极性统一:尽量让所有从机使用相同的片选极性。如果实在不行,驱动里要单独维护一个极性表。
- 片选切换加延迟:从一个从机切换到另一个时,中间至少留一个SCK周期的空闲时间。我习惯加2-3个SCK周期的延迟,确保总线完全释放。
下面是我常用的片选管理结构体:
typedef struct {
GPIO_TypeDef *port; // CS引脚端口
uint16_t pin; // CS引脚号
uint8_t polarity; // 0:低有效, 1:高有效
uint32_t delay_us; // 切换延迟(微秒)
} SPI_CS_Config_t;
typedef struct {
SPI_TypeDef *spi; // SPI外设基址
SPI_CS_Config_t cs[8]; // 最多8个从机
uint8_t cs_count;
uint8_t current_cs; // 当前选中的从机索引
} SPI_MultiCS_Handle_t;
3.2 双工模式切换:全双工、半双工、单工,你分得清吗?
很多人以为SPI天生就是全双工。其实不然。你想想看,有些从机只发不收,比如温度传感器;有些只收不发,比如DAC。这时候你还用全双工模式,浪费带宽不说,还可能引入干扰。
我一般把SPI的双工模式分成三种:
| 模式 | 数据流向 | 典型应用 | 注意事项 |
|---|---|---|---|
| 全双工 | MISO和MOSI同时传输 | ADC、Flash、传感器 | 需要4线,速度最快 |
| 半双工 | 分时复用MISO/MOSI | 某些EEPROM、RF模块 | 需要配置方向切换 |
| 单工(只发) | 仅MOSI有效 | DAC、LED驱动 | MISO可复用为GPIO |
| 单工(只收) | 仅MISO有效 | 按键扫描、温度传感器 | MOSI可接地或悬空 |
嗯,这里要注意。半双工模式切换时,有个很容易忽略的点:方向切换的时机。我曾经在调试一个RF模块时,半双工模式下数据收发总是错位。后来发现,从发送切换到接收时,需要先等当前字节传输完成,再拉高CS,然后重新拉低CS并切换方向。说白了,就是需要一个完整的总线释放周期。
3.3 高速模式下的时钟相位调整:CPOL和CPHA的玄学
终于到了最让人头疼的部分——时钟相位。很多工程师一看到CPOL和CPHA就头大。其实说白了,就两个问题:时钟空闲时是高还是低?数据是在上升沿采样还是下降沿采样?
但到了高速模式(比如20MHz以上),事情就没那么简单了。信号在PCB上传输有延迟,从机内部也有延迟。你想想看,20MHz时一个时钟周期才50ns,而PCB走线延迟可能就有5-10ns,再加上从机内部延迟,搞不好数据就采错了。
我个人的经验是,高速模式下时钟相位调整要遵循「三步法」:
- 查手册:先看从机手册,找到它支持的CPOL和CPHA组合。大部分从机只支持模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。
- 看波形:用示波器抓SCK和MISO/MOSI的波形,看数据是在SCK的哪个边沿稳定的。我习惯把示波器时基调到2ns/div,这样才能看清细节。
- 调相位:如果发现数据采样点刚好在边沿附近,那就调整CPHA。比如从模式0切换到模式1,相当于把采样点往后挪了半个时钟周期。
下面是我常用的时钟相位配置函数:
/**
* @brief 配置SPI时钟相位
* @param handle SPI句柄
* @param mode SPI模式(0-3)
* @param speed_hz 目标频率(Hz)
* @return 实际配置后的频率
*/
uint32_t SPI_ConfigClockPhase(SPI_Handle_t *handle, uint8_t mode, uint32_t speed_hz) {
// 先根据目标频率计算分频系数
uint32_t prescaler = SPI_CalcPrescaler(handle, speed_hz);
uint32_t actual_speed = handle->base_clock / prescaler;
// 配置CPOL和CPHA
switch(mode) {
case 0: // CPOL=0, CPHA=0
handle->instance->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA);
break;
case 1: // CPOL=0, CPHA=1
handle->instance->CR1 &= ~SPI_CR1_CPOL;
handle->instance->CR1 |= SPI_CR1_CPHA;
break;
case 2: // CPOL=1, CPHA=0
handle->instance->CR1 |= SPI_CR1_CPOL;
handle->instance->CR1 &= ~SPI_CR1_CPHA;
break;
case 3: // CPOL=1, CPHA=1
handle->instance->CR1 |= (SPI_CR1_CPOL | SPI_CR1_CPHA);
break;
}
// 高速模式下,建议使能NSS脉冲模式和CRC校验
if(actual_speed > 10000000) { // 10MHz以上
handle->instance->CR2 |= SPI_CR2_NSSP; // NSS脉冲模式
handle->instance->CR1 |= SPI_CR1_CRCEN; // 使能CRC
}
return actual_speed;
}
最后说一句,高速SPI调试时,示波器是你的好朋友。别光靠猜,抓波形看才是最直接的。我每次调SPI,第一件事就是把探头夹上去,看看实际波形和预期是否一致。很多时候,问题出在PCB布局上,而不是软件配置上。
好了,这一章就到这里。下一章我们聊聊SPI的DMA传输和中断处理,那又是另一番天地了。