引言
在STM32等MCU上,UART接收数据通常采用中断或DMA方式。对于不定长数据,DMA+环形缓冲区是兼顾实时性与效率的优选方案。然而,DMA的半满中断(HT)和全满中断(TC)触发时机与数据帧边界往往不对齐,导致数据被截断或粘包。本文将深入探讨如何利用半满中断与环形缓冲区特性,实现精准的帧边界识别与数据提取。
DMA环形缓冲区基础
原理概述
DMA环形缓冲区(Circular Buffer)是一种固定大小的内存区域,DMA控制器在传输数据时自动回绕,实现连续写入。配合半满中断(Half Transfer)和全满中断(Transfer Complete),CPU可在数据达到一半或写满时介入处理,避免数据覆盖。
- 半满中断:当DMA传输计数达到缓冲区大小的一半时触发。
- 全满中断:当DMA传输计数达到缓冲区大小时触发(在循环模式下,即写满一圈)。
为什么需要半满中断?
对于不定长数据,若仅使用全满中断,则必须等待缓冲区写满才能处理,延迟大且可能丢失数据。半满中断允许CPU在缓冲区半满时提前处理,降低延迟,同时配合环形特性,可处理任意长度数据。
帧边界检测策略
核心问题
DMA只负责搬运数据,不关心帧结构。当一帧数据跨越半满或全满边界时,若简单按中断点切割,会导致帧被拆分。因此,我们需要一种机制来识别帧的起始和结束。
常用方法
- 空闲中断(IDLE):UART的IDLE中断在总线空闲时触发,可标记一帧结束。这是最常用的帧边界检测手段。
- 帧头帧尾匹配:在数据流中查找特定字节(如0xAA, 0x55)作为帧头,或使用长度字段。
- 超时判断:通过定时器,若超过一定时间未收到新数据,则认为帧结束。
本文重点结合半满中断与IDLE中断,实现精准帧边界。
实现方案
硬件配置(以STM32F4为例)
- UART配置:使能DMA接收,开启IDLE中断。
- DMA配置:设置为循环模式(Circular),缓冲区大小设为2的幂次(如256字节),使能半满和全满中断。
- 中断优先级:DMA中断优先级高于UART IDLE中断,确保数据不丢失。
软件设计
数据结构
#define BUF_SIZE 256 // 必须是2的幂
#define BUF_MASK (BUF_SIZE-1)
uint8_t dma_buf[BUF_SIZE];
volatile uint16_t last_pos = 0; // 上次处理位置
volatile uint8_t frame_ready = 0; // 帧完成标志
DMA中断处理
void DMA1_Stream5_IRQHandler(void) {
uint16_t cur_pos;
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_HT)) {
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_HT);
cur_pos = BUF_SIZE/2; // 半满位置
process_data(cur_pos); // 处理从last_pos到cur_pos的数据
last_pos = cur_pos;
}
if (DMA_GetITStatus(DMA1_Stream5, DMA_IT_TC)) {
DMA_ClearITPendingBit(DMA1_Stream5, DMA_IT_TC);
cur_pos = 0; // 回绕到0
process_data(cur_pos); // 处理从last_pos到缓冲区末尾的数据
last_pos = cur_pos;
}
}
IDLE中断处理
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_IDLE)) {
USART_ClearITPendingBit(USART1, USART_IT_IDLE);
// 读取当前DMA剩余计数,计算实际数据位置
uint16_t remaining = DMA_GetCurrDataCounter(DMA1_Stream5);
uint16_t cur_pos = BUF_SIZE - remaining;
// 处理从last_pos到cur_pos的数据(可能包含完整帧)
process_data(cur_pos);
last_pos = cur_pos;
frame_ready = 1; // 通知主循环处理帧
}
}
数据处理函数
void process_data(uint16_t end_pos) {
// 从last_pos到end_pos的数据,可能包含多个帧或半帧
// 需要根据帧协议解析,这里以帧头0xAA 0x55为例
static uint8_t in_frame = 0;
static uint8_t frame_len = 0;
static uint8_t frame_data[256];
static uint16_t frame_idx = 0;
uint16_t i = last_pos;
while (i != end_pos) {
uint8_t byte = dma_buf[i];
if (!in_frame) {
// 查找帧头
if (byte == 0xAA) {
in_frame = 1;
frame_idx = 0;
frame_data[frame_idx++] = byte;
}
} else {
frame_data[frame_idx++] = byte;
// 假设帧格式:AA 55 LEN DATA... CHECK
if (frame_idx == 2) {
frame_len = byte; // 长度字段
} else if (frame_idx == frame_len + 3) { // 帧头2字节+长度1字节+数据+校验
// 帧完整,处理帧
handle_frame(frame_data, frame_idx);
in_frame = 0;
}
}
i = (i + 1) & BUF_MASK; // 环形回绕
}
}
注意事项
- 缓冲区大小:必须大于最大帧长,且为2的幂,便于取模运算。
- 临界区保护:在中断中修改last_pos时,主循环读取需关中断,防止数据竞争。
- 半满与全满中断处理:在循环模式下,全满中断意味着DMA计数器回绕到0,此时last_pos可能大于cur_pos,需注意环形处理。
- IDLE中断时机:IDLE中断在总线空闲时触发,但可能发生在DMA中断处理期间,需确保顺序正确。
- 性能优化:若帧率较高,可将数据处理放在主循环,中断只置标志位。
完整示例代码
以下是一个简化的STM32F4工程核心代码,展示如何整合以上逻辑。
// main.c
int main(void) {
// 初始化UART、DMA、中断等
while (1) {
if (frame_ready) {
frame_ready = 0;
// 处理完整帧,此时last_pos已更新
// 可调用解析函数
}
}
}
// 中断服务函数已在上面给出,此处省略
总结
通过DMA环形缓冲区结合半满中断和IDLE中断,我们能够高效、精准地处理UART不定长接收。关键在于理解DMA中断的触发点,并利用环形缓冲区的回绕特性,配合帧协议解析,确保帧边界不丢失。此方案在STM32等主流MCU上广泛适用,可大幅提升通信可靠性。