一、原理讲解
1.1 DMA+空闲中断接收机制
- DMA循环模式:将串口接收数据寄存器(DR)自动搬运到用户缓冲区,无需CPU干预。
- 空闲中断(IDLE):当串口总线空闲(一个字节时间无新数据)时触发,标志一帧不定长数据接收完成。
- 典型流程:初始化DMA为循环模式,开启串口空闲中断;在IDLE中断中计算已接收数据长度,处理数据,并重置DMA接收位置。
1.2 缓冲区边界问题
- DMA循环模式下的覆盖风险:若数据处理不及时,DMA会回绕覆盖未处理数据。
-
半传输/传输完成中断:HAL库提供
HAL_UART_RxHalfCpltCallback和HAL_UART_RxCpltCallback,可用于分段处理,但需注意与IDLE中断的协同。 -
长度计算:使用
__HAL_DMA_GET_COUNTER(&hdma_usart_rx)获取剩余传输数量,结合缓冲区总大小计算已接收长度。
1.3 溢出检测与恢复
- 溢出条件:当DMA写指针追上读指针(环形缓冲区满)时发生溢出。
- 检测方法:维护读指针和写指针(或利用DMA计数器),比较距离判断是否溢出。
- 恢复策略:溢出时丢弃旧数据或重置DMA,并通知应用层;建议使用双缓冲区或环形缓冲区+临界区保护。
二、配置步骤
2.1 CubeMX配置
- 启用USART1,模式为Asynchronous。
- 在DMA Settings中添加USART1_RX,模式为Circular,优先级High。
- 开启USART1全局中断(NVIC)。
- 生成代码,HAL库初始化DMA和串口。
2.2 关键代码初始化
#define RX_BUFFER_SIZE 256
uint8_t rx_buffer[RX_BUFFER_SIZE];
volatile uint16_t rx_read_pos = 0;
volatile uint8_t rx_overflow = 0;
void UART_Init(void) {
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
三、完整代码示例
3.1 空闲中断处理与数据提取
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
UART_IdleCallback();
}
HAL_UART_IRQHandler(&huart1);
}
void UART_IdleCallback(void) {
static uint16_t old_pos = 0;
uint16_t curr_pos;
uint16_t dma_remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
curr_pos = RX_BUFFER_SIZE - dma_remain;
if (curr_pos != old_pos) {
uint16_t len;
if (curr_pos > old_pos) {
len = curr_pos - old_pos;
// 处理数据从 old_pos 到 curr_pos-1
ProcessData(&rx_buffer[old_pos], len);
} else {
// 回绕情况
len = RX_BUFFER_SIZE - old_pos;
ProcessData(&rx_buffer[old_pos], len);
len = curr_pos;
ProcessData(&rx_buffer[0], len);
}
old_pos = curr_pos;
}
}
3.2 溢出检测与恢复
// 在数据处理函数中检查读指针与写指针距离
void ProcessData(uint8_t *data, uint16_t len) {
// 假设应用层维护读指针 rx_read_pos
uint16_t write_pos = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
uint16_t distance = (write_pos >= rx_read_pos) ? (write_pos - rx_read_pos) : (RX_BUFFER_SIZE - rx_read_pos + write_pos);
if (distance > RX_BUFFER_SIZE - len) {
// 溢出:丢弃最旧数据或重置
rx_overflow = 1;
rx_read_pos = write_pos; // 简单丢弃
}
// 正常处理...
}
// 溢出恢复:重新初始化DMA
void UART_Recover(void) {
HAL_UART_DMAStop(&huart1);
rx_read_pos = 0;
rx_overflow = 0;
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
四、注意事项
-
临界区保护:读写指针的修改需在中断与主循环间加锁(如
__disable_irq())。 -
DMA计数器读取:
__HAL_DMA_GET_COUNTER需在DMA使能时读取,否则值无效。 -
空闲中断清除:必须使用
__HAL_UART_CLEAR_IDLEFLAG,否则会连续触发。 - 缓冲区大小:根据数据速率和處理延迟合理设置,建议≥最大帧长的2倍。
- 溢出恢复:重置DMA会丢失当前数据,应在应用层允许时执行。
- 调试技巧:使用GPIO翻转或打印溢出标志,辅助定位边界问题。
通过以上设计,可有效处理不定长数据的边界与溢出,提升串口通信的鲁棒性。