51单片机(三) --- UART串口
·
一、UART基本概念
1.1 什么是UART?
UART:Universal Asynchronous Receiver/Transmitter 通用异步收发器
一句话定位:全双工、串行、异步通信,单片机最常用的 “对话接口”。
- 全双工:两根线(TXD/RXD),收发同时进行,互不耽误
- 串行:数据是一位一位(bit by bit)地通过单根信号线顺序传输
- 异步:没有时钟线 SCL,靠波特率同步
- 引脚:
- TXD:发送端
- RXD:接收端
- 注意:TX 接 RX,RX 接 TX 交叉相连

1.2 通信方式(单工 / 半双工 / 全双工)
1. 单工 Simplex
- 方向固定:只能 A→B,不能反向
- 一根线
- 例子:广播、老式红外
2. 半双工 Half-Duplex
- 可双向,但同一时刻只能一个方向
- 一根线
- 例子:I2C、对讲机
3. 全双工 Full-Duplex
- 双向同时收发
- 两根线
- 例子:UART、SPI
二、串行通信 VS 并行通信
1. 串行(Serial)
- 一位一位传输
- 优点:线少、成本低、距离远、抗干扰强(RS485)
- 缺点:速度慢
- 适用:UART、RS232、RS485
2. 并行(Parallel)
- 多位同时传输
- 优点:速度快
- 缺点:线多、成本高、距离近(<30 米)、干扰大
- 适用:内部总线、老式并口
三、同步通信 VS 异步通信
同步 Sync
- 有时钟线(SCL/SCLK)
- 时钟对齐,速度快
- I2C、SPI
异步 Async
- 无时钟线
- 靠波特率对齐
- UART 属于异步
四、电平标准:TTL / RS232 / RS485
1. TTL 电平(单片机原生)
- 高:5V
- 低:0V
- 距离近,板内通信
2. RS232(负逻辑)
- 高:-3V ~ -15V
- 低:+3V ~ +15V
- 老式串口,易干扰
3. RS485(差分信号,工业常用)
- 差分电压:A-B
- 高:2~6V
- 低:-2~-6V
- 抗干扰极强、距离远
CH340 就是 USB ↔ TTL 电平转换芯片!
五、串口通信核心参数
串口通信的双方必须完全一致,否则会乱码、收不到数据。
标准格式写法:波特率 数据位 校验位 停止位
1. 波特率(Baud Rate)
- 含义:每秒传输多少位(bit/s)
- 作用:决定通信 “快慢节奏”
- 常用值:2400、4800、9600、115200
- 51 单片机:由 定时器 1 模式 2 自动重装产生
- 2400bps 示例:
TH1=232; TL1=232;
- 2400bps 示例:
注意:收发两端波特率必须相同!
2. 数据位(Data Bits)
- 表示真正传输的数据长度
- 常用:8 位(最标准)
- 对应二进制:
0000 0000 ~ 1111 1111
3. 校验位(Parity)
用于简单检错,有三种:
- N(None):无校验
- O(Odd):奇校验
- E(Even):偶校验
奇校验规则
数据中 1 的个数 + 校验位 = 奇数个 1
偶校验规则
数据中 1 的个数 + 校验位 = 偶数个 1
奇偶校验无法检测偶数个位出错!
4. 停止位(Stop Bits)
- 表示一帧数据结束
- 常用:1 位
- 作用:给接收端一个 “帧结束” 的标志
最常用组合
- 9600 8 N 1波特率 9600,8 位数据,无校验,1 位停止位
- 115200 8 N 1高速通信,最常用
- 115200 8 E 1带偶校验
- 115200 8 O 1带奇校验
六、串口发送规则:LSB 低位先行
UART 发送遵循:先发送最低位 bit0,最后发送最高位 bit7
例子:数据 0xAC = 1010 1100
实际发送顺序:0 0 1 1 0 1 0 1
七、UART串口编程
7.1 串口相关寄存器
在写代码前,必须搞懂操作哪几个寄存器,这是串口配置的基石:
| 寄存器 | 作用 | 代码对应点 |
|---|---|---|
| SBUF | 数据缓冲寄存器 | 发送 / 接收数据都通过它。SBUF = ch 触发发送;ch = SBUF 读取接收数据。 |
| SCON | 串口控制寄存器 | 核心配置寄存器。控制串口模式、允许接收、标志中断。 |
| PCON | 电源控制寄存器 | 控制波特率加倍(SMOD位)。 |
| TMOD | 定时器模式寄存器 | 配置定时器 1 为8 位自动重装载模式(用于产生波特率)。 |
| TH1/TL1 | 定时器 1 初值寄存器 | 存放波特率重装值,决定通信快慢。 |
| IE | 中断允许寄存器 | 控制总中断 (EA) 和串口中断 (ES)。 |
7.2 串口初始化 uart_init(void)
这是代码的入口,我们按步骤拆解位操作逻辑:
步骤 1:配置串口工作模式(方式 1)
// 清零 SCON.7 和 SCON.6 (即 SM0 和 SM1 位)
SCON &= ~(3 << 6);
// 设置 SM1 = 1 (SCON.6)
SCON |= (1 << 6);
// 设置 SM0 = 0 (SCON.7)
SCON &= ~(1 << 7);
- 51 单片机串口有 4 种工作方式,我们使用方式 1(8 位 UART,波特率可变)。
- 模式由
SM0(bit7)和SM1(bit6)决定:01对应方式 1。 - 代码先通过
& ~(3<<6)清零这两位,再分别置 1 和置 0,确保配置准确。
步骤 2:允许串口接收
// 设置 REN = 1 (SCON.4),允许接收
SCON |= (1 << 4);
REN(Receive Enable) 是串口接收的总开关。- 置 1 后,串口硬件才会采样 RXD 引脚接收数据,否则接收不到数据。
步骤 3:配置波特率加倍(SMOD)
// 清零 PCON.6 和 PCON.7 (SMOD0 和 SMOD)
PCON &= ~(3 << 6);
// 设置 SMOD0 = 1 (此时不影响本次业务,但按代码规范写)
PCON &= ~(1 << 6);
// 设置 SMOD = 1 (PCON.7),波特率加倍
PCON |= (1 << 7);
SMOD位用于控制波特率计算公式:
- 设置为
1时,波特率加倍(计算 TH1 时要注意),通信速度更快。
步骤 4:配置定时器 1 模式(波特率发生器)
// 清零 TMOD 高4位 (对应定时器1的 M0, M1, C/T, GATE)
TMOD &= ~(0X0F << 4);
// 设置 M1=1, M0=0 (TMOD.5 = 1),即模式2:8位自动重装载
TMOD |= (1 << 5);
- 串口方式 1 的波特率由定时器 1提供。
- 模式 2 (8 位自动重装载) 是最常用的方式,定时器溢出时会自动把
TH1的值重装到TL1,无需手动重装,波特率更稳定。
步骤 5:设置波特率初值
// 2400bps 配置:TH1 = TL1 = 232 (十进制)
TL1 = 232;
TH1 = 232;
- 根据公式计算得出的值。在晶振
11.0592MHz、SMOD=1、2400bps下,定时器初值为232。 - 这是通信的节奏,发送端和接收端必须完全一致!
步骤 6:启动定时器
// 设置 TR1 = 1 (TCON.6),启动定时器1
TCON |= (1 << 6);
- 配置好后,必须启动定时器,波特率发生器才开始工作。
步骤 7 & 8:开启中断
IE |= (1 << 7); // EA = 1,开启总中断
IE |= (1 << 4); // ES = 1,开启串口中断
- EA (Enable All):总中断允许位,必须打开,否则任何中断都不会响应。
- ES (Enable Serial):串口中断允许位,打开后,接收完成才会触发
uart_handler函数。
7.3 发送功能模块(轮询方式)
7.3.1 发送单个字符 uart_sendchar(char ch)
void uart_sendchar(char ch)
{
// 1. 将数据写入缓冲寄存器,触发发送硬件
SBUF = ch;
// 2. 等待发送完成
// TI (Transmit Interrupt Flag) 位为1时表示发送完成
while((SCON & (1 << 1)) == 0);
// 3. 手动清除 TI 标志 (非常重要!否则下一次发不出去)
SCON &= ~(1 << 1);
}
- SBUF = ch:这是触发信号。单片机检测到对 SBUF 赋值,就会自动把数据移位发送出去。
- while 等待:
TI (SCON.1)是发送完成标志。如果为 0,说明还在发,死等;为 1 则发送完毕。 - 清 TI:这一步不能忘! 硬件只会置 1,不会自动清 0。如果不清 0,下次调用函数时会一直卡在 while 循环里。
7.3.2 发送字符串 uart_sendstr(const char *p)
void uart_sendstr(const char *p)
{
while(*p) // 直到遇到 '\0' 字符串结束符才停止
{
uart_sendchar(*p++); // 循环调用发送单字符函数
}
}
- 利用指针遍历字符串,每取一个字符就调用一次
uart_sendchar。
7.3.3 发送指定长度数据 uart_sendbuff(const char *p,int len)
void uart_sendbuff(const char *p,int len)
{
while(len--) // 循环 len 次
{
uart_sendchar(*p++);
}
}
- 适合发送二进制数据流(比如你笔记里的
0xAA 0x02 0x01 ...协议帧)。
7.4 接收功能模块(中断方式)
接收使用中断方式,这是嵌入式串口编程的标准做法(效率高,不阻塞主程序)。
7.4.1 全局变量定义
// 接收缓冲区,存储接收到的字节
xdata char recv_buffer[32];
// 缓冲区索引指针
unsigned int pos = 0;
recv_buffer用来存一帧数据。pos记录当前存到第几个位置。- 使用
xdata是为了扩展到外部 RAM(51 内部 RAM 较小,存 32 字节数据通常够用,但按工程规范写)。
7.4.2 中断服务函数 uart_handler(void) interrupt 4
void uart_handler(void) interrupt 4
{
// 1. 判断是否是接收中断 (RI 标志位)
// RI (Receive Interrupt Flag) 位为1表示接收完成
if((SCON & (1 << 0)) == 1)
{
// 2. 防止缓冲区溢出
if(pos < 32)
{
// 3. 读取数据,SBUF 自动清 RI 标志 (部分编译器需手动,此处按逻辑写)
recv_buffer[pos++] = SBUF;
// 4. 字符串补零,方便后续处理 (非必须,但实用)
recv_buffer[pos] = 0;
}
}
// 5. 手动清除 RI 标志 (关键!防止重复中断)
SCON &= ~(1 << 0);
}
interrupt 4:51 单片机中,串口中断的中断号是 4。必须写对,否则程序跑飞。- 判断 RI:
RI (SCON.0)是接收完成标志。 - 读取 SBUF:读取
SBUF会自动将 RI 标志清零(部分老旧编译器可能需手动清,此处代码包含手动清操作,双重保险)。 - 清 RI:无论是否处理数据,都要手动清除标志,否则会一直进入中断死循环。
八、Modbus 协议


核心是:
- 主机发指令 → 从机解析功能码 → 执行动作 → 回复应答
- 帧结构固定,包含帧头 / 地址 / 功能码 / 数据 / 校验 / 帧尾
8.1 主机 → 从机:下发指令帧解析
示例帧:0xAA 0x01 0x01 0x42 0x00 0xEE 0xBB
| 字节序号 | 数值 | 含义 | 说明 |
|---|---|---|---|
| 0 | 0xAA | 起始位 / 帧头 | 固定值,标识一帧数据的开始 |
| 1 | 0x01 | 地址码 | 从机地址,用于多机通信时指定目标设备 |
| 2 | 0x01 | 功能码 | 核心!决定从机要做什么动作 |
| 3 | 0x42 | 数据位 1 | 功能码对应的参数 1 |
| 4 | 0x00 | 数据位 2 | 功能码对应的参数 2 |
| 5 | 0xEE | 校验码 | 校验数据完整性,防止传输错误 |
| 6 | 0xBB | 结束位 / 帧尾 | 固定值,标识一帧数据的结束 |
- Modbus 的标准约定:主机指令帧功能码 = 0x01,从机正常应答帧功能码 = 0x81
- 主机收到
0x81就知道:这是0x01指令的应答,不是新的指令- 同时,错误应答会用
0x80 | 功能码 | 错误码,方便主机判断执行结果
功能码定义(示例)
| 功能码 | 含义 |
|---|---|
| 0x01 | 控制 LED |
| 0x02 | 控制数码管 |
| 0x03 | 控制蜂鸣器 |
| 0x04 | 温度采集 |
所以这一帧的含义是:
主机对地址为 0x01 的从机,下发 控制 LED 的指令,参数为
0x42 0x00,校验码0xEE,帧尾0xBB
8.2 从机 → 主机:应答帧解析
示例应答帧:0xAA 0x01 0x81 0x42 0x00 0x6E 0xBB
关键细节:功能码 + 0x80
标准 Modbus 中,从机应答会把功能码的最高位置 1,表示 “应答帧”。
- 主机发
0x01→ 从机回0x81(0x01 | 0x80) - 这是一种约定:让主机一眼就知道 “这是哪条指令的应答”
| 字节序号 | 数值 | 含义 | 说明 | |
|---|---|---|---|---|
| 0 | 0xAA | 帧头 | 和主机指令帧格式保持一致 | |
| 1 | 0x01 | 地址码 | 回传从机自身地址,主机确认应答来源 | |
| 2 | 0x81 | 功能码(应答) | `0x01 | 0x80`,表示 “LED指令的应答” |
| 3 | 0x42 | 数据位 1 | 回传主机指令的参数,或执行结果 | |
| 4 | 0x00 | 数据位 2 | 同上 | |
| 5 | 0x6E | 校验码 | 应答帧数据的校验和 | |
| 6 | 0xBB | 帧尾 | 固定结束标志 |
8.3 核心流程:主机下发 → 从机解析 → 执行 → 应答
整个交互逻辑可以分为 4 步:
1. 主机下发指令
- 按帧格式打包数据,发送给从机
- 示例:
0xAA 0x01 0x01 0x42 0x00 0xEE 0xBB
2. 从机接收并校验
- 串口中断接收数据,存到缓冲区
- 校验帧头(
0xAA)、帧尾(0xBB)和校验码 - 校验失败:丢弃数据,等待下一帧
3. 解析功能码并执行动作
根据功能码分支执行不同外设操作:
switch(功能码)
{
case 0x01: 控制LED(data1, data2); break;
case 0x02: 控制数码管(data1, data2); break;
case 0x03: 控制蜂鸣器(data1, data2); break;
case 0x04: 温度采集(&data1, &data2); break;
default: 错误处理; break;
}
4. 从机生成应答帧并发送
- 功能码 + 0x80(如
0x01→0x81) - 打包地址、数据、校验码、帧尾
- 发送给主机,完成一次交互
更多推荐

所有评论(0)