接上串口,115200 收发正常;换一块板子,同一个程序开始偶发乱码。 用示波器一量,实际波特率是 115942。
这不是 bug,是分频算不出来。串口的波特率不是随便设的,它必须从外设时钟除出来, 而除数能表示的小数位数是有限的。
USARTDIV 只有 4 位小数
STM32 这类 USART 的公式是:
USARTDIV = PCLK / (16 × baud)
BRR = (整数部分 << 4) | 小数部分 // 小数是 1/16 步进
72MHz 配 115200:72e6 / (16 × 115200) = 39.0625。
整数 39,小数 0.0625 × 16 = 1,所以 BRR = (39 << 4) | 1 = 0x271。
反算回去正好 115200,误差 0。
8MHz 配 115200:8e6 / (16 × 115200) = 4.3403。
小数部分 0.3403 × 16 = 5.44,只能取 5,于是做出来是 4 + 5/16 = 4.3125,
实际波特率 115942,误差 +0.64%。
这 0.64% 就是全部问题的来源。它不是编译器的问题,也不是代码写错了, 是 8MHz 这个时钟除不出 115200。
什么时钟能配出整数
要让误差为零,需要 PCLK / (16 × baud) 恰好是 1/16 的整数倍,
也就是 PCLK 必须是 16 × baud 的整数倍。
115200 × 16 = 1,843,200。乘 4 得 7,372,800——这就是为什么那么多开发板 用 7.3728MHz 晶振:它的整数倍(14.7456M、22.1184M、29.4912M)都能把常用波特率配成整数。 72MHz 也恰好是 1,843,200 的 39.0625 倍——不是整数倍,但小数部分正好落在 1/16 的格点上, 所以同样精确。
反过来 8MHz、12MHz、16MHz 就都有小数尾巴。工具里的误差表能给出一整列结果: 同一块板子上,9600 误差 −0.02%(能用),115200 误差 +0.64%(也能用), 但再往上到 921600 就彻底崩了。
误差为什么会变成乱码
UART 没有时钟线。接收端靠起始位的下降沿对齐,然后在每个位时间的中间采样。 如果发送方实际偏快 0.64%,到第 10 位时采样点偏移 0.064 个位宽,仍在安全区内; 偏 5% 就是 0.5 个位宽,正好踩在边沿上,采到 0 还是 1 全看抖动。
所以容忍度要按整帧累积算。一个 10 位帧(起始 + 8 数据 + 停止), 理论极限大约 5%,工程上一般留一倍余量,把单向误差压到 2% 以内; 高波特率、长连发还要更严。
定时器:两个寄存器都要 +1
定时器的公式简单,但坑在细节:
更新频率 = TIMCLK / ((PSC + 1) × (ARR + 1))
占空比 = CCR / (ARR + 1) PSC = 0 表示不分频(除以 1),ARR = 0 表示数一次就溢出。
所以 72MHz、PSC = 71、ARR = 999 得到 72e6 / (72 × 1000) = 1000Hz,
刚好 1ms 一次中断。写代码时忘了 +1,频率就差 72 倍或 1000 倍。
PSC 和 ARR 怎么分配
同一个频率有很多组解:72000 可以拆成 72000×1、720×100、72×1000…… 选哪一组取决于你要什么:
- 要占空比细:ARR 拉大、PSC 压小。ARR = 999 时占空比能按 0.1% 调
- 要长定时:ARR 顶到 65535 还不够,就加 PSC
- 要中断轻:频率别设太高,1kHz 已经意味着每秒 1000 次进中断
工具的反解会按「误差最小、ARR 尽量大」来挑,这也是大多数场合的默认答案。
占空比的分母是 ARR+1
计数器从 0 数到 ARR,一共 ARR+1 个状态。CCR 表示「计数值小于 CCR 时输出有效」,
所以有效状态正好 CCR 个,占空比就是 CCR / (ARR + 1)。
想让 CCR 直接等于百分比,把 ARR 设成 99 或 999 就行——这也是为什么很多例程里 ARR 是 999。
算一遍再动手
这些公式都不难,难的是记不住 +1、也想不清 8MHz 到底能不能用。 用波特率 / 定时器计算器把时钟和波特率填进去, 误差表会直接标出哪些组合能用;定时器那边填目标频率点一下反解,PSC/ARR 就出来了。
如果还涉及把浮点系数搬到没有 FPU 的芯片上,那边还有一套 Q 格式定点数的换算。