Appearance
UART 串口通信
概念
上一章让一根引脚能变高变低,但那是"一位"信息。要把一个字节甚至一整句话送出去,还得约定:什么时候算开始、哪几位是数据、什么时候算结束。 UART 就是最古老也最普及的那套约定。
一句话说清它是什么:UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是把并行数据变成串行比特流送出去、再在另一端还原回来的一对收发电路。
"异步"两个字是关键——它和 SPI、I2C 的根本差别在这里:
| 总线 | 时钟线 | 靠什么对齐 | 线数 |
|---|---|---|---|
| UART | 没有 | 起始位 + 双方约定同一个波特率 | 2(TX、RX) |
| SPI | 有(SCK) | 时钟边沿 | 4 |
| I2C | 有(SCL) | 时钟线 | 2 |
没有时钟线,就只能靠"约定":双方必须事先说好"每秒发多少位"(波特率)。一旦两个方向配的波特率不一致,收到的就是乱码——这是串口调试排在第一位的检查项。
这一层回答了上一层什么问题:上一章只给了一根能通断的线,没说这根线上"1"和"0"的序列怎么划分成字节。UART 补的就是帧的划分规则与速率约定。
原理
一、异步意味着什么:没有时钟线
同步通信有专门的时钟线,收方跟着时钟采样就行。异步通信省掉了这根线(省一根线、省一个引脚),代价是收方必须自己"数拍子":
text
发送方: ... 空闲(高) 0 1 0 0 0 0 0 1 0 1 (停止,高) 空闲(高) ...
^起始位 ^停止位
接收方: 检测到下降沿 -> 启动内部计数 -> 在第 1.5 位、2.5 位 ... 采样三个要点:
- 空闲态是高电平:所以"起始位"是一个下降沿,这让收方能识别"一个新帧来了"。
- 靠起始沿重新同步:每一帧都重新对齐一次,所以双方只要波特率足够接近,累积误差不会跨帧累积。
- 帧与帧之间必须有空闲:停止位保证至少有一段高电平,否则下一帧的起始沿就没了。
二、帧格式:起始位、数据位、校验位、停止位
一帧就是"1 个起始位 + 若干数据位 + 可选校验位 + 若干停止位":
| 段 | 位数 | 说明 |
|---|---|---|
| 起始位 | 固定 1 | 恒为低电平 |
| 数据位 | 5 / 6 / 7 / 8(9 也有) | LSB 先发 |
| 校验位 | 0 或 1 | 偶校验/奇校验/无 |
| 停止位 | 1 / 1.5 / 2 | 恒为高电平 |
记法是 数据位 + 校验 + 停止 三个字母数字:
8N1 = 8 数据 + None 校验 + 1 停止 = 1 + 8 + 0 + 1 = 10 位/字节
8E1 = 8 数据 + Even 偶校验 + 1 停止 = 1 + 8 + 1 + 1 = 11 位/字节
7O2 = 7 数据 + Odd 奇校验 + 2 停止 = 1 + 7 + 1 + 2 = 11 位/字节注意数据位是 LSB 先发:发 0x41(二进制 01000001)时,线上先出现的是 bit0(1),最后才是 bit7(0)。这是初学者最常搞反的细节。
"1 个起始位"是恒定的:起始位永远是 1 位,可变的是数据位、校验位和停止位。所以总位数最小是 1+5+0+1 = 7,最大是 1+9+1+2 = 13。
三、波特率与位时间
波特率是每秒传输的位数(不是字节数),单位 bps 或 Baud。它与位时间互为倒数:
text
T_bit = 1 / 波特率
一帧时间 = 帧总位数 / 波特率常见的波特率以及 8N1(10 位/字节)下的时间:
9600 bps -> T_bit = 104.1667 us,一字节 = 1.0417 ms
115200 bps -> T_bit = 8.6806 us,一字节 = 0.0868 ms两个换算要能心算:9600 的位时间约 104 μs(约 1/9600 秒),115200 是它的 12 倍速。波特率越高,一帧越短、相同数据传得越快——但也有代价(后面讲误差与电平)。
传一整块数据的账:
text
时间 = 字节数 x 帧位数 / 波特率
1 MB @ 9600 bps = 1048576 x 10 / 9600 = 1092.267 s(约 18 分钟)
1 MB @ 115200 bps = 1048576 x 10 / 115200 = 91.022 s这就是为什么早期下载固件很慢:9600 下传 1 MB 要 18 分钟。
四、波特率发生器:分频与误差
收发的位拍子从哪来?从系统时钟分频。STM32 的公式是:
text
USARTDIV = f_CK / (16 x 波特率)其中 16 就是过采样倍数(下一节)。USARTDIV 是一个有整数部和小数部(4 位小数)的数,硬件尽量逼近它。
关键问题:分频比做不到无限精细,所以实际波特率与目标有偏差:
目标 9600 bps @ 72 MHz -> USARTDIV = 468.75 -> 整数部 468,小数部 12/16 -> 实际 9600.00 bps,误差 +0.0000%
目标 115200 bps @ 72 MHz -> USARTDIV = 39.0625 -> 整数部 39,小数部 1/16 -> 实际 115200.00 bps,误差 +0.0000%
目标 921600 bps @ 72 MHz -> USARTDIV = 4.8828 -> 整数部 4,小数部 14/16 -> 实际 923076.92 bps,误差 +0.1603%前两行正好整除(幸运),第三行就不能整除,误差 +0.1603%。
误差容忍度是多少:一帧 10 位,采样点在每位的中间,所以允许的总偏差大约是半个位宽(0.5 / 10 = 5%)。但这 5% 是收发双方共同的预算,单边一般要求 小于 2%~3%。上面 0.16% 完全安全;但如果误差接近 2%,长帧的最后几位就会"采样到隔壁位"。
五、接收端怎么对得准:16 倍过采样
只靠"每帧重新对齐"还不够——一帧内部也可能逐渐漂移。解决办法是每位采多次:
text
每一位被划分成 16 个采样时钟:
位宽 = |<---------------- 16 个采样时钟 ---------------->|
采样点 = 1 2 3 4 5 6 7 [8] [9] [10] 11 12 13 14 15 16
^^^^^^^^^^^^
取中间第 8、9、10 个做 3 取 2 表决三个好处:
- 采样点在位的正中间(第 8~10 个采样时钟),离两侧边沿最远,最抗漂移。
- 3 取 2 表决:三个采样值里有两个一致就算这个位是它,能滤掉单点噪声。
- 漂移容限约 ±50%:只要累积偏差不超过半个位宽,中间采样点仍落在本位的范围内。
六、电平标准:TTL、RS-232、RS-485
"串口"这个词并不唯一——同样的帧格式,可以跑在不同的物理电平上:
| 标准 | 逻辑 1 | 逻辑 0 | 距离 | 特点 |
|---|---|---|---|---|
| TTL | 3.3 V(或 5 V) | 0 V | 板内几十 cm | 直连 MCU,最常用 |
| RS-232 | −3 ~ −15 V | +3 ~ +15 V | 十几米 | 负逻辑,抗干扰强 |
| RS-485 | 差分 A/B 两线 | 差分反向 | 千米级 | 抗共模,可挂 32 节点以上 |
RS-232 是"负逻辑":负电压表示 1,正电压表示 0。所以从 MCU 的 TTL 接到电脑的串口,中间必须过一个电平转换芯片(如 MAX232),不能直连——直连既收不到正确电平,还可能把引脚打坏。
RS-485 用差分:两根线传同一个信号的反相,干扰对两线的影响相同,相减就抵消了。所以它能跑上千米、能挂一堆设备,是工业现场总线(Modbus RTU)的物理层。
七、差错检测与流控
UART 有三层"出错提示",都通过状态寄存器暴露:
| 错误标志 | 触发条件 | 含义 |
|---|---|---|
| 校验错误 PE | 校验位对不上 | 数据位里有一位翻了 |
| 帧错误 FE | 停止位不是高电平 | 波特率不匹配或线路噪声 |
| 溢出错误 ORE | 上一字节还没读走,新的又来了 | CPU 响应太慢,数据丢了 |
溢出错误是最要紧的:它说明软件有问题(没及时读数据寄存器),不是线路问题。
流控解决的是"收方来不及":硬件流控用 RTS / CTS 两根额外线,收方通过 RTS 告诉发方"先别发"。软件流控则用特殊字符 XON / XOFF(0x11 / 0x13)——但传输二进制数据时软件流控可能误判,所以高速链路一般用硬件流控。
示例
例 1:帧格式与位数
三种常见配置的总位数:
8N1: 1 起始 + 8 数据 + 0 校验 + 1 停止 = 10 位
8E1: 1 起始 + 8 数据 + 1 校验 + 1 停止 = 11 位
7O2: 1 起始 + 7 数据 + 1 校验 + 2 停止 = 11 位同样的数据,换个配置总位数就变了,算传输时间时必须用配置对应的位数,不能默认 10。
例 2:位时间与传输耗时
9600 bps -> T_bit = 104.1667 us,一字节(10 位) = 1.0417 ms
19200 bps -> T_bit = 52.0833 us,一字节(10 位) = 0.5208 ms
115200 bps -> T_bit = 8.6806 us,一字节(10 位) = 0.0868 ms
921600 bps -> T_bit = 1.0851 us,一字节(10 位) = 0.0109 ms传 1 MB(8N1)的账:
9600 bps -> 1092.267 s
115200 bps -> 91.022 s
921600 bps -> 11.378 s例 3:STM32 波特率误差
f_CK = 72 MHz,逐档算:
9600 bps -> 整数部 468,小数部 12/16 -> 实际 9600.00 bps,误差 +0.0000%
115200 bps -> 整数部 39,小数部 1/16 -> 实际 115200.00 bps,误差 +0.0000%
921600 bps -> 整数部 4,小数部 14/16 -> 实际 923076.92 bps,误差 +0.1603%
2000000 bps -> 整数部 2,小数部 4/16 -> 实际 2000000.00 bps,误差 +0.0000%921600 的 0.16% 是安全区;但如果把系统时钟改成非整倍的频率,误差可能顶到 1% 以上,这时就要么改时钟、要么降波特率。
例 4:C 实现——帧组装与波特率分频
把帧组装(含 LSB 先发)和 USARTDIV 的分频算法都写出来。
/* uart.c —— 用 C 模拟 UART 的波特率分频与帧组装 */
#include <stdio.h>
#include <stdint.h>
#define F_CK 72000000u
#define OVERSAMPLE 16u
static uint32_t usartdiv_int;
static uint32_t usartdiv_frac;
/* 算出最接近的整数部 + 小数部(4 位小数) */
static void uart_init(uint32_t baud)
{
uint32_t den = OVERSAMPLE * baud;
uint32_t rem = F_CK % den;
usartdiv_int = F_CK / den;
usartdiv_frac = (rem * 16u + den / 2u) / den; /* 四舍五入 */
if (usartdiv_frac >= 16u) {
usartdiv_int++;
usartdiv_frac -= 16u;
}
}
/* 打印一帧:起始位 + 8 位数据(LSB 先发)+ 停止位 */
static void uart_send_frame(uint8_t c)
{
int i;
printf("TX 0x%02X (0", c);
for (i = 0; i < 8; i++) {
printf("%d", (c >> i) & 1u); /* LSB 先发 */
}
printf("1) = 10 bits\n");
}
int main(void)
{
uint32_t baud = 115200u;
uart_init(baud);
printf("HSE 8 MHz -> f_CK %u MHz\n", (unsigned)(F_CK / 1000000u));
printf("baud %u : USARTDIV int %u, frac %u/16\n",
(unsigned)baud, (unsigned)usartdiv_int, (unsigned)usartdiv_frac);
printf("bit time = 1/%u s = %.4f us, frame of 10 bits = %.4f ms\n",
(unsigned)baud, 1e6 / baud, 1e4 / baud);
printf("\n");
uart_send_frame(0x41); /* 'A' */
uart_send_frame(0x55); /* 0x55 = 01010101 */
printf("\n");
printf("\"Hello\" 5 bytes @ %u bps takes %.4f ms\n", (unsigned)baud, 5 * 1e4 / baud);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
HSE 8 MHz -> f_CK 72 MHz
baud 115200 : USARTDIV int 39, frac 1/16
bit time = 1/115200 s = 8.6806 us, frame of 10 bits = 0.0868 ms
TX 0x41 (0100000101) = 10 bits
TX 0x55 (0101010101) = 10 bits
"Hello" 5 bytes @ 115200 bps takes 0.4340 ms看 0x41 那一行:0100000101 去掉首尾的起始位与停止位,中间 8 位是 10000010,正好是 0x41 = 01000001 的倒序——这就是"LSB 先发"的直接证据。
例 5:Python——帧、位时间、误差验算
# UART:帧格式、位时间、波特率误差与过采样
print("=== 一、帧格式与总位数 ===")
for cfg, b, p, s in [("8N1", 8, 0, 1), ("8E1", 8, 1, 1), ("7O2", 7, 1, 2)]:
print(f"{cfg}: 1 起始 + {b} 数据 + {p} 校验 + {s} 停止 = {1 + b + p + s} 位")
print("\n=== 二、位时间与一帧时间(8N1)===")
for baud in (9600, 19200, 115200, 921600):
tb = 1 / baud
print(f"{baud:7d} bps -> T_bit = {tb * 1e6:10.4f} us,一字节(10 位) = {tb * 10 * 1e3:9.4f} ms")
print("\n=== 三、传 1 MB 需要多久(8N1,10 位/字节)===")
size = 1024 * 1024
for baud in (9600, 115200, 921600):
print(f"{baud:7d} bps -> {size * 10 / baud:8.3f} s")
print("\n=== 四、STM32 波特率发生器:USARTDIV = f_CK / (16 x baud) ===")
f_ck = 72_000_000
for baud in (9600, 115200, 921600, 2000000):
d = f_ck / (16 * baud)
mant = int(d)
frac = round((d - mant) * 16)
if frac == 16:
mant, frac = mant + 1, 0
if mant == 0:
print(f"{baud:8d} bps -> USARTDIV = {d:.4f} -> 分频 < 1,须改用 8 倍过采样")
continue
real = f_ck / (16 * (mant + frac / 16))
print(f"{baud:8d} bps -> 整数部 {mant},小数部 {frac}/16 -> 实际 {real:.2f} bps,误差 {(real - baud) / baud * 100:+.4f} %")
print("\n=== 五、16 倍过采样取 3 取 2 ===")
print("每 bit 采 16 个采样时钟,取第 8/9/10 个做 3 取 2 表决")
print(f"可容忍的最大相位偏差约 = {8 / 16 * 100:.1f} %(1/2 位)")
print("\n=== 六、电平标准 ===")
for name, desc in [("TTL", "0 V = 0,3.3 V = 1(板内短距)"),
("RS-232", "+3~+15 V = 0,-3~-15 V = 1(负逻辑,走线远)"),
("RS-485", "差分 A/B 双绞线,抗共模,可挂 32 节点以上")]:
print(f"{name:7s} {desc}")
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
=== 一、帧格式与总位数 ===
8N1: 1 起始 + 8 数据 + 0 校验 + 1 停止 = 10 位
8E1: 1 起始 + 8 数据 + 1 校验 + 1 停止 = 11 位
7O2: 1 起始 + 7 数据 + 1 校验 + 2 停止 = 11 位
=== 二、位时间与一帧时间(8N1)===
9600 bps -> T_bit = 104.1667 us,一字节(10 位) = 1.0417 ms
19200 bps -> T_bit = 52.0833 us,一字节(10 位) = 0.5208 ms
115200 bps -> T_bit = 8.6806 us,一字节(10 位) = 0.0868 ms
921600 bps -> T_bit = 1.0851 us,一字节(10 位) = 0.0109 ms
=== 三、传 1 MB 需要多久(8N1,10 位/字节)===
9600 bps -> 1092.267 s
115200 bps -> 91.022 s
921600 bps -> 11.378 s
=== 四、STM32 波特率发生器:USARTDIV = f_CK / (16 x baud) ===
9600 bps -> 整数部 468,小数部 12/16 -> 实际 9600.00 bps,误差 +0.0000 %
115200 bps -> 整数部 39,小数部 1/16 -> 实际 115200.00 bps,误差 +0.0000 %
921600 bps -> 整数部 4,小数部 14/16 -> 实际 923076.92 bps,误差 +0.1603 %
2000000 bps -> 整数部 2,小数部 4/16 -> 实际 2000000.00 bps,误差 +0.0000 %
=== 五、16 倍过采样取 3 取 2 ===
每 bit 采 16 个采样时钟,取第 8/9/10 个做 3 取 2 表决
可容忍的最大相位偏差约 = 50.0 %(1/2 位)
=== 六、电平标准 ===
TTL 0 V = 0,3.3 V = 1(板内短距)
RS-232 +3~+15 V = 0,-3~-15 V = 1(负逻辑,走线远)
RS-485 差分 A/B 双绞线,抗共模,可挂 32 节点以上考点
- 帧位数算总长:
1 + 数据位 + 校验位 + 停止位。起始位恒为 1 位;8N1是 10 位,8E1是 11 位。算传输时间必须用对总位数。 - 波特率是"每秒位数"不是"每秒字节数":
T_bit = 1 / 波特率,一帧时间 =帧位数 / 波特率。 - 数据位 LSB 先发:
0x41在线上的数据段是10000010(倒序)。这是最经典的选择题考点。 - 波特率误差容忍:一帧 10 位时总预算约 5%(半个位宽),单边要求小于 2%~3%。算完
USARTDIV必须回代算实际波特率与误差。 - 过采样:16 倍、采样点在第 8~10 个、3 取 2 表决。别把"16 倍"记成"采 16 次求平均"——是取中间三个做多数表决。
- RS-232 是负逻辑(负电压是 1),且电平远高于 TTL,必须经电平转换芯片。RS-485 是差分、能挂多机、能跑千米。
- 三个错误标志:奇偶错 PE(数据位翻了一位)、帧错 FE(停止位不对,通常是波特率不匹配)、溢出错 ORE(软件没及时读,最要紧)。
- 易错:把 TX 与 RX 接反(要交叉接);忘记共地(GND 必须连);以为波特率配置错了会引起"校验错"(其实先表现成帧错)。
小结
- 异步 = 没有时钟线,靠"起始位下降沿重新同步" + "双方约定同一个波特率"。
- 一帧的组成:1 起始位 + 5~9 数据位(LSB 先发)+ 可选校验位 + 1/1.5/2 停止位;空闲态是高电平。
- 位时间
1 / 波特率,帧时间帧位数 / 波特率;传 1 MB @ 9600 要 18 分钟。 - 波特率由系统时钟分频得到(
USARTDIV = f_CK / (16 × 波特率)),分频取整会带来误差,必须回代校核,单边误差要小于 2%~3%。 - 16 倍过采样 + 中间三点 3 取 2,把采样点放在位的正中间,换来约 ±50% 的漂移容限。
- 同样一帧可以跑在三种电平上:TTL(板内)、RS-232(负逻辑、必须转换)、RS-485(差分、多机、千米)。
- 三类错误标志 + 硬件流控,其中溢出错误(ORE)是软件问题而非线路问题。
回到主线:这一章把上一章的"一根线"变成了"一套协议"。它依赖 GPIO 的模式与复用功能 来把引脚交给 USART 外设,也复用了 net 里 物理层与编码 的"比特怎么上线"这套视角——只是把网络的"成帧"缩到了两个芯片之间。它交出去的下一个问题是:如果我要接的不止一个设备,两根线够不够?
这一章只答了"两个设备之间怎么传字节",还没答"怎么把多个设备挂到同一组线上"。下一章换两种总线,专门解决"多设备"这件事。
下一篇:SPI 与 I2C 总线
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。