当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]在嵌入式系统串口通信中,可变长帧解析是一个经典难题——上位机或外部模块发送的数据帧长度不固定,MCU必须在运行时动态识别帧边界、提取有效载荷。GD32系列MCU提供了三种主流实现路径,各有适用场景和性能边界。

一、程序原理说明

在嵌入式系统串口通信中,可变长帧解析是一个经典难题——上位机或外部模块发送的数据帧长度不固定,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循环缓冲保证了零丢包的数据搬运,软件状态机保证了任意帧间距下的可靠分帧,两者通过读写指针实现生产者-消费者解耦,是构建高可靠串口通信系统的工程基石。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

在STM32上实现FOC控制时,电流采样是最关键也最棘手的一环。传统做法是在ADC中断里读取数据、做变换、更新PWM,CPU在一个25kHz的控制周期内被完全占据。那么有没有办法让硬件自己把采样、传输、同步都搞定,CPU...

关键字: DMA ADC

串口链路少几个字节时,线缆和干扰常常先背锅,但真正原因可能藏在时钟误差和缓冲余量里。单片机串口若只按平均吞吐设计,突发报文一来就会暴露尾部丢字。

关键字: 单片机 串口 缓冲水位

在嵌入式开发的日常工作里,几乎每个工程师都曾和串口通信打过无数交道。当系统需要频繁输出传感器数据、调试日志或者控制指令时,大家第一反应往往是把阻塞式的查询发送换成DMA传输——毕竟所有人都知道,DMA能把CPU从逐字节搬...

关键字: 嵌入式 串口

在嵌入式开发中,串口、UART、SPI、USB、红外等各类通信协议都离不开数据接收环节,很多开发者习惯用延迟等待、标志位轮询的方式实现接收,不仅代码耦合度高,移植性差,还容易出现漏字节、帧错误等问题。而基于有限状态机(F...

关键字: 串口 UART

在数字电子技术飞速发展的今天,尽管USB、以太网、蓝牙等高速通信技术层出不穷,但有一种古老的通信方式却始终活跃在工业控制、嵌入式开发、设备调试等诸多领域,那就是串口通信。从老式计算机的COM接口到如今无处不在的USB转串...

关键字: 串口通信 串口

串口(UART/TTL/RS232/RS485)凭借接线简单、兼容性强、调试成本低的优势,一直是嵌入式设备、工业控制、智能家居设备中最常用的短距离通信方式。但串口本身是基于字节流的底层通信协议,只保证单个字节的传输,不处...

关键字: 串口 串口协议

在STM32等MCU的裸机开发中,DMA(直接存储器访问)是释放CPU、实现高速数据传输的核心。然而,DMA常与中断(如传输完成、半传输)配合使用,一旦中断优先级分组(NVIC)配置不当,极易导致“数据还没传完就被打断”...

关键字: 裸机开发 DMA

在嵌入式系统的复杂架构中,数据传输的效率直接决定了系统的整体性能。想象一个繁忙的物流中心:成千上万的包裹需要快速分拣、配送,如果每件货物都需要总控中心亲自处理,整个系统将立即陷入瘫痪。嵌入式系统中的DMA(直接存储器存取...

关键字: DMA CPU

链路吞吐一上来,最难查的往往不是带宽不足,而是数据明明搬完了却仍然不对。嵌入式平台只要同时启用 DMA 和 Cache,缓冲区所有权与同步时机没管住,内存里看到的就会是一份被不同主机各自相信的旧数据。

关键字: 嵌入式 DMA 缓存

串口丢包常被归咎于线材或干扰,但很多系统在实验室里安静放着也会少字节。嵌入式通信一旦让缓冲水位和硬件流控彼此脱节,接收链路就会出现一种很危险的错觉:两端都认为自己已经提醒过对方减速,可数据还是继续冲过来。

关键字: 嵌入式 串口 丢包
关闭