一、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;

注意:收发两端波特率必须相同!

2. 数据位(Data Bits)

  • 表示真正传输的数据长度
  • 常用:8 位(最标准)
  • 对应二进制:0000 0000 ~ 1111 1111

3. 校验位(Parity)

用于简单检错,有三种:

  • N(None):无校验
  • O(Odd):奇校验
  • E(Even):偶校验

奇校验规则

数据中 1 的个数 + 校验位 = 奇数个 1

偶校验规则

数据中 1 的个数 + 校验位 = 偶数个 1

奇偶校验无法检测偶数个位出错!

4. 停止位(Stop Bits)

  • 表示一帧数据结束
  • 常用:1 位
  • 作用:给接收端一个 “帧结束” 的标志

最常用组合

  1. 9600 8 N 1波特率 9600,8 位数据,无校验,1 位停止位
  2. 115200 8 N 1高速通信,最常用
  3. 115200 8 E 1带偶校验
  4. 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.0592MHzSMOD=12400bps 下,定时器初值为 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。必须写对,否则程序跑飞。
  • 判断 RIRI (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 → 从机回 0x810x01 | 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(如 0x010x81
  • 打包地址、数据、校验码、帧尾
  • 发送给主机,完成一次交互
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐