用DMA环形缓冲区接收不定长串口帧:半满中断与空闲中断的竞态处理

引言

在嵌入式系统中,串口通信是基础且关键的环节。对于不定长数据帧的接收,DMA环形缓冲区结合空闲中断(IDLE)和半满中断(HT)是高效方案。然而,当缓冲区大小设置不当或中断处理不严谨时,半满中断与空闲中断可能同时触发,导致数据竞争、重复处理或漏数据。本文将深入剖析这一竞态问题,并提供一套稳健的解决方案。

原理剖析

DMA环形缓冲区工作机制

环形缓冲区通过DMA将串口接收数据连续写入内存,当写入位置达到缓冲区中点时触发半满中断,达到末尾时触发传输完成中断(TC)。空闲中断则在总线空闲时触发,标志一帧数据接收完毕。

  • 半满中断:DMA当前传输地址等于缓冲区中点,表示已接收一半数据。
  • 空闲中断:串口接收线空闲超过一个字节时间,表示当前帧结束。

竞态场景

假设缓冲区大小为N,数据帧长度恰好接近N/2或N,可能出现:

  • 场景1:数据长度略大于N/2,半满中断先触发,处理前半部分,随后空闲中断触发,处理剩余部分,但若处理顺序不当,可能重复处理或遗漏。
  • 场景2:数据长度恰好等于N/2,半满中断和空闲中断几乎同时触发,若中断优先级相同,则顺序不定,导致数据错乱。
  • 场景3:数据长度大于N,半满中断触发后,后续数据继续写入,可能覆盖未处理的数据。

解决方案设计

核心思路

引入状态标志位和缓冲区索引管理,确保每个数据字节只被处理一次。具体策略:

  1. 使用环形缓冲区索引:记录已处理位置(read_index)和DMA当前写入位置(write_index),通过差值计算可读数据量。
  2. 中断优先级划分:将空闲中断优先级设为高于半满中断,确保帧结束信号优先处理。
  3. 状态机处理:在半满中断中仅更新标志,不立即处理数据;在空闲中断中统一处理所有数据,避免重复。

详细步骤

  1. 配置DMA:设置循环模式,缓冲区大小N(建议为最大帧长的2倍以上)。
  2. 使能中断:开启DMA半满中断、传输完成中断,以及串口空闲中断。
  3. 中断服务函数
    • 半满中断:设置半满标志,不处理数据。
    • 空闲中断:清除标志,计算有效数据长度,调用处理函数。
  4. 数据处理函数:根据标志和索引,从缓冲区读取数据。

代码实现

以下以STM32 HAL库为例,展示核心代码。

#define BUFFER_SIZE 256
uint8_t rx_buffer[BUFFER_SIZE];
volatile uint16_t read_index = 0;
volatile uint8_t half_flag = 0;
volatile uint8_t idle_flag = 0;

// 初始化DMA和串口
void UART_DMA_Init(void) {
    // 配置UART,使能IDLE中断
    // 配置DMA循环模式,缓冲区为rx_buffer,大小为BUFFER_SIZE
    HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}

// 串口中断服务函数(在UART_IRQHandler中调用)
void UART_IDLE_Callback(void) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        idle_flag = 1;
        // 获取当前DMA写入位置
        uint16_t write_index = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        // 计算有效数据长度
        uint16_t data_len = (write_index >= read_index) ? (write_index - read_index) : (BUFFER_SIZE - read_index + write_index);
        // 处理数据
        ProcessData(data_len);
        // 更新读索引
        read_index = write_index;
        idle_flag = 0;
    }
}

// DMA半满中断回调
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        half_flag = 1;
        // 注意:此处不处理数据,仅置标志
    }
}

// DMA传输完成中断回调(环形缓冲区回绕)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 环形缓冲区回绕,可在此处理,但通常由空闲中断统一处理
    }
}

// 数据处理函数
void ProcessData(uint16_t len) {
    // 从read_index开始读取len字节
    // 注意处理环形回绕
    for (uint16_t i = 0; i < len; i++) {
        uint8_t data = rx_buffer[(read_index + i) % BUFFER_SIZE];
        // 处理数据,例如存入应用缓冲区
    }
}

关键点说明

  • 半满中断中仅置标志,不处理数据,避免与空闲中断冲突。
  • 空闲中断中统一计算数据长度并处理,确保数据完整性。
  • 使用read_index和DMA计数器计算有效数据,避免重复或遗漏。

注意事项

  1. 缓冲区大小选择:应大于最大帧长,且为2的幂次方,便于取模运算。
  2. 中断优先级:空闲中断优先级应高于半满中断,确保帧结束及时处理。
  3. 临界区保护:在访问共享变量(如read_index)时,可临时屏蔽中断或使用临界区。
  4. DMA计数器读取:读取__HAL_DMA_GET_COUNTER时,需确保DMA未在传输中,否则可能读到不一致值,建议在空闲中断中读取。
  5. 测试验证:使用不同长度数据帧(短帧、半满边界、长帧)进行压力测试,确保无数据丢失。

总结

通过合理设计中断处理逻辑和状态标志,可以有效解决DMA环形缓冲区中半满中断与空闲中断的竞态问题。本文提供的方案简单可靠,适用于大多数STM32项目。开发者可根据实际需求调整缓冲区大小和中断优先级,以达到最佳性能。