一、引言
在STM32开发中,使用HAL库的DMA+空闲中断(IDLE)接收不定长串口数据是一种高效方案。但许多开发者在使用时遭遇数据丢失、缓冲区溢出等问题。本文将分析溢出风险根源,并给出环形缓冲区改造方案。
二、传统DMA+空闲中断方案原理
2.1 基本工作流程
- 初始化DMA,将串口接收数据寄存器(DR)映射到内存缓冲区。
- 使能串口空闲中断。
- 当一帧数据接收完成,总线空闲,触发IDLE中断。
- 在中断中计算接收长度,处理数据,重新启动DMA。
典型代码片段:
uint8_t rx_buf[256];
void HAL_UART_IDLECallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
// 清除空闲标志
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 停止DMA获取已接收长度
HAL_UART_DMAStop(huart);
uint16_t len = 256 - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 处理数据...
// 重新启动DMA
HAL_UART_Receive_DMA(huart, rx_buf, 256);
}
}
2.2 潜在溢出风险
- DMA传输完成与空闲中断竞争:若一帧数据长度恰好等于缓冲区大小,DMA传输完成中断与空闲中断可能同时触发,导致重复处理或数据覆盖。
- 处理不及时:在中断中处理数据耗时过长,期间新数据到达会覆盖缓冲区。
- 重新启动DMA的间隙:停止DMA到重新启动之间,串口仍可能接收数据,造成丢失。
- 多帧连续到达:若两帧数据间隔极短,空闲中断可能来不及响应,第二帧数据覆盖第一帧。
三、环形缓冲区改造方案
3.1 设计思路
- 使用环形缓冲区(FIFO)作为数据暂存区,DMA始终向环形缓冲区写入,避免停止/重启DMA。
- 利用DMA的半传输(HT)和传输完成(TC)中断动态更新写指针。
- 空闲中断仅用于标记一帧结束,通知应用层处理。
- 应用层从环形缓冲区读取数据,实现生产者-消费者模型。
3.2 环形缓冲区结构
typedef struct {
uint8_t *buffer; // 数据区
uint16_t size; // 总大小(2的幂次)
volatile uint16_t head; // 写指针(DMA写入位置)
volatile uint16_t tail; // 读指针(应用读取位置)
} ring_buffer_t;
3.3 关键实现
3.3.1 初始化与DMA配置
- 分配环形缓冲区内存(建议大小256/512/1024,2的幂次便于取模)。
- 配置DMA为循环模式(Circular),使能半传输和传输完成中断。
- 启动DMA接收,指向环形缓冲区起始地址。
#define RB_SIZE 512
static uint8_t rb_data[RB_SIZE];
static ring_buffer_t rb = {rb_data, RB_SIZE, 0, 0};
void uart_dma_init(UART_HandleTypeDef *huart) {
// 使能空闲中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
// 启动DMA循环接收
HAL_UART_Receive_DMA(huart, rb.buffer, rb.size);
}
3.3.2 DMA中断处理
在DMA半传输和传输完成中断中更新写指针(head)。由于DMA循环写入,head始终指向DMA下一个要写入的位置。
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
// 半传输:已接收size/2字节
rb.head = rb.size / 2;
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 传输完成:已接收size字节,回绕
rb.head = 0;
}
3.3.3 空闲中断处理
空闲中断表示一帧结束,此时DMA当前写入位置可通过__HAL_DMA_GET_COUNTER计算。更新head为实际写入位置,并通知应用层。
void HAL_UART_IDLECallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 计算当前DMA写入位置
uint16_t remain = __HAL_DMA_GET_COUNTER(huart->hdmarx);
uint16_t pos = rb.size - remain;
// 更新head(考虑回绕)
rb.head = pos;
// 设置帧结束标志,通知应用层
frame_ready = 1;
}
}
3.3.4 应用层读取
应用层从tail读取到head的数据,处理完后更新tail。注意处理回绕。
uint16_t rb_read(ring_buffer_t *rb, uint8_t *data, uint16_t len) {
uint16_t available = (rb->head - rb->tail) & (rb->size - 1);
if (len > available) len = available;
for (uint16_t i = 0; i < len; i++) {
data[i] = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) & (rb->size - 1);
}
return len;
}
四、完整代码示例
以下为基于STM32 HAL库的环形缓冲区串口接收完整实现(以UART1为例)。
// ring_buffer.h
typedef struct {
uint8_t *buffer;
uint16_t size;
volatile uint16_t head;
volatile uint16_t tail;
} ring_buffer_t;
void rb_init(ring_buffer_t *rb, uint8_t *buf, uint16_t size);
uint16_t rb_read(ring_buffer_t *rb, uint8_t *data, uint16_t len);
uint16_t rb_available(ring_buffer_t *rb);
// ring_buffer.c
void rb_init(ring_buffer_t *rb, uint8_t *buf, uint16_t size) {
rb->buffer = buf;
rb->size = size;
rb->head = 0;
rb->tail = 0;
}
uint16_t rb_available(ring_buffer_t *rb) {
return (rb->head - rb->tail) & (rb->size - 1);
}
uint16_t rb_read(ring_buffer_t *rb, uint8_t *data, uint16_t len) {
uint16_t avail = rb_available(rb);
if (len > avail) len = avail;
for (uint16_t i = 0; i < len; i++) {
data[i] = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) & (rb->size - 1);
}
return len;
}
// main.c
#define RB_SIZE 512
static uint8_t rb_data[RB_SIZE];
static ring_buffer_t rb;
volatile uint8_t frame_ready = 0;
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
rb.head = rb.size / 2;
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
rb.head = 0;
}
}
void HAL_UART_IDLECallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
uint16_t remain = __HAL_DMA_GET_COUNTER(huart->hdmarx);
uint16_t pos = rb.size - remain;
rb.head = pos;
frame_ready = 1;
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
rb_init(&rb, rb_data, RB_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, rb.buffer, rb.size);
while (1) {
if (frame_ready) {
frame_ready = 0;
uint8_t data[RB_SIZE];
uint16_t len = rb_read(&rb, data, RB_SIZE);
// 处理数据
}
}
}
五、注意事项
- 缓冲区大小:建议为2的幂次,便于使用位与操作取模。大小需根据数据速率和处理能力权衡。
- 中断优先级:DMA中断和串口中断优先级需合理设置,避免长时间阻塞。
- 临界区保护:head和tail的读写可能被中断打断,必要时使用关中断或原子操作。
- 内存对齐:DMA缓冲区需对齐,避免未定义行为。
- 错误处理:添加DMA错误中断处理,防止死锁。
- 性能考量:应用层处理速度应大于数据到达速度,否则环形缓冲区仍会溢出。
六、总结
通过环形缓冲区改造,DMA+空闲中断方案可有效避免数据覆盖和丢失,实现稳定可靠的不定长串口接收。本文所述方法已在实际项目中验证,开发者可根据需求调整缓冲区大小和中断策略。