
Ciallo(∠・ω< )⌒☆
| 系列专栏 | 直达链接 |
|---|---|
| 🔤 C 语言 | 进入专栏 → |
| 🧱 数据结构 | 进入专栏 → |
| 🔧 C++ | 进入专栏 → |
| 🐍 Python | 进入专栏 → |
| 🔌 STM32 | 进入专栏 → |
🏠 博客主页:暖焰核心
文章目录
- 引言
-
- 一、通信的方式
-
- [1.1 串行还是并行](#1.1 串行还是并行)
- [1.2 有没有时钟线](#1.2 有没有时钟线)
- [1.3 单工、半双工、全双工](#1.3 单工、半双工、全双工)
- [1.4 一对一](#1.4 一对一)
- 二、波特率与比特率
-
- [2.1 比特率](#2.1 比特率)
- [2.2 波特率与码元](#2.2 波特率与码元)
- [2.3 串口里两者相等](#2.3 串口里两者相等)
- 三、帧格式
-
- [3.1 空闲状态](#3.1 空闲状态)
- [3.2 起始位](#3.2 起始位)
- [3.3 数据位](#3.3 数据位)
- [3.4 校验位](#3.4 校验位)
- [3.5 停止位](#3.5 停止位)
- [3.6 0x55 的完整一帧](#3.6 0x55 的完整一帧)
- 四、接线
- 五、移位寄存器与标志位
-
- [5.1 数据的搬运路线](#5.1 数据的搬运路线)
- [5.2 覆盖问题与发送标志位](#5.2 覆盖问题与发送标志位)
- [5.3 接收标志位 RXNE](#5.3 接收标志位 RXNE)
- [5.4 TDR 与 RDR 同一个地址](#5.4 TDR 与 RDR 同一个地址)
- 六、收发函数的阻塞语义
-
- [6.1 阻塞是怎么个阻塞法](#6.1 阻塞是怎么个阻塞法)
- [6.2 两个收发函数](#6.2 两个收发函数)
- [6.3 句柄是什么](#6.3 句柄是什么)
- [七、STM32 里的 USART](#七、STM32 里的 USART)
-
- [7.1 五个串口与两条总线](#7.1 五个串口与两条总线)
- [7.2 寄存器与地址](#7.2 寄存器与地址)
- [7.3 USART 与 UART](#7.3 USART 与 UART)
引言
点灯、按键、时钟都齐了,芯片该跟别人说话了。
芯片对外的引脚,一次只能给出一个电平。要传一个字节,8 个比特,最直接的办法是并排拉 8 根线一次发完,这就是并行通信。但单片机世界里主导一切的是另一条路:一根线,把 8 个比特按时间顺序一位一位排着发,这就是串行通信
一、通信的方式
1.1 串行还是并行
把一个字节从 A 送到 B,物理上就两种走法。
并行通信一次上多条数据线(8 条、16 条、32 条),一个字节的所有位同时出发,一人一条线。听着痛快,毛病也实在:
- 成本高:线多,PCB 布线难,接口插件贵;
- 速率受限:所有数据位必须同时有效才能被正确采样,物理上做不到绝对同步,时钟只能放慢,给走得慢的信号留时间;
- 距离短:线与线之间串扰严重,基本只能板级通信;
- 扩展性差:16 位升 32 位,连线得重铺。
串行通信只用一条(或一对)数据线,字节的各个位按时间顺序一比特接一比特地走。线少了,每一位反而可以走得更干脆、传得更远。STM32 世界里的主流通信技术,SPI、I2C、CAN、USART,全是串行的。
注意:串行不等于 USART 串口。串口只是串行通信的一种具体实现,这个系列要写的正是它。
1.2 有没有时钟线
同步与异步的分界线就一条:有没有专门的时钟线。
同步通信里,发送方额外送出一根时钟线,接收方在时钟的边沿上采数据,节奏由时钟统一指挥:
text
同步通信:时钟线统一节拍
CLK _|-|_|-|_|-|_|-| 在时钟边沿处采样
DATA 1 0 1 1 0 0 每个边沿读一位
异步通信没有这根线,接收方不知道对方什么时候发、什么节奏。它靠两个约定补上:一是约定好每一位占用多长时间,这就是波特率;二是约定好从什么信号开始数、一共数多少位,这就是帧格式。USART 没有时钟线,是异步通信。
1.3 单工、半双工、全双工
按传输方向分三档:
- 单工:只能一个方向,A 到 B,B 说不了话;
- 半双工:两个方向都行,但同一时刻只能走一个。就是人正常聊天,我说你听,你说我听;
- 全双工:两个方向同时进行。就是两个人吵架,你说你的,我说我的,互不耽误。
USART 有独立的 Tx、Rx 两条信号线,收发同时进行,是全双工。但别把「串口」和「全双工」绑死:基于串口扩展出来的 RS-485 就是半双工。
1.4 一对一
串口单元只跟另一个串口单元说话,一个串口对多个设备的场面它撑不起来,那是 I2C、SPI 的舞台。而且双方没有主从,地位对等:规则一样,谁都能先开口。这点跟 I2C、SPI 那种主从架构很不一样。
二、波特率与比特率
2.1 比特率
比特率是每秒钟传送的比特个数,也叫传信率,单位 bit/s(也写 bps)。家里装的千兆带宽,那个「兆」就是它:1000 Mbit/s,意思是网口每秒最多收发 1000×10^6 个比特。
有个容易踩的小坑:传输速率里的 K、M、G 按十进制走(103、106、10^9),存储容量里的 K、M、G 才按 1024 走。1000 Mbit 的宽带换算成字节,是 1000 Mbit ÷ 8 = 125 MB/s,这里的 M 是十进制的 10^6。
2.2 波特率与码元
波特率衡量的是另一件事:每秒钟传送的码元个数,也叫传码率,单位波特(Baud)。
码元(也叫符号)是通信系统里携带数据信息的信号单元,一次可以只携带 1 个比特,也可以携带多个。打个比方:张三给李四扔球,约好四种颜色各代表一个意思,蓝 00、绿 01、黄 10、红 11。每扔一个球,就是发送一次信息,一个球就是一个码元;球的颜色不同,载荷不同,一个球顶两个比特。张三每秒扔一个球,波特率是 1 Baud,比特率却是 2 bit/s。
两个率的关系一句话:比特率 = 波特率 × 每个码元携带的比特数。
2.3 串口里两者相等
USART 传信号用的是高低电平两种状态,一个码元要么高要么低,只携带 1 个比特,属于 2 进制码元。这时比特率 = 波特率,所以串口工具里把波特率写成 9600 b/s、115200 b/s,单位不算严谨,大家也都能懂。
串口最常用的波特率就是 9600 和 115200。拿 115200 算一笔账:按 1 起始位 + 8 数据位 + 无校验 + 1 停止位的帧格式,一帧 10 位,每秒能传 115200 ÷ 10 = 11520 个字节。不算快,但对调试打印、传感器数据这个量级绰绰有余。
三、帧格式
波特率解决了快慢的问题,还剩一件事:接收方怎么知道数据开始发了、发了多少、什么时候结束。双方约定的这套格式叫帧格式,通信领域也叫协议,单片机圈更爱叫帧格式。
一帧的标准结构:空闲状态 → 起始位 → 数据位 → 校验位 → 停止位,然后回到空闲。
text
空闲(高) -> 起始(低) -> b0 b1 ... b7 低位在前 -> 校验(可选) -> 停止(高) -> 回到空闲

其中空闲、起始、停止三段任何一帧都必须有;数据位长度可选 8 或 9,校验位可选有无。可变的不可怕,只要双方配成一样,各自都能解对。
3.1 空闲状态
没有数据传输时,线路保持逻辑高电平。这个默认高不是随便挑的:起始位要靠拉低来标记,线路平时就得停在高处,才给下一帧留出「从高变低」的动作空间。
3.2 起始位
起始位就是那个下降沿(1 变 0),只持续 1 个比特的时间,作用是告诉接收端:新的一帧来了。这是接收端做比特同步的关键信号,从这一刻起,接收端按波特率定好的时间格,一格一格往后数。
3.3 数据位
STM32 的数据位可选 8 位或 9 位,最常用 8 位,正好一个字节。
发送顺序是低位在前(LSB first):最低有效位最先出门,然后次低位,最后是最高有效位。注意两个缩写:LSB 是 Least Significant Bit,最低有效位,二进制数里最右边那位;MSB 是 Most Significant Bit,最高有效位,最左边那位。
要发 0x55(0101 0101),线上依序出现的是 1、0、1、0、1、0、1、0。顺序别想反了,这是串口新手常栽的一个坑。
3.4 校验位
校验位是个简单的错误检测机制,可选无校验、奇校验、偶校验:
- 无校验:不发这一位;
- 奇校验:让数据位加校验位里 1 的总个数为奇数;
- 偶校验:让 1 的总个数为偶数。
比如数据 11110000 里有 4 个 1:配奇校验就补 1,凑成 5 个;配偶校验就补 0,保持 4 个。
这招的局限也要说清:它只能检测奇数个比特翻转,偶数个翻转它视而不见,而且发现了也没法纠正。拿偶校验想:一袋苹果必须凑偶数个,路上坏 1 个,收货一数变奇数,立刻喊停;路上要是坏了 2 个,总数还是偶数,神不知鬼不觉。
3.5 停止位
停止位标记一帧的结束,固定高电平,可以配 1 位、1.5 位或 2 位,指的是高电平保持多少个比特时间。结束后线路回到空闲。
为什么结束一定要拉高?假如没有停止位,线路电平就停在数据最后一位的值上,高低都有可能。下一帧的起始位靠下降沿标记,前一个电平要是本来就低,这个下降沿就不成立了。强制拉高保证每帧结束到下帧开始之间,线路上至少有 1 个比特周期的高电平,下一帧的起始动作永远做得出来。
3.6 0x55 的完整一帧
发送 0x55(0101 0101),配 1 起始 + 8 数据 + 奇校验 + 1 停止,一共 11 位。
0x55 里有 4 个 1,奇校验补 1。数据位低位在前,线上依次是 1、0、1、0、1、0、1、0,跟着校验位 1,前后各包上拉低的起始位和拉高的停止位。接收方从下降沿开始按时间格逐位采样,数满约定的 11 位,取出中间 8 位,0x55 就到了。
注意:帧是按字节独立封的。连发 5 个字节就是 5 个帧,每个字节都带自己的起始位和停止位,不是一个大火车皮装 5 个字节。
四、接线
text
MCU 串口设备
+------------+ +------------+
| TX | -------------------> | RX |
| | | |
| USART | | 串口模块 |
| | | |
| RX | <------------------- | TX |
| | | |
| GND | -------------------- | GND |
+------------+ +------------+
Tx 接对端 Rx,Rx 接对端 Tx,交叉相连,再接上公共地。发送方发 1 就输出高电平,对面 Rx 上出现高电平;发 0 出低,对面读低。数据就是这样一位一位传过去的。
Tx/Rx 里的 x,有一种好记的说法是它长得就像两根线交叉的样子。
MCU 引脚上跑的是 TTL 电平,两边都是芯片的话直接对连就行。引脚本身还是 GPIO 复用出来的:配置串口的时候,对应的 GPIO 也得配成复用功能,信号才能从引脚出去。
五、移位寄存器与标志位
5.1 数据的搬运路线

手册里的 USART 功能框图,我们可以简化一下:
text
发送:用户写入 硬件搬运 硬件移位
代码 --------> TDR ----------> 发送移位寄存器 --------> TX 引脚
|
+-- TXE 置 1:TDR 空了,可以写下一个
接收:RX 引脚
--------> 接收移位寄存器 ----------> RDR ----------> 用户读取
凑满约定的位数 |
+-- RXNE 置 1:RDR 有数据可读
发送时,用户把字节写进 TDR(Transmit Data Register,发送数据寄存器),USART 硬件把它整批搬进发送移位寄存器,再按波特率的节拍把位一个个从 Tx 引脚送出去。接收是镜像过程:Rx 引脚上的位逐个移进接收移位寄存器,凑满约定的位数,整批搬进 RDR(Receive Data Register,接收数据寄存器),等用户来读。
两个移位寄存器在 USART 模块内部,用户看不见也摸不着。对用户来说,除了 TDR 和 RDR,其余全是黑盒,搬来移去都是 USART 自己干完的。
5.2 覆盖问题与发送标志位
发送不是一步到位的:TDR 里的数据搬进移位寄存器需要时间,移位寄存器一位一位发出去也需要时间。要是发送还没完,用户又往 TDR 写了一个字节呢?
USART 的真实做法是排队:只要移位寄存器空了,就把 TDR 里的数据搬过去,TDR 继续接下一个字节。但用户完全看不到这套内部调度。要是 TDR 里的字节还没被搬走,用户再往里写一个,新字节就把旧的盖了,数据就这么丢了。
所以 USART 必须把内部状态亮出来,这就是状态寄存器 USART_SR。发送侧先看两个标志位:
TXE(Transmit data register empty):TDR 里的数据被硬件搬进移位寄存器后置 1,意思是「我空了,可以写下一个」。对USART_DR的写操作会把它自动清 0;TC(Transmission complete):一帧发完、且TXE已经是 1 时置 1,意思是移位寄存器和 TDR 都空了,数据真的全部发完。
两个标志位分工明确:TXE 回答「还能不能再写」,TC 回答「是不是彻底发完了」。该盯哪个,取决于你想问哪个问题。连发三帧时它们怎么变化,手册给过一张时序图:

5.3 接收标志位 RXNE
接收侧对应的是 RXNE(Read data register not empty):接收移位寄存器的数据搬进 RDR 后硬件置 1,用户读 USART_DR 会把它清 0;RDR 没数据时它是 0。
它的用法跟 TXE 正好反着:TXE 等它变 1 好去写,RXNE 等它变 1 好去读。
还有一条手册明说的规矩:RXNE 还没清零时又来一个字节,就是溢出错误,新来的数据丢,RDR 里保住的是上一个数据。所以接收不能攒着不读。
5.4 TDR 与 RDR 同一个地址
手册的寄存器表里,USART_DR 这个名字底下藏着两个物理寄存器,发送用的 TDR 和接收用的 RDR,两个被映射到同一个地址。往这个地址写,数据落进 TDR;从这个地址读,出来的是 RDR。
具体怎么做到的,手册没有正面披露。一个说得通的推测是硬件按操作方向区分:写访问落 TDR,读访问落 RDR,这是我标明的推测,不是手册原文。从使用者角度,记住「写就是发、读就是收」就够了。
六、收发函数的阻塞语义
6.1 阻塞是怎么个阻塞法
阻塞就是程序停在原地反复查一个条件,查到为止。简单可靠,代价是 CPU 在这干等,一步不能挪。具体到收发两侧:
发送:把数据写进 TDR,然后盯 TXE,等它变 1(TDR 空了)再写下一个;全部写完再盯 TC,等它变 1,确认彻底发完。
接收:盯 RXNE,是 0 就在原地等;变 1 说明到了一个字节,读走(读的同时 RXNE 自动清 0),接着等下一个;收够个数,收工。
6.2 两个收发函数
HAL 把这套流程封装成两个函数,代码如下:
c
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,
const uint8_t *pData,
uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size, uint32_t Timeout);
HAL_UART_Transmit 参数说明:
huart:串口句柄,指向要用的那个串口;pData:要发出去的数据放在哪,const表示函数只读不改;Size:发多少个字节;Timeout:等标志位的耐心上限,单位 ms。
返回值:HAL_StatusTypeDef,取值 HAL_OK / HAL_ERROR / HAL_BUSY / HAL_TIMEOUT。
HAL_UART_Receive 参数说明:
huart:串口句柄;pData:读到的数据往哪放,是输出参数;Size:读多少个字节;Timeout:同上。
返回值:同上。
Timeout 有三种取法:
0xFFFFFFFF(HAL 里叫HAL_MAX_DELAY):标志位没就绪就一直等下去,纯阻塞;0:一次都不等,条件不满足立刻返回;- 具体数值比如 1000:最多等 1000 ms,等到就继续,等不到带着超时错误返回。后两种都算非阻塞。
6.3 句柄是什么
huart 的类型是 UART_HandleTypeDef,一个把某个串口家底打包起来的结构体:寄存器基地址、波特率配置、收发状态都在里面。函数拿到句柄,就知道自己在操作哪个串口、它现在什么状态。
类比 C 语言的文件操作:fopen 返回一个 FILE*,之后的读写都带着它,不必关心文件系统内部怎么运转。句柄干的是同一件事:拿着它说话,底层细节它去操心。
七、STM32 里的 USART
7.1 五个串口与两条总线
STM32F103ZET6 内置 5 个串口模块:USART1、USART2、USART3、UART4、UART5。注意名字,前三个叫 USART(多同步功能),后两个直接叫 UART(纯异步)。表格如下:
| 外设 | 所在总线 | 起始地址 | 类型 |
|---|---|---|---|
USART1 |
APB2 |
0x4001 3800 |
通用同步/异步收发器 |
USART2 |
APB1 |
0x4000 4400 |
通用同步/异步收发器 |
USART3 |
APB1 |
0x4000 4800 |
通用同步/异步收发器 |
UART4 |
APB1 |
0x4000 4C00 |
通用异步收发器 |
UART5 |
APB1 |
0x4000 5000 |
通用异步收发器 |
USART1 挂在 APB2 上,吃 PCLK2 的 72 MHz;其余四个挂在 APB1 上,最高 36 MHz。RCC 篇讲的外设时钟归属在这里落了地:用哪个串口,就去使能哪条总线上的哪个串口时钟。
以最常用的 USART1 为例,它复用 PA9(TX)和 PA10(RX)两个引脚,重映射后是 PB6、PB7。
7.2 寄存器与地址
串口是片上外设,寄存器统一编址进 CPU 的地址空间,GPIO 输出篇讲过这套编址。USART1 分到 1 KB 地址空间:0x4001 3800 到 0x4001 3BFF,实际用到的只有开头一小段。访问某个寄存器 = 基址 + 偏移,表格如下:
| 寄存器 | 偏移 | 最终地址 | 干什么用 |
|---|---|---|---|
USART_SR |
0x00 |
0x4001 3800 |
状态标志,TXE/TC/RXNE 都在这 |
USART_DR |
0x04 |
0x4001 3804 |
数据,TDR/RDR 同地址 |
USART_BRR |
0x08 |
0x4001 3808 |
波特率配置 |
USART_CR1 |
0x0C |
0x4001 380C |
控制位:使能、中断使能等 |
USART_CR2 |
0x10 |
0x4001 3810 |
控制位:停止位长度等 |
USART_CR3 |
0x14 |
0x4001 3814 |
控制位:DMA、硬件流控等 |
USART_GTPR |
0x18 |
0x4001 3818 |
预分频与守护时间 |
其余四个串口同样各有 1 KB 空间、一套寄存器,偏移一致,只是基址不同。需要哪个寄存器就查哪个,不用背。
7.3 USART 与 UART
USART 比 UART 多一个 synchronous:多出来的同步模式是给串口外加一根时钟线(CK 引脚),双方按时钟节拍传数据。这个同步模式实际场景里几乎不用,忽略它,USART 就是 UART。本系列统一说 USART,因为 STM32 里挂的就是 USART。
波特率由 USART_BRR 寄存器配置,它是个分数波特率发生器,12 位整数加 4 位小数,具体怎么算,等真要上手配波特率的时候再细看。