1.4 UART通信
1.4 UART通信
Sarzn通用的协议分层思想
初来乍到, 我们需要有一个对于通信协议的大致框架
我们知道有很多通信协议, 比如I2C/SPI/CAN, 他们是具有分层框架的, 也就是说我们要有一个基本的分层思想, 这对于我们初学者来说是至关重要的
适用于所有通信协议的通用模型
- 应用数据层: 他就是我们真正要传输的数据本身
- 链路传输层: 规定了地址(ID), 冲突检测与避免(仲裁), 误码校验, 滤波器与掩码, 该层的帧格式
- 物理信号层: 规定了硬件电路的设计规则, 用户使用的接线方式, 线路上传输信号与电平格式的对应关系, 波特率与比特率, 数据同步与异步传输, 收发模式等内容
这里对每一个具体的作用看不太懂没有关系, 以后会慢慢学习到, 但是现必须要有一个分层的思想, 知道通讯协议是分层的, 但是这个层次如何运用的呢? 我们要有一个”分层 + 封装” 的思想
分层 + 封装
核心就两件事:每层只干一件事,以及数据往下走时被一层层包起来”(封装),往上走时再一层层拆开(解封装)
比方说你要发送数据, 那么你的数据就是在应用数据层, 层层封装, 经过链路传输加地址, 检验, 帧格式, 最后变成电信号从物理信号层发送出去. 如果你是接受到数据, 那么就是反过来, 经过层层解封最后拿到应用数据层
为什么要分层
这和我们熟悉的软件抽象是一回事:每层只通过固定的接口跟上下层打交道,内部实现可以随便换。比如你的应用层代码(发送温度 25.6)完全不需要知道底下是 TTL 电平、RS-232 还是 RS-485, 你如果换物理介质,上层一行都不用改。这跟网络里的 OSI 七层 / TCP-IP 四层是一个道理. 我前文所讲的通讯协议只是把它简化到三层, 事实里的通讯协议具体可能更复杂, 但是对于初学者不用太在意硬件协议细节, 知道大体的框架就够了
不同的协议…
按照上文所讲的框架, 每个通讯协议都可以套用进去, 但是每一个通讯协议又有不同, 差别主要体现在链路层上, 有些协议比较简单(比如我们要学的 UART), 有些比较复杂那么它在链路层上的内容就比 UART 丰富(比如 CAN)
我们来简单对比一下:
| 链路层特性 | UART | I2C | SPI | CAN |
|---|---|---|---|---|
| 地址 / ID | 无(点对点) | 有 | 用片选 CS 区分 | 有(报文 ID) |
| 总线仲裁 | 无 | 有(多主时) | 无(单主) | 有(按 ID 优先级) |
| 滤波器 / 掩码 | 无 | 无 | 无 | 有 |
| 内置校验 | 仅奇偶校验位 | ACK/NACK | 无 | CRC |
| 拓扑 | 点对点 | 多从总线 | 一主多从 | 多主总线 |
UART 是点到点, 因此没有地址和 ID 的概念, 想发就发, 想咋收都行, 没有冲突检测与避免以及仲裁的说法. 滤波器和掩码, 这两个概念是地址衍生出来的, 既然没有地址, 那么也没有这两个概念. 只剩下奇偶校验和帧格式 这里只做了解就行, 你知道有这回事就行了, 具体他们都是什么意思之后再来
物理信号
UART 是两个设备之间相互联系, 注意, 这里是两个设备! 两个设备的点对点通信
这两个设备在已有供电的情况下只需要接三个线即可, 分别是
- Rx: Receive (接受) 这个是接收端
- Tx: Transmitter (发送) 这个是发送端
- GND: 两个设备需要共地以此区分 Rx 和 Tx 两根线上的电平高低
接线方式:
如上图这样接就好了, 需要注意的一点是, 因为两个设备之间要相互传输数据 - 比如设备 1 给设备 2 发数据, 那么设备 1 是发送端(Tx), 设备 2 是接收端(Rx) , 所以设备 1 的 Tx 线要和设备 2 的 Rx 线相连才是对的. 反过来, 如果设备 2 是 Tx 设备 1 是 Rx 也是同理
电平与信号格式
- 简单的并且直观的信号传输格式, 直接传输 0/1 码元
- 高电平是 1 , 低电平是 0
波特率 与 比特率
- 波特率是每秒多少个符号(symbol/s) - 每秒传递有效信息码元的数量
- 比特率是每秒多少 bit(bit/s, bps) - 每秒传递的二进制码元的数量
举例: 十六进制字符编码中, 如果一秒传输 1000 个16 进制字符 - 也就是 1000 个有效信息码元(16 进制字符) 被传递了, 那么波特率就是 1000 symbol/s - 因为一个 16 进制字符要用 4 为二进制表示, 所以它的比特率就是 4000 bit/s
但是UART 是二电平的,一个符号就是一个 bit,所以比特率和波特率两者的数值相等
采样是什么意思? 帧 与 位
采样: 采样就是读取并锁定数据, 芯片会像照相机按下快门一样, 记录下此时数据线上的电平是 0 还是 1
位(Bit - 比特) 位是计算机中最小、最基础的数据单位
- 一个位只能表示两个状态:0 或 1
- 在电路上,它对应一个电压状态, 例如:高电平代表 1,低电平代表 0
帧 (Frame - 数据帧) 帧是一个有完整独立意义的数据块
- 1 帧由多个位按照特定的顺序组合而成。单独一个位没有完整意义(比如 A), 但把它们拼成一帧,就有了具体含义 (比如 APPLE)
UART通信帧介绍 在计算机网络上一般而言物理层叫信号,链路层叫帧,网络层叫报文,传输层叫流,数据层叫数据,当然也可以统一都叫数据
UART
通用异步收发协议 - Universal Asynchronous Receiver Transmitter
异步通信
异步的意思是收发双方事先约定好一个波特率(比如 115200), 然后各跑各的本地时钟, 谁也不给谁发时钟信号
发送端空闲的时候会把线拉高,
要发送的时候先发送一个起始位(拉低) ,
接收端一直在盯着这条线, 一看到线被拉低了(下降沿),
就知道有一帧要来了, 于是以这个下降沿为基准开始计时,
然后按照约定的波特率在每一位的中心去采样(注意是每一位的中心)
误差: 看到这里我们知道 UART 其实每一帧都靠起始位置对齐一次, 那么误差是哪里来的?
在接受消息的时候, 误差的主要出现是在单帧内的累计漂移, 一帧大概在 10 位左右 (1 起始 + 8 数据 + 1 结束) 每一帧在起始位对齐之后, 只要漂移到最后的停止位还没偏出半个位宽, 就不会报错.
为什么是半个位宽? 因为一帧内 10 位, 而我是在每一位的中心采样, 所以从对齐点到最后一位是 9.5 个位宽, 所以允许的累积误差 < 0.5 位
所以两端时钟总偏差的极限大约是 0.5 / 9.5 ≈ 5%,平摊到收发两边各约 ±2.5%。实际工程一般留余量按 ±2% 左右设计
这就是 UART 帧做得短, 有起始和停止位的原因, 但是同步协议(I2C, SPI) 因为有一条时钟线的存在, 所有设备都拿这根线作为统一节拍, 也就没有漂移误差这回事
采样 8 次 / 16 次到底在采什么?
我们现在知道每一帧大概有 10 位, 那我们把每一位放大来看,
看看他到底是怎么采样的, 过采样 8 次/16 次又是什么意思?
采样 16
次:
前面说到我们是采集每一位的中心点, 但是单片机怎么知道哪里是中心点呢? 他不是靠估算, 而是靠数数. 只要我的采样次数是波特率的 16 倍, 那么每一位我就会采样 16 次, 我只要数第 8 次就知道中心点了
过采样: - 精确找到位中心:接收方内部时钟跑在波特率的 16 倍(或 8 倍),从起始位边沿起数,数到第 8 个采样点就正好是这一位的正中央,不是猜猜,而是数数 - 抗噪声:它不是只看 1 个点,而是取中间三个采样(第 8、9、10)做多数表决,3 选 2, 少数服从多数。如果这 3 个采样不一致,说明线上有噪声,STM32 就会把 NF(噪声标志) 置位告诉你这一帧有噪声
其他的协议…?
CAN - 异步协议: CAN 与 UART 类似也是把每一位切成若干时间份额(Tq),在某个采样点(通常在一位的 75%~87.5% 处)读取。但它避免误差的手段比 UART 更牛🐂: UART 只在每帧的起始位对齐一次,CAN 则在每个隐性到显性跳变沿都做重新同步(靠 SJW 同步跳转宽度限定调整量),还用位填充强制制造跳变沿来保证总有边沿可对齐。所以 CAN 可以持续对齐, 抗漂移能力比 UART 强得多. 这就是它能跑长总线, 多节点的原因之一
I2C - 同步协议: 它有一根 SCL 时钟线, 主机驱动, 所有设备拿这根线当统一节拍。数据在 SCL 为高时必须稳定,接收方就照着 SCL 的节奏读,没有漂移问题, 没有过采样
SPI - 同步协议: 与 I2C 同理, 有时钟线SCK
USART - 同步协议, USART = UART +
S(Synchronous, 同步的) , 多出来的 S
表示它不仅可以在异步模式下工作, 还能工作在同步模式下,
在 CK 引脚上输出一根时钟线(主机方向),用来和移位寄存器、某些
ADC/DAC 这类同步器件对接
收发模式:
- 仅接收, MCU仅可接受外来消息, 不可发送
- 仅发送, 仅可发送, 不可接受
- 收发双向, 可发可收
误码校验
误码校验在链路层上减少了硬件偶然错误的信息编码被解析利用,和可能存在的强干扰环境噪声导致信道电压波动很大导致改变电平的风险
在 UART 中采用的是奇偶校验 - Parity Check ### 奇偶校验算法
对帧中的比特流进行计算, 统计 1 的个数来决定 1bit 的校验位
- 偶校验: 在一帧中, 除校验位以外的其他位. 其他位有偶数个 1, 则校验位是 0; 其他位奇数个 1, 则校验位是 1
- 奇校验: 其他位有偶数个 1, 则校验位是 1; 其他位有奇数个 1, 则校验位是 0
这个等效于异或操作, 对所有其他位进行异或, 直接作为结果的是偶校验; 将异或结果取反后作为校验位的是奇校验
我的技法: 偶校验就是让包含校验位在内的所有位 1 的总数是偶数; 奇校验就是1 的总和是奇数
劣势:
1. 奇偶校验只能发现奇数个 bit
翻转
- 翻 1 位(奇数个位翻转): 1 的总数奇偶性改变, 一定会被发现
- 翻 2 位(偶数个位翻转): 奇偶性转了一圈又回来, 校验通过,错误被忽视了
2. 只能检错不能纠错, 知道这一帧坏了, 但不知道具体是哪一位坏的,没法修,只能丢弃或要求重发
在 STM32 上…
UART 的校验在 STM32 里由 USART_CR1 的两个位控制: -
PCE(Parity Control Enable,开/关校验) -
PS(Parity Selection,0=偶、1=奇)
接收端一旦发现校验对不上,就把状态寄存器里的 PE(Parity
Error)标志置位,HAL 收发函数里会读它
提醒! CubeMX 中的 Word
Length配置项, Word Length 听起来像是说数据帧的长度,
但是它不仅是数据帧, 它其实包含着校验位,
但在协议层上校验位是不同于数据位的单独的一位 也就是说,如果你在 CubeMX
里把 Word Length 设成 8 bit 又开了校验, 那么实际只剩 7 个数据位
+ 1 个校验位. 如果想要8 个数据位 + 1 个校验位,
必须把 Word Length 设成 9 bit(M 位)
其他校验算法
还有很多比奇偶校验更强的算法比如, 校验和(checksum), CRC(循环冗余校验) CAN, 以太网, SD 卡中都在广泛的使用 CRC 校验
UART帧格式
仔细看图, 一UART 帧包含: 起始位, 数据位(- 校验位), 停止位
发送一个字母A举例
接下来, 我们真的用 UART 发一个字母 A 第一步: 首先我们要找到发送字母 A
具体要发什么数据 (应用数据层)

0100 0001
第二步: 接下来, 我们继续往上走, 到链路传输层 根据
UART 的帧格式, 我们将数据封装,
并且开启偶校验
起始位(必须是低电平, 1 位)
线平时空闲在高电平, 发送方一拉低, 就制造出一个下降沿, 接收方平时就盯着这根线, 一抓到这个下降沿, 立刻知道一帧来了, 并以此为基准重新对齐、开始按波特率采样 起始位为低、空闲为高, 这样就保证了总有一个可识别的下降沿
数据位(5~8 位)
他是真正你要发的数据
在此我们要搞清楚数据的高低位的问题: 比如 A 的二进制是: (最高位)0100 0001(最低位 D0)
但是 UART 是 LSB 先发(有效最低位先发), 他会先发低位
D0,最后发高位 D7 所以你看上面那帧,发的是
A=0x41=0100 0001,但线上的顺序是
1 0 0 0 0 0 1 0(D0→D7),跟你写出来的二进制是反过来的
常见数据位是 8 位(正好一个字节), 老式设备也有用 7 位发 ASCII 的(ASCII
只用 7 位)
为啥 ASCII 是 7 位, 上面那个表 A 的 ASCII 码不是有 8 位吗? ASCII 只有 128 个, 刚好是 2的 7 次方, 用七位就够了, 剩下的一个最高位它恒为 0
校验位(可选,1 位)
就是上一页讲的奇偶校验,把数据位异或一下放进来。注意它是可选的,不用校验时这一位直接没有
停止位(必须高,1 位)
标志帧结束
为什么必须高? 设想一下: 如果停止位是低, 两帧背靠背连发时, 中间就没有下降沿的跳变, 接收方根本分不出第二帧从哪开始。停止位除了 1 位, 还可以配 0.5 / 1.5 / 2 位(STM32 支持), 更长的停止位给慢速接收方多一点处理余量
8N1
按照数据位 + 校验 + 停止位总结你的配置 把这帧的配置总结一下就是: 8 数据位 + E 校验 + 1 停止位
8N1= 8 数据位、No parity 无校验、1 停止位, 这是最常见的方式8E1= 8 数据位、Even 偶校验、1 停止位, 上文的例子帧8O1= 奇校验7E1= 7 位 + 偶校验, 老 ASCII 设备使用
效率: 8N1 一帧 = 1 起始 + 8 数据 + 1
停止 = 10 位线上时间换 8 位有效数据,效率 80%.
如果加了校验的 8E1 是 11 位换 8 位,约 73%
第三步: 走完链路层, 就继续往上通过物理信号层发送出去
三种不同的通信方式 - 阻塞/中断/DMA中断
阻塞式通信:
- 发送: MCU 会发送一个数据给外设, 然后等着, 一直等到外设将这个数据真的发出去了才去做别的事情
- 接受: MCU
会死守在UART的数据寄存器(DR)门口,不停地在
while循环里读取标志位(RXNE),问硬件:“数据来了吗?来了吗?”。在数据没来之前,CPU 100%被占用,一行后续代码都执行不了
中断式通信:
- 发送: 利用中断, MCU 发送一个字节给外设, 然后立马去做别的事情, 不会等待外设发完这个数据, 当外设发完这个数据之后发一个”发送完成” 的中断信号给 MCU, 这时 MCU 再去发第二个数据
- 接受: MCU 正在做别的事情, 当别人发数据过来的时候, 产生一个接受数据的中断, 然后 MCU 把数据接收了
DMA中断:
DMA - Direct Memory Access 直接内存访问 他不产生数据, 只是数据的搬运工, 它代劳了 MCU 将数据发送给外设的过程 DMA 是在”外设的数据寄存器”和”内存里的缓冲区”之间搬数据
- 发送: MCU 把指定要发送的数据的起始地址与长度告诉 DMA, DMA 帮忙搬运给外设, 这个时候 MCU 去做别的事情了, 等 DMA 搬运好了之后发送中断告诉 MCU “发送完成”
- 接受: MCU 告诉 DMA 去把外设接受到的信息搬运到内存的指定位置
DMA 运行的两种方式:
- 在代码里具体写让 DMA 去搬运内存, 搬运完这一段之后 DMA 就可以停下来了
- 配置 DMA 一直搬运, 让 DAM 一直搬运这段内存, 搬运完了之后发出一个中断信号告诉 MCU”搬完了” , 但是它仍然不会停, 继续搬运(除非 MCU 在中断里面写让他停下)
空闲中断
顾名思义他就是在空闲的时候发生的中断. 比如你再给单片机连续的发送 4 个数据, 然后你突然停下来不发了, 这个时候就会触发空闲中断
你可以在这个中断里面去处理你之前收到的消息
- 工作原理:当 UART 的 RX
引脚(接收线)上超过 1
个字节的传输时间没有新数据到来时,硬件就认为“对方把话说完了”,于是触发空闲中断。在这个中断里,你就可以去写自己的回调函数,
你可以直接把 DMA
刚搬过来的一堆字节,当作一条完整的消息来处理(比如字符串
LED_ON)
但是多段有效信息连着发送,空闲中断可能无法区分
举个例子:你的设备收到了 LED_ONSWITCH
如果上位机(发送方)在发完 LED_ON 后,间隔极短(小于
1 个字节的传输时间)就接着发 SWITCH。STM32 的 UART
会认为数据一直没断,不会产生空闲中断,硬件会把这两条消息粘在一起变成一条 LED_ONSWITCH,
但是你可以给这个产生空闲中断的函数传参, 给他的 size 传为 5,
这样子它的逻辑就是:
- 要么收满 5 个字节的消息然后回调
- 要么对方发完一阵、线路空闲下来了(哪怕只发了 1 个、3 个),也回调
谈谈内部到底发生了什么
在你把数据发出去, 或者把数据收进来的时候, 单片机的内部到底发生了什么?
发送
我们先来仔细看看发送的过程, 之后接受的过程也大差不差
- 左边青色是并行的Byte 字节世界(CPU/DMA 可以通过 8 条总线一次性读或写一整个字节,很快)
- 右边橙色是串行的Bit 比特世界(线上一次只能走一位,慢,由波特率决定)
- 中间那道虚线,就是整字节被拆成一位位的转换点
DR(Data Register - 数据寄存器) 分为TDR
(Transmitter) 和 RDR(Receiver)
这张图里是TDR,它是 CPU/DMA
能直接读写的唯一的大小为一字节的门 CPU(或
DMA)要发数据,就是往这个TDR 写入字节。它的地址是固定的 –
这和你在实验篇会遇到的问题”为什么 DMA 外设侧不自增”相呼应:
所有字节都使用着这同一个DR。(小知识:F1 上发送和接收共用同一个 DR
地址,你写它就是写 TDR、你读它就是读
RDR,硬件内部其实是两个寄存器, 但是它真正的共用一个 DR
地址)
移位寄存器 是真正发电平信号的那个, 它把
TDR
交来的一整个并行字节,改成一位一位地往外吐(并行转串行),吐的速度就是波特率。它还顺手在前后补上起始位、停止位。CPU
和 DMA 都碰不到它,它是 UART 自己的内部部件
我们还需要了解一些信号:
TXE(Transmit data register Empty 发送数据寄存器空):TDR这个门的字节已经搬进移位寄存器了,门空了,可以塞下一个字节, 但注意,这时候移位寄存器还在吐,线上还没发完呢TXNE(Transmit data register Not Empty), TDR 还不是空TC(Transmission Complete 发送完成): 连移位寄存器都吐完了、最后一位停止位都离开引脚了,整个发送真正干净结束
接收
接受就是把发送反过来, 但是你要考虑到一个溢出的问题, 后文会讲
它的整体过程就是:
引脚上的比特一位位进接收移位寄存器(串行转并行,把零散的位拼回一个字节)
拼满一个字节就搬进 RDR(接收数据寄存器,那个一字节的门)
同时硬件把 RXNE 置 1,意思是”门里有货了,快来读” CPU 或 DMA
把 RDR 读走,存进 RxData[i]
我们还需要了解一些信号:
RXE(Read data register Empty 读取数据寄存器空):RDR的内容已经被读走RXNE(Read data register Not Empty), RDR 还不是空IDLE(IDLE line detected, 空闲): 收过数据后,接收线连续空闲了一帧时间(数据发送方停了)
溢出
如果移位寄存器在拼下一个字节的同时,RDR
里还存着上一个等你读,
也就是说如果你读得太慢,下一个字节拼好了要进
RDR,可上一个还没被读走, 它没地方放了,
就溢出(ORE),字节丢掉(注意! ,
这里是把溢出的字节直接丢掉, 不是覆盖!)
如果你使用了 DMA 并且开启了 FIFO(这个在实验篇会讲, 这里看不懂可以硬着头皮看下去之后再回头), 在发生上述情况的时候, DMA 会把溢出的数据缓存下来, 让你的 CPU 接着一个一个读, 保证了信息的完整, 但是会失去信息的时效性
从硬件的角度看阻塞中断和 DMA(这部分看不懂没关系, 再做完实验篇回头再来看)
假如你要发 5 个字节(注意, 是 5 个字节不是 5个比特)
无论哪种方式阻塞中断还是 DMA, 发 5 个字节这件事, 都必须往
DR 写 5 次 , 一次写一个字节,等 TXE
说门空了再写下一个. 这是肯定的, 而区别只在于: 这 5 次写 DR + 等
TXE的动作,是谁来执行? 执行的时候 CPU 被占住没?
阻塞: CPU 亲自盯着,寸步不离
CPU 自己跑一个循环: 写第 1 个字节 然后 死等
TXE(就杵在那儿反复看标志), 然后 写第 2 个 接着 死等 ……
写完第 5 个字节, 等 TC 最后才能返回做别的事
整个过程 CPU 的全程被占着, 它就站在 UART 旁边盯着, 一个字节没发完它哪也不能去, 期间啥也干不了
中断: CPU 干别的,每发完一个被叫回来一次
你调用Transmit_IT 后,它立刻把第一个字节的
TDR 写好然后返回,CPU 就去干别的事了UART
每把一个字节发走、TXE
一置位,就触发一次中断,把 CPU 临时叫回来进入中断,
写下一个字节进 TDR,
然后马上又出去继续干它的事. 发 5 个字节,就这样被叫回来
5 次,每次只占用一丁点时间
好处: CPU 不用死等,大部分时间能干别的 弊端: 发 5 个字节就被打断 5 次, 字节多了打断就频繁, CPU 的时间被切碎(可能是弊端吧) 与阻塞相比, 这是被中断叫回来干而不是站着一直干, 但是写 DR 这个活还是 CPU 干的
DMA: 雇了个搬运工, CPU 全程不用管
一开始把任务交代给 DMA : 告诉他, 要发的数据源在
TxData数组 、目标是 DR、共 5
个、每搬一个内存地址 +1交代完,CPU 就彻底不管了
接下来那 5 次, 等 TXE、写 DR, 全由
DMA 自己干,跟 CPU 一点关系都没有, CPU 这期间想干嘛干嘛,
一次都不被打断. 直到 5 个全搬完,DMA
才触发一次中断, 通知你发完了
所以 DMA 是三者里最节省 CPU 时间的: 中断要被叫回来 5 次, DMA 从头到尾只在最后打扰你 1 次. 平常我们做这些小实验 DMA 的效果不是很明显, 但是发大批数据 DMA 的优势就凸显出来了











