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配反了。
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传输的典型流程:
- 配置DMA通道,源地址指向发送缓冲区,目的地址指向SPI_DR寄存器
- 设置传输长度(数据个数)
- 使能DMA请求,启动SPI传输
- 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,最后一个字节就丢了。
while(SPI1->SR & SPI_SR_BSY);
4.4 混合验证中的SPI时序检查
混合验证环境里,SPI时序检查是重头戏。为什么?因为仿真波形是理想化的,但实际芯片有PVT(工艺、电压、温度)变化,时序裕量必须留够。
我通常把SPI时序检查分为三个层次:
- 协议层检查:模式匹配、数据格式、CS行为是否正确
- 时序参数检查:SCLK频率、建立时间、保持时间、CS有效到SCLK第一个边沿的时间
- 系统级检查:DMA传输完整性、中断响应时间、多从设备切换时的总线冲突
下面这张图展示了混合验证中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驱动适配不是配通就完事了。真正的挑战在于把协议理解、寄存器配置、DMA优化和验证检查串成一个闭环。你想想看,如果每个环节都留一点余量,整个系统的可靠性就会大幅提升。这就是我这些年做SPI驱动的心得。