问题现象与背景

在使用STM32 HAL库进行SPI高速数据传输时,许多开发者习惯在DMA传输完成回调(如HAL_SPI_TxCpltCallback)中直接调用HAL_SPI_Transmit_DMA()启动下一次传输,以实现连续数据流。然而,这种写法常导致系统进入HardFault或BusFault,表现为程序跑飞、DMA通道挂死。

典型错误代码:

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi == &hspi1) {
        // 错误:在回调中立即重启DMA
        HAL_SPI_Transmit_DMA(&hspi1, buf, len);
    }
}

根源分析:HAL库DMA状态机与中断时序

1. DMA传输完成中断的处理流程

当DMA传输完成时,硬件置位传输完成标志(TC),触发DMA中断。HAL库的DMA中断服务程序(DMAx_ChannelY_IRQHandler)会调用HAL_DMA_IRQHandler(),该函数依次执行:

  • 清除TC标志
  • 调用XferCpltCallback(即SPI的DMA完成回调)
  • 将DMA状态机从HAL_DMA_STATE_BUSY切换为HAL_DMA_STATE_READY

关键点:状态机切换发生在用户回调函数执行完毕之后。若在回调内直接调用HAL_SPI_Transmit_DMA(),此时DMA状态仍为BUSY,HAL库会返回HAL_BUSY错误,但更严重的是,部分系列(如STM32F4)的HAL实现中,HAL_SPI_Transmit_DMA会直接操作DMA寄存器(如设置外设地址、数据长度),而此时DMA通道尚未完全释放,导致总线访问冲突,触发BusFault。

2. SPI句柄状态冲突

HAL_SPI_Transmit_DMA内部会检查hspi->State。在回调执行时,SPI状态仍为HAL_SPI_STATE_BUSY_TX,因为HAL库在DMA完成回调中并未立即修改SPI状态,而是在HAL_SPI_IRQHandler或DMA中断的后续处理中才更新。因此,重复启动会破坏状态一致性。

规避方案与代码实现

方案一:延迟重启(推荐)

在回调中设置标志,在主循环或定时器中检测标志后再启动DMA。确保DMA状态机已回到READY。

volatile uint8_t spi_tx_done = 0;

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi == &hspi1) {
        spi_tx_done = 1;  // 仅置标志
    }
}

// 主循环中
while (1) {
    if (spi_tx_done) {
        spi_tx_done = 0;
        // 此时DMA状态已为READY
        HAL_SPI_Transmit_DMA(&hspi1, buf, len);
    }
    // 其他任务
}

方案二:在DMA中断服务程序末尾重启

直接修改DMA中断服务程序,在HAL_DMA_IRQHandler返回后、状态机已更新时启动。但需注意HAL库的封装,可重写HAL_DMA_XferCpltCallback或在DMAx_ChannelY_IRQHandler中手动调用。

void DMA1_Channel3_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
    // 此时DMA状态已为READY
    if (need_restart) {
        need_restart = 0;
        HAL_SPI_Transmit_DMA(&hspi1, buf, len);
    }
}

方案三:使用循环模式(Circular Mode)

若数据流是连续且周期性的,配置DMA为循环模式,无需在回调中重启。HAL库支持HAL_SPI_Transmit_DMA后手动设置循环模式(需修改DMA句柄)。

// 启动前配置
hdma_spi1_tx.Init.Mode = DMA_CIRCULAR;
HAL_DMA_Init(&hdma_spi1_tx);
HAL_SPI_Transmit_DMA(&hspi1, buf, len);
// 之后DMA自动重装,无需回调重启

完整示例:安全连续发送

#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;

uint8_t tx_buf[64];
volatile uint8_t tx_cplt_flag = 0;

void SystemClock_Config(void);
static void MX_DMA_Init(void);
static void MX_SPI1_Init(void);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_DMA_Init();
    MX_SPI1_Init();

    // 填充数据
    for (int i = 0; i < 64; i++) tx_buf[i] = i;

    // 首次启动
    HAL_SPI_Transmit_DMA(&hspi1, tx_buf, 64);

    while (1) {
        if (tx_cplt_flag) {
            tx_cplt_flag = 0;
            // 安全重启:此时DMA状态为READY
            HAL_SPI_Transmit_DMA(&hspi1, tx_buf, 64);
        }
        // 其他应用代码
    }
}

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi == &hspi1) {
        tx_cplt_flag = 1;  // 仅置标志,不直接启动
    }
}

static void MX_SPI1_Init(void) {
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;
    if (HAL_SPI_Init(&hspi1) != HAL_OK) {
        Error_Handler();
    }
}

static void MX_DMA_Init(void) {
    __HAL_RCC_DMA1_CLK_ENABLE();
    HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
}

void DMA1_Channel3_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
}

注意事项与调试技巧

  • 检查HAL版本:不同系列(F1/F4/H7)的HAL实现有差异,H7系列使用DMAMUX,状态机更严格。
  • 避免在中断中调用阻塞函数:如HAL_SPI_Transmit(非DMA)会死等。
  • 使用调试器观察状态:在回调中查看hspi->State和hdma->State,若为BUSY则不可重启。
  • 启用BusFault异常:在HardFault_Handler中分析SCB->CFSR寄存器,定位总线错误地址。
  • 考虑使用LL库:若对性能要求高,LL库的DMA操作更直接,但需自行管理状态。
  • 循环模式限制:循环模式下DMA不产生传输完成中断(除非半传输/全传输中断使能),需调整回调逻辑。

通过以上方法,可彻底规避在DMA完成回调中直接重启导致的总线错误,实现稳定可靠的SPI高速数据传输。