# 串口即桥梁: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仅需半日,设计出稳定、节俭、易维护的串口协议栈,却是贯穿整个嵌入式开发生涯的修行。