1. 先把这两个参数的关系讲透:为什么波特率总跟晶振绑在一起
刚接触单片机串口的人,十有八九踩过同一个坑:代码照着例程抄,TH1 = 0xFD也抄了,结果接上串口助手一看,满屏都是烫嘴的乱码。换个波特率,有时候好有时候坏,最后怀疑是线的问题、是 USB 转串口模块的问题、是电脑的问题,折腾一晚上,其实根子在板子上那颗小小的晶振。
晶振频率与波特率这件事,说白了就是一个"分频能不能整除"的数学问题,但它牵扯出来的东西比想象中多:定时器的工作方式、SMOD 位的设置、机器周期的分频系数、芯片是 12T 还是 1T、误差能容忍到什么程度、什么情况下必须换晶振、什么情况下换晶振也救不了。这篇文章我打算把这条链路从头到尾走一遍,从公式推导到实际焊接、从手算到用示波器验证,把我这些年在这上面栽过的跟头和总结出来的经验都摊开讲。
适合谁看?如果你正在用 8051 内核或者 Cortex-M 内核的单片机做串口通信,遇到过乱码、丢包、高波特率跑不起来,或者正准备画一块新板子、纠结晶振到底选 11.0592MHz 还是 12MHz,那这篇基本能把你的疑问覆盖掉。哪怕你只是刚学串口、还不知道TH1里面那个数是怎么来的,前面两节也能让你彻底搞明白。
我先把结论摆在这儿:异步串口没有时钟线,收发双方全靠各自的时钟"数拍子",所以你的波特率精度,完全取决于晶振频率能不能被波特率干净地除出来。这不是配置问题,是硬件决定的物理约束,软件层面只能优化,不能突破。
1.1 异步串口没有时钟线,只能靠"约定时间"
同步通信用两根线,一根数据一根时钟,接收方跟着时钟沿采样,永远不会错位。异步串口(UART)为了省线,把时钟线砍掉了,只留 TX 和 RX。
省线的代价是什么?代价是双方必须事先约定好每一位持续多长时间,然后各自用自己的时钟去数。发送方按 104.17 微秒一位往外吐,接收方也按 104.17 微秒一位往里收,两边节奏对上了,数据就正确;节奏对不上,采样的位置就会一帧一帧地漂移,漂到最后一位的时候可能已经偏了半个位宽,于是把 1 读成 0、把停止位读成数据位,表现出来就是乱码。
这里有个很重要的细节值得强调:UART 是每帧重新同步的。每一帧的起始位是一个下降沿,接收方检测到这个下降沿就开始重新计时,所以误差不会跨帧累积。这一点让 UART 比想象中皮实一些——哪怕你的波特率偏了 3%,只要单帧内不崩,连续发几万字节也不会越错越多。
但反过来说,这个"每帧重同步"的机制也掩盖了一个问题:起始位下降沿的检测本身是有抖动的,如果波特率误差加上线路噪声,让停止位的采样点正好落在边沿附近,就会出现间歇性的帧错误(FE),表现为"大部分数据对,偶尔错一个字节"。这种问题最难查,因为它是概率性的。
所以理解 UART 的采样机制,比记住任何一个公式都重要。它决定了你后面所有的误差计算到底在算什么。
1.2 波特率本质是晶振频率的一次整数分频
大多数单片机产生波特率的方式非常朴素:拿一个定时器,让它以固定频率溢出,溢出的节奏就是波特率的节奏。
以经典的 8051 为例,定时器 1 工作在方式 2(8 位自动重装),每 256 减去 TH1 个计数就溢出一次。定时器的计数时钟来自晶振除以 12(12T 模式下),所以溢出率是:
溢出率 = fosc / (12 × (256 - TH1))而串口模块在溢出率的基础上再做一次固定分频,分频系数是 32 或者 16(由 SMOD 位决定),于是:
波特率 = (2^SMOD / 32) × fosc / (12 × (256 - TH1)) = 2^SMOD × fosc / (384 × (256 - TH1))反解一下,就是工程上最常用的那个式子:
256 - TH1 = 2^SMOD × fosc / (384 × 波特率)看到这里应该就明白了:右边的计算结果如果是个整数,波特率就精确;如果是小数,你就只能四舍五入取一个整数,误差从此产生。
举个例子,fosc = 11.0592MHz,要 9600:
256 - TH1 = 11059200 / (384 × 9600) = 11059200 / 3686400 = 3正好是 3,所以TH1 = 253 = 0xFD,误差为零。这就是 11.0592MHz 这颗"奇怪频率"存在的全部理由——它不是随便定的,是专门为了让 9600、4800、2400、1200 这些标准波特率能整除而设计出来的。
同样的算式换成 12MHz:
256 - TH1 = 12000000 / 3686400 = 3.2552...取 3,实际波特率 = 12000000 / (384 × 3) = 10416.67,误差 +8.5%。这个数字已经远超 UART 的容忍范围了,所以 12MHz 配 9600 必然是乱码。
我知道有人会说"我也用 12MHz 跑过 9600 啊,能用"。确实有时候"看起来能用"——因为 PC 端的串口芯片容忍度稍微宽一点,加上数据短、没校验,错了你也不一定发现。但只要数据量一大、或者换成双向通信,问题立刻暴露。这种"薛定谔的能用"是最坑的,后面我会专门讲怎么验证。
1.3 误差到底能容忍多少:从采样点算给你看
"误差不能超过 2%"这句话你可能听过很多遍,但很少有人告诉你这个 2% 是怎么来的。我试着把它算清楚,因为算清楚之后你就知道什么时候可以放宽,什么时候必须死抠。
UART 接收端的标准做法是 16 倍过采样:检测到起始位下降沿后,内部时钟开始计数,数到第 8 个时钟时正好落在起始位的正中间,采样确认这是一个有效的起始位(防止噪声误触发);之后每隔 16 个时钟采样一次,依次采第 0 位到第 7 位数据,最后采停止位。
关键在最后一个采样点。以 8 位数据 + 1 停止位为例,停止位的采样点大约在起始沿之后9.5 个位时间的位置。如果收发双方的时钟偏差导致这个采样点偏移了半个位宽(0.5 位),就会采到边沿上,读出错误的值。
那么允许的相对误差是:
0.5 / 9.5 ≈ 5.3%看起来挺宽松?但这是理论极限,实际要打折:
- 这个 5.3% 是收发双方误差之和,单边只有 2.6%
- 线路上的容性负载、上升沿变缓会让有效边沿位置偏移
- 起始沿检测本身有 1 个内部时钟(1/16 位)的量化误差
- 电源噪声、地弹会让采样点抖动
工程上比较稳妥的做法是:单边误差控制在 1% 以内算优秀,2% 以内可以用,超过 3% 就不要指望它能稳定工作了。我个人的习惯是,只要算出来的误差超过 2%,就停下来重新选晶振或者换方案,绝不抱着"先试试"的心态往下走——因为在实验室能跑通和在现场能跑通,完全是两回事。
还有一个容易被忽略的点:误差是有方向的。如果发送方偏快 +1%、接收方也偏快 +1%,那么接收方实际上是"追着"发送方的节奏跑,最后一位的采样点会提前 0.19 个位宽,问题不大;但如果一个偏快一个偏慢,误差就是 2%,采样点偏移翻倍。所以两块板子对通的时候,最好用同型号、同批次的晶振,让误差同向。
2. 8051 上的完整计算:从公式到 TH1 的每一个数
8051 是理解波特率生成机制最好的教材,因为它的结构足够简单、寄存器足够少,所有环节都摆在明面上。搞懂了它,再去看 Cortex-M 的分数分频器,会发现只是把"整数分频"升级成了"整数 + 小数分频"而已,本质没变。
这一节我把 8051 上所有会影响波特率的开关都过一遍,包括几个特别容易混淆的地方。尤其是"串口工作方式"和"定时器工作方式"这两组编号——它们是完全独立的两套体系,但因为都叫"方式1""方式2",坑了无数人。
2.1 串口方式和定时器方式是两码事
先把这两个概念彻底分开,这是新手最容易绕晕的地方。
串口的工作方式(由SCON寄存器的 SM0、SM1 两位决定)说的是"这一帧数据长什么样":
| 串口方式 | SM0 SM1 | 帧格式 | 波特率来源 |
|---|---|---|---|
| 方式 0 | 0 0 | 8 位同步移位 | 固定 fosc/12 |
| 方式 1 | 0 1 | 1 起始 + 8 数据 + 1 停止 | 定时器 1 溢出率 |
| 方式 2 | 1 0 | 1 起始 + 8 数据 + 1 位第9位 + 1 停止 | 固定 fosc/64 或 fosc/32 |
| 方式 3 | 1 1 | 同方式 2 | 定时器 1 溢出率 |
定时器的工作方式(由TMOD的低 4 位或高 4 位决定)说的是"这个定时器怎么数数":
| 定时器方式 | 位宽 | 特点 |
|---|---|---|
| 方式 0 | 13 位 | 老古董,基本不用 |
| 方式 1 | 16 位 | 精度高,但溢出后要手动重装 |
| 方式 2 | 8 位自动重装 | 溢出后自动从 TH 重装到 TL,适合做波特率 |
| 方式 3 | 双 8 位 | 特殊用途 |
做串口通信,标准搭配是:串口用方式 1(或 3),定时器 1 用方式 2。注意串口方式 1 和定时器方式 2 是完全不同的东西,一个管帧格式,一个管计数方式。
为什么定时器要用方式 2 而不是方式 1?因为方式 1 是 16 位,溢出后需要软件在中断里重新写初值,这中间会有几个机器周期的延迟,导致波特率抖动;而且高波特率下中断频率极高,CPU 根本忙不过来。方式 2 是硬件自动重装,溢出瞬间就完成重载,没有任何软件延迟,波特率绝对稳定。
代价是方式 2 只有 8 位,分频范围受限。前面算过,256 - TH1最小取 1,所以 11.0592MHz 下的最高波特率是:
SMOD = 1 时:11059200 / (384 × 1) = 28800 → 不对,还要乘 2^SMOD 实际:2 × 11059200 / (384 × 1) = 57600对,SMOD=1 时上限是 57600。这就是为什么很多老 51 板子跑不了 115200——不是代码写错了,是硬件架构不允许。想要更高,只能换 1T 内核的单片机,或者换带独立波特率发生器的型号。
顺带提一下 SMOD 位的设置。它在PCON寄存器的最高位(bit7),复位默认是 0。PCON的低 7 位是掉电控制之类的功能,所以设置的时候千万不要整个赋值,要用位操作:
PCON |= 0x80; // 置 SMOD = 1 PCON &= 0x7F; // 清 SMOD = 0我见过有人写PCON = 0x80;的,如果你的程序里没用到低 7 位,倒也不会立刻出问题,但这是个坏习惯,迟早踩坑。
2.2 手算示例与常见频率对照表
公式有了,我把常见晶振和常见波特率的组合全部算一遍,做成表。这张表你可以直接拿去对照,省得每次都要按计算器。
先说 11.0592MHz 这颗"标准答案":
| 目标波特率 | SMOD | 256-TH1 | TH1 值 | 实际波特率 | 误差 |
|---|---|---|---|---|---|
| 57600 | 1 | 1 | 0xFF | 57600 | 0 |
| 38400 | 1 | 1.5 | 不可用 | — | — |
| 28800 | 1 | 2 | 0xFE | 28800 | 0 |
| 19200 | 1 | 3 | 0xFD | 19200 | 0 |
| 19200 | 0 | 1.5 | 不可用 | — | — |
| 9600 | 0 | 3 | 0xFD | 9600 | 0 |
| 4800 | 0 | 6 | 0xFA | 4800 | 0 |
| 2400 | 0 | 12 | 0xF4 | 2400 | 0 |
| 1200 | 0 | 24 | 0xE8 | 1200 | 0 |
可以看到,除了 38400 这个"不合群"的波特率,其余全部完美整除。38400 之所以不行,是因为计算出来是 1.5,不是整数——这是 11.0592MHz 的唯一短板。如果项目必须用 38400,要么换 7.3728MHz(7372800/(384×38400) = 0.5,配合 SMOD=1 得 1,可行),要么用 1T 内核。
再看 12MHz,这是国内开发板上最常见、也是坑最多的频率:
| 目标波特率 | SMOD | 计算值 | 取整 | 实际波特率 | 误差 |
|---|---|---|---|---|---|
| 9600 | 0 | 3.2552 | 3 | 10416.67 | +8.51% |
| 9600 | 1 | 6.5104 | 6 | 10416.67 | +8.51% |
| 4800 | 0 | 6.5104 | 6 | 5208.33 | +8.51% |
| 2400 | 0 | 13.0208 | 13 | 2403.85 | +0.16% |
| 1200 | 0 | 26.0417 | 26 | 1201.92 | +0.16% |
有意思的现象出现了:12MHz 在低波特率下反而挺准。因为取整误差被更大的分频系数稀释了——2400 的分频系数是 13,取整误差 0.0208 只占 0.16%;而 9600 的分频系数只有 3.25,取整误差 0.25 占了 7.7%。
这个规律可以推广成一个判断法则:分频系数越大,取整带来的相对误差越小。所以在晶振选得不理想的情况下,降低波特率是提高可靠性的有效手段。如果 12MHz 跑 9600 必然乱,那就降到 2400,能稳定工作,代价只是速度慢点——在很多传感器读数、命令交互的场景里,2400 完全够用。
再补几个"专用频率"的数据,方便你选型时参考:
| 晶振频率 | 最高可用波特率(方式2) | 9600 是否精确 | 典型用途 |
|---|---|---|---|
| 7.3728MHz | 38400 | 是 | 低功耗、低速场景 |
| 11.0592MHz | 57600 | 是 | 最通用 |
| 14.7456MHz | 76800 | 是 | 需要稍高主频 |
| 18.432MHz | 96000 | 是 | 高速 51 |
| 22.1184MHz | 115200 | 是 | 老 51 里的天花板 |
| 12MHz | — | 否(+8.5%) | 只适合 2400 以下 |
注意最后一列"最高可用波特率"是按定时器方式 2、SMOD=1、256-TH1 = 1算的,实际工程中一般不会真的取 1(没有调整余量),会用次高档。
2.3 12MHz 到底能不能用:分场景给结论
这个问题我被问过太多次,干脆给个明确的分场景答案。
能用的情况:波特率 ≤ 2400,且通信数据量不大、没有校验需求。误差 0.16% 完全在安全范围内,跑起来和 11.0592MHz 没区别。
勉强能用的情况:波特率 4800,但双方都用同一批次的晶振,且线路很短(板内或 10cm 以内)。误差 8.5% 已经超限,但如果你只发不收、或者接收端容忍度高,可能"看起来正常"。我不建议这么干,因为这种方案没有任何余量,温度变一点、换一批晶振就崩。
绝对不能用的情况:9600 及以上。误差 8.5% 意味着第 8 位数据的采样点已经偏了 0.7 个位宽,必然错。
那为什么开发板都用 12MHz?因为 12MHz 是 USB 转串口芯片、定时器计时、延时函数的"整数频率",做非串口的例程(LED 闪烁、数码管扫描、定时器中断)时算时间特别方便——1 个机器周期正好 1 微秒。这是教学便利性和通信可靠性的权衡,做产品的时候千万别照抄。
如果板子已经焊死了 12MHz 又必须跑 9600,有几条出路:
- 改用定时器 2 做波特率发生器(52 系列才有)。定时器 2 是 16 位自动重装,分频系数可以做得更细,能凑出更接近 9600 的值。但 12MHz 下依然很难精确,只能减小误差。
- 换 1T 内核的单片机。1T 模式下定时器不分频 12,公式变成
256-TH1 = fosc/(32×baud)(SMOD=0),12MHz 跑 9600 得12000000/(32×9600) = 39.06,取 39,实际 9615.4,误差 +0.16%。直接可用。 - 用芯片内部的独立波特率发生器。很多新型 51 内核单片机内置了专门的波特率发生器,带小数分频,可以做到误差极小。这时候晶振频率就不再是硬约束了。
- 换晶振。最土但最有效,一颗 11.0592MHz 晶振几毛钱。
我个人的选择顺序是:新设计直接上 11.0592MHz 或内部高精度时钟;老板子改造优先考虑换芯片型号;实在不行才动烙铁换晶振。
3. Cortex-M 内核的算法:分数分频器怎么用才不浪费精度
从 8 位机换到 32 位机,很多人会觉得"波特率不用算了,配个115200就行"。这话对了一半——HAL 库确实帮你算了,但算出来的值准不准、会不会因为时钟树配错而偏掉,还是得自己心里有数。而且 Cortex-M 的波特率计算引入了两个新变量:外设总线时钟和过采样模式,比 8051 复杂得多。
3.1 USARTDIV 与 BRR 寄存器的编码
Cortex-M 内核的通用串口外设,核心是一个分数分频器。它的基本公式是:
USARTDIV = fCK / (8 × (2 - OVER8) × 波特率)OVER8是过采样模式位:等于 0 时是 16 倍过采样(默认,抗噪好),等于 1 时是 8 倍过采样(能跑更高波特率,但容错差)。大多数场合用 16 倍。
算出来的USARTDIV是个浮点数,硬件会把它拆成整数部分和小数部分,分别写进BRR寄存器:
BRR = (整数部分 << 4) | (小数部分 × 16 取整)小数部分只有 4 位,所以分辨率是 1/16 = 0.0625。
举个例子,72MHz 的总线时钟跑 115200:
USARTDIV = 72000000 / (16 × 115200) = 39.0625 整数部分 = 39 小数部分 = 0.0625 × 16 = 1 BRR = (39 << 4) | 1 = 0x271误差为零。这不是巧合——72000000 / 115200 = 625,625 / 16 = 39.0625,小数部分正好是 1/16 的整数倍。很多 32 位机的默认时钟配置(8MHz 晶振经 PLL 倍频到 72MHz)就是专门为了让 115200 精确而设计的。
再看一个不那么巧的例子。假设总线时钟是 36MHz,还是 115200:
USARTDIV = 36000000 / (16 × 115200) = 19.53125 整数部分 = 19 小数部分 = 0.53125 × 16 = 8.5 → 取 8 或 9取 8:实际分频 = 19.5,实际波特率 = 36000000 / (16 × 19.5) = 115384.6,误差+0.16%取 9:实际分频 = 19.5625,实际波特率 = 36000000 / (16 × 19.5625) = 115015.9,误差-0.16%
两个都可用,但差了 0.32% 的方向。这就是为什么我说"两块板子最好用同样的时钟配置"——如果一边取 8 一边取 9,误差就叠加了。
HAL 库里的UART_SetConfig函数会自动做这个四舍五入,但它用的是"就近取整",不会告诉你误差是多少。我一般在初始化之后会手动算一遍,确认误差在 1% 以内才放心。
3.2 时钟树决定了你的误差上限
32 位机最容易出错的地方不是 BRR 的计算,而是你根本不知道串口用的是哪个时钟。
Cortex-M 的串口挂在 APB 总线上,APB1 和 APB2 的频率可能不同,而且还会受到 AHB 分频、PLL 倍频的影响。你在 CubeMX 里点几下把主频拖到 168MHz,背后的分频关系可能已经变了三轮。如果串口挂在 APB1 上,而 APB1 的最大频率只有主频的一半,那么实际喂给串口的时钟是 84MHz 而不是 168MHz。
用错时钟的后果是什么?波特率整体偏移一倍或者一半,表现为"完全收不到数据"或者"全是乱码"。这种错误在调试时特别隐蔽,因为代码编译没报错、初始化函数返回 HAL_OK、示波器上看波形也规规矩矩——只是频率不对。
我的排查习惯是:先用示波器量一位的时间,反推实际波特率,再倒推实际时钟。比如你配的是 115200(位宽 8.68 微秒),实测位宽 17.36 微秒,正好翻倍,那就是时钟源选错了,去查RCC配置里串口挂在哪条总线上。
还有一个隐藏坑:低功耗模式下时钟会变。有些芯片进入低功耗后会切换到内部低速时钟,串口如果没关,醒来之后波特率就完全不对了。做电池供电的项目一定要检查这一点。
3.3 内部 RC 与外部晶振的取舍
现在的 32 位机基本都内置了 RC 振荡器,出厂校准精度标称 ±1%,省掉一颗晶振和两个电容,PCB 面积也小。听起来很美,但用在串口上要谨慎。
原因在于 RC 振荡器的精度是全温度范围、全电压范围的指标,实验室 25 度下可能是 ±0.5%,到了 -20 度或者 85 度,漂到 ±2% 甚至 ±3% 都很正常。而串口的总误差预算只有 3% 左右,光时钟就吃掉一大半,剩下的余量全给了线路和采样误差,等于没有余量。
我的经验判断:
| 场景 | 时钟源建议 |
|---|---|
| 波特率 ≤ 9600,室温环境,短距离 | 内部 RC 可用 |
| 波特率 19200 ~ 57600,一般工业环境 | 建议外部晶振 |
| 波特率 ≥ 115200 | 必须外部晶振 |
| 有温度变化、长线传输、多机通信 | 必须外部晶振 |
| 需要长时间稳定运行、不能重启 | 必须外部晶振 |
外部晶振也不是随便焊一颗就行。常见的 8MHz 无源晶振,负载电容要匹配(一般 12pF 到 22pF,具体看晶振规格书),电容选大了起振慢甚至不起振,选小了频率偏移。我见过为了"保险"焊 30pF 的,结果起振时间超过 10 毫秒,芯片等不及就切到内部 RC 了,然后串口一路乱码——查了两天才发现是电容的问题。
还有一个细节:晶振的频率精度本身也有误差,普通晶振是 ±20ppm 到 ±30ppm,换算成百分比是 0.002% 到 0.003%,相对串口 3% 的预算几乎可以忽略。所以选晶振的时候优先看"频率值是否合适",而不是"精度是否够高",方向别搞反了。
4. 实操:从选晶振到用示波器验证的完整链路
前面讲的都是"应该怎样",这一节讲"实际怎么做"。我按一个新项目的顺序走一遍:怎么选频、怎么焊、代码怎么写、怎么验证、怎么调。中间穿插一些踩过的坑,都是文档里不会写的。
4.1 选型与硬件落地
选晶振频率的优先级,我总结成一句话:先看波特率表,再看主频需求,最后看成本。
具体步骤:
- 列出项目需要的所有波特率。如果只有一个(比如只跟一个模块通信,固定 9600),选择面很宽;如果有多个(比如同时接 GPS 和蓝牙,一个 9600 一个 115200),就要找公约数。
- 用
fosc / (384 × 波特率)逐个验算,看是否都是整数。有一个不是整数,就换频率再试。 - 确认主频够不够用。有些场景需要较高的主频做运算或者刷屏,11.0592MHz 可能不够,那就往上选 22.1184MHz。
- 确认芯片支持的最高晶振频率。老 51 一般到 24MHz 就到顶了,超了不稳定。
硬件落地环节,有几个细节值得单独说:
负载电容的计算。晶振规格书里会给一个CL值(负载电容),外接的两个电容C1、C2大致满足CL = C1×C2/(C1+C2) + Cstray,其中Cstray是 PCB 寄生电容,一般估 3pF 到 5pF。如果CL = 12pF、Cstray = 4pF,那么C1 = C2 ≈ 16pF。实际取值 15pF 到 22pF 都能工作,但要想频率准,最好按算出来的值选。
走线。晶振到芯片的两个引脚,走线要尽可能短(最好 5mm 以内)、等长、远离其他信号线,下方铺地。晶振外壳如果接地,抗干扰会好很多。我见过把晶振放在板子边缘、走线 20mm 的,跑起来时好时坏,改成紧贴芯片之后就稳了。
不要用示波器探头直接戳晶振引脚。探头的输入电容有 10pF 到 15pF,并联上去会改变负载电容,可能导致停振或者频率偏移。要测的话,用高阻探头(1:10 档)或者从芯片的时钟输出脚(MCO)测。
电源去耦。晶振电路对电源噪声敏感,芯片的电源脚旁边一定要有 0.1μF 陶瓷电容,距离越近越好。
4.2 代码配置:两套模板
先给 8051 的。这段代码可以直接抄,改TH1就行:
/* 8051 串口初始化:11.0592MHz,9600bps,8N1 */ void UART_Init(void) { SCON = 0x50; /* 串口方式1,8位数据,允许接收 */ TMOD &= 0x0F; /* 清除定时器1的配置位 */ TMOD |= 0x20; /* 定时器1,方式2,8位自动重装 */ TH1 = 0xFD; /* 256 - 3 = 253,对应9600bps */ TL1 = 0xFD; /* 首次装载,之后硬件自动重装 */ PCON &= 0x7F; /* SMOD = 0,波特率不加倍 */ TR1 = 1; /* 启动定时器1 */ ES = 1; /* 允许串口中断 */ EA = 1; /* 开总中断 */ }几个注意点:
TH1和TL1都要赋初值。虽然方式 2 下TL1会在第一次溢出后被硬件重装,但启动瞬间TL1的值是随机的,不初始化的话第一个字节可能就错了。TMOD用"与或"操作,不要整个赋值。因为TMOD的高 4 位是定时器 1 的配置、低 4 位是定时器 0 的,如果你程序里定时器 0 也在用(比如做系统滴答),整个赋值会把它干掉。- 如果要用 SMOD=1 提高波特率,把
PCON &= 0x7F改成PCON |= 0x80,然后TH1重新算。
再给 Cortex-M 的寄存器级写法(不用库,方便理解):
/* Cortex-M 串口初始化:72MHz,115200bps,8N1 */ void UART_Init(void) { /* 1. 使能串口和 GPIO 时钟(略,各芯片不同) */ /* 2. 配置 GPIO 复用为串口功能(略) */ /* 3. 计算并写入 BRR */ /* USARTDIV = 72000000 / (16 * 115200) = 39.0625 * BRR = (39 << 4) | (0.0625 * 16) = 0x271 */ USART1->BRR = 0x271; /* 4. 使能收发、使能串口 */ USART1->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; /* 5. 如果需要中断接收 */ USART1->CR1 |= USART_CR1_RXNEIE; NVIC_EnableIRQ(USART1_IRQn); }这段代码的重点在BRR的计算。我建议你在代码里加一行注释,把计算过程写清楚——因为半年后你回头看,或者换个人接手,光看0x271是完全不知道为什么的。更稳妥的做法是写成宏或者常量表达式:
#define UART_BRR_VAL ((72000000UL / (16UL * 115200UL)) << 4) \ | (((72000000UL % (16UL * 115200UL)) * 16UL / (16UL * 115200UL)) & 0x0F)这样改了时钟频率,BRR会自动跟着变,比硬编码安全得多。
4.3 用 0x55 波形验证波特率
代码写完了,怎么确认波特率是对的?最实用的一招:发一个 0x55,用示波器看波形。
为什么是 0x55?因为它的二进制是01010101,UART 是低位先发,所以实际发送的位序列是1,0,1,0,1,0,1,0,加上起始位(0)和停止位(1),整帧是:
0 1 0 1 0 1 0 1 0 1 起 D0 D1 D2 D3 D4 D5 D6 D7 停完美的方波,5 个完整周期,10 个位。所以:
方波频率 = 波特率 / 29600 波特率对应 4800Hz 方波,115200 对应 57600Hz。示波器上一测频率,就知道波特率对不对,比看串口助手直观一百倍。
更进一步,可以量位宽:
| 波特率 | 理论位宽 | 允许范围(±2%) |
|---|---|---|
| 2400 | 416.67 μs | 408.3 ~ 425.0 μs |
| 4800 | 208.33 μs | 204.2 ~ 212.5 μs |
| 9600 | 104.17 μs | 102.1 ~ 106.3 μs |
| 19200 | 52.08 μs | 51.0 ~ 53.1 μs |
| 38400 | 26.04 μs | 25.5 ~ 26.6 μs |
| 57600 | 17.36 μs | 17.0 ~ 17.7 μs |
| 115200 | 8.68 μs | 8.51 ~ 8.85 μs |
| 230400 | 4.34 μs | 4.25 ~ 4.43 μs |
量的时候注意:示波器的时间基准要够细。测 115200 的位宽只有 8.68 微秒,如果你的示波器采样率只有 100MHz,每个位宽只能采到 800 多个点,够用;但如果带宽不够(建议 ≥100MHz),上升沿会被削平,测出来的位宽会偏大。
没有示波器怎么办?用逻辑分析仪,几块钱的那种就行。逻辑分析仪的好处是能直接解码 UART,把每一位的值标出来,如果波特率不对,你会看到解码出来的数据是错的或者干脆解不出来。我一般会把逻辑分析仪和串口助手同时开着,一个看时序,一个看内容。
还有一种"土办法":自发自收回环测试。把 TX 和 RX 短接,发什么收什么。如果回环正常,说明芯片内部的发送和接收时钟是一致的(因为它们同源),但这不能证明波特率的绝对值是对的——两边都错成一样的,回环依然正常。所以回环只能验证"逻辑没问题",不能验证"频率没问题"。想要验证绝对值,必须用外部设备(示波器、另一块已知正确的板子、或者 PC 串口)。
这个区别很重要,很多教程只教回环测试,导致大家以为回环通了就万事大吉,接上 PC 才发现乱码。
4.4 一个自动枚举最优分频值的小脚本
每次手算太麻烦,我写了个小脚本,把所有可能的TH1值遍历一遍,找出误差最小的那个,顺便告诉你误差是多少。8051 的场景很适用:
def find_th1(fosc, baud, smod_list=(0, 1)): """遍历所有 TH1,找出误差最小的配置""" best = None for smod in smod_list: for th1 in range(256): div = 256 - th1 actual = (2 ** smod) * fosc / (384 * div) err = (actual - baud) / baud * 100 if best is None or abs(err) < abs(best[3]): best = (smod, th1, actual, err) return best # 试一试 for b in [1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]: smod, th1, actual, err = find_th1(11.0592e6, b) flag = "OK " if abs(err) < 2.0 else "BAD" print(f"{flag} {b:>7} bps -> SMOD={smod} TH1=0x{th1:02X} " f"实际={actual:>9.1f} 误差={err:+.3f}%")用 11.0592MHz 跑一遍,输出大致是这样:
OK 1200 bps -> SMOD=0 TH1=0xE8 实际= 1200.0 误差=+0.000% OK 2400 bps -> SMOD=0 TH1=0xF4 实际= 2400.0 误差=+0.000% OK 4800 bps -> SMOD=0 TH1=0xFA 实际= 4800.0 误差=+0.000% OK 9600 bps -> SMOD=0 TH1=0xFD 实际= 9600.0 误差=+0.000% OK 19200 bps -> SMOD=1 TH1=0xFD 实际= 19200.0 误差=+0.000% BAD 38400 bps -> SMOD=1 TH1=0xFE 实际= 28800.0 误差=-25.000% OK 57600 bps -> SMOD=1 TH1=0xFF 实际= 57600.0 误差=+0.000% BAD 115200 bps -> SMOD=1 TH1=0xFF 实际= 57600.0 误差=-50.000%注意 38400 和 115200 的结果:脚本找到的"最优解"误差依然大得离谱。这说明这两个波特率在 11.0592MHz + 12T 的架构下根本无解,不是算法问题,是硬件限制。看到这种结果,就别在代码上折腾了,去改硬件。
这个脚本改成 32 位机的版本也很简单,把分频公式换成fCK / (16 × baud),然后量化到 1/16 的步进,算一下实际值和误差就行。我在选时钟树的时候经常用它快速扫一遍,看看哪个主频下所有目标波特率都能压到 0.5% 以内。
5. 乱码排查实录:把问题按概率排个序
串口乱码是单片机开发里最常见的故障,没有之一。但"乱码"这两个字太笼统了,不同表现对应完全不同的原因。我按现象分类整理了一张速查表,基本能覆盖九成以上的情况。
5.1 高频故障速查表
| 现象 | 最可能的原因 | 排查手段 |
|---|---|---|
全是0x00或0xFF | 接收端没配对,或 TX/RX 接反 | 交换 TX/RX 再试 |
| 全是乱码,字符随机 | 波特率不匹配(最常见) | 示波器量位宽,反推波特率 |
| 前几个字节对,后面乱 | 缓冲区溢出,或中断响应太慢 | 降低波特率、开 DMA |
| 偶尔错一个字节 | 误差接近临界值、线路干扰 | 重算误差、加共地线、缩短线长 |
| 固定位置出错 | 帧格式不一致(校验位、停止位) | 核对两端的 8N1 配置 |
| 只能收不能发 | TX |