在嵌入式系统开发中,串口通信是最基础也最常用的外设接口之一。不同于标准协议如UART、SPI等固定格式的数据传输,实际项目中我们经常需要处理各种自定义协议的不定长数据帧。最近在使用沁恒CH58x系列蓝牙MCU开发智能家居网关时,就遇到了MODBUS协议和自定义透传协议的不定长数据处理需求。经过反复实测验证,我总结出两种稳定可靠的实现方案,下面将完整分享从原理到代码的实战细节。
在工业控制领域,MODBUS RTU协议是典型的可变长度协议。一帧完整的数据由以下部分组成:
这种结构导致帧长度从最短4字节(如功能码03读保持寄存器)到最长256字节不等。更复杂的是,许多设备厂商会在标准协议基础上扩展自定义指令,进一步增加了帧长度不确定性。
早期在STM32平台常用的空闲中断检测法,在沁恒芯片上存在两个明显缺陷:
经过示波器抓包分析发现,当蓝牙处于广播或连接状态时,UART中断响应时间会出现10-15us不等的抖动,这对115200等高波特率通信会产生致命影响。
该方案的核心思想是:每次收到数据时重置超时计时器,当连续超过设定时间未收到新数据时,判定为一帧结束。具体实现涉及三个关键组件:
重要提示:根据MODBUS RTU规范,帧间间隔需大于3.5个字符传输时间。以9600波特率为例,单个字符时间=11bit/9600=1.14ms,因此超时应设置为至少4ms。
c复制// 全局变量定义
#define RX_TIMEOUT_MS 4 // MODBUS标准超时
volatile uint8_t rx_buffer[256];
volatile uint16_t rx_index = 0;
volatile uint8_t frame_ready = 0;
// 定时器初始化
void TIM_Init(void) {
TMR0_TimerInit(FREQ_SYS / 1000); // 1ms定时基准
TMR0_ITCfg(ENABLE, TMR0_3_IT_CYC_END);
PFIC_EnableIRQ(TMR0_IRQn);
TMR0_Disable(); // 初始不启动
}
// 串口中断服务程序
__INTERRUPT
void UART1_IRQHandler(void) {
if(UART1_GetITFlag() == UART_II_RECV_RDY) {
rx_buffer[rx_index++] = UART1_RecvByte();
TMR0_CounterSet(0); // 计数器清零
if(!TMR0_IsEnable()) TMR0_Enable();
}
}
// 定时器中断服务程序
__INTERRUPT
void TMR0_IRQHandler(void) {
static uint8_t timeout_cnt = 0;
TMR0_ClearITFlag(TMR0_3_IT_CYC_END);
if(++timeout_cnt >= RX_TIMEOUT_MS) {
timeout_cnt = 0;
TMR0_Disable();
frame_ready = 1; // 帧接收完成标志
}
}
实测数据:在115200波特率+蓝牙连接状态下,该方案帧丢失率<0.1%,满足多数工业场景需求。
CH58x的UART内置16字节FIFO,提供两个关键中断源:
通过合理配置这两个中断,可以大幅降低CPU负载。经测试,在57600波特率下,方案二比方案一减少约65%的中断次数。
c复制// 初始化配置
UART1_ByteTrigCfg(UART_4BYTE_TRIG); // 4字节触发
UART1_INTCfg(ENABLE, RB_IER_RECV_RDY | RB_IER_RECV_TOUT);
// 中断处理优化版
__INTERRUPT
void UART1_IRQHandler(void) {
uint8_t it_flag = UART1_GetITFlag();
if(it_flag == UART_II_RECV_RDY) {
// 仅读取n-1个字节,保留1字节触发超时中断
for(uint8_t i=0; i<3; i++) {
rx_buffer[rx_index++] = R8_UART1_RBR;
}
}
else if(it_flag == UART_II_RECV_TOUT) {
// 读取剩余所有字节
while(UART1_GetRxFIFOCount()) {
rx_buffer[rx_index++] = R8_UART1_RBR;
}
frame_ready = 1;
}
}
问题现象:帧长度恰为FIFO阈值整数倍时丢帧
根因分析:完全读取FIFO后无法触发超时中断
解决方案:始终保留1字节在FIFO中(如上文i<3的写法)
| 指标 | 方案一(定时器) | 方案二(FIFO) |
|---|---|---|
| 中断次数/帧(32B) | 32 | 5-8 |
| 最小帧间隔(9600bps) | 6ms | 4ms |
| 蓝牙连接时稳定性 | 中等 | 高 |
| RAM占用 | 低 | 中 |
对于波特率变化的应用,可实时计算超时值:
c复制void UpdateTimeout(uint32_t baudrate) {
// 3.5字符时间 = (11bits/baudrate)*3.5 * 1000ms
uint16_t timeout_ms = (38500 + baudrate/2) / baudrate; // 四舍五入
TMR0_PeriodSet(timeout_ms * (FREQ_SYS/1000));
}
建议添加以下保护机制:
在电池供电设备中,可结合CH58x的低功耗特性:
c复制void UART_SleepConfig(void) {
// 接收期间保持高频时钟
UART1_INTCfg(ENABLE, RB_IER_RECV_RDY);
PFIC_EnableIRQ(UART1_IRQn);
// 无通信时切换至低速时钟
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
PWR_PeriphWakeUpCfg(ENABLE, RB_PWR_UART1_WAKE);
}
在某智能电表项目中,我们采用方案二实现了以下性能指标:
关键优化点在于将FIFO阈值初始设为4,当检测到连续长帧时自动调整为8,并通过DMA将数据从UART缓冲区搬运至专用存储区,避免内存拷贝开销。