一、DMA半传输与全传输中断原理

STM32的DMA控制器支持两种关键中断:

  • 半传输中断(HT):当DMA传输数据量达到设定缓冲区大小的一半时触发。
  • 全传输中断(TC):当DMA传输数据量达到设定缓冲区大小时触发。

以串口接收为例,若配置DMA接收缓冲区长度为BUFFER_SIZE,则:

  • 接收前BUFFER_SIZE/2字节时触发HT中断;
  • 接收满BUFFER_SIZE字节时触发TC中断。

利用这两个中断,可实现双缓冲(Ping-Pong) 机制:在HT中断中处理前半缓冲区数据,在TC中断中处理后半缓冲区数据,从而避免数据覆盖,实现连续不间断接收。

二、HAL库中的中断回调与协同配置

HAL库为DMA传输提供了三个回调函数:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
  • HAL_UART_RxCpltCallback 对应TC中断;
  • HAL_UART_RxHalfCpltCallback 对应HT中断。

协同处理的关键:在HT和TC回调中分别处理缓冲区的前半段和后半段,并重置相应的读指针。注意,HAL库的DMA接收在循环模式下会自动重载计数器,无需手动重启。

三、配置步骤(以UART+DMA接收为例)

  1. CubeMX配置:

    • 启用USART1,模式为Asynchronous;
    • 在DMA Settings中添加USART1_RX,模式为Circular,数据宽度Byte;
    • 使能USART1全局中断和DMA中断(在NVIC中勾选USART1 global interrupt)。
  2. 代码初始化:

    #define BUFFER_SIZE 64
    uint8_t rx_buf[BUFFER_SIZE];
    
    // 启动DMA接收(循环模式)
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUFFER_SIZE);
    
  3. 实现回调函数:

    void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
        if (huart->Instance == USART1) {
            // 处理前半缓冲区 rx_buf[0] ~ rx_buf[BUFFER_SIZE/2 - 1]
            process_data(rx_buf, BUFFER_SIZE/2);
        }
    }
    
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
        if (huart->Instance == USART1) {
            // 处理后半缓冲区 rx_buf[BUFFER_SIZE/2] ~ rx_buf[BUFFER_SIZE - 1]
            process_data(&rx_buf[BUFFER_SIZE/2], BUFFER_SIZE/2);
        }
    }
    

四、完整代码示例(不定长数据接收)

以下代码实现基于空闲中断(IDLE)与DMA HT/TC的协同,适用于不定长数据帧接收:

#define RX_BUF_SIZE 128
uint8_t rx_buf[RX_BUF_SIZE];
volatile uint16_t last_pos = 0;

void start_reception(void) {
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 使能空闲中断
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// 空闲中断处理(在USART1_IRQHandler中调用)
void USART1_IRQHandler(void) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        uint16_t current_pos = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        if (current_pos != last_pos) {
            // 处理从last_pos到current_pos的数据
            process_frame(&rx_buf[last_pos], current_pos - last_pos);
            last_pos = current_pos;
        }
    }
    HAL_UART_IRQHandler(&huart1);
}

// HT回调:处理前半段
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        process_frame(rx_buf, RX_BUF_SIZE/2);
        last_pos = RX_BUF_SIZE/2;
    }
}

// TC回调:处理后半段
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        process_frame(&rx_buf[RX_BUF_SIZE/2], RX_BUF_SIZE/2);
        last_pos = 0; // 循环缓冲区,重置位置
    }
}

五、常见丢包问题排查

  1. 中断优先级冲突:

    • DMA中断与UART中断优先级需合理设置,避免高优先级中断长时间阻塞导致DMA溢出。
    • 建议DMA中断优先级高于UART中断。
  2. 缓冲区大小不足:

    • 若数据处理耗时较长,应增大缓冲区或提高处理速度。
    • 使用双缓冲时,确保半缓冲区处理时间小于接收半缓冲区数据的时间。
  3. 未及时清除中断标志:

    • 在回调中避免耗时操作,尽快返回。
    • 检查是否因未清除IDLE标志导致重复进入中断。
  4. DMA模式配置错误:

    • 循环模式下DMA自动重载,无需手动重启;正常模式需在TC回调中重新启动接收。
    • 确认HAL_UART_Receive_DMA调用后DMA使能位已置位。
  5. 内存对齐与Cache问题(针对Cortex-M7等):

    • 若使用D-Cache,需确保DMA缓冲区位于非缓存区域或手动维护Cache一致性。
  6. 串口波特率与DMA请求不匹配:

    • 过高波特率可能导致DMA请求丢失,可降低波特率或使用硬件流控。

六、注意事项

  • 在HT和TC回调中避免调用阻塞函数(如HAL_Delay)。
  • 使用__HAL_DMA_GET_COUNTER获取当前传输计数时,注意该寄存器在循环模式下会重载。
  • 若同时使用HT和TC,需确保数据处理逻辑不会重复处理同一段数据。
  • 调试时可通过翻转GPIO或打印时间戳观察中断触发时序。

通过合理协同HT与TC中断,并结合空闲中断,可构建高效可靠的DMA数据接收框架,显著降低CPU负载并提升系统实时性。