SPI(Serial Peripheral Interface)作为一种高速全双工同步串行通信协议,在嵌入式开发领域占据着重要地位。我第一次接触SPI是在开发一个工业传感器项目时,当时需要以10MHz的速率采集32位高精度数据,经过对比UART、I2C等方案后,最终选择了SPI这个"速度狂魔"。
SPI总线通常由四根线构成:
在实际项目中,我遇到过一个典型的布线问题:当SCK频率达到8MHz时,如果MOSI走线长度超过15cm,就会出现数据采样错误。后来通过缩短走线距离并添加33Ω终端电阻解决了这个问题。这提醒我们:高频SPI通信必须考虑信号完整性。
SPI协议的精妙之处在于其极简的设计哲学:
在我的一个OLED显示模块项目中,就曾因为CPOL/CPHA设置错误导致显示乱码。通过逻辑分析仪捕获波形后发现,模块要求CPOL=1/CPHA=1,而我的初始化代码设置的是CPOL=0/CPHA=0。
以STM32F4系列为例,其SPI控制器包含以下关键部件:
在CubeMX配置时,我习惯先确定时钟源:
c复制RCC_PCLK1Config(RCC_HCLK_Div4); // 设置APB1时钟为42MHz
RCC_PCLK2Config(RCC_HCLK_Div2); // 设置APB2时钟为84MHz
这样SPI2(挂载在APB1)最大频率为21MHz,SPI1(挂载在APB2)可达42MHz。
CR1控制寄存器:
SR状态寄存器:
经验分享:在调试SPI时,我通常会先检查SR寄存器的BSY位,确保SPI不处于忙状态再进行操作。曾经因为忽略这一点导致连续发送时数据错位。
以SPI1主模式为例,推荐配置步骤:
特别注意:如果使用硬件NSS,必须使能SPI的硬件NSS输出功能,否则从机设备可能无法正确响应。
对于追求极致效率的场景,可以直接操作寄存器:
c复制void SPI1_Init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // 使能SPI1时钟
SPI1->CR1 = SPI_CR1_MSTR // 主机模式
| SPI_CR1_BR_1 // 波特率=fPCLK/8
| SPI_CR1_CPOL // CPOL=1
| SPI_CR1_CPHA // CPHA=1
| SPI_CR1_SSM // 软件NSS管理
| SPI_CR1_SSI; // 内部NSS信号
SPI1->CR2 = SPI_CR2_SSOE // NSS输出使能
| SPI_CR2_DS_2 | SPI_CR2_DS_1 | SPI_CR2_DS_0; // 8位数据
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
}
时钟问题:
模式冲突:
数据错位:
基础发送函数示例:
c复制void SPI_SendByte(uint8_t data)
{
while(!(SPI1->SR & SPI_SR_TXE)); // 等待发送缓冲区空
SPI1->DR = data; // 写入数据
while(SPI1->SR & SPI_SR_BSY); // 等待传输完成
}
全双工收发函数:
c复制uint8_t SPI_TransferByte(uint8_t txData)
{
SPI1->DR = txData; // 启动传输
while(!(SPI1->SR & SPI_SR_RXNE));// 等待接收完成
return SPI1->DR; // 返回接收数据
}
性能提示:在STM32F4上,使用寄存器操作的单字节传输耗时约0.5μs(@84MHz),比库函数快约30%。
对于高速数据传输,必须使用DMA:
c复制void SPI_DMA_Init(void)
{
// 1. 配置DMA流
DMA2_Stream3->CR = DMA_SxCR_CHSEL_0 // 通道3(SPI1_TX)
| DMA_SxCR_PL_0 // 中等优先级
| DMA_SxCR_MSIZE_0 // 内存8位
| DMA_SxCR_PSIZE_0 // 外设8位
| DMA_SxCR_MINC // 内存地址递增
| DMA_SxCR_DIR_0; // 内存到外设
DMA2_Stream3->PAR = (uint32_t)&SPI1->DR; // 外设地址
DMA2_Stream3->M0AR = (uint32_t)txBuffer; // 内存地址
// 2. 使能SPI DMA请求
SPI1->CR2 |= SPI_CR2_TXDMAEN;
}
实测数据:使用DMA传输1KB数据,8MHz SCK:
适合中等速率、需要事件响应的场景:
c复制void SPI_IRQ_Init(void)
{
SPI1->CR2 |= SPI_CR2_RXNEIE; // 使能RXNE中断
NVIC_EnableIRQ(SPI1_IRQn); // 使能SPI1中断
}
void SPI1_IRQHandler(void)
{
if(SPI1->SR & SPI_SR_RXNE) {
uint8_t data = SPI1->DR; // 读取数据清除标志
// 处理接收数据...
}
}
现象:高频率(>5MHz)下数据错误
解决方案:
排查步骤:
在控制多个SPI从机时,我推荐两种方案:
方案一:硬件CS控制
c复制void SPI_SelectDevice(uint8_t devNum)
{
switch(devNum) {
case 0: GPIO_ResetBits(GPIOA, GPIO_Pin_4); break; // CS1
case 1: GPIO_ResetBits(GPIOB, GPIO_Pin_0); break; // CS2
// ...
}
Delay_us(1); // 满足从机CS建立时间
}
方案二:SPI总线开关
使用模拟开关(如74HC4052)切换不同从机,适合超多设备场景。
FIFO使用:
c复制// 在STM32H7等高级系列中
SPI1->CR2 |= SPI_CR2_FRXTH; // 设置RXFIFO阈值为8位
内存布局优化:
c复制__attribute__((aligned(4))) uint8_t spiBuffer[1024]; // 4字节对齐
时钟配置:
c复制// 超频SPI示例(需确认芯片支持)
RCC->DCKCFGR |= RCC_DCKCFGR_SPI1SEL_0; // 选择PLLR作为SPI1时钟源
在实际项目中,通过上述优化,我将一个SPI Flash的读写速度从原来的5MB/s提升到了12MB/s,效果显著。