一、问题背景:从机模式下的丢包现象
在STM32作为SPI从机与主机(如FPGA、另一颗MCU)通信时,若主机以“NSS脉冲+连续时钟”方式传输数据,常出现偶发丢包:DMA传输完成中断触发,但数据缓冲区内容错位或部分数据丢失。
根本原因在于NSS脉冲的边沿与DMA双缓冲切换的时序竞争。SPI从机在NSS下降沿开始接收,上升沿结束帧;而DMA双缓冲在传输完成中断中切换缓冲区,若中断响应不及时或NSS脉冲过窄,会导致DMA尚未完成切换,下一帧数据已开始覆盖。
二、原理讲解:NSS脉冲与DMA双缓冲的时序关系
2.1 NSS脉冲模式
STM32 SPI从机支持NSS脉冲模式(SSM=0, SSOE=0),NSS引脚由硬件管理。主机拉低NSS启动一帧传输,拉高结束。关键时序参数:
- NSS建立时间:NSS下降沿到第一个SCK边沿的最小时间(通常≥1个SCK周期)
- NSS保持时间:最后一个SCK边沿到NSS上升沿的最小时间
- NSS脉冲宽度:两帧之间NSS高电平的最小宽度
若主机NSS脉冲过窄(<2个SCK周期),从机可能无法在脉冲间隔内完成DMA缓冲切换。
2.2 DMA双缓冲机制
DMA双缓冲(Double Buffer Mode)使用两个内存缓冲区(M0AR、M1AR),通过CT位切换。当DMA传输完成一半(HT)或全部(TC)时触发中断,在中断中切换缓冲区。
关键点:DMA传输完成中断的响应时间必须小于NSS脉冲宽度,否则下一帧数据会写入尚未切换的缓冲区,造成覆盖丢包。
2.3 时序配合要点
- NSS脉冲宽度 ≥ DMA中断响应时间 + 缓冲区切换时间(约1-2μs)
- 建议NSS脉冲宽度 ≥ 4个SCK周期(对于10MHz SCK,即400ns以上)
- DMA中断优先级应高于其他可能阻塞的中断
三、配置步骤与完整代码
3.1 硬件连接
- SPI从机:SCK、MISO、MOSI、NSS连接主机对应引脚
- NSS必须使用硬件NSS引脚(非软件管理),以捕获脉冲边沿
3.2 SPI从机初始化(NSS脉冲模式)
void SPI_Slave_Init(void)
{
// 使能SPI时钟和GPIO时钟
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// PA4: NSS, PA5: SCK, PA6: MISO, PA7: MOSI
GPIOA->MODER &= ~(GPIO_MODER_MODER4 | GPIO_MODER_MODER5 |
GPIO_MODER_MODER6 | GPIO_MODER_MODER7);
GPIOA->MODER |= (2 << 8) | (2 << 10) | (2 << 12) | (2 << 14); // 复用功能
GPIOA->AFR[0] |= (5 << 16) | (5 << 20) | (5 << 24) | (5 << 28); // AF5
// SPI1配置:从机模式,NSS硬件管理,8位数据,CPOL=0, CPHA=0
SPI1->CR1 = 0;
SPI1->CR1 |= SPI_CR1_CPHA; // CPHA=1, 根据主机调整
SPI1->CR1 &= ~SPI_CR1_CPOL; // CPOL=0
SPI1->CR1 |= SPI_CR1_MSTR_0; // 从机模式(MSTR=0)
SPI1->CR1 &= ~SPI_CR1_SSM; // 硬件NSS管理
SPI1->CR1 &= ~SPI_CR1_SSI; // SSI=0
SPI1->CR1 |= SPI_CR1_DFF_0; // 8位数据
SPI1->CR1 |= SPI_CR1_SPE; // 使能SPI
}
3.3 DMA双缓冲配置
#define BUF_SIZE 64
uint8_t buf0[BUF_SIZE], buf1[BUF_SIZE];
volatile uint8_t active_buf = 0;
void DMA_DoubleBuffer_Init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
// DMA2 Stream0 Channel3 对应 SPI1_RX
DMA2_Stream0->CR &= ~DMA_SxCR_EN;
while (DMA2_Stream0->CR & DMA_SxCR_EN);
DMA2_Stream0->CR = 0;
DMA2_Stream0->CR |= (3 << 25); // 通道3
DMA2_Stream0->CR |= DMA_SxCR_PL_1; // 高优先级
DMA2_Stream0->CR |= DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0; // 8位
DMA2_Stream0->CR |= DMA_SxCR_MINC; // 内存递增
DMA2_Stream0->CR |= DMA_SxCR_CIRC; // 循环模式(双缓冲需使能)
DMA2_Stream0->CR |= DMA_SxCR_DBM; // 双缓冲模式
DMA2_Stream0->NDTR = BUF_SIZE;
DMA2_Stream0->M0AR = (uint32_t)buf0;
DMA2_Stream0->M1AR = (uint32_t)buf1;
DMA2_Stream0->PAR = (uint32_t)&SPI1->DR;
// 使能传输完成中断
DMA2_Stream0->CR |= DMA_SxCR_TCIE;
NVIC_SetPriority(DMA2_Stream0_IRQn, 1); // 高优先级
NVIC_EnableIRQ(DMA2_Stream0_IRQn);
DMA2_Stream0->CR |= DMA_SxCR_EN;
}
3.4 DMA中断服务函数(缓冲区切换)
void DMA2_Stream0_IRQHandler(void)
{
if (DMA2->LISR & DMA_LISR_TCIF0) {
DMA2->LIFCR = DMA_LIFCR_CTCIF0; // 清除标志
// 当前传输完成的缓冲区是 active_buf 指向的
// 切换 active_buf 到另一个缓冲区
if (active_buf == 0) {
active_buf = 1;
// 处理 buf0 数据(已满)
process_data(buf0, BUF_SIZE);
} else {
active_buf = 0;
process_data(buf1, BUF_SIZE);
}
}
}
3.5 主机侧NSS脉冲时序要求
主机需保证:
- NSS脉冲宽度 ≥ 4个SCK周期(建议≥1μs)
- 帧间NSS高电平期间,从机DMA中断必须完成
四、丢包排查方法
4.1 常见原因与对策
- NSS脉冲过窄:用逻辑分析仪测量NSS高电平宽度,确保>2μs。若主机无法调整,可降低SCK频率或改用软件NSS(但会失去脉冲检测)。
- DMA中断被阻塞:检查中断优先级,确保DMA中断不被其他长耗时中断(如USB、以太网)抢占。可将DMA中断设为最高优先级(0)。
- 双缓冲切换逻辑错误:确认DBM位使能,且M0AR/M1AR地址正确。注意循环模式下DBM的行为与普通模式不同。
- SPI溢出(OVR):从机未及时读取DR会导致OVR。DMA应始终使能,且传输完成中断中尽快处理。
- 内存对齐问题:DMA内存地址需按数据宽度对齐(8位无需,16位需2字节对齐)。
4.2 调试技巧
- 在DMA中断中翻转GPIO,用示波器观察中断响应时间
- 使用STM32的SPI错误标志(OVR、MODF)判断是否溢出
- 在缓冲区首尾添加魔数(如0xAA55),传输后校验是否被覆盖
五、注意事项
- NSS脉冲模式下,SPI从机必须在NSS下降沿前使能(SPE=1),否则首字节丢失
- DMA双缓冲的TC中断在每次缓冲区满时触发,需在中断中及时切换active_buf
- 若主机连续发送且NSS脉冲极短,建议改用“NSS常低+固定帧长”模式,避免脉冲竞争
- 调试时先用低速SCK(如1MHz)验证逻辑,再逐步提高频率
- 确保DMA和SPI时钟使能顺序正确,先配置DMA再使能SPI
暂无评论,来抢沙发~