做嵌入式这些年,“串口不够用”这个问题几乎每做一个新项目都要碰上一次。ESP32看似给了三个UART,实际上Serial0被烧录和调试占用,留给外设的往往只有一两个,而一个稍复杂的IoT设备里,GPS模块要串口、LoRa模块要串口、4G模组要串口、几个RS485仪表还要串口。这时候最省心的方案就是外挂串口扩展芯片。沁恒的CH432、CH438、CH9434正是这个领域最常见的三颗器件,分别对应2路、4路和高速4路三种定位。这篇文章不打算念手册,而是从一个常年跟这三颗芯片打交道的开发者角度,把选型逻辑、寄存器使用、波特率计算和调试教训一起讲清楚。
1. 为什么突然对比这三颗芯片:从ESP32串口不够用说起
1.1 一个典型的IoT项目是怎么把串口耗尽的
去年我做过一个环境监测网关,主控用的是ESP32-S3,板上要接的设备包括:一个GPS定位模块、一个NB-IoT模组、一个RS485温湿度采集器、还有一个用来做本地配置的蓝牙串口透传模块。地盘还没捂热,串口先不够用了——ESP32-S3的UART0被USB烧录占用,UART1留给日志,真正能自由分配给外设的只有UART2一个。最后没办法,只能让RS485和GPS分时复用同一个物理串口,靠软件切GPIO电平来切换收发方向。当天夜里调试的时候,GPS报文和RS485轮询指令相互踩踏,日志刷出来全是乱码,那一刻我就下定决心:下一版一定要加串口扩展芯片。
这不是个例。很多MCU的串口数量都是“纸上够用,实际缩水”:有的UART被Bootloader占用,有的和JTAG复用引脚,有的默认分配给外设Flash。一旦设备同时接了多路传感器、无线模块、工业仪表,板内串口资源就像高峰期的高架桥,永远在堵。串口扩展芯片就是在这个背景下被大量采用的,说白了就是把“串口数量不够”这个硬件问题,从“改主控”或“改架构”变成“加一颗芯片”这种标准操作。
1.2 三颗芯片的家族定位:2路/4路/高性能4路
很多朋友在选型的时候容易陷入一个误区:一上来就看“谁路数多”。其实CH432、CH438、CH9434这三颗芯片在产品线里的定位差异非常明显,搞清楚定位比背参数重要得多。
CH432定位是双串口扩展,SPI接口,适合只需要多出两路UART的单片机系统,芯片体积小、外围简单,是低成本扩展串口的“轻骑兵”。CH438定位是四串口扩展,以8位并行总线访问为主,适合本身有宽松IO资源的总线型MCU或工控板卡,处理并发串口能力不错,是“老成持重”的路子。CH9434同样是四串口,但它走SPI接口,而且内置了大容量FIFO,SPI时钟可以跑得很高,面向的是“我需要快、需要大缓冲、需要尽量少打断CPU”的场景,属于三把剑里最能打的那个。
这三颗芯片不是替代关系,而是互补关系。选型的第一步,其实不是比谁参数高,而是先想明白你的项目到底缺几路串口、主控还能拿出什么接口来跟扩展芯片通信、系统对实时性和大流量数据有没有要求。这三个问题想清楚了,选型表翻起来才有意义。
2. CH432拆解:轻量双串口,SPI直连的单片机好搭档
2.1 内部结构与寄存器存取方式
CH432通过SPI接口和主控通信,支持SPI Mode 0和Mode 3,标准的四线制:片选CS、时钟SCK、主出从入MOSI、主入从出MISO。从MCU侧看,它就像一颗挂在SPI总线上的从设备。芯片内部每一路UART都有完整的寄存器组,包括收发保持寄存器、中断使能、中断状态、FIFO控制、线路控制、调制解调控制、线路状态等,寄存器的整体布局沿用了经典的16C550风格,写过串口驱动的人上手会很快。
访问方式上CH432比较特殊,它不是简单的“地址即片选”,而是先通过SPI写入要访问的寄存器地址,再读写数据。这个“先地址后数据”的时序和很多SPIFlash类似,所以习惯写SPI驱动的人不会觉得别扭。需要注意的是,CH432支持单次读写和多字节连续读写两种模式,如果要连续填充发送FIFO,务必用上连续读写,否则每个字节都走一遍“地址+数据”流程,SPI总线效率会低一半以上。
2.2 16字节FIFO在实测中的表现
先说结论:CH432标配的发送/接收FIFO是16字节级别,这个容量放在9600波特率下够用,放到115200波特率下顶着用,放到更高波特率下就明显吃力。
我在一个传感器采集项目里用CH432扩展了两路RS485,波特率跑115200,每路每秒上报大约20帧数据,每帧70字节左右。实测下来,如果只开接收FIFO中断,MCU大约每收到十几到几十个字节就要进一次中断去把数据搬走,CPU占用虽然不高,但要注意SPI读取必须赶在下一次数据覆盖前完成。一旦主控同时处理WiFi协议栈和日志输出,SPI响应偶尔被延迟,CH432的FIFO就顶不住了,会出现串口数据丢失。解决的办法是:降低MCU其他中断的响应时间,或者把波特率降到57600以下给FIFO争取余量。
所以CH432的定位非常明确:适合串口路数缺口不大、波特率要求不高、数据量可控的场景。如果只是接一两个9600波特率的GPS或温湿度传感器,它非常省心。
2.3 CH432适合什么场合
从我实际项目经验看,CH432最典型的应用场景是这两个:
第一,ESP32或者STM32这类串口富余量小的MCU,通过SPI口扩展两路低速UART去接传感器或蓝牙模块。第二,做“串口转SPI”转接板,把主控的SPI扩展成两个RS232/RS485调试口。它最不适合的场景是高速大流量场合,比如连续往4G模组灌日志,或者做串口服务器高并发转发,这种任务超出它的FIFO承载能力,硬上只会给自己找麻烦。
3. CH438拆解:并行总线起家的四串口常青树
3.1 并口总线和SPI从模式的取舍
CH438是一只四通道UART扩展芯片,它的传统访问方式走8位并行总线,地址线和数据线直接接到主控的总线控制器上。这种设计在单片机时代非常实用,因为当时的处理器往往有外部总线接口,挂上CH438就像直接扩展内存一样,读一个寄存器就是一条总线读指令,效率非常高。即使主控没有外部总线,只要能抠出十几根GPIO,也可以用软件模拟总线时序访问。
CH438在部分型号和应用场景里也支持SPI从模式,但我个人建议:如果你打算用SPI方式访问,又对传输速率有要求,优先考虑CH9434而不是死磕CH438。原因在于CH438的并行总线设计天然对口“一次读写一个寄存器”的访问模式,而SPI方式一旦遇到大块数据搬移,效率和CH9434这类原生SPI大FIFO芯片不在一个量级。如果你的主控并行IO很充裕、项目原本就是总线型架构、追求稳定成熟,CH438并口路线是可靠的;如果主控只剩SPI口,直接去看CH9434更明智。
3.2 四路中断管理与并发串口
CH438把四路UART的中断统一管理和输出,一颗芯片只有一个中断引脚,送进主控的EXINT。四路UART可以分别独立屏蔽和使能中断,中断产生后,主控通过读芯片的中断状态寄存器来判断当前是哪一路、什么事件触发了中断。我从调试角度说一个心得:如果你的代码里有多路串口同时收发,中断服务程序里拿到中断状态后,最好“把所有置位的中断源全部处理完再退出”,而不是只处理第一个找到的。因为在并发数据到达时,中断状态寄存器可能同时标记了多路事件,只处理一个就退出,另一个会被漏掉,直到下一次中断才追回来,极端情况下就会丢数据。
3.3 老将的优势和明显短板
CH438的优势是成熟、稳定、路数多、并发处理能力强,寄存器模型对老工程师非常友好。过去很多工控板卡上挂着好几片CH438,轻松扩展出十几路串口,这种并行扩展的思路在工业场合久经考验。
短板也很明显:并行总线在今天的MCU平台上越来越成了“奢侈品”。很多现代MCU为了节省引脚不再开放外部并行总线,你要用CH438就得用GPIO模拟总线时序,这一模拟,频率上不去不说,还白白占掉十几个IO,和当初省串口的初衷有点背道而驰。所以我现在推荐CH438,更多是推荐给还在做并行总线方案、或者需要稳定扩展四路以上串口的工控老朋友,新项目但凡能用SPI接口的,我都会建议看CH9434。
4. CH9434拆解:大FIFO+高SPI速率,为高速而生
4.1 256字节FIFO到底改变了什么
CH9434是目前这个系列里我用得最顺手的一颗,原因就一个字:大。它的每路UART内置256字节接收FIFO和256字节发送FIFO,这个容量相比CH432/CH438的16字节级别,不是翻几倍的问题,而是使用模型发生了质变。
用CH432的时候,我通常在接收FIFO过半时就进中断搬数据,每次只搬几个到十几个字节;用CH9434后,我可以放任数据在FIFO里积累,等攒够了再一次性发给主机。举个实际例子,前阵子我用CH9434接一个4G Cat.1模组,模组以921600波特率往上吐数据,主控侧只需要在接收FIFO触发的阈值中断到来后,通过SPI一次性把几百字节取走,CPU占用率比之前用CH432低了不止一半。对于要做数据透传、串口服务器的项目,这颗芯片明显是更合适的选择。
4.2 16C550寄存器兼容的实际意义
很多国产串口扩展芯片的驱动难写,难在寄存器模型“自成一派”,跟主流UART对不上。CH9434直接兼容16C550风格的寄存器布局,这意味着什么?意味着Linux下已有的uart驱动、Windows下的串口驱动、各大RTOS里的串口驱动框架,都可以用熟悉的方式快速适配。写驱动的时候,大部分函数几乎是照着STM32的标准UART驱动改个名字就能用。
另外,CH9434支持设置FIFO触发阈值,这个细节非常重要。你可以根据实际流控需求把接收FIFO的触发点配置到不同的深度,比如低水位适合低延迟场景,高水位适合大数据包场景。我一般会在低速传感器场景把阈值设低些(比如16字节),保证数据实时性;在4G透传场景把阈值设高些(比如200字节),减少SPI中断频率,换CPU占用率。
4.3 用SPI 40MHz读出数据流的体验
CH9434的SPI接口可以跑到几十MHz级别,这个速率在实际操作中非常舒服。一根线速率为2Mbps的串口数据,在SPI侧换算下来根本构不成压力,我用逻辑分析仪量过,大块数据搬移时SPI总线占空比很低,大量时间片都留给了主控干别的事。
不过这里有一个细节容易翻车:SPI速率高不等于可以随便配。如果你的主控SPI外设不支持那么高的时钟,或者板子上走线太长、没有处理好地与电源,SPI信号质量会在高速率下急剧恶化。我建议先按中低速率(如20MHz左右)完成功能调试,确认无误后再尝试提高SPI时钟。别一上来就拉到最高标称值,然后被随机性丢数据折磨得怀疑人生。
5. 横向对比:一张表把三把剑摆在一起
5.1 核心规格对照表
下面的表格是我实际选型时最常看的维度,整理出来直接给各位抄作业:
| 对比项 | CH432 | CH438 | CH9434 |
|---|---|---|---|
| 串口路数 | 2路 | 4路 | 4路 |
| 主控接口 | SPI | 8位并行总线为主 | SPI |
| FIFO深度 | 16字节级 | 16字节级 | 256字节收发 |
| 寄存器模型 | 16C550风格 | 16C550风格 | 16C550兼容 |
| 典型应用 | 轻量双串口扩展 | 并行总线多串口工控 | 高速、大流量四串口 |
| 封装形态 | 小封装为主 | LQFP等较大封装 | QFN等紧凑封装 |
| 适合主控 | SPI口空闲的MCU | 有外部总线/IO充裕的MCU | 任何有SPI口的主控 |
注意,这个表格是选型参考,不是替代手册。实际项目必须到沁恒官网下载对应型号的最新数据手册核对详细电气参数,尤其是芯片后缀不同时,封装和接口细节可能有差异。
5.2 封装与PCB布局的现实差异
封装差异直接影响画板子的工作量。CH432体积小、引脚少,画在双层板上也不费劲,特别适合塞进空间紧张的传感器节点。CH438的封装做完后,复位电路、去耦电容、总线上下拉这些周边器件一多,面积就不太友好,更适合板卡类产品。CH9434的QFN封装小巧但引脚密,建议画板时在芯片下面打过孔到地平面,电源引脚附近多放几个不同容值的去耦电容,SPI信号线和串口信号线尽量短且远离大电流走线,否则高速时钟下信号质量容易劣化。
这三颗芯片在IO电平兼容性上都做得不错,但有的版本支持3.3V和5V两种供电工作,有的则是固定3.3V。同一颗芯片在不同供电下,串口引脚的电平特性会有差别,连RS232电平转换器或RS485芯片时一定要先核对电平匹配,别直接就把单片机的TTL串口怼到5V供电的芯片上。
6. 波特率计算工具:原理、公式、代码与使用
6.1 波特率发生器原理与误差根源
所有硬件UART的波特率都来自一个可编程分频器。芯片内部有一个参考时钟,经过一个分频系数后,再按16倍过采样产生发送和接收时钟。通用的核心公式是:
实际波特率 = 参考时钟频率 / (16 × 分频系数)
这个分频系数通常保存在16位的分频寄存器里,取值范围从1到65535。问题也出在这里:当参考时钟频率除以目标波特率后不是整数时,分频系数只能取整,实际波特率和目标波特率之间就有误差。串口通信双方各自产生的波特率误差如果叠加起来超过一定范围(一般UART能容忍约2%~3%的误差,但最好是1%以内),就会出现高概率误码。
所以选参考时钟频率非常讲究。为什么过去很多老设计用11.0592MHz晶振?因为115200、57600、38400、9600这些经典波特率除以16再取整后,误差在很大范围内都是0。反例是24MHz时钟配115200:24MHz除(115200×16)不是整数,会产生接近0.16%的误差,单看不大,但在一些苛刻环境下会放大成偶发乱码。
6.2 一个C语言计算函数
基于上面的原理,我写了一个可以直接跑在单片机或PC上的C函数,用来计算分频系数和误差:
#include <stdint.h> /** * 计算UART波特率分频系数 * 实际波特率 = clock_hz / (16 * div) * @param clock_hz 参考时钟频率,单位Hz * @param baud 目标波特率 * @param real_bps 返回实际波特率,单位bps * @param err_pct 返回误差百分比,例如 1.5 表示1.5% * @return div 分频系数,失败返回0或-1 */ int uart_calc_divider(uint32_t clock_hz, uint32_t baud, uint32_t *real_bps, float *err_pct) { uint32_t div; if (clock_hz == 0 || baud == 0) return -1; div = clock_hz / (baud * 16); if (div == 0) div = 1; /* 波特率太高,分频至少为1 */ if (div > 0xFFFF) return -2; /* 波特率太低,超过16位分频范围 */ *real_bps = clock_hz / (div * 16); *err_pct = ((float)(*real_bps) - (float)baud) / (float)baud * 100.0f; return (int)div; }注意,函数里div取的是整数除法结果,是“向下取整”。有的分频器支持四舍五入取整,效果可能更好,实际使用时要根据芯片手册里分频系数是直接取整还是“最近取整”来调整。我在很多项目里都是把这个函数编译成小命令行工具,接到板子上后直接输入目标波特率,几秒钟就能得到该波特率下的实际分频寄存器值和误差,调试效率高不少。
6.3 Python批量分析工具
单一计算函数还不够,很多时候我们需要遍历一批常用波特率、多种参考时钟,找出某颗芯片在指定晶振下的最佳配置。这时候用Python写个批处理脚本更直观:
def calc_divider(clock_hz, baud): """返回分频系数、实际波特率、误差百分比,失败返回None""" if clock_hz == 0 or baud == 0: return None div = clock_hz // (baud * 16) if div == 0: div = 1 if div > 0xFFFF: return None real_bps = clock_hz / (div * 16) err_pct = (real_bps - baud) / baud * 100.0 return div, real_bps, err_pct if __name__ == "__main__": clocks = [11_059_200, 14_745_600, 24_000_000, 48_000_000] bauds = [1200, 9600, 57600, 115200, 230400, 460800, 921600, 2000000] print(f"{'时钟':>12} {'目标波特率':>10} {'分频':>6} {'实际':>12} {'误差%':>8}") for clk in clocks: for bd in bauds: r = calc_divider(clk, bd) if r is None: continue div, real, err = r print(f"{clk:>12} {bd:>10} {div:>6} {real:>12.2f} {err:>7.4f}%")跑一遍这个脚本,你就能直观看到11.0592MHz在115200档位误差为0,而24MHz在115200档位存在误差。项目定方案之前跑一次这个工具,比焊完板子发现乱码再回头折腾晶振要省力太多。
6.4 实际使用中的波特率误差数据
我把自己项目里实际量到的一组误差数据贴出来供参考(基于24MHz参考时钟):9600波特率时分频系数为156,实际9600左右,误差极小;115200波特率时分频系数为13,实际约115384.6,误差约0.16%;230400波特率时分频系数为6,实际250000,误差约8.5%。也就是说,24MHz时钟配230400波特率基本不可用,必须换时钟源或改用芯片支持的预分频模式。
这个例子说明了一个铁律:波特率越高,分频系数越小,取整误差的绝对值可能越大。所以做高速串口方案时,不能只问“芯片最高波特率多少”,还要问“参考时钟是多少”“在那个目标波特率下误差到底是多少”。这也是为什么我做任何串口选型,一定会先跑一遍波特率计算工具。
7. 选型决策:按项目场景对号入座
7.1 典型场景的推荐结论
结合前面这些实际体验,我把最常见的几类项目场景和对应的选型建议理一下:
- 场景一是ESP32、STM32等普通MCU,只需要多出2路串口接传感器或蓝牙模块,波特率不超过115200,优先选CH432。它体积小、外围简单、驱动逻辑轻,刚好补足缺口。
- 场景二是主控有外部并行总线,或者IO资源极其充裕,需要四路及以上串口做工业网关、协议转换器,优先选CH438。它的并发多串口总线访问模式在工控场合更可靠。
- 场景三是需要四路串口、SPI是主控最方便腾出的接口,同时涉及4G透传、高波特率、大数据量搬运,直接选CH9434。大FIFO和高SPI速率能极大降低CPU占用,选它所付出的成本值得。
- 还有一类场景是“我还不确定要几路”,这种我建议直接按四路规格预留,宁可芯片支持的路数富余,也不要等产品定型后才发现串口又不够用。
7.2 给新项目的最终建议
如果你现在正在画一块新板子,我的个人建议是:只要主控有空闲SPI接口,优先走CH9434这条路线。它向下兼容访问简单、寄存器模型主流、FIFO又大,哪怕你现在只用了两路串口,多出来的两路将来也能当调试口或备用口。如果板上空间实在紧张、功耗要求苛刻,再回头看CH432。
另外提醒一句,无论选哪颗芯片,都别省手上验证的功夫。先把最小系统焊出来,跑一遍自测程序,把自己真实用到的波特率全部过一遍,确认无误再画正式PCB。串口这东西,原理看着简单,实际跑起来受时钟误差、电源噪音、地回路和SPI信号质量影响非常大,多花半天做验证,比量产后再改板省下几个礼拜。
我自己的习惯是,在项目文件夹里永远放着一个前面那个Python脚本的副本,定波特率之前先跑一遍,然后把分频系数和误差记录在设计文档里。这颗动手敲几行代码的时间,换来的是调试阶段少走一个巨大的弯路。