基于DMA双缓冲的串口不定长帧接收:空闲中断与半传输中断的协同配置
1. 引言
在嵌入式系统中,串口(UART)是常用的通信接口之一。对于不定长帧的接收,传统方法通常采用字节中断或轮询方式,前者频繁打断CPU,后者浪费CPU周期。STM32的DMA控制器支持双缓冲模式,配合串口的空闲中断(IDLE)和DMA的半传输中断(HT),可以高效地实现不定长帧接收,极大减轻CPU负担。
2. 原理讲解
2.1 DMA双缓冲模式
DMA双缓冲模式使用两个内存缓冲区(Buffer0和Buffer1),DMA在填充一个缓冲区时,可以处理另一个缓冲区。当DMA完成一个缓冲区的传输(或传输过半)时,会产生相应的中断,并自动切换到另一个缓冲区继续传输。这种机制非常适合连续数据流的处理。
2.2 串口空闲中断
当串口总线在接收完一个字节后,如果在一个字节时间内没有新的数据到来,则产生空闲中断。利用空闲中断可以判断一帧数据的结束,从而处理不定长数据。
2.3 半传输中断与空闲中断的协同
- 半传输中断(HT):当DMA传输数据量达到缓冲区一半时触发。在双缓冲模式下,每个缓冲区大小通常设置为帧最大长度的两倍,这样半传输中断和传输完成中断(TC)分别对应两个缓冲区的边界。
- 空闲中断(IDLE):用于检测帧结束。当一帧数据接收完毕,串口空闲,触发IDLE中断。此时可以读取当前DMA已传输的数据量,确定帧长度,并处理数据。
协同工作流程:
- 初始化时,DMA以双缓冲模式循环接收,缓冲区大小设置为最大帧长的2倍(例如最大帧长128字节,则每个缓冲区128字节,总大小256字节)。
- 当接收数据达到半传输位置(即填满第一个缓冲区)时,触发HT中断,此时可以处理第一个缓冲区的数据(如果帧未结束,则继续等待)。
- 当接收数据达到传输完成位置(填满第二个缓冲区)时,触发TC中断,处理第二个缓冲区。
- 当一帧数据提前结束(长度小于缓冲区大小),串口空闲触发IDLE中断,此时根据DMA当前剩余传输数量计算已接收数据长度,并处理当前缓冲区中的数据。
3. 配置步骤(以STM32CubeMX为例)
- 使能USART1,配置为异步模式,波特率115200,8位数据,无校验,1停止位。
- 使能USART1全局中断(NVIC中使能USART1中断)。
- 配置DMA:在USART1_RX的DMA请求中,选择DMA1 Stream0(根据具体型号),模式为循环(Circular),数据宽度字节,使能双缓冲模式(在CubeMX中勾选“Double Buffer”或手动配置)。
- 在DMA配置中使能半传输中断和传输完成中断(NVIC中使能对应DMA中断)。
- 生成代码,并在用户代码中添加空闲中断使能和处理逻辑。
4. 完整代码示例
#include "main.h"
#include "usart.h"
#include "dma.h"
#define BUFFER_SIZE 128
uint8_t rx_buf[2][BUFFER_SIZE]; // 双缓冲区
volatile uint8_t frame_ready[2] = {0}; // 帧就绪标志
volatile uint16_t frame_len[2] = {0}; // 帧长度
// 串口空闲中断回调(需在stm32f1xx_it.c中调用)
void USART1_IRQHandler(void) {
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 获取当前DMA剩余传输数量
uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 计算已接收数据长度(总大小256 - 剩余数量)
uint16_t received = (BUFFER_SIZE * 2) - remain;
// 判断当前活动缓冲区(根据DMA当前目标地址)
uint8_t buf_idx = (huart1.hdmarx->Instance->CR & DMA_SxCR_CT) ? 1 : 0;
// 如果接收长度小于缓冲区大小,则一帧结束
if (received <= BUFFER_SIZE) {
frame_len[buf_idx] = received;
frame_ready[buf_idx] = 1;
} else {
// 跨缓冲区帧,需特殊处理(略)
}
// 重新启动DMA接收(如果非循环模式)
// 循环模式下无需重启
}
HAL_UART_IRQHandler(&huart1);
}
// DMA半传输中断回调
void DMA1_Stream0_IRQHandler(void) {
if (__HAL_DMA_GET_FLAG(huart1.hdmarx, DMA_FLAG_HT0)) {
__HAL_DMA_CLEAR_FLAG(huart1.hdmarx, DMA_FLAG_HT0);
// 半传输:第一个缓冲区满
// 可在此处理第一个缓冲区数据(如果帧未结束,可能只是部分数据)
// 通常结合空闲中断使用,此处仅作标志
}
if (__HAL_DMA_GET_FLAG(huart1.hdmarx, DMA_FLAG_TC0)) {
__HAL_DMA_CLEAR_FLAG(huart1.hdmarx, DMA_FLAG_TC0);
// 传输完成:第二个缓冲区满
// 类似处理
}
HAL_DMA_IRQHandler(huart1.hdmarx);
}
// 主循环中处理帧
void process_frames(void) {
for (int i = 0; i < 2; i++) {
if (frame_ready[i]) {
// 处理rx_buf[i]中的数据,长度为frame_len[i]
// ...
frame_ready[i] = 0;
}
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 启动DMA双缓冲接收
HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buf, BUFFER_SIZE * 2);
// 使能空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
while (1) {
process_frames();
}
}
5. 注意事项
- 缓冲区大小:每个缓冲区大小应不小于最大帧长,总大小为2倍,确保一帧数据不会跨越两个缓冲区(除非使用特殊处理)。
- 中断优先级:DMA中断和串口中断的优先级需合理设置,避免数据竞争。
-
空闲中断清除:必须先读SR再读DR来清除空闲中断标志,HAL库提供了宏
__HAL_UART_CLEAR_IDLEFLAG。 - 双缓冲切换:在双缓冲模式下,DMA自动切换缓冲区,但需注意当前活动缓冲区的判断(通过DMA_SxCR寄存器的CT位)。
- 数据一致性:在中断中修改的共享变量应使用volatile修饰,并在主循环中处理时注意临界区保护。
- 错误处理:添加DMA传输错误中断处理,增强鲁棒性。
通过以上配置,可实现高效、低CPU占用的串口不定长帧接收,适用于高速数据采集、通信协议解析等场景。
暂无评论,来抢沙发~