4、SPI驱动适配:SPI协议回顾、主从模式配置、SPI DMA传输实现、混合验证中的SPI时序检查

SPI,串行外设接口,嵌入式世界里最常用的同步串行总线之一。说实话,它比I2C简单多了,但简单不代表不会出问题。我在混合验证项目中见过太多SPI时序翻车的案例——从时钟极性配反到DMA传输断流,每个坑都值得好好聊聊。

这一章,我们聚焦SPI驱动的适配实战。从协议基础到DMA传输,再到混合验证环境下的时序检查,一条线串下来。

4.1 SPI协议回顾——别小看这四根线

SPI本质上是主从架构的同步通信。主设备产生时钟,控制片选,决定什么时候跟谁说话。从设备被动响应。就这么简单?嗯,但细节都在时序参数里。

信号 方向(主->从) 作用
SCLK 主->从 串行时钟,由主设备产生
MOSI 主->从 主输出,从输入
MISO 从->主 从输出,主输入
CS/SS 主->从 片选,低电平有效

SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合决定。我习惯用一个表格把它们理清楚:

模式 CPOL CPHA 数据采样边沿 时钟空闲电平
模式0 0 0 上升沿采样 低电平
模式1 0 1 下降沿采样 低电平
模式2 1 0 下降沿采样 高电平
模式3 1 1 上升沿采样 高电平

为什么这个重要?因为主从设备的模式必须匹配。我曾经遇到一个案子,主设备配了模式0,从设备默认模式3,结果数据全是乱的。查了两天才发现是CPOL配反了。

我的习惯:项目初期就把所有SPI外设的时序模式整理成一张表,贴在调试笔记首页。每次配驱动前先对一遍,能省下大量排错时间。

4.2 主从模式配置——寄存器里的小心机

配置SPI主从模式,说白了就是设置几个关键寄存器。以STM32为例,SPI_CR1寄存器里需要配置:

  • MSTR位:置1为主模式,清0为从模式
  • CPOL/CPHA位:选择时序模式
  • BR[2:0]:波特率分频系数
  • LSBFIRST位:数据位顺序,MSB还是LSB先传

主模式配置示例:

void SPI_Master_Init(void) {
    // 使能SPI时钟
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
    
    // 配置GPIO为复用功能
    // ... GPIO初始化代码 ...
    
    // 复位SPI
    SPI1->CR1 = 0;
    
    // 配置为主模式,模式0,8位数据,MSB先传
    SPI1->CR1 |= SPI_CR1_MSTR | SPI_CR1_BR_2 | SPI_CR1_BR_1;
    // CPOL=0, CPHA=0 默认就是模式0
    
    // 使能SPI
    SPI1->CR1 |= SPI_CR1_SPE;
}

从模式配置更简单,但有个坑:从设备的时钟必须由主设备提供,且SCLK频率不能超过从设备支持的上限。我见过有人把从设备的SPI时钟配得比主设备还高,结果从设备根本采不到数据。

注意:从模式下,数据寄存器必须在主设备发起传输之前准备好。否则主设备读到的是垃圾数据。我曾经在验证一个传感器驱动时,发现前几个字节总是错的,后来才意识到是数据准备时序没对齐。

4.3 SPI DMA传输实现——解放CPU的关键

SPI配合DMA,是高性能嵌入式系统的标配。没有DMA,SPI每收发一个字节都要CPU干预,效率极低。尤其是大块数据传输,比如LCD刷屏、SD卡读写,DMA几乎是必须的。

SPI DMA传输的典型流程:

  1. 配置DMA通道,源地址指向发送缓冲区,目的地址指向SPI_DR寄存器
  2. 设置传输长度(数据个数)
  3. 使能DMA请求,启动SPI传输
  4. DMA自动搬运数据,传输完成后触发中断

以STM32的SPI1 TX DMA为例:

void SPI_DMA_TX_Init(uint8_t *data, uint16_t len) {
    // 使能DMA时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    
    // 配置DMA通道3(SPI1_TX)
    DMA1_Channel3->CPAR = (uint32_t)&(SPI1->DR);
    DMA1_Channel3->CMAR = (uint32_t)data;
    DMA1_Channel3->CNDTR = len;
    
    // 方向:内存到外设,增量模式,8位数据
    DMA1_Channel3->CCR |= DMA_CCR_DIR | DMA_CCR_MINC | DMA_CCR_MSIZE_0;
    DMA1_Channel3->CCR |= DMA_CCR_PL_0;  // 中等优先级
    
    // 使能DMA通道
    DMA1_Channel3->CCR |= DMA_CCR_EN;
    
    // SPI使能DMA发送请求
    SPI1->CR2 |= SPI_CR2_TXDMAEN;
    
    // 启动SPI传输(使能SPI)
    SPI1->CR1 |= SPI_CR1_SPE;
}

这里有个关键点:DMA传输完成后,一定要检查SPI的忙状态标志。因为DMA把数据搬完了,但SPI可能还在发送最后一个字节。如果这时候关闭SPI或释放CS,最后一个字节就丢了。

避坑指南:我曾经在做一个音频播放器项目时,SPI DMA传输结束后立即释放CS,结果每次播放最后都有个爆音。后来加上SPI忙状态等待,问题解决。代码就一行:while(SPI1->SR & SPI_SR_BSY);

4.4 混合验证中的SPI时序检查

混合验证环境里,SPI时序检查是重头戏。为什么?因为仿真波形是理想化的,但实际芯片有PVT(工艺、电压、温度)变化,时序裕量必须留够。

我通常把SPI时序检查分为三个层次:

  • 协议层检查:模式匹配、数据格式、CS行为是否正确
  • 时序参数检查:SCLK频率、建立时间、保持时间、CS有效到SCLK第一个边沿的时间
  • 系统级检查:DMA传输完整性、中断响应时间、多从设备切换时的总线冲突

下面这张图展示了混合验证中SPI时序检查的核心流程:

混合验证 SPI 时序检查流程 测试激励生成 协议层检查 时序参数检查 系统级检查 通过/失败判定 失败反馈 说明: 每个检查层次都有对应的断言和覆盖率收集点。失败反馈路径确保时序违规能被快速定位到激励生成阶段。

在实际项目中,我习惯在验证环境里加入SPI时序断言。比如检查CS低电平到SCLK第一个边沿的时间是否满足从设备要求:

// SystemVerilog断言示例
property cs_to_sclk;
    @(posedge sclk)
    $rose(cs_n) |=> ##[1:10] $fell(cs_n);
endproperty

assert_cs_to_sclk: assert property(cs_to_sclk)
    else $error("CS to SCLK timing violation");

还有一个容易被忽略的点:多从设备共享SPI总线时,CS信号的切换时序。如果前一个从设备的CS释放到下一个从设备的CS拉低之间没有足够间隔,总线可能发生冲突。

我的经验:在混合验证中,SPI时序检查最好做成自动化脚本。每次回归测试后自动生成时序报告,标出所有违规点。这样在芯片流片前就能把大部分时序问题消灭掉。

最后说一句,SPI驱动适配不是配通就完事了。真正的挑战在于把协议理解、寄存器配置、DMA优化和验证检查串成一个闭环。你想想看,如果每个环节都留一点余量,整个系统的可靠性就会大幅提升。这就是我这些年做SPI驱动的心得。