一、为什么需要DMA+空闲中断+环形缓冲区
在嵌入式开发中,串口接收不定长数据是常见需求。传统轮询或单字节中断方式会占用大量CPU资源,且难以应对高速数据流。DMA(直接存储器访问)可以自动搬运数据,空闲中断(IDLE)则能在总线空闲时通知CPU,两者结合可实现高效的不定长数据接收。然而,DMA接收的数据需要及时取出处理,否则新数据会覆盖旧数据。环形缓冲区(Ring Buffer)作为经典的数据结构,能有效解耦生产(DMA搬运)与消费(应用处理)速度,并支持溢出检测与处理。
二、原理讲解
2.1 DMA与空闲中断协作机制
- 配置DMA为循环模式(Circular)或普通模式(Normal),将串口数据寄存器(DR)自动搬运到指定内存数组。
- 使能串口空闲中断:当串口总线在接收完一帧数据后出现空闲(超过一个字节时间无新数据),硬件置位IDLE标志并触发中断。
- 在空闲中断中,通过计算DMA剩余传输数量(
__HAL_DMA_GET_COUNTER)得到本次接收的数据长度,然后将数据从DMA缓冲区搬运到环形缓冲区。
2.2 环形缓冲区设计
环形缓冲区由固定大小的数组和读、写指针构成。写指针由DMA数据搬运更新,读指针由应用任务更新。关键操作:
- 写入:将新数据从DMA缓冲区复制到环形缓冲区写指针处,并更新写指针(模缓冲区大小)。
- 读取:从读指针处取出数据,更新读指针。
- 溢出判断:当写指针追上读指针时表示缓冲区满,新数据将覆盖旧数据或丢弃,需根据策略处理。
2.3 溢出处理策略
- 覆盖模式:新数据覆盖最旧数据,保证最新数据可用,但会丢失历史数据。
- 丢弃模式:拒绝新数据,保护已有数据完整性,但可能丢失最新数据。
- 动态扩容:不推荐在资源受限的MCU中频繁使用。
本文采用覆盖模式,并在溢出时置位错误标志,供应用层决策。
三、配置步骤(以STM32CubeMX+HAL库为例)
- 使能USART1,配置波特率、字长、停止位等参数。
- 使能USART1全局中断(NVIC中勾选USART1 global interrupt)。
- 配置DMA:选择USART1_RX,模式为Circular(循环模式),数据宽度Byte,优先级中。
- 生成代码,HAL库会自动初始化DMA和串口。
- 在main.c中调用自定义的初始化函数,启动DMA接收和空闲中断。
四、完整代码示例
4.1 环形缓冲区定义与操作
#define RING_BUF_SIZE 512
typedef struct {
uint8_t buffer[RING_BUF_SIZE];
volatile uint16_t write_idx;
volatile uint16_t read_idx;
volatile uint8_t overflow; // 溢出标志
} RingBuffer_t;
RingBuffer_t ring_buf;
// 初始化环形缓冲区
void RingBuffer_Init(RingBuffer_t *rb) {
rb->write_idx = 0;
rb->read_idx = 0;
rb->overflow = 0;
}
// 向环形缓冲区写入数据(覆盖模式)
uint16_t RingBuffer_Write(RingBuffer_t *rb, const uint8_t *data, uint16_t len) {
uint16_t written = 0;
for (uint16_t i = 0; i < len; i++) {
uint16_t next_write = (rb->write_idx + 1) % RING_BUF_SIZE;
if (next_write == rb->read_idx) {
// 缓冲区满,覆盖最旧数据(读指针后移)
rb->read_idx = (rb->read_idx + 1) % RING_BUF_SIZE;
rb->overflow = 1;
}
rb->buffer[rb->write_idx] = data[i];
rb->write_idx = next_write;
written++;
}
return written;
}
// 从环形缓冲区读取数据
uint16_t RingBuffer_Read(RingBuffer_t *rb, uint8_t *data, uint16_t len) {
uint16_t read = 0;
while (read < len && rb->read_idx != rb->write_idx) {
data[read++] = rb->buffer[rb->read_idx];
rb->read_idx = (rb->read_idx + 1) % RING_BUF_SIZE;
}
return read;
}
// 获取当前缓冲区中数据长度
uint16_t RingBuffer_Available(RingBuffer_t *rb) {
if (rb->write_idx >= rb->read_idx) {
return rb->write_idx - rb->read_idx;
} else {
return RING_BUF_SIZE - (rb->read_idx - rb->write_idx);
}
}
4.2 DMA与空闲中断初始化
#define DMA_BUF_SIZE 256
uint8_t dma_buf[DMA_BUF_SIZE]; // DMA接收缓冲区
void UART_DMA_IDLE_Init(void) {
RingBuffer_Init(&ring_buf);
// 启动DMA接收(循环模式)
HAL_UART_Receive_DMA(&huart1, dma_buf, DMA_BUF_SIZE);
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
4.3 空闲中断处理函数
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
// 检查空闲中断标志
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 停止DMA以获取准确剩余计数(可选,但推荐)
HAL_UART_DMAStop(&huart1);
uint16_t remaining = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
uint16_t received = DMA_BUF_SIZE - remaining;
if (received > 0) {
// 将DMA缓冲区数据写入环形缓冲区
RingBuffer_Write(&ring_buf, dma_buf, received);
}
// 重新启动DMA接收
HAL_UART_Receive_DMA(&huart1, dma_buf, DMA_BUF_SIZE);
}
}
4.4 应用层读取示例
void Process_UART_Data(void) {
uint8_t temp[64];
uint16_t len;
while ((len = RingBuffer_Read(&ring_buf, temp, sizeof(temp))) > 0) {
// 处理接收到的数据,例如解析协议
// ...
}
if (ring_buf.overflow) {
// 处理溢出事件,如记录日志、重置缓冲区等
ring_buf.overflow = 0;
}
}
五、注意事项
- DMA模式选择:循环模式下DMA自动重装,但空闲中断中需停止DMA再获取计数器,否则计数器可能已重装导致长度计算错误。普通模式则需每次重新启动DMA。
- 中断优先级:空闲中断优先级应合理设置,避免被高优先级中断长时间阻塞,导致数据丢失。
- 缓冲区大小:环形缓冲区大小应大于单次最大可能接收的数据量,并留有余量。DMA缓冲区大小建议为2的幂次,便于计算。
-
临界区保护:环形缓冲区的读写指针在中断和主循环中均被修改,需使用
__disable_irq()/__enable_irq()或原子操作保护关键段。 - 溢出处理:覆盖模式会丢失数据,应用层应通过溢出标志感知并采取相应措施(如请求重传)。
-
内存对齐:DMA缓冲区建议使用
__attribute__((aligned(4)))确保对齐,提高传输效率。 - 调试技巧:可利用GPIO翻转或打印调试信息观察中断触发频率和数据流。
通过以上方案,可构建一个高效、可靠的不定长串口数据接收框架,适用于大多数嵌入式应用场景。
暂无评论,来抢沙发~