搞嵌入式这些年,最难缠的问题之一就是“串口不够用”。一个项目里要挂蓝牙模块、GPS、定位模块、串口屏,还要给RTOS留一个调试口,板子上的UART瞬间就没了。尤其ESP32这种“看着外设很多,真用起来捉襟见肘”的主控,默认日志还要吃掉一个通道,真正能自由支配的串口往往只剩一两个。这时候我一般不去换主控,而是直接加串口扩展芯片。国产方案里,CH432、CH438、CH9434这三款是绕不开的选择,我习惯叫它们“串口扩展三剑客”。
这篇博文会把三款芯片的定位、接口差异、波特率计算方法和实测经验一次讲透。无论你是做工业网关、多路传感器采集,还是给ESP32扩展四路UART,都可以直接拿这份内容当选型参考。懂的人省时间,刚入门的人也能少踩几个坑。
1. 为什么需要串口扩展芯片,ESP32用户尤其明白
1.1 串口资源的“硬瓶颈”
很多MCU的数据手册上写着一堆UART,但真到画板子、写驱动的时候,你会发现“可用的”根本没几个。举几个常见场景:
- 主控和电脑之间要一个调试串口,这是不能动的底线,一占就是一路。
- 外挂设备多:4G模块、GPS模组、RS485总线节点、指纹模块、串口摄像头,每个都是标准UART接口。
- 板子上还要留SPI给Flash、留I2C给传感器,引脚资源被层层瓜分。
- 有些低功耗主控的UART还和下载引脚复用,你想烧录就不能接设备。
尤其是ESP32,虽然芯片内部有3个UART,但日常开发时串口0固定连接USB转串口芯片用于下载和日志,串口1常被Flash占用,串口2一旦接了蓝牙音频或其他外设,又不方便复用。真正能拿来扩展设备串口的通道,经常只剩下一个。这个“硬瓶颈”不是靠改软件能解决的,必须从硬件层面拆掉。
好消息是,串口扩展芯片正好解决这个痛点:主控只需要提供一个SPI或并行总线,就能换来2路、4路甚至8路独立UART,而且每路波特率可单独配置,互不干扰。
1.2 三条路线:软件模拟、换主控、外置扩展芯片
面对串口不够用,大多数人的第一反应是软件模拟UART,也就是用两个GPIO自己“bit-bang”时序。这个方案不是不能用,但局限性非常明显。
软件模拟UART在低速场景,比如9600bps或19200bps,配合定时器还能凑合。可一旦上了115200bps,GPIO翻转频率要求就很高,而且系统里只要有点中断抖动或任务调度延迟,波形就会变形,轻则误码,重则整个通信链路瘫痪。对稳定性要求高的产品,我基本不会考虑这条路线。
第二条路线是换一个串口资源更丰富的主控。比如从STM32F103换到STM32F429,或者从ESP32换到ESP32-S3。这个方法暴力有效,但代价很大:原有代码要移植、PCB要重画、物料成本上升、供货周期也可能变长。仅仅为了多几个UART就动主控,多少有点“杀鸡用牛刀”。
第三条路线就是外置串口扩展芯片。主控端只占一个SPI接口,剩余繁琐的UART时序、FIFO、中断全交给扩展芯片。CH432、CH438、CH9434这三款我都实际用过,各有擅长场景,下面从规格到调试逐一说清楚。
2. 三款芯片核心规格对比,别只盯通道数
2.1 CH432:轻量双通道,小项目首选
CH432是一款双通道UART扩展芯片,支持SPI和并行接口两种访问方式,通信接口非常灵活。它的最高波特率做到4Mbps,日常工作电压范围宽,3.3V和5V系统都能用,这在很多老式工控板上很吃香。
我最早用CH432是在一个农用设备控制器上,主控MCU只剩一组空闲SPI,需要同时挂一个RS485模块和一个串口显示屏。CH432一片搞定,寄存器风格简洁,初始化代码几十行就能跑通。它内置了收发FIFO,虽然深度不算大,但对中小数据包场景足够用,主控不需要在每次收发时被中断打断太多次。
选CH432时要注意一点:它只有两路UART,适合那种“我只要再补两个串口”的项目。如果预期以后还要挂更多设备,直接上4路或8路会更省事,否则后期改板成本更高。
2.2 CH438:八通道老将,多设备采集主力
CH438是八通道UART扩展芯片,同样支持SPI和并行接口,最高波特率也能到4Mbps。它最大的特点是每个通道拥有独立的波特率发生器、独立的FIFO和独立的Modem控制信号,八个通道之间基本不互相牵制。
我曾经用一个CH438做工业现场采集网关,下面挂8路RS485传感器,每路波特率甚至都不一样:有的9600,有的115200,有的用偶校验,还有一路是9位数据模式。CH438把这些全扛下来了,主控只需要通过SPI去读FIFO里的数据。相比CH432,CH438的寄存器功能更完整,中断处理也更复杂,适合做“小网关”类产品。
另外,CH438的并口模式适合没有SPI总线或者SPI被占满的主控。并行接口数据吞吐更高,但占用的GPIO数量会显著增加,选型时需要权衡。如果你只是单纯想扩展两路串口,上CH438有点大材小用,成本也会上去。
2.3 CH9434:SPI四通道,大FIFO新选择
CH9434是三款里面我目前最喜欢的,四路UART,纯SPI从机接口,最高波特率做到6Mbps。它比CH432多两路,比CH438少四路,但它的优势不在“数量”,而在“接口清爽”和“FIFO更大”。
CH9434的SPI从接口很适合ESP32这类SPI主机外设丰富的现代主控。主控上只占SCK、MOSI、MISO、CS四条线,加上中断和复位,总共也就六根线,却能换回四路独立UART,还支持RTS/CTS硬件流控。对设计紧凑的板子来说,这个性价比非常高。
我实测过CH9434在四路全开115200bps的情况,配合大容量FIFO,主控不需要每秒处理海量中断,数据可以先在芯片内部缓冲,等攒到一定数量再一次性读走。这种设计在RTOS场景下特别友好,任务调度不慌张,丢包率也低。
2.4 一张表看懂三款差异
| 参数项 | CH432 | CH438 | CH9434 |
|---|---|---|---|
| UART通道数 | 2路 | 8路 | 4路 |
| 主机接口 | SPI / 并口 | SPI / 并口 | SPI从机 |
| 最高波特率 | 4Mbps | 4Mbps | 6Mbps |
| 硬件流控 | 视具体型号 | 支持 | 支持RTS/CTS |
| FIFO特性 | 收发FIFO,深度较小 | 每路独立FIFO | 大容量收发FIFO |
| 典型封装 | SOP类小封装 | LQFP类多引脚封装 | LQFP/QFN类紧凑封装 |
| 推荐场景 | 轻量补两路串口 | 八路采集网关、串口服务器 | ESP32等主控扩展四路UART |
只看通道数量很容易选错。比如项目需要四路串口,有人觉得CH438更划算,但CH9434往往引脚占用更少、FIFO更充裕。反过来,如果只差两路串口,上四路或八路芯片纯属浪费成本。选型第一原则永远是“够用就好,留有余量”。
3. 波特率计算原理和配套工具
3.1 波特率是怎么产生的,为什么总是乱码
很多朋友用串口扩展芯片遇到乱码,第一反应就是怀疑芯片坏了。其实八成是波特率没算对。要理解这个问题,先得明白UART波特率在芯片内部是怎么产生的。
典型UART内部有一个波特率发生器,本质是个分频器。外部给一个基准时钟,比如22.1184MHz晶振或PLL合成时钟,经过分频后产生串口的工作时钟。因为UART接收端要在每一位的中间采样,通常会做16倍过采样,所以实际波特率公式是:
目标波特率 = 基准时钟频率 / (分频系数 × 16)也就是说,我们往寄存器里写的“分频值”并不是直接等于波特率,而是要除以16。很多新手直接在寄存器里填115200,芯片当然不可能认识,结果就是输出一堆乱码。
更麻烦的是,如果分频系数是小数,而芯片只支持整数分频,那实际波特率和目标波特率之间就会有误差。当误差超过一定范围,通信双方就无法准确采样。一般来说,单侧误差尽量控制在1%以内,双方累计误差超过3%~4%就容易出错。这也是为什么工业上特别钟爱22.1184MHz晶振——它能被很多常见波特率整数整除。
3.2 手算示例:22.1184MHz下面的常见波特率
用22.1184MHz作为基准时钟时,很多常见波特率能得到完全整数的分频系数:
| 目标波特率 | 分频系数(÷16后) | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 144 | 9600 | 0% |
| 57600 | 24 | 57600 | 0% |
| 115200 | 12 | 115200 | 0% |
| 230400 | 6 | 230400 | 0% |
| 460800 | 3 | 460800 | 0% |
| 921600 | 1.5 | 737280 | -20% |
上面最后一行很关键:22.1184MHz下的921600bps,分频系数是1.5。如果芯片支持小数分频,可以把整数位写1、小数位写0.5,精准得到921600。如果芯片只支持整数分频,那就只能取1或2,实际波特率偏差非常大,基本不能可靠通信。
我建议在做串口扩展设计时,先确认三件事:外部时钟是多少?目标波特率是多少?芯片支不支持小数分频。把这三个问题搞清楚,再写驱动,能省下一大半调试时间。
3.3 附一个能直接跑的波特率计算工具
很多时候不是不会算,而是算起来烦。我给项目组写过一个Python小脚本,输入基准时钟和目标波特率,自动输出分频系数、实际波特率和误差值。这里直接分享出来,支持普通整数分频和带小数分频两种模式。
#!/usr/bin/env python3 def calc_divider(clock_hz, target_baud, oversample=16): """整数分频计算:返回分频系数、实际波特率、误差百分比""" div = clock_hz / (target_baud * oversample) int_div = max(1, int(div)) actual = clock_hz / (int_div * oversample) error = (actual - target_baud) / target_baud * 100.0 return int_div, actual, error def calc_frac_divider(clock_hz, target_baud, frac_bits=2, oversample=16): """小数分频计算:返回整数部分、小数部分、实际波特率、误差百分比""" div = clock_hz / (target_baud * oversample) int_part = int(div) frac_scale = 1 << frac_bits frac_part = int(round((div - int_part) * frac_scale)) if frac_part >= frac_scale: int_part += 1 frac_part = 0 total = int_part + frac_part / frac_scale actual = clock_hz / (total * oversample) error = (actual - target_baud) / target_baud * 100.0 return int_part, frac_part, actual, error if __name__ == "__main__": clock = 22_118_400 # 22.1184MHz for baud in [9600, 57600, 115200, 230400, 460800, 921600]: div, actual, err = calc_divider(clock, baud) printf = f"{baud:>8} | div={div:<8} | actual={actual:>12.3f} | err={err:+.4f}%" print(printf)脚本输出的分频系数,最终要写成寄存器值时,需要再按芯片手册做一次换算,比如高字节、低字节、小数分频位分别填哪里。不同芯片寄存器布局不一样,但计算公式是通用的。
4. 选型决策和ESP32实战
4.1 选型决策:先回答四个问题
别人问我选哪款,我一般会让他先回答四个问题。
第一个问题:到底需要几路UART?2路就选CH432,4路优先看CH9434,8路直接看CH438。通道数量是硬指标,别的再好也替代不了。
第二个问题:主控还有没有空闲SPI?CH432和CH438都支持并口,如果你的主控SPI已经被Flash、LCD、传感器占满,可以考虑走并口模式。CH9434是纯SPI从机,没有SPI接口的主控就不要硬选。
第三个问题:波特率要求多高?最高的那一路如果超过4Mbps,CH9434更合适。如果只是常规115200bps,三款都能胜任。
第四个问题:要不要硬件流控?外接4G模块或者高速数传时,RTS/CTS流控能有效避免FIFO溢出。CH9434在这方面做得比较完整。
把这四个问题回答完,选型基本就落地了。为了省成本硬上低规格芯片,或者为了“以后可能用得上”盲目上高规格,都是浪费钱的做法。
4.2 ESP32 + CH9434的接线与初始化
ESP32扩展四路串口,最实用的组合就是ESP32的硬件SPI加CH9434。接线方式不复杂,典型连接如下:
| ESP32引脚 | CH9434引脚 | 说明 |
|---|---|---|
| GPIO5 | CS# | 片选,低有效 |
| GPIO18 | SCK | SPI时钟 |
| GPIO23 | MOSI | 主出从入 |
| GPIO19 | MISO | 主入从出 |
| GPIO17 | INT# | 中断输出,接ESP32输入 |
| GPIO21 | RST# | 复位控制,可复用普通IO |
| 3V3 | VCC | 电源 |
| GND | GND | 共地 |
- 注意CH9434是SPI从机,ESP32是SPI主机,MOSI和MISO千万别接反,我第一次焊就是接反了,排查了半天才发现。
- INT#和RST#建议都接到ESP32上,虽然不接也能跑,但有中断配合时CPU开销低很多,复位也方便。
初始化代码我这里给一个Arduino框架的简单示意,里面用到了SPI库和前面计算的波特率分频值。命令字和寄存器地址要根据CH9434手册确认,我这里给的是通用框架。
#include <SPI.h> #define CS_PIN 5 #define INT_PIN 17 #define RST_PIN 21 void ch9434_write(uint8_t reg, uint8_t val) { SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); digitalWrite(CS_PIN, LOW); SPI.transfer(0x01); // 写命令字,具体以手册为准 SPI.transfer(reg); SPI.transfer(val); digitalWrite(CS_PIN, HIGH); SPI.endTransaction(); } uint8_t ch9434_read(uint8_t reg) { uint8_t val; SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)); digitalWrite(CS_PIN, LOW); SPI.transfer(0x00); // 读命令字,具体以手册为准 SPI.transfer(reg); val = SPI.transfer(0xFF); digitalWrite(CS_PIN, HIGH); SPI.endTransaction(); return val; } void setup() { SPI.begin(); pinMode(CS_PIN, OUTPUT); pinMode(INT_PIN, INPUT_PULLUP); pinMode(RST_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); // 复位CH9434 digitalWrite(RST_PIN, HIGH); delay(50); digitalWrite(RST_PIN, LOW); delay(50); digitalWrite(RST_PIN, HIGH); delay(100); // 用脚本算好的分频值配置UART通道,这里示例写0x0C对应115200 ch9434_write(0x10, 0x0C); // 通道0波特率寄存器 ch9434_write(0x11, 0x00); // 通道0波特率高字节 ch9434_write(0x20, 0x03); // 使能FIFO和接收中断,简化示意 } void loop() { // 业务代码 }写驱动时建议加一个“读ID寄存器”的步骤。很多扩展芯片都有厂家ID或版本寄存器,初始化后先读一遍,值对得上再继续配置。这个习惯能帮你快速排除SPI接线问题。
4.3 硬件上容易踩的坑
第一个坑是电平匹配。CH9434是3.3V芯片,不能直接接5V,如果主控IO是5V电平,中间要加电平转换或串联电阻分压。CH432支持电压范围更宽,但也别把5V电源直接往3.3V的IO上怼。
第二个坑是电源电流。四路UART全双工同时收发时,芯片瞬间电流不小。我习惯在VCC引脚旁边放一个0.1uF陶瓷电容加一个10uF钽电容,靠近芯片放置,避免电源噪声影响高频通信。
第三个坑是SPI线长。CH9434的SPI从机时序对线长敏感,我实测下来,SCK/MOSI/MISO/CS这四条线尽量控制在10cm以内,并且不要跨分割区。线太长时SPI速率要适当降低,否则通信偶发错误很头大。
第四个坑是中断处理。四路UART共用一个INT引脚,中断服务程序里一定要通过寄存器判断到底是哪一路触发的,然后再去读对应FIFO。如果漏了这个判断,经常会出现某一路数据覆盖另一路的情况。
5. 调试中踩过的坑与排查清单
5.1 乱码和波特率不准
串口扩展芯片最典型的故障现象就是“所有通道都乱码”,或者“第一路正常,第二路乱码”。前者基本是共用配置写错了,后者多半是每路独立配置没写全。
排查思路我一般按这个顺序走:
- 用示波器量扩展芯片发送引脚的TX波形,看每一位的实际宽度。比如115200bps下,1位起始位宽度应该是8.68us左右,偏差太大就是波特率没配准。
- 用逻辑分析仪抓RX/TX时序,对比主机和从机的波特率是否一致。
- 回头用脚本算一遍分频值,看看是不是取整导致误差超限。
- 检查外部晶振频率是不是真的符合手册要求,之前遇到过有人把24MHz晶振当成22.1184MHz用的,结果128000bps以下全乱。
如果你用的是支持小数分频的芯片,一定记得把分频的小数部分写进寄存器,不要只写整数部分。很多“偶发乱码”就是因为把小数分频位漏掉了,平时速率低看不出问题,一旦高速就立刻暴露。
5.2 中断与FIFO配置
有个问题我调试时遇到过很多次:启用FIFO后,数据反而“卡住”了,要等很久才刷出来。原因通常是接收FIFO触发阈值设置得太高,小包数据一直攒不够触发线,CPU就没收到中断。
解决方法是把FIFO触发阈值调低一些。比如每次来一个字节就触发中断,虽然CPU开销大一点,但延迟最低;如果数据量大,可以设成FIFO半满才触发,然后在中断里批量读取。这个阈值要根据实际通信包大小反复调,没有绝对最优值。
另外,硬件流控接线要特别注意RTS和CTS的方向。设备A的RTS要接设备B的CTS,两个信号是交叉的。接成直连的话,流控永远失效,发送端不会停止发送,接收端FIFO一满就开始丢数据。这种问题用万用表都不一定看得出来,最好用逻辑分析仪抓RTS/CTS的时序变化。
5.3 实测经验
最后说几个我自己测试出来的经验值,供参考。
在ESP32 + CH9434的组合下,SPI时钟设4MHz,四路UART全部开115200bps,全双工持续收发,跑72小时没出现丢字节。如果把SPI时钟提高到十几兆赫兹,同时信号线又比较长,偶发错包率会明显上升,所以稳定优先。
CH438八路全开时,如果各路都在低速收发,比如1200bps,反而容易让主控频繁进中断。因为低速波特率下每个字节的持续时间更长,中断触发频率不一定低。这时候要开FIFO,还要考虑在中断里同时处理多路数据,别一次中断只读一个字节就退出。
测试工具我用的是最土的办法:串口扩展芯片的某一对TX/RX短接做回环,然后通过另一个USB转串口工具往它发数据,看能不能原样收回来。这个办法虽然土,但能快速验证本轮驱动改动是否破坏了基础收发链路。把基础的收发链路打通之后,再去验证流控、中断、FIFO这些高级功能,调试效率会高很多。
最后再分享一个小技巧:设计PCB时,给每路串口的TX/RX都留一组测试点,最好还能用跳线短接。产品量产之后如果偶尔出问题,可以直接在现场做回环测试,不用拆开外壳再去飞线。这个习惯帮我在售后阶段省了太多事。