一、问题背景:从机模式下的丢包现象

在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