3、SPI高级驱动设计:多从机片选管理、双工模式切换、高速模式下的时钟相位调整

好,咱们今天聊点硬核的。SPI这个总线,很多人觉得简单——不就是四根线嘛,MISO、MOSI、SCK、CS,搞定。但真到了实际项目里,尤其是ECU这种对可靠性要求极高的环境,你会发现坑一个接一个。我个人习惯把SPI驱动分成三个层次:能跑、跑稳、跑快。今天讲的就是后两层。

3.1 多从机片选管理:别让CS线坑了你

先说说多从机片选。很多工程师喜欢用GPIO直接拉CS,简单粗暴。但你想过没有,当总线上挂了四五个从机,每个从机的片选极性还不一样,有的高有效,有的低有效,这时候怎么办?

我在项目中遇到过一件事:某次调试一个六轴传感器,SPI总线上同时挂了三个传感器和一个Flash。结果发现,每次读取传感器数据时,Flash的数据莫名其妙被篡改了。查了两天,最后发现是片选信号毛刺导致的——CS线在切换时产生了短暂的抖动,Flash误以为被选中了。

⚠️ 避坑指南: 我曾经因为片选信号没做去毛刺处理,导致产线上批量ECU的校准数据被意外擦除。从那以后,我所有项目的CS线都加了硬件滤波,软件上也做了防抖处理。

多从机片选管理,我建议遵循这几个原则:

  • 片选信号独立控制:每个从机用独立的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并切换方向。说白了,就是需要一个完整的总线释放周期。

💡 实战技巧: 我习惯在驱动层封装三个API:spi_full_duplex()、spi_half_duplex_tx()、spi_half_duplex_rx()。这样上层应用不用关心底层模式切换的细节,调用起来清爽很多。

3.3 高速模式下的时钟相位调整:CPOL和CPHA的玄学

终于到了最让人头疼的部分——时钟相位。很多工程师一看到CPOL和CPHA就头大。其实说白了,就两个问题:时钟空闲时是高还是低?数据是在上升沿采样还是下降沿采样?

但到了高速模式(比如20MHz以上),事情就没那么简单了。信号在PCB上传输有延迟,从机内部也有延迟。你想想看,20MHz时一个时钟周期才50ns,而PCB走线延迟可能就有5-10ns,再加上从机内部延迟,搞不好数据就采错了。

我个人的经验是,高速模式下时钟相位调整要遵循「三步法」:

  1. 查手册:先看从机手册,找到它支持的CPOL和CPHA组合。大部分从机只支持模式0(CPOL=0, CPHA=0)或模式3(CPOL=1, CPHA=1)。
  2. 看波形:用示波器抓SCK和MISO/MOSI的波形,看数据是在SCK的哪个边沿稳定的。我习惯把示波器时基调到2ns/div,这样才能看清细节。
  3. 调相位:如果发现数据采样点刚好在边沿附近,那就调整CPHA。比如从模式0切换到模式1,相当于把采样点往后挪了半个时钟周期。
🔧 调试小技巧: 我曾经调试一个20MHz的SPI Flash,怎么调都读不对。后来发现,从机内部有2ns的延迟,导致数据在SCK上升沿时还没稳定。我把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传输和中断处理,那又是另一番天地了。