串口即桥梁:51单片机UART从寄存器配置到帧协议的状态机实践

# 串口即桥梁:51单片机UART从寄存器配置到帧协议的状态机实践


串行通信是单片机与外界交换数据最基础的通道。51内核将UART控制器集成于芯片内部,然而**硬件仅完成字节的收发,数据的边界划分、完整性与语义解析完全依赖开发者**。本文从SCON/TMOD寄存器配置、波特率发生器的时基原理入手,延展至中断服务函数的设计约束,最终落足于应对不定长数据帧的**状态机解析**与**超时判帧**两种工程范式。


---


## 一、寄存器级配置:UART模式1的时基与帧格式


51单片机的串口有四种工作模式,**模式1(8位UART,波特率可变)是应用最广泛的配置** 。其硬件链路仅依赖TXD(P3.1)与RXD(P3.0)两根引脚,全双工异步通信。


**波特率发生器**:模式1的移位时钟由定时器1的溢出率决定,公式为:


```

波特率 = (2^SMOD / 32) × (定时器1溢出率)

```


工程中通常将定时器1配置为**8位自动重装模式(方式2)**,溢出率由TH1初值精确控制。使用11.0592MHz晶振时,常用配置如下 :


```c

void Uart_Init_9600(void) {

    SCON = 0x50;        // 模式1,8位UART,REN=1允许接收

    TMOD &= 0x0F;       // 清除定时器1相关位

    TMOD |= 0x20;       // 定时器1,方式2(8位自动重装)

    TH1 = 0xFD;         // 9600bps @ 11.0592MHz(SMOD=0)

    TL1 = 0xFD;         

    TR1 = 1;           // 启动定时器1

    ES = 1;            // 使能串口中断

    EA = 1;            // 总中断使能

}

```


**帧结构**:模式1固定为1位起始位(低电平)、8位数据位(低位先行)、1位停止位(高电平),无硬件校验位 。每帧10位,**字节传输时间=10/波特率**,这是后续超时判帧的时间基准。


---


## 二、发送与接收的惯用架构:查询发送 + 中断接收


51单片机串口编程存在一条稳定范式:**发送采用查询阻塞,接收采用中断异步** 。


**查询发送**:TI标志位由硬件置1,需软件清零。


```c

void Uart_SendByte(unsigned char dat) {

    SBUF = dat;

    while(!TI);        // 等待发送完成

    TI = 0;            // 软件清零

}

```


**中断接收**:接收中断服务函数必须遵循**短小、快速、非阻塞**原则 。典型错误是在中断内进行长字符串比较、循环移位或复杂协议解析——这会导致后续字节丢失或栈溢出。


```c

void Uart_ISR(void) interrupt 4 {

    if(RI) {

        RI = 0;

        uart_process(SBUF);    // 建议:仅做状态流转,不阻塞

    }

    // TI中断通常在此处不处理,或置标志位

}

<"a9.a8k1.org.cn"><"d2.a8k1.org.cn"><"h5.a8k1.org.cn">

```


---


## 三、协议解析的分水岭:完整缓冲 vs. 逐字节状态机


当系统需要处理**不定长、带帧头校验**的命令帧时,架构选择直接影响资源占用与实时性。


**方案A:完整接收后再解析**。开辟缓冲区`rx_buffer[]`,将字节顺序存入,待超时或帧尾到达后统一处理。该方案逻辑清晰,但**51的片上RAM(128~256字节)极易被帧缓冲区耗尽**,且接收完成后集中处理延长了响应延迟 。


**方案B:状态机逐字节解析**。在`uart_process()`中维护当前协议解析进度,每字节仅执行常数次比较与赋值,**无需完整帧缓冲区** 。


以自定义协议 `AA 55 CMD LEN DATA... SUM` 为例:


```c

typedef enum { FRAME_IDLE, FRAME_HEAD1, FRAME_HEAD2, FRAME_CMD, FRAME_LEN, FRAME_DATA, FRAME_CH K } UartState;

UartState uart_state = FRAME_IDLE;

unsigned char rx_sum = 0, rx_len = 0, rx_cnt = 0, rx_cmd = 0;

unsigned char rx_payload[16];   // 仅存数据域,按LEN动态截取


void uart_process(unsigned char dat) {

    switch(uart_state) {

        case FRAME_IDLE:    if(dat == 0xAA) { uart_state = FRAME_HEAD1; } break;

        case FRAME_HEAD1:   uart_state = (dat == 0x55) ? FRAME_HEAD2 : FRAME_IDLE; break;

        case FRAME_HEAD2:   rx_cmd = dat; rx_sum = dat; uart_state = FRAME_CMD; break;

        case FRAME_CMD:     rx_len = dat; rx_sum += dat; rx_cnt = 0; 

                            uart_state = (rx_len > 0) ? FRAME_DATA : FRAME_CH K; break;

        case FRAME_DATA:    rx_payload[rx_cnt++] = dat; rx_sum += dat;

                            if(rx_cnt >= rx_len) uart_state = FRAME_CH K; break;

        case FRAME_CH K:     if(dat == rx_sum) { /* 帧有效,执行命令 */ }

                            uart_state = FRAME_IDLE; break;

        default:            uart_state = FRAME_IDLE; break;

    }

}

```


**优势**:无帧级缓冲,每字节处理时间固定,**与帧长度无关**;响应可在接收完毕的同一中断轮次内触发。


---


## 四、帧边界判定:超时法与特殊字符法


状态机可解析已知结构,但如何判断“一帧结束”是独立命题。**两种主流方法各有适用场景** 。


**超时判帧**:利用定时器监测字节间隔。若间隔超过**3~5个字节传输时间**,则判定前一帧结束。该方法**不依赖特定结束符**,兼容纯二进制数据,但需额外占用一个定时器资源 。


```c

void Timer0_ISR(void) interrupt 1 {

    if(frame_timeout_cnt < 0xFFFF) frame_timeout_cnt++;

}<"v1.a8k1.org.cn"><"s8.a8k1.org.cn"><"f0.a8k1.org.cn">


// 在uart_process中每次接收字节时重置计数器

void uart_process(unsigned char dat) {

    frame_timeout_cnt = 0;    // 喂狗

    // ...状态机流转

}

// 主循环中检查

if(frame_timeout_cnt > FRAME_GAP_LIMIT && frame_active) {

    process_complete_frame(); // 超时,强制结束当前帧

}

```


**特殊字符法**:以固定的帧尾(如`\r\n`、`0x0D0A`)标记结束。实现简单,**不占用定时器**,但数据域内部若出现与帧尾相同的字节需转义处理,增加复杂度 。


---


## 五、资源约束下的优化技巧


1. **中断内禁写长循环**。如`for(i=0;i

2. **巧用环形队列**。中断仅负责`SBUF→ringbuffer`,主循环取数解析,解耦收发与处理 。

3. **校验累积增量式**。如协议含累加和,可在接收同时计算,不必二次遍历 。

4. **联合体节省RAM**。协议中可变长字段可用`union`覆盖,或指定存储类型为`xdata` 。


---


从寄存器初值到状态机流转,UART的每一层抽象都伴随着对51架构的深刻理解。硬件串口降低了位时序的门槛,但**帧同步、错误隔离、协议可扩展**仍是软件工程师必须填补的鸿沟。掌握SCON/TMOD仅需半日,设计出稳定、节俭、易维护的串口协议栈,却是贯穿整个嵌入式开发生涯的修行。


请使用浏览器的分享功能分享到微信等