串口(USART)中断接收与DMA传输:GD32上可变长帧解析的三种方案对比
一、程序原理说明
在嵌入式系统串口通信中,可变长帧解析是一个经典难题——上位机或外部模块发送的数据帧长度不固定,MCU必须在运行时动态识别帧边界、提取有效载荷。GD32系列MCU提供了三种主流实现路径,各有适用场景和性能边界。
方案一:纯中断逐字节接收
USART每接收到一个字节,硬件自动置位RBNE(接收缓冲区非空)标志,触发RBNE中断。ISR中读取USART_DATA寄存器取出数据,推入环形缓冲区。帧边界检测依赖软件状态机——根据协议定义的帧头、长度字段和校验尾逐字节解析。该方案实现最简单,但每字节一次中断,在115200bps以上波特率时CPU负载急剧上升,且中断嵌套风险高。
方案二:DMA + IDLE空闲中断
DMA控制器将USART接收到的字节自动搬运至内存缓冲区,全程无需CPU介入。当总线上出现一个完整帧时间的空闲(无电平跳变),USART硬件置位IDLE标志,触发空闲中断。ISR中通过读取DMA剩余计数寄存器(DMA_CNT)计算已接收字节数,从而确定帧长度。该方案CPU占用率极低(<1%),适合高速持续数据流,但IDLE中断要求帧间至少有一个完整字符时间的静默间隔,背靠背连续帧可能出现帧粘连。
方案三:DMA循环缓冲 + 软件状态机
DMA配置为循环模式,持续向环形缓冲区写入数据,配合半传输完成(HTC)和传输完成(TC)中断更新写指针。主循环中运行软件状态机,从环形缓冲区持续读取字节流,通过帧头匹配、长度字段解析和CRC校验实现帧切割。该方案彻底解耦了硬件接收与软件解析,即使帧间无空闲间隔也能正确分帧,是生产环境中最健壮的方案。
二、程序框架
三种方案共享统一的四层架构:
硬件抽象层:负责USART外设初始化(波特率、数据位、停止位)、GPIO配置、DMA通道映射和NVIC中断优先级设置。
数据缓冲层:方案一使用纯软件环形缓冲区;方案二使用DMA线性缓冲区+空闲中断截断;方案三使用DMA循环缓冲区+读写指针管理。
帧检测层:方案一在中断中逐字节驱动状态机;方案二在IDLE中断中一次性获取整帧;方案三在主循环中运行独立的协议解析状态机。
应用分发层:解析完成的有效帧通过回调函数或消息队列传递给业务模块,实现通信与业务的完全解耦。
三、具体程序实现
以下基于GD32F450(120MHz主频),以USART0(PA9/PA10)、115200bps为例,演示三种方案的核心代码。
1. USART与GPIO初始化(三种方案共用)
#include "gd32f4xx.h"
#define RX_BUF_SIZE 256
void usart_gpio_config(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
/* PA9=TX(复用推挽), PA10=RX(复用输入) */
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9 | GPIO_PIN_10);
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_9 | GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
}
void usart_init_config(void)
{
usart_deinit(USART0);
usart_baudrate_set(USART0, 115200U);
usart_word_length_set(USART0, USART_WL_8BIT);
usart_stop_bit_set(USART0, USART_STB_1BIT);
usart_parity_config(USART0, USART_PM_NONE);
usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE);
usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_enable(USART0);
}
2. 方案一:纯中断逐字节接收
/* 环形缓冲区 */
static uint8_t rx_ring[RX_BUF_SIZE];
static volatile uint16_t ring_head = 0, ring_tail = 0;
void usart_rxne_interrupt_enable(void)
{
usart_interrupt_enable(USART0, USART_INT_RBNE);
nvic_irq_enable(USART0_IRQn, 0, 1);
}
/* 中断服务函数:每字节触发一次 */
void USART0_IRQHandler(void)
{
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE) != RESET) {
uint8_t byte = (uint8_t)usart_data_receive(USART0);
uint16_t next = (ring_head + 1) % RX_BUF_SIZE;
if (next != ring_tail) {
rx_ring[ring_head] = byte;
ring_head = next;
}
}
}
/* 主循环中的帧解析状态机 */
typedef enum { STATE_WAIT_HEADER, STATE_GET_LEN, STATE_GET_DATA, STATE_GET_CRC } parse_state_t;
void frame_parse_poll(void)
{
static parse_state_t state = STATE_WAIT_HEADER;
static uint8_t frame_buf[128], frame_len = 0, frame_idx = 0;
while (ring_tail != ring_head) {
uint8_t byte = rx_ring[ring_tail];
ring_tail = (ring_tail + 1) % RX_BUF_SIZE;
switch (state) {
case STATE_WAIT_HEADER:
if (byte == 0xAA) state = STATE_GET_LEN;
break;
case STATE_GET_LEN:
frame_len = byte;
frame_idx = 0;
frame_buf[frame_idx++] = byte;
state = (frame_len > 0) ? STATE_GET_DATA : STATE_GET_CRC;
break;
case STATE_GET_DATA:
frame_buf[frame_idx++] = byte;
if (frame_idx >= frame_len + 1) state = STATE_GET_CRC;
break;
case STATE_GET_CRC:
/* 校验并分发 */
if (verify_crc(frame_buf, frame_idx, byte)) {
dispatch_frame(frame_buf, frame_len);
}
state = STATE_WAIT_HEADER;
break;
}
}
}
3. 方案二:DMA + IDLE空闲中断
static uint8_t dma_rx_buf[RX_BUF_SIZE];
static volatile uint16_t rx_frame_len = 0;
static volatile uint8_t rx_frame_ready = 0;
void dma_rx_config(void)
{
rcu_periph_clock_enable(RCU_DMA1);
dma_single_data_parameter_struct dma_param;
dma_single_data_struct_para_init(&dma_param);
dma_param.periph_addr = (uint32_t)&USART_DATA(USART0);
dma_param.memory0_addr = (uint32_t)dma_rx_buf;
dma_param.direction = DMA_PERIPH_TO_MEMORY;
dma_param.memory_burst_width = DMA_MEMORY_BURST_SINGLE;
dma_param.periph_burst_width = DMA_PERIPH_BURST_SINGLE;
dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_param.memory_data_width = DMA_MEMORY_DATA_WIDTH_8BIT;
dma_param.periph_data_width = DMA_PERIPH_DATA_WIDTH_8BIT;
dma_param.number = RX_BUF_SIZE;
dma_param.priority = DMA_PRIORITY_HIGH;
dma_param.circulation = DMA_CIRCULAR_DISABLE;
dma_single_data_mode_init(DMA1_Channel4, &dma_param);
dma_channel_enable(DMA1_Channel4, ENABLE);
/* 使能USART0空闲中断 */
usart_interrupt_enable(USART0, USART_INT_IDLE);
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
nvic_irq_enable(USART0_IRQn, 0, 1);
}
void USART0_IRQHandler(void)
{
if (usart_interrupt_flag_get(USART0, USART_INT_FLAG_IDLE) != RESET) {
/* 清除IDLE标志:先读STAT再读DATA */
__IO uint32_t temp;
temp = USART_STAT(USART0);
temp = USART_DATA(USART0);
(void)temp;
/* 计算已接收字节数 */
dma_channel_disable(DMA1_Channel4);
rx_frame_len = RX_BUF_SIZE - dma_transfer_number_get(DMA1_Channel4);
if (rx_frame_len > 0) {
rx_frame_ready = 1;
}
/* 重启DMA准备下一帧 */
dma_transfer_number_config(DMA1_Channel4, RX_BUF_SIZE);
dma_channel_enable(DMA1_Channel4, ENABLE);
}
}
/* 主循环处理 */
void main_loop(void)
{
if (rx_frame_ready) {
rx_frame_ready = 0;
process_frame(dma_rx_buf, rx_frame_len);
}
}
4. 方案三:DMA循环缓冲 + 软件状态机(生产推荐)
static uint8_t circ_buf[RX_BUF_SIZE];
static volatile uint32_t write_idx = 0; /* DMA写入位置 */
void dma_circular_rx_config(void)
{
rcu_periph_clock_enable(RCU_DMA1);
dma_single_data_parameter_struct dma_param;
dma_single_data_struct_para_init(&dma_param);
dma_param.periph_addr = (uint32_t)&USART_DATA(USART0);
dma_param.memory0_addr = (uint32_t)circ_buf;
dma_param.direction = DMA_PERIPH_TO_MEMORY;
dma_param.memory_burst_width = DMA_MEMORY_BURST_SINGLE;
dma_param.periph_burst_width = DMA_PERIPH_BURST_SINGLE;
dma_param.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_param.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_param.memory_data_width = DMA_MEMORY_DATA_WIDTH_8BIT;
dma_param.periph_data_width = DMA_PERIPH_DATA_WIDTH_8BIT;
dma_param.number = RX_BUF_SIZE;
dma_param.priority = DMA_PRIORITY_HIGH;
dma_param.circulation = DMA_CIRCULAR_ENABLE; /* 循环模式 */
dma_single_data_mode_init(DMA1_Channel4, &dma_param);
/* 使能半传输和传输完成中断,用于更新写指针 */
dma_interrupt_enable(DMA1_Channel4, DMA_INT_HTC | DMA_INT_FTF, DMA_FEE_HALF);
dma_channel_enable(DMA1_Channel4, ENABLE);
usart_dma_receive_config(USART0, USART_RECEIVE_DMA_ENABLE);
nvic_irq_enable(DMA1_Channel4_IRQn, 0, 1);
}
/* DMA中断:仅更新写指针,不做任何数据处理 */
void DMA1_Channel4_IRQHandler(void)
{
if (dma_interrupt_flag_get(DMA1_Channel4, DMA_INT_FLAG_HTC) != RESET) {
write_idx = RX_BUF_SIZE / 2; /* 半传输完成,前半区已满 */
dma_interrupt_flag_clear(DMA1_Channel4, DMA_INT_FLAG_HTC);
}
if (dma_interrupt_flag_get(DMA1_Channel4, DMA_INT_FLAG_FTF) != RESET) {
write_idx = 0; /* 传输完成,后半区已满,指针回绕 */
dma_interrupt_flag_clear(DMA1_Channel4, DMA_INT_FLAG_FTF);
}
}
/* 主循环中的软件帧解析器 */
static uint32_t read_idx = 0;
void software_frame_parser(void)
{
static uint8_t frame_buf[128];
static uint8_t state = 0; /* 0=等帧头, 1=读长度, 2=读数据, 3=读校验 */
static uint8_t expected_len = 0, collected = 0;
while (read_idx != write_idx) {
uint8_t byte = circ_buf[read_idx];
read_idx = (read_idx + 1) % RX_BUF_SIZE;
switch (state) {
case 0: /* 等待帧头 0xAA */
if (byte == 0xAA) state = 1;
break;
case 1: /* 读取长度字段 */
expected_len = byte;
collected = 0;
state = (expected_len > 0) ? 2 : 3;
break;
case 2: /* 收集数据载荷 */
frame_buf[collected++] = byte;
if (collected >= expected_len) state = 3;
break;
case 3: /* 校验字节 */
if (verify_checksum(frame_buf, collected, byte)) {
dispatch_frame(frame_buf, collected);
}
state = 0;
break;
}
}
}
四、三种方案对比与选型建议
|
维度 |
方案一:纯中断 |
方案二:DMA+IDLE |
方案三:DMA循环+状态机 |
|
CPU占用 |
高(每字节一次中断) |
极低(仅帧尾中断) |
极低(DMA零介入) |
|
帧粘连处理 |
天然支持(逐字节解析) |
不支持(帧间需空闲) |
天然支持(流式解析) |
|
实现复杂度 |
低 |
中 |
中高 |
|
丢帧风险 |
高波特率下易丢 |
背靠背帧可能丢失 |
几乎为零 |
|
适用场景 |
低速调试、简单协议 |
中速、帧间有间隔 |
高速、连续数据流、生产环境 |
GD32平台特殊注意事项:在GD32F4系列上使用DMA接收不定长数据时,IDLE中断或超时中断中重新配置DMA前,必须先关闭DMA通道,然后清除DMA传输完成标志(DMA_INT_FLAG_FTF),否则DMA将无法重新配置。此外,若先使能DMA后使能USART的Tx功能,会因时间差导致首字节丢失,应严格遵循"先配置USART,再配置DMA"的初始化顺序。在强干扰或长距离485通信场景下,建议将16倍过采样切换为8倍过采样以提高波特率容错能力。
方案三是当前工业级产品的首选方案——DMA循环缓冲保证了零丢包的数据搬运,软件状态机保证了任意帧间距下的可靠分帧,两者通过读写指针实现生产者-消费者解耦,是构建高可靠串口通信系统的工程基石。





