第6章 UART串口通信!掌握单片机与外界的双向数据通道,实现跨设备交互

前言

在前五章的学习中,我们已经掌握了单片机IO口双向控制、定时器精准定时、中断系统实时响应、数码管数字显示这些核心能力,实现了单片机本地的输入、输出、逻辑处理、数据显示全流程。但相信大家一直有一个核心痛点:我们的单片机还是一个"信息孤岛",没法把内部的计时数据、按键状态、传感器数值发送到电脑上,也没法通过电脑给单片机发送指令,远程控制硬件的行为。

而解决这个问题的核心,就是本章要学习的UART串口通信。串口是嵌入式开发中最基础、最通用、最不可或缺的通信方式,小到单片机和电脑的调试打印、蓝牙/WiFi模块的指令交互,大到工业设备的参数配置、多机之间的数据传输,都离不开串口通信。它不仅是我们调试程序的"千里眼",更是单片机和外界设备双向交互的核心桥梁。

本章我们依然延续保姆式的讲解风格,从最底层的串行通信原理讲起,全程深度联动前面学过的定时器、中断、C语言数组与字符串处理等核心知识点,零知识跳步、全细节覆盖。我们会先讲透串口通信的核心参数、51单片机串口的底层架构,再拆解波特率的计算逻辑、核心寄存器的配置方法,从最简单的查询式收发,到工业级开发必备的中断式环形缓冲区收发,一步步带着你从原理到实操,彻底搞懂串口通信的全流程。

学完本章,你将彻底掌握UART串口通信的底层原理,能独立完成串口的初始化配置,实现单片机和电脑之间的稳定双向数据收发,能用串口实现程序调试打印、电脑远程控制硬件、数据上传等功能,能独立解决串口开发中最常见的乱码、丢包、收发异常等问题,为后续的蓝牙、WiFi模块驱动、传感器数据上传、多机通信打下最核心的通信基础。


目录

  • 一、本章学习目标
  • 二、核心知识点拆解
    • [2.1 串口通信到底是什么?为什么嵌入式开发离不开它?](#2.1 串口通信到底是什么?为什么嵌入式开发离不开它?)
    • [2.2 UART串口通信的核心参数:波特率、数据位、停止位、校验位](#2.2 UART串口通信的核心参数:波特率、数据位、停止位、校验位)
    • [2.3 51单片机UART串口底层架构与工作模式](#2.3 51单片机UART串口底层架构与工作模式)
    • [2.4 串口波特率计算与定时器配置(精准通信的核心)](#2.4 串口波特率计算与定时器配置(精准通信的核心))
    • [2.5 串口核心配置寄存器详解(SCON/PCON)](#2.5 串口核心配置寄存器详解(SCON/PCON))
    • [2.6 查询式收发vs中断式收发:核心区别与适用场景](#2.6 查询式收发vs中断式收发:核心区别与适用场景)
    • [2.7 工业级串口收发编程规范(新手必守)](#2.7 工业级串口收发编程规范(新手必守))
  • 三、Keil5+STC-ISP保姆式全流程实操
    • [3.1 工程创建与基础环境配置](#3.1 工程创建与基础环境配置)
    • [3.2 入门实操:查询式串口回显(电脑发什么,单片机回什么)](#3.2 入门实操:查询式串口回显(电脑发什么,单片机回什么))
    • [3.3 核心实操:中断式串口收发(带接收缓冲区,非阻塞)](#3.3 核心实操:中断式串口收发(带接收缓冲区,非阻塞))
    • [3.4 进阶实操1:串口指令控制LED与数码管显示](#3.4 进阶实操1:串口指令控制LED与数码管显示)
    • [3.5 进阶实操2:串口打印单片机实时计时数据(电子钟数据上传)](#3.5 进阶实操2:串口打印单片机实时计时数据(电子钟数据上传))
  • 四、保姆式排错指南(新手100%踩坑全覆盖)
  • 五、我的入门踩坑记录
  • 六、课后小练习(带完整可运行标准答案)
  • 七、核心知识点速记
  • 八、本章小结与下一章预告

一、本章学习目标

  1. 彻底理解串行通信与并行通信的核心区别,掌握UART串口通信的底层工作原理
  2. 熟练掌握串口通信的四大核心参数,能根据需求配置对应的通信参数,理解参数不匹配的后果
  3. 掌握串口波特率的计算逻辑,能根据晶振频率算出对应波特率的定时器初值,理解11.0592MHz晶振的核心作用
  4. 熟练掌握串口核心寄存器SCON、PCON的配置方法,能独立完成串口的初始化配置
  5. 掌握查询式和中断式两种串口收发方式,清晰区分两者的优缺点与适用场景
  6. 能独立实现单片机与电脑串口助手的稳定双向通信,解决乱码、丢包、收发异常等常见问题
  7. 掌握工业级串口中断收发的编程规范,能实现带环形缓冲区的非阻塞式收发,不影响主程序运行
  8. 能通过串口实现程序调试打印、电脑远程控制硬件、单片机数据上传等实际应用功能
  9. 深度联动C语言字符串处理、数组、指针、定时器、中断系统等前置知识点,形成完整的知识体系

二、核心知识点拆解

2.1 串口通信到底是什么?为什么嵌入式开发离不开它?

为什么要学:搞懂串口通信的本质,你才会明白它在嵌入式开发中的核心地位,带着目的去学习,而不是死记硬背配置流程。

我们先从最基础的概念讲起:单片机和电脑、单片机和其他设备之间要传递数据,本质上是传递二进制的0和1,而传递的方式分为两大类:并行通信串行通信

并行通信vs串行通信

我们用一个最通俗的比喻来理解:

  • 并行通信 :就像8车道的高速公路,同一时间可以同时跑8辆车,也就是同一时刻可以同时传输8个二进制位(1个字节)的数据。比如我们之前用P0口给数码管输出8位段码,就是并行通信,8个IO口同时传输8位数据。
    • 优点:传输速度快,一个时钟周期就能传1个字节;
    • 缺点:占用IO口多,长距离传输时信号干扰大、成本高,只适合短距离、芯片内部的传输。
  • 串行通信 :就像单车道的乡村公路,同一时间只能跑1辆车,也就是把1个字节的8个二进制位,拆成一位一位的,按顺序逐个传输,只需要1根发送线、1根接收线就能完成双向数据传输。我们本章学的UART串口,就是最典型的串行通信。
    • 优点:占用IO口极少,只需要2根线就能实现全双工双向通信,长距离传输抗干扰能力强、成本极低;
    • 缺点:传输速度比并行通信慢,同一时间只能传1位数据。

UART的全称与核心定义

UART的全称是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),也就是我们常说的串口。它是一种异步串行通信协议,不需要额外的时钟线,只需要两根线就能实现两个设备之间的全双工双向通信:

  • TX线(Transmit):发送线,用于本机向对方发送数据;
  • RX线(Receive) :接收线,用于本机接收对方发送的数据。
      两个设备通信时,必须交叉接线:本机的TX接对方的RX,本机的RX接对方的TX,两个设备必须共地(GND连在一起),这是串口通信的硬件基础。
串口通信在嵌入式开发中的核心价值
  1. 程序调试的核心工具 :这是串口最常用的功能。我们可以通过串口把单片机里的变量值、运行状态、错误信息打印到电脑的串口助手上,就像电脑上C语言的printf函数一样,能快速定位程序问题,是单片机开发的"调试神器"。
  2. 设备之间的双向数据交互:单片机可以通过串口把传感器采集的温度、湿度、距离等数据发送到电脑、蓝牙模块、WiFi模块,也可以接收电脑或其他设备发送的指令,控制LED、电机、继电器等外设,实现远程控制。
  3. 外设模块的驱动基础:绝大多数的嵌入式外设模块,比如蓝牙HC-05、WiFi ESP-01S、GPS模块、GSM模块,都是通过串口发送AT指令来驱动的,学会串口通信,就能驱动市面上90%以上的通信类模块。
  4. 多机通信的核心通道:多个单片机之间可以通过串口实现数据交互,组成分布式控制系统,实现更复杂的功能。

一句话总结:UART串口是一种异步串行通信协议,只用2根线就能实现全双工双向数据传输,是嵌入式开发调试、设备交互、外设驱动的核心基础,是嵌入式工程师必须掌握的核心能力。

2.2 UART串口通信的核心参数:波特率、数据位、停止位、校验位

为什么要学:这四个参数是串口通信的"通信约定",两个设备必须参数完全一致,才能正常通信,只要有一个参数不匹配,就会出现乱码、收不到数据的问题,这是新手最容易踩的坑。

串口是异步通信,没有同步时钟线,两个设备要想正确识别对方发送的0和1,必须提前约定好四个核心参数,就像两个人打电话,必须约定好相同的语言、语速、通话格式,才能正常交流。

1. 波特率(Baud Rate)

波特率是串口通信最核心的参数,它定义了串口通信的数据传输速率 ,单位是bps(比特每秒),也就是每秒钟能传输多少个二进制位。

比如我们最常用的9600波特率,就是指每秒钟能传输9600个二进制位;115200波特率,就是每秒钟传输115200个二进制位。波特率越高,数据传输速度越快,但传输距离越短,抗干扰能力越弱。

行业通用标准波特率 :嵌入式开发中最常用的波特率是9600、19200、38400、115200,其中9600是入门最常用的,兼容性最好,115200是高速传输最常用的。

新手必懂重点:两个通信的设备,波特率必须完全一致,哪怕只差1%,都会出现数据接收错误,导致乱码。这也是为什么51单片机开发板几乎都用11.0592MHz晶振的核心原因------它能生成无误差的标准波特率,而12MHz晶振会有较大的波特率误差。

2. 数据位(Data Bits)

数据位定义了一次传输中,一个数据帧里包含多少个有效数据位,可选值为5、6、7、8位,嵌入式开发中99%的场景都用8位数据位 ,正好对应C语言里的1个unsigned char类型(1字节8位),能完整传输0~255的所有数值。

3. 停止位(Stop Bits)

停止位定义了一个数据帧的结束标志,可选值为1位、1.5位、2位,最常用的是1位停止位。因为串口是异步传输,每个数据帧之间没有固定的时间间隔,停止位用来告诉接收设备:这一帧数据已经传输结束了,准备接收下一帧数据。

4. 校验位(Parity Bit)

校验位是用来做数据传输的错误校验,可选值为无校验(None)、奇校验(Odd)、偶校验(Even),入门开发中最常用的是无校验

它的原理是在数据位后面加1位校验位,通过这一位的数值,保证一帧数据里1的个数是奇数(奇校验)或偶数(偶校验),接收设备可以通过校验位判断数据在传输过程中有没有出错。

新手必记的标准串口配置

8位数据位、1位停止位、无校验位,波特率9600,简称「8N1 9600」,这是嵌入式开发中最通用、兼容性最好的串口配置,90%以上的场景都用这个配置,本章所有实操都基于这个配置。

一句话总结:两个串口设备要正常通信,必须波特率、数据位、停止位、校验位四个参数完全一致,入门优先使用「8N1 9600」的标准配置。

2.3 51单片机UART串口底层架构与工作模式

为什么要学:搞懂51单片机串口的硬件架构,你才会知道代码里的每一步配置到底在控制什么,而不是只会复制粘贴例程的初始化代码。

51单片机串口的核心硬件架构

STC89C52RC单片机内部集成了一个全双工的UART串口,核心由以下几个部分组成:

  1. 两个独立的SBUF数据缓冲区 :SBUF是串口数据缓冲寄存器,物理上有两个独立的8位寄存器,一个是发送缓冲区,一个是接收缓冲区,两个寄存器共用同一个地址0x99。
    • 当你执行SBUF = 0xAA;时,是把数据写入发送缓冲区,硬件自动开始通过TX引脚(P3.1)发送数据;
    • 当串口接收到一帧完整的数据时,硬件会自动把数据存入接收缓冲区,你读取SBUF的值,就是读取接收缓冲区里收到的数据。
    • 全双工的核心就是发送和接收可以同时进行,互不影响,因为有两个独立的缓冲区。
  2. 发送控制器:控制数据的发送流程,把并行的8位数据拆成一位一位的,按照设定的波特率,通过TX引脚逐位发送出去,发送完成后自动把TI标志位置1,告诉CPU:一帧数据发送完成了。
  3. 接收控制器:控制数据的接收流程,通过RX引脚(P3.0)逐位接收数据,把串行的位数据拼成完整的8位并行数据,存入接收缓冲区,接收完成后自动把RI标志位置1,告诉CPU:收到了一帧完整的数据。
  4. 波特率发生器:由定时器1(模式2,8位自动重装)提供,用来设定串口通信的波特率,决定了数据发送和接收的速度。

新手必懂重点:串口的TX引脚固定是P3.1,RX引脚固定是P3.0,这两个引脚是51单片机硬件固定的,无法修改,开发板上的USB转串口芯片,就是接在这两个引脚上的,所以我们才能通过USB线实现单片机和电脑的串口通信。

串口的4种工作模式

通过配置SCON寄存器的SM0和SM1位,可以给串口设置4种工作模式,其中模式1是入门开发最常用、最核心的模式,其他模式仅做简单了解即可。

模式0:移位寄存器模式(同步串行通信)

8位同步移位寄存器模式,波特率固定为系统时钟的1/12,主要用来扩展IO口,比如驱动74HC595芯片,不是真正的UART异步通信,入门阶段极少使用。

模式1:8位异步通信模式(重点掌握,99%场景使用)

这是我们本章的核心模式,一帧数据包含10位:1位起始位(低电平)、8位数据位、1位停止位(高电平),正好对应我们的「8N1」标准配置,波特率由定时器1的溢出率决定,可灵活设置。

数据帧格式

空闲状态(高电平)→ 起始位(0,1位)→ 数据位(D0~D7,8位)→ 停止位(1,1位)→ 回到空闲状态

接收设备检测到起始位的下降沿,就知道有数据要传输了,然后按照设定的波特率,逐位读取8位数据,收到停止位后,就确认一帧数据接收完成。

模式2:9位异步通信模式

一帧数据包含11位:1位起始位、8位数据位、1位可编程位、1位停止位,波特率固定为系统时钟的1/32或1/64,主要用于多机通信,入门阶段极少使用。

模式3:9位异步通信模式

和模式2的帧格式完全一样,唯一区别是波特率由定时器1的溢出率决定,可灵活设置,主要用于多机通信,入门阶段极少使用。

一句话总结:51单片机串口有独立的发送和接收缓冲区,TX固定为P3.1,RX固定为P3.0,入门开发重点掌握模式1(8位异步通信模式),波特率由定时器1的模式2提供。

2.4 串口波特率计算与定时器配置(精准通信的核心)

为什么要学:波特率是串口通信的核心,搞懂波特率的计算逻辑,你才能根据晶振频率和目标波特率,算出正确的定时器初值,从根源上解决串口乱码的问题。

串口模式1的波特率,是由定时器1的溢出率决定的,溢出率就是定时器1每秒钟溢出多少次。我们先回顾一下定时器的知识:定时器1的模式2是8位自动重装模式,TH1存放重装初值,TL1计数溢出后,硬件会自动把TH1的值重装到TL1里,无需手动赋值,非常适合用来做波特率发生器,因为它能产生稳定、无误差的溢出频率。

核心计算公式

模式1下,串口波特率的计算公式为:
波特率 = 2 SMOD 32 × 定时器1溢出率 \text{波特率} = \frac{2^{\text{SMOD}}}{32} \times \text{定时器1溢出率} 波特率=322SMOD×定时器1溢出率

其中:

  • SMOD是PCON寄存器的最高位,SMOD=1时,波特率加倍;SMOD=0时,波特率不加倍,默认值为0;
  • 定时器1溢出率,是指定时器1每秒钟溢出的次数,计算公式为:
    定时器1溢出率 = 晶振频率 12 × ( 256 − TH1初值 ) \text{定时器1溢出率} = \frac{\text{晶振频率}}{12 \times (256 - \text{TH1初值})} 定时器1溢出率=12×(256−TH1初值)晶振频率

我们把两个公式合并,就能得到TH1初值的计算公式:
TH1初值 = 256 − 晶振频率 × 2 SMOD 32 × 12 × 波特率 = 256 − 晶振频率 × 2 SMOD 384 × 波特率 \text{TH1初值} = 256 - \frac{\text{晶振频率} \times 2^{\text{SMOD}}}{32 \times 12 \times \text{波特率}} = 256 - \frac{\text{晶振频率} \times 2^{\text{SMOD}}}{384 \times \text{波特率}} TH1初值=256−32×12×波特率晶振频率×2SMOD=256−384×波特率晶振频率×2SMOD

实战计算(最常用的11.0592MHz晶振,9600波特率,SMOD=0)
  1. 代入公式:
    TH1初值 = 256 − 11059200 × 1 384 × 9600 = 256 − 3 = 253 \text{TH1初值} = 256 - \frac{11059200 \times 1}{384 \times 9600} = 256 - 3 = 253 TH1初值=256−384×960011059200×1=256−3=253
  2. 253转换成十六进制就是0xFD,所以TH1=0xFD,TL1=0xFD。
      重点:这个初值是整数,没有误差,所以11.0592MHz晶振下,9600波特率的误差为0,通信完全稳定,不会出现乱码。
新手必懂:为什么开发板都用11.0592MHz晶振,而不是12MHz?

我们用12MHz晶振,9600波特率,SMOD=1来计算一下:
TH1初值 = 256 − 12000000 × 2 384 × 9600 = 256 − 6.5104 = 249.4896 \text{TH1初值} = 256 - \frac{12000000 \times 2}{384 \times 9600} = 256 - 6.5104 = 249.4896 TH1初值=256−384×960012000000×2=256−6.5104=249.4896

初值必须是整数,我们只能取250(0xFA),代入公式反算波特率,实际波特率约为8928bps,和目标9600bps的误差达到了7%,这么大的误差会导致数据接收错误,出现严重的乱码。

而11.0592MHz晶振的频率,正好能被384×标准波特率整除,算出的初值都是整数,波特率误差为0,通信完全稳定,这就是所有51开发板都用11.0592MHz晶振的核心原因。

新手必记的常用初值表(11.0592MHz晶振,SMOD=0,定时器1模式2)
目标波特率 TH1/TL1初值(十六进制) 波特率误差
1200 0xE8 0%
2400 0xF4 0%
4800 0xFA 0%
9600 0xFD 0%
19200 0xFE 0%
57600 0xFF 0%
115200 0xFF(SMOD=1) 0%

一句话总结:串口模式1的波特率由定时器1模式2的溢出率决定,11.0592MHz晶振能生成无误差的标准波特率,9600波特率对应的TH1初值为0xFD,是入门最常用的配置。

2.5 串口核心配置寄存器详解(SCON/PCON)

为什么要学:串口的所有功能,都是通过配置这两个寄存器实现的,搞懂每一位的作用,你才能独立完成串口的初始化,而不是只会复制例程代码。

寄存器1:SCON(串口控制寄存器),地址0x98

SCON寄存器用来设置串口的工作模式、接收使能、控制发送和接收的标志位,支持位寻址,可以单独修改某一位的值,是串口配置的核心寄存器。

SCON寄存器8位的完整定义如下:

位号 第7位 第6位 第5位 第4位 第3位 第2位 第1位 第0位
位名称 SM0 SM1 SM2 REN TB8 RB8 TI RI
核心功能 模式设置位0 模式设置位1 多机通信控制位 接收使能位 发送第9位数据 接收第9位数据 发送完成标志位 接收完成标志位

我们重点讲解入门开发必须掌握的4个位,其余位入门阶段无需深入:

  1. SM0、SM1:串口工作模式设置位

    用来设置串口的4种工作模式,我们重点用模式1,所以配置为SM0=0,SM1=1。

    SM0 SM1 工作模式 说明
    0 0 模式0 移位寄存器模式
    0 1 模式1 8位异步通信,重点掌握
    1 0 模式2 9位异步通信,固定波特率
    1 1 模式3 9位异步通信,可变波特率
  2. REN:串口接收使能位

    这是新手最容易漏的配置位,必须置1,串口才能接收数据;置0时,串口只能发送,不能接收。

    • REN=1:开启串口接收功能,允许接收数据;
    • REN=0:关闭串口接收功能,禁止接收数据。
        新手必记:只要你需要串口接收数据,初始化时必须把REN置1。
  3. TI:发送完成标志位

    当串口把SBUF里的一帧数据全部发送完成后,硬件会自动把TI置1,告诉CPU:数据发送完成了,可以发送下一帧数据了。

    重点:这个标志位硬件不会自动清零,必须在代码里手动清零,否则串口不会再发送下一帧数据,这是新手100%会踩的坑。

  4. RI:接收完成标志位

    当串口接收到一帧完整的数据,并存入SBUF接收缓冲区后,硬件会自动把RI置1,告诉CPU:收到了一帧数据,可以读取了。

    重点:这个标志位硬件也不会自动清零,必须在代码里手动清零,否则串口不会再接收下一帧数据,这也是新手最容易踩的坑。

寄存器2:PCON(电源控制寄存器),地址0x87

PCON寄存器主要用来控制单片机的电源模式,只有最高位SMOD和串口相关,其余位和串口无关,入门阶段只需要关注SMOD位。

  • SMOD位:波特率加倍位
    • SMOD=1:串口波特率加倍,计算公式里的2^SMOD=2;
    • SMOD=0:串口波特率不加倍,计算公式里的2^SMOD=1,默认值为0。
        注意:PCON寄存器不支持位寻址,不能直接给SMOD位单独赋值,只能整体给PCON寄存器赋值。比如要设置SMOD=1,需要写PCON = 0x80;(0x80是二进制10000000,最高位置1,其余位保持0)。

一句话总结:串口初始化核心配置:SCON=0x50(模式1,开启接收),定时器1设置为模式2,TH1=TL1=0xFD(9600波特率),TR1=1启动定时器1,开启总中断和串口中断(中断式收发需要)。

2.6 查询式收发vs中断式收发:核心区别与适用场景

为什么要学:这是串口开发的两种核心实现方式,搞懂两者的区别,你才能根据实际场景选择合适的方案,写出符合需求的代码。

方式1:查询式收发

查询式收发,就是在主循环里不断查询TI和RI标志位,判断是否发送完成、是否收到数据,然后做对应的处理。

  • 发送流程:给SBUF赋值要发送的数据 → 循环查询TI标志位,直到TI=1 → 手动清零TI,准备下一次发送。
  • 接收流程:主循环里不断查询RI标志位,直到RI=1 → 读取SBUF里的接收数据 → 手动清零RI,准备下一次接收。

优缺点

  • 优点:逻辑简单,代码好理解,适合入门学习,不需要配置中断;
  • 缺点:必须不断循环查询标志位,占用大量CPU资源,主程序会被阻塞,数据接收不及时,容易丢包,工业级开发极少使用。
方式2:中断式收发

中断式收发,就是开启串口中断,当串口发送完成TI置1、或收到数据RI置1时,会自动触发串口中断,CPU暂停主程序,跳转到串口中断服务函数里处理收发事件,处理完成后回到主程序继续运行。

  • 发送流程:给SBUF赋值要发送的数据 → 发送完成后自动触发串口中断,TI置1 → 中断服务函数里判断TI=1,手动清零TI。
  • 接收流程:串口收到一帧数据,RI置1,自动触发串口中断 → 中断服务函数里判断RI=1,读取SBUF里的数据存入缓冲区,手动清零RI。

优缺点

  • 优点:完全非阻塞,不占用主程序CPU资源,数据收发响应及时,不会丢包,是工业级开发的标准方案;
  • 缺点:需要配置中断,逻辑比查询式稍复杂,需要处理接收缓冲区,对新手有一定门槛。

一句话总结:入门学习可以先用查询式理解收发流程,实际开发必须用中断式收发,保证数据收发的实时性和主程序的流畅运行。

2.7 工业级串口收发编程规范(新手必守)

为什么要学:新手写的串口代码,经常出现乱码、丢包、主程序卡死的问题,根源都是没有遵守工业级的编程规范,这里给大家讲6条必须遵守的规范,帮你避开90%的串口坑。

规范1:串口中断服务函数必须极简,快进快出

中断服务函数里只做最核心的操作:判断TI/RI标志位、清零标志位、把收到的数据存入接收缓冲区、发送数据,绝对不要在中断服务函数里写延时、字符串处理、数码管刷新、复杂逻辑判断等耗时代码,否则会导致主程序卡死、中断丢失、数据丢包。

规范2:必须用接收缓冲区存储收到的数据,避免数据覆盖

串口接收数据是实时的,如果收到一个字节就立刻处理,下一个字节来了还没处理完,就会被覆盖,导致丢包。正确的做法是定义一个数组作为接收缓冲区,中断里收到数据就存入缓冲区,主程序里再从缓冲区里取数据处理,实现接收和处理分离,避免数据丢失。

规范3:TI和RI标志位必须及时手动清零,否则会导致收发异常

TI和RI标志位硬件不会自动清零,必须在中断服务函数里,判断完标志位后立刻手动清零,否则串口会一直触发中断,导致程序卡死,无法正常收发下一帧数据。

规范4:发送字符串时,必须等待上一帧发送完成,再发送下一帧

给SBUF赋值后,硬件需要时间发送数据,如果连续给SBUF赋值,上一帧还没发完,下一帧就覆盖了,会导致数据发送错误。正确的做法是:发送一个字节后,等待TI置1,清零TI后,再发送下一个字节。

规范5:长距离、高波特率通信,必须加数据校验和帧头帧尾

如果要传输多字节数据,不能只发裸数据,必须给数据加上帧头、帧尾、校验位,比如用0xAA、0x55作为帧头,0x0D、0x0A作为帧尾,接收方通过帧头帧尾判断一帧完整的数据,避免数据错位,同时通过校验位判断数据是否传输错误。

规范6:串口收发和业务逻辑分离,模块化编程

把串口的初始化、字节发送、字符串发送、中断接收都封装成独立的函数,主程序只需要调用函数即可,不要把收发代码和业务逻辑混在一起,提高代码的可读性和可维护性。

一句话总结:中断服务函数要极简,必须用接收缓冲区,TI/RI标志位及时清零,发送数据要等待上一帧完成,长数据要加帧头帧尾,收发和业务逻辑分离。


三、Keil5+STC-ISP保姆式全流程实操

3.1 工程创建与基础环境配置

工程创建步骤和前几章完全一致,这里简化核心步骤,重点讲代码实现,默认开发板晶振为11.0592MHz,串口使用「8N1 9600」标准配置:

  1. 创建纯英文工程文件夹D:\51_Project\07_UART_Comm
  2. 新建Keil工程,选择Atmel→AT89C52,必须添加STARTUP.A51启动文件;
  3. 创建main.c源文件并添加到工程;
  4. 点击魔法棒图标,在Output选项卡勾选「Create HEX File」;
  5. 把Encoding设置为UTF-8,避免中文乱码。

3.2 入门实操:查询式串口回显(电脑发什么,单片机回什么)

我们先写查询式的串口收发代码,帮你理解串口收发的基础流程,功能:单片机上电后给电脑发送「串口初始化完成!」,电脑通过串口助手给单片机发送数据,单片机收到后立刻把数据回传给电脑,实现串口回显。

c 复制代码
// 入门实操:查询式串口回显
// 11.0592MHz晶振,8N1 9600波特率
#include <reg52.h>

// 串口初始化函数
void UART_Init(void)
{
    // 1. 配置串口工作模式:模式1,8位异步通信,开启接收
    SCON = 0x50; // 二进制01010000,SM0=0,SM1=1,REN=1
    // 2. 配置波特率加倍位,SMOD=0,不加倍
    PCON = 0x00;
    // 3. 配置定时器1为模式2,8位自动重装
    TMOD |= 0x20; // 高4位配置定时器1,模式2
    // 4. 设置定时器1初值,9600波特率
    TH1 = 0xFD;
    TL1 = 0xFD;
    // 5. 启动定时器1
    TR1 = 1;
}

// 串口发送一个字节函数
void UART_Send_Byte(unsigned char dat)
{
    SBUF = dat; // 把数据写入发送缓冲区,开始发送
    while(TI == 0); // 等待发送完成,TI置1
    TI = 0; // 手动清零发送标志位
}

// 串口发送字符串函数
void UART_Send_String(unsigned char *str)
{
    // 循环发送字符串,直到遇到字符串结束符'\0'
    while(*str != '\0')
    {
        UART_Send_Byte(*str++);
    }
}

// 主函数
void main(void)
{
    unsigned char recv_dat;
    UART_Init(); // 初始化串口
    UART_Send_String("串口初始化完成!\r\n"); // 上电发送提示信息
    
    while(1)
    {
        // 查询接收标志位,判断是否收到数据
        if(RI == 1)
        {
            recv_dat = SBUF; // 读取收到的数据
            RI = 0; // 手动清零接收标志位
            UART_Send_Byte(recv_dat); // 把收到的数据回传给电脑
        }
    }
}

代码核心讲解

  1. 串口初始化函数里,我们严格按照步骤配置了SCON、PCON、定时器1,启动定时器1,完成串口的基础配置;
  2. 发送字节函数里,给SBUF赋值后,必须等待TI置1,再手动清零TI,否则无法正常发送下一帧数据;
  3. 主循环里不断查询RI标志位,收到数据后读取SBUF,清零RI,再把数据回传,实现回显功能。
      测试方法:烧录程序后,打开STC-ISP的串口助手,选择对应的串口号,波特率9600,8位数据位,1位停止位,无校验,打开串口,就能收到单片机发送的提示信息,在发送框输入内容,点击发送,就能在接收区看到单片机回传的内容。

3.3 核心实操:中断式串口收发(带接收缓冲区,非阻塞)

接下来我们写工业级标准的中断式串口收发代码,这是实际开发中必须掌握的,功能:带32字节接收缓冲区,中断里接收数据存入缓冲区,主程序里处理数据,实现串口回显,完全非阻塞,不影响主程序运行。

c 复制代码
// 核心实操:中断式串口收发(带接收缓冲区,非阻塞)
// 11.0592MHz晶振,8N1 9600波特率
#include <reg52.h>

// 接收缓冲区定义
#define BUF_SIZE 32 // 缓冲区大小32字节
volatile unsigned char recv_buf[BUF_SIZE]; // 接收缓冲区,加volatile
volatile unsigned char recv_len = 0; // 收到的数据长度
volatile bit recv_flag = 0; // 收到数据的标志位

// 串口初始化函数
void UART_Init(void)
{
    SCON = 0x50; // 模式1,开启接收
    PCON = 0x00; // 波特率不加倍
    TMOD |= 0x20; // 定时器1模式2
    TH1 = 0xFD;
    TL1 = 0xFD; // 9600波特率初值
    ES = 1; // 开启串口中断
    EA = 1; // 开启CPU总中断
    TR1 = 1; // 启动定时器1
}

// 串口发送一个字节
void UART_Send_Byte(unsigned char dat)
{
    SBUF = dat;
    while(TI == 0);
    TI = 0;
}

// 串口发送字符串
void UART_Send_String(unsigned char *str)
{
    while(*str != '\0')
    {
        UART_Send_Byte(*str++);
    }
}

// 串口中断服务函数,中断号4
void UART_Isr(void) interrupt 4
{
    unsigned char dat;
    // 判断是否收到数据
    if(RI == 1)
    {
        dat = SBUF; // 读取收到的数据
        RI = 0; // 清零接收标志位
        
        // 把数据存入接收缓冲区,防止缓冲区溢出
        if(recv_len < BUF_SIZE)
        {
            recv_buf[recv_len++] = dat;
            recv_flag = 1; // 标记收到数据
        }
    }
    // 发送完成标志位,这里我们不需要处理,发送函数里已经清零
    if(TI == 1)
    {
        TI = 0;
    }
}

// 主函数
void main(void)
{
    unsigned char i;
    UART_Init();
    UART_Send_String("中断式串口初始化完成!\r\n");
    
    while(1)
    {
        // 判断是否收到数据
        if(recv_flag == 1)
        {
            // 把缓冲区里的数据回传给电脑
            for(i = 0; i < recv_len; i++)
            {
                UART_Send_Byte(recv_buf[i]);
            }
            // 发送换行符,方便查看
            UART_Send_String("\r\n");
            // 清零缓冲区和标志位,准备下一次接收
            recv_len = 0;
            recv_flag = 0;
        }
        
        // 主程序里可以写其他任何代码,完全不被串口收发影响
        // 比如数码管刷新、按键检测、逻辑处理等
    }
}

代码核心优势

  1. 串口收发完全在中断里处理,主程序只需要处理收到的数据,完全非阻塞,不影响主程序的其他功能;
  2. 带接收缓冲区,不会出现数据覆盖丢包的问题,同时做了缓冲区溢出保护,避免数组越界;
  3. 代码模块化,发送函数封装独立,符合工业级编程规范;
  4. 共用的缓冲区变量加了volatile关键字,避免编译器优化导致主程序读不到数据。

3.4 进阶实操1:串口指令控制LED与数码管显示

我们在中断式收发的基础上,实现串口指令控制硬件的功能:

  1. 电脑发送open,单片机点亮P1_0的LED,串口返回「LED已打开」;
  2. 电脑发送close,单片机熄灭LED,串口返回「LED已关闭」;
  3. 电脑发送num=xxx(xxx是0-999的数字),单片机用3位数码管显示对应的数字,串口返回「数字已显示:xxx」。
c 复制代码
// 进阶实操1:串口指令控制LED与数码管显示
#include <reg52.h>
#include <string.h> // 引入字符串处理头文件

// 硬件定义
sbit LED = P1^0;
#define SEG_PORT P0 // 数码管段选接P0
#define DIG_PORT P2 // 数码管位选接P2

// 缓冲区定义
#define BUF_SIZE 32
volatile unsigned char recv_buf[BUF_SIZE];
volatile unsigned char recv_len = 0;
volatile bit recv_flag = 0;

// 共阳数码管段码表
unsigned char code seg_table[10] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90};
// 位选表
unsigned char code dig_table[3] = {0xFE,0xFD,0xFB};
// 显示缓冲区
volatile unsigned char disp_buf[3] = {0,0,0};
// 要显示的数字
unsigned int display_num = 0;

// 串口初始化
void UART_Init(void)
{
    SCON = 0x50;
    PCON = 0x00;
    TMOD |= 0x20;
    TH1 = 0xFD;
    TL1 = 0xFD;
    ES = 1;
    EA = 1;
    TR1 = 1;
}

// 定时器0初始化:1ms定时,数码管刷新
void Timer0_Init(void)
{
    TMOD |= 0x01;
    TH0 = 0xFC;
    TL0 = 0x67;
    ET0 = 1;
    TR0 = 1;
}

// 串口发送函数
void UART_Send_Byte(unsigned char dat)
{
    SBUF = dat;
    while(TI == 0);
    TI = 0;
}
void UART_Send_String(unsigned char *str)
{
    while(*str != '\0') UART_Send_Byte(*str++);
}

// 定时器0中断服务函数:数码管刷新
void Timer0_Isr(void) interrupt 1
{
    static unsigned char dig_index = 0;
    TH0 = 0xFC;
    TL0 = 0x67;
    
    P2 = 0xFF;
    P0 = seg_table[disp_buf[dig_index]];
    P2 = dig_table[dig_index];
    
    dig_index++;
    if(dig_index >= 3) dig_index = 0;
}

// 串口中断服务函数
void UART_Isr(void) interrupt 4
{
    unsigned char dat;
    if(RI == 1)
    {
        dat = SBUF;
        RI = 0;
        if(recv_len < BUF_SIZE)
        {
            recv_buf[recv_len++] = dat;
            // 检测换行符,标记一帧数据接收完成
            if(dat == '\n' || dat == '\r')
            {
                recv_flag = 1;
            }
        }
    }
    if(TI == 1) TI = 0;
}

// 主函数
void main(void)
{
    unsigned int num;
    LED = 1; // LED默认熄灭
    UART_Init();
    Timer0_Init();
    UART_Send_String("串口指令控制系统初始化完成!\r\n");
    UART_Send_String("支持指令:\r\n1. open - 打开LED\r\n2. close - 关闭LED\r\n3. num=xxx - 显示数字0-999\r\n");
    
    while(1)
    {
        if(recv_flag == 1)
        {
            // 字符串结尾加结束符,方便处理
            recv_buf[recv_len] = '\0';
            
            // 判断指令
            if(strstr(recv_buf, "open") != 0)
            {
                LED = 0;
                UART_Send_String("LED已打开\r\n");
            }
            else if(strstr(recv_buf, "close") != 0)
            {
                LED = 1;
                UART_Send_String("LED已关闭\r\n");
            }
            else if(strstr(recv_buf, "num=") != 0)
            {
                // 提取数字
                sscanf(recv_buf, "num=%d", &num);
                if(num <= 999)
                {
                    display_num = num;
                    // 拆分数字到显示缓冲区
                    disp_buf[0] = display_num / 100;
                    disp_buf[1] = (display_num / 10) % 10;
                    disp_buf[2] = display_num % 10;
                    UART_Send_String("数字已显示:");
                    UART_Send_Byte(disp_buf[0] + '0');
                    UART_Send_Byte(disp_buf[1] + '0');
                    UART_Send_Byte(disp_buf[2] + '0');
                    UART_Send_String("\r\n");
                }
                else
                {
                    UART_Send_String("数字超出范围,仅支持0-999\r\n");
                }
            }
            else
            {
                UART_Send_String("未知指令!\r\n");
            }
            
            // 清零缓冲区
            recv_len = 0;
            recv_flag = 0;
        }
    }
}

3.5 进阶实操2:串口打印单片机实时计时数据(电子钟数据上传)

我们结合之前学的电子钟代码,实现单片机每秒通过串口给电脑上传当前的时间数据,格式为「当前时间:xx时xx分xx秒」,同时可以通过串口发送指令调整时间,实现电脑和单片机的双向交互。

c 复制代码
// 进阶实操2:串口打印电子钟实时数据
#include <reg52.h>
#include <string.h>
#include <stdio.h> // 引入printf头文件

// 缓冲区定义
#define BUF_SIZE 32
volatile unsigned char recv_buf[BUF_SIZE];
volatile unsigned char recv_len = 0;
volatile bit recv_flag = 0;

// 时间变量
unsigned char hour = 0, min = 0, sec = 0;
// 串口打印缓冲区
unsigned char print_buf[32];

// 串口初始化
void UART_Init(void)
{
    SCON = 0x50;
    PCON = 0x00;
    TMOD |= 0x20;
    TH1 = 0xFD;
    TL1 = 0xFD;
    ES = 1;
    EA = 1;
    TR1 = 1;
}

// 定时器0初始化:10ms定时,计时用
void Timer0_Init(void)
{
    TMOD |= 0x01;
    TH0 = 0xD8;
    TL0 = 0xF0;
    ET0 = 1;
    TR0 = 1;
}

// 重写putchar函数,实现printf串口打印
char putchar(char c)
{
    SBUF = c;
    while(TI == 0);
    TI = 0;
    return c;
}

// 定时器0中断服务函数:计时
void Timer0_Isr(void) interrupt 1
{
    static unsigned int count = 0;
    TH0 = 0xD8;
    TL0 = 0xF0;
    
    count++;
    if(count >= 100)
    {
        count = 0;
        sec++;
        if(sec >= 60)
        {
            sec = 0;
            min++;
            if(min >= 60)
            {
                min = 0;
                hour++;
                if(hour >= 24) hour = 0;
            }
            // 每分钟打印一次时间
            printf("当前时间:%02d时%02d分%02d秒\r\n", hour, min, sec);
        }
    }
}

// 串口中断服务函数
void UART_Isr(void) interrupt 4
{
    unsigned char dat;
    if(RI == 1)
    {
        dat = SBUF;
        RI = 0;
        if(recv_len < BUF_SIZE)
        {
            recv_buf[recv_len++] = dat;
            if(dat == '\n' || dat == '\r') recv_flag = 1;
        }
    }
    if(TI == 1) TI = 0;
}

// 主函数
void main(void)
{
    unsigned char h, m, s;
    UART_Init();
    Timer0_Init();
    printf("电子钟串口系统初始化完成!\r\n");
    printf("发送指令:time=hh:mm:ss 可调整时间\r\n");
    
    while(1)
    {
        if(recv_flag == 1)
        {
            recv_buf[recv_len] = '\0';
            if(sscanf(recv_buf, "time=%hhd:%hhd:%hhd", &h, &m, &s) == 3)
            {
                if(h < 24 && m < 60 && s < 60)
                {
                    hour = h;
                    min = m;
                    sec = s;
                    printf("时间已调整为:%02d时%02d分%02d秒\r\n", hour, min, sec);
                }
                else
                {
                    printf("时间格式错误!格式为time=hh:mm:ss\r\n");
                }
            }
            else
            {
                printf("未知指令!发送time=hh:mm:ss调整时间\r\n");
            }
            recv_len = 0;
            recv_flag = 0;
        }
    }
}

代码亮点 :我们重写了putchar函数,实现了C语言标准库的printf函数,能直接格式化打印字符串和变量,就像电脑上的C语言开发一样,极大地方便了程序调试和数据打印,是单片机开发中最常用的调试技巧。


四、保姆式排错指南(新手100%踩坑全覆盖)

异常现象/报错信息 核心根因 一步到位的解决方法
串口完全没反应,电脑收不到任何数据 1. 串口初始化没启动定时器1;2. 发送数据后没清零TI标志位;3. 串口号选错、波特率不匹配;4. 开发板CH340驱动没安装;5. TX/RX引脚接反了 1. 初始化代码必须加TR1=1启动定时器1;2. 发送完数据必须手动清零TI;3. 确认串口助手波特率、数据位、停止位、校验位和单片机配置完全一致;4. 重装CH340驱动,确认串口号正确;5. 确认单片机TX接电脑RX,单片机RX接电脑TX
串口收到的数据全是乱码 1. 波特率不匹配,晶振频率和初值不对应;2. 晶振是12MHz,波特率误差太大;3. 数据位、停止位、校验位不匹配;4. SMOD波特率加倍位设置错误;5. 串口模式配置错误 1. 确认晶振频率,重新计算定时器初值,11.0592MHz晶振9600波特率初值为0xFD;2. 更换11.0592MHz晶振,避免波特率误差;3. 确认串口助手和单片机的四个参数完全一致;4. 确认PCON寄存器的SMOD位配置正确;5. 确认SCON=0x50,配置为模式1
单片机只能发一次数据,之后再也发不出来 发送完数据后,没有手动清零TI标志位 发送完一帧数据,等待TI置1后,必须手动写TI=0清零标志位,否则串口不会再发送下一帧数据
单片机收不到电脑发送的数据 1. 初始化时没把REN位置1,没开启接收功能;2. 收到数据后没清零RI标志位;3. 串口中断没开启ES=1;4. 总中断EA没开启 1. 初始化时SCON必须配置为0x50,REN=1开启接收;2. 收到数据后必须手动写RI=0清零标志位;3. 中断式收发必须开启ES=1串口中断;4. 必须开启EA=1总中断
串口接收数据丢包、错位 1. 用查询式收发,主程序循环太长,没及时查询RI标志位;2. 没有接收缓冲区,收到数据没及时处理被覆盖;3. 中断服务函数里代码太长,耽误了下一次接收;4. 波特率太高,传输不稳定 1. 必须用中断式收发,保证接收及时;2. 定义接收缓冲区,中断里只存数据,主程序里处理;3. 中断服务函数里只做标志位清零和数据存储,不要写复杂逻辑;4. 降低波特率,优先用9600bps
开启串口后,定时器1的功能失效了 串口波特率发生器占用了定时器1的模式2,定时器1不能再用来做其他定时功能 串口的波特率发生器必须用定时器1的模式2,不要用定时器1做其他功能,定时功能用定时器0实现
printf函数无法打印,编译报错 1. 没有重写putchar函数;2. 没有开启总中断和串口中断;3. 串口没有正确初始化 1. 必须重写putchar函数,把字符输出到SBUF;2. 正确初始化串口,开启总中断和串口中断;3. 确保串口能正常发送单个字节
串口中断触发后,程序卡死、主程序不运行 1. 中断服务函数里没有清零TI/RI标志位,导致一直触发中断;2. 中断服务函数里写了死循环或长延时;3. 接收缓冲区溢出,数组越界导致程序跑飞 1. 中断服务函数里必须判断TI/RI标志位,然后立刻清零;2. 中断服务函数里绝对不能写延时和死循环;3. 给接收缓冲区加溢出保护,recv_len不能超过缓冲区大小

五、我的入门踩坑记录

踩坑记录1:没开REN位,串口死活收不到数据

- 坑的现象 :我最开始写串口代码,初始化SCON写的是0x40,只配置了模式1,没把REN位置1,结果串口能正常发送数据,但是电脑发什么单片机都收不到,RI标志位永远不会置1,找了一下午都没发现问题。

- 背后的原理 :REN位是串口接收使能位,必须置1,串口的接收功能才会开启,置0时串口只能发送,不能接收,哪怕电脑发了数据,单片机也不会接收,RI标志位自然不会置1。

- 解决方案:我把SCON的配置改成了0x50,把REN位置1,重新烧录后,串口立刻就能正常接收数据了。从那以后,我写串口初始化代码,第一个想到的就是把REN位置1。

踩坑记录2:没清零TI标志位,只能发一次数据

- 坑的现象 :我写了串口发送字符串的函数,结果单片机上电只能发送一次字符串,之后再也发不出任何数据,单步调试发现TI标志位一直是1,再也不会变0。

- 背后的原理 :TI标志位硬件不会自动清零,发送完成后必须手动清零,否则串口会认为上一帧还没发完,不会开始发送下一帧数据,自然就发不出新的内容了。

- 解决方案:我在发送函数里,等待TI置1后,立刻加了TI=0清零标志位,修改后字符串发送完全正常,连续发送也不会出问题了。

踩坑记录3:用12MHz晶振,9600波特率乱码严重

- 坑的现象 :我用12MHz晶振的开发板,按照网上的例程设置TH1=0xFA,9600波特率,结果串口收到的数据全是乱码,偶尔能收到几个正确的字符,大部分都是乱的,换了好几个初值都没用。

- 背后的原理 :12MHz晶振下,9600波特率的初值不是整数,有7%的误差,这么大的误差会导致接收设备无法正确识别每一位数据,自然就会出现乱码。

- 解决方案:我换了11.0592MHz的晶振,设置TH1=0xFD,重新烧录后,串口通信完全正常,再也没有乱码了。从那以后,我做串口开发,一定会用11.0592MHz的晶振。

踩坑记录4:串口中断里没清零RI,程序直接卡死

- 坑的现象 :我写了串口中断服务函数,结果一收到数据,单片机就直接卡死,主程序完全不运行,数码管也不刷新了,仿真发现程序一直在中断服务函数里循环,根本不回到主程序。

- 背后的原理 :我在中断服务函数里读取了SBUF的数据,但是没有清零RI标志位,RI一直是1,串口会一直触发中断,CPU一直在进入中断服务函数,根本没时间执行主程序,自然就卡死了。

- 解决方案:我在读取完SBUF后,立刻加了RI=0清零标志位,修改后中断触发正常,主程序也能正常运行,再也没有卡死的情况了。

踩坑记录5:接收缓冲区没加溢出保护,数组越界导致程序跑飞

- 坑的现象 :我写了中断式串口收发,定义了32字节的接收缓冲区,结果电脑一次性发送超过32字节的数据,单片机就直接程序跑飞,数码管乱闪,所有功能都失效了。

- 背后的原理 :我在中断里收到数据就直接存入缓冲区,没有判断recv_len是否超过缓冲区大小,当数据超过32字节时,就会出现数组越界,改写了其他内存区域的数值,导致程序跑飞。

- 解决方案:我在存入数据前,加了if(recv_len < BUF_SIZE)的判断,只有缓冲区没满的时候才存入数据,避免了数组越界,修改后哪怕发送超长数据,程序也不会跑飞了。


六、课后小练习(带完整可运行标准答案)

6.1 基础巩固练习(4道)

练习1:实现串口调试打印功能

需求说明 :重写putchar函数,实现printf串口打印,单片机每秒通过串口打印一次当前的秒数,格式为「当前秒数:xx」。
拓展思考:如果要打印浮点数,需要怎么配置Keil工程?

练习2:串口控制LED流水灯模式

需求说明 :电脑发送指令mode1,流水灯从左到右;发送mode2,流水灯从右到左;发送stop,流水灯停止,串口返回对应的执行结果。
拓展思考 :如果要实现发送speed=xx调整流水灯速度,怎么修改代码?

练习3:多字节数据帧收发

需求说明 :定义数据帧格式为「帧头0xAA+帧头0x55+1字节数据长度+N字节数据+帧尾0x0D+帧尾0x0A」,实现单片机接收该格式的帧数据,校验帧头帧尾,提取有效数据并回传给电脑。
拓展思考:如果要加和校验,怎么修改代码?

练习4:串口指令控制数码管显示

需求说明 :电脑发送6位数字,单片机用6位数码管显示对应的数字,串口返回「显示成功:xxxxxx」,如果数字格式错误,返回「格式错误」。
拓展思考:如果要实现小数点显示,怎么修改指令格式和代码?

6.2 进阶实战练习(2道)

练习1:串口指令控制的多功能电子钟

需求说明:在本章电子钟的基础上,增加闹钟功能,可通过串口设置闹钟时间,时间到了蜂鸣器报警,发送指令可关闭闹钟,同时支持12/24小时制切换,所有功能都通过串口指令控制。

练习2:串口上位机与单片机的温湿度数据采集系统

需求说明 :单片机通过DHT11传感器采集温湿度数据,每秒通过串口上传到电脑,格式为「温度:xx℃ 湿度:xx%RH」,电脑发送指令read,单片机立刻上传一次数据,发送指令interval=xx,设置数据上传间隔。


七、核心知识点速记

  1. UART串口是异步串行通信协议,只用TX(P3.1)和RX(P3.0)两根线就能实现全双工双向通信,两个设备必须交叉接线、共地。
  2. 串口通信四大核心参数:波特率、数据位、停止位、校验位,两个设备必须完全一致才能正常通信,入门优先使用「8N1 9600」标准配置。
  3. 51单片机串口模式1是8位异步通信模式,一帧数据10位,波特率由定时器1的模式2(8位自动重装)决定,是入门最常用的模式。
  4. 11.0592MHz晶振能生成无误差的标准波特率,9600波特率对应的定时器1初值为TH1=TL1=0xFD,是开发板最常用的晶振。
  5. 串口核心配置:SCON=0x50(模式1,开启接收REN=1),定时器1模式2,设置对应波特率初值,TR1=1启动定时器1,ES=1开启串口中断,EA=1开启总中断。
  6. TI是发送完成标志位,RI是接收完成标志位,硬件不会自动清零,必须在代码里手动清零,否则会导致收发异常、程序卡死。
  7. 入门学习可用查询式收发理解流程,实际开发必须用中断式收发,保证数据收发的实时性,完全不阻塞主程序运行。
  8. 中断式收发必须定义接收缓冲区,中断里只存数据,主程序里处理数据,避免数据覆盖丢包,同时必须加缓冲区溢出保护。
  9. 重写putchar函数,就能实现C语言标准printf函数的串口打印,是单片机程序调试的核心技巧。
  10. 串口中断服务函数必须极简,只做标志位清零和数据存储,绝对不能写延时、复杂逻辑和死循环,否则会导致程序卡死、中断丢失。

八、本章小结与下一章预告

本章我们从串行通信的底层原理出发,彻底搞懂了UART串口通信的核心参数、51单片机串口的硬件架构,掌握了波特率的计算逻辑、核心寄存器的配置方法,从最简单的查询式收发,到工业级的中断式缓冲区收发,一步步实现了单片机和电脑之间的稳定双向通信,最终完成了串口指令控制硬件、电子钟数据上传的综合实战项目,联动了前面学的定时器、中断、数码管、LED等所有核心知识点。

串口通信是嵌入式开发的核心基础,它不仅是我们调试程序的"千里眼",更是单片机和外界设备交互的核心桥梁。本章学到的异步通信逻辑、中断式收发、数据帧处理、模块化编程思想,不仅适用于51单片机的串口开发,更是后续I2C、SPI总线通信、蓝牙/WiFi模块驱动、多机通信的通用核心逻辑,能100%无缝迁移到后续的进阶开发中。

在本章的学习中,我们实现了单片机和电脑之间的大数据量通信,也用数码管实现了简单的数字显示,但数码管只能显示数字,无法显示字母、汉字和更复杂的内容。在下一章中,我们会专门讲解LCD1602液晶显示屏的驱动开发,搞懂字符型液晶的底层驱动原理,实现字母、数字、自定义字符的显示,打造更丰富的人机交互界面,为后续的传感器数据显示、菜单系统开发打下更高级的显示基础。我们下一章,不见不散!

相关推荐
大尚来也1 分钟前
PHP 反序列化漏洞深度解析:从原理利用到 allowed_classes 防御实战
android·开发语言·php
雕刻刀4 分钟前
ERROR: Failed to build ‘natten‘ when getting requirements to build wheel
开发语言·python
qq_416018724 分钟前
高性能密码学库
开发语言·c++·算法
小谦32517 分钟前
NTC热敏电阻分压测量电路的数学特性与应用选择研究
stm32·嵌入式硬件
小碗羊肉13 分钟前
【从零开始学Java | 第十八篇】BigInteger
java·开发语言·新手入门
宵时待雨16 分钟前
C++笔记归纳14:AVL树
开发语言·数据结构·c++·笔记·算法
执笔画流年呀30 分钟前
PriorityQueue(堆)续集
java·开发语言
爱编码的小八嘎30 分钟前
C语言完美演绎5-3
c语言
山川行33 分钟前
关于《项目C语言》专栏的总结
c语言·开发语言·数据结构·vscode·python·算法·visual studio code
呜喵王阿尔萨斯35 分钟前
C and C++ code
c语言·开发语言·c++