一、原理讲解:DMA+空闲中断为何需要边界处理
在STM32中,利用DMA接收串口数据并结合空闲中断(IDLE)是处理不定长数据的经典方案。其基本流程为:
- 串口初始化时启动DMA接收,将数据搬运至缓冲区。
- 使能空闲中断,当总线空闲时触发中断,此时DMA已接收完一帧数据。
- 在中断中计算已接收数据长度,并通知应用层处理。
然而,实际应用中存在两个关键问题:
- 缓冲区边界问题:DMA通常配置为循环模式或普通模式。若为普通模式,每次接收前需重新设置DMA地址和长度;若为循环模式,DMA指针会回绕,导致数据覆盖。此外,空闲中断触发时,DMA可能尚未完成最后一次传输(尤其在高波特率下),直接读取长度可能不准确。
- 溢出风险:当应用层处理速度跟不上数据接收速度时,缓冲区可能被写满。若未及时处理,新数据将覆盖旧数据,造成数据丢失甚至解析错误。
因此,必须设计合理的缓冲区管理策略,包括边界检测、长度计算和溢出恢复机制。
二、配置步骤:CubeMX与代码初始化
2.1 CubeMX配置
- 使能USART1,模式为Asynchronous。
- 在DMA Settings中添加USART1_RX,模式选择Circular(循环模式),数据宽度Byte。
- 使能USART1全局中断,并在NVIC中使能USART1中断。
2.2 初始化代码
#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_len = 0;
volatile uint8_t rx_flag = 0;
void MX_USART1_UART_Init(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
HAL_UART_Init(&huart1);
// 启动DMA接收(循环模式)
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
三、完整代码示例:边界处理与溢出恢复
3.1 空闲中断处理函数
在stm32f1xx_it.c中实现USART1_IRQHandler,调用HAL_UART_IRQHandler后检查空闲标志:
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 计算已接收数据长度
uint16_t dma_remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
static uint16_t last_pos = 0;
uint16_t current_pos = RX_BUFFER_SIZE - dma_remaining;
if (current_pos >= last_pos) {
rx_len = current_pos - last_pos;
} else {
// 发生回绕,数据分两段
rx_len = RX_BUFFER_SIZE - last_pos + current_pos;
}
// 标记数据就绪,通知应用层
rx_flag = 1;
last_pos = current_pos;
// 若数据长度超过缓冲区一半,触发溢出恢复
if (rx_len > RX_BUFFER_SIZE / 2) {
// 可在此处增加错误计数或复位DMA
}
}
}
3.2 应用层处理与溢出恢复
主循环中检查rx_flag,处理数据并重置标志。为防止处理过程中新数据覆盖,采用双缓冲或拷贝方式:
void process_uart_data(void) {
if (rx_flag) {
rx_flag = 0;
// 拷贝数据到临时缓冲区(避免处理时被DMA覆盖)
uint8_t temp[RX_BUFFER_SIZE];
uint16_t len = rx_len;
// 根据last_pos计算起始地址
static uint16_t last_pos = 0;
if (last_pos + len <= RX_BUFFER_SIZE) {
memcpy(temp, &rx_buffer[last_pos], len);
} else {
uint16_t first_part = RX_BUFFER_SIZE - last_pos;
memcpy(temp, &rx_buffer[last_pos], first_part);
memcpy(temp + first_part, rx_buffer, len - first_part);
}
// 处理temp中的数据
parse_data(temp, len);
// 更新last_pos
last_pos = (last_pos + len) % RX_BUFFER_SIZE;
}
}
3.3 溢出恢复策略
当检测到接收长度异常(如超过缓冲区80%)或DMA错误时,执行恢复:
void uart_overflow_recovery(void) {
// 停止DMA
HAL_UART_DMAStop(&huart1);
// 清空缓冲区
memset(rx_buffer, 0, RX_BUFFER_SIZE);
// 重置状态
rx_len = 0;
rx_flag = 0;
// 重新启动DMA接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
// 重新使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
四、注意事项
- DMA模式选择:循环模式适合连续接收,但需自行管理指针回绕;普通模式需每次重新启动,适合低频率数据。
-
空闲中断与DMA完成中断的竞态:高波特率下,空闲中断可能早于DMA传输完成触发,此时
__HAL_DMA_GET_COUNTER返回值可能未更新。建议在中断中稍作延时或结合DMA传输完成中断判断。 - 缓冲区大小:根据最大帧长和数据处理速度合理设置,通常为最大帧长的2倍以上。
-
临界区保护:在读写
rx_len和rx_flag时,若主循环与中断可能并发,需使用__disable_irq()/__enable_irq()或原子操作。 - 溢出检测:定期检查DMA剩余计数,若长时间不变且数据未处理,说明处理阻塞,应触发恢复。
- 错误处理:使能UART错误中断(如ORE、FE),在错误回调中执行恢复流程。
通过上述策略,可显著提升串口通信的鲁棒性,适应复杂嵌入式环境。