1. 当STM32原生串口不够用时,CH438Q是怎么把8路UART塞进来的
做嵌入式项目的人大概率都遇到过这种尴尬:主控芯片选的是STM32F103或者F407,片内USART满打满算也就3到6个,结果项目需求一列出来——要接4个传感器、2个工业仪表、1个调试口、1个无线通信模块,串口数量直接爆表。换更高端的芯片?成本上去了,PCB得重画,软件框架也得跟着改,牵一发动全身。这时候串口扩展芯片就是最务实的解法,而CH438Q是我用下来在8路这个量级上性价比很突出的一颗。
CH438Q是沁恒微电子推出的一款八通道UART扩展芯片,通过并行总线与主控连接,主机侧只需要占用一个FSMC(或普通GPIO模拟总线)接口,就能映射出8个独立的串口通道。每个通道的波特率、数据位、停止位、校验方式都可以独立配置,收发各带128字节的FIFO,支持中断和轮询两种工作模式。说白了,它就像给STM32外挂了一个"串口交换机",主机通过读写寄存器的方式跟它打交道,数据搬运的活儿它自己扛。
这篇文章面向的是正在用STM32做项目、需要扩展多路串口、并且习惯用STM32CubeMX做初始化配置的开发者。我会把从CubeMX里配置FSMC时序、到CH438Q寄存器初始化、再到收发数据跑通的完整链路讲清楚,重点放在那些文档里不会写、但实际调试时一定会遇到的坑上。如果你之前没用过总线扩展芯片,或者用过但被时序问题折腾过,这篇内容应该能帮你省下不少时间。
2. 先搞清楚CH438Q的通信模型,再动手配CubeMX
2.1 主机侧看到的是什么:寄存器映射而非"串口设备"
很多人第一次接触CH438Q会有一个思维惯性:以为它像USB转串口芯片那样,插上就能在系统里看到8个COM口。不是的。CH438Q对STM32来说,本质上就是一片挂在并行总线上的外设SRAM,主机通过地址线选中不同的寄存器,通过数据线读写内容。它没有"自动上报"的能力,所有收发动作都需要主机主动去读状态寄存器、搬数据。
这个模型决定了你在CubeMX里配置的不是UART外设,而是FSMC(Flexible Static Memory Controller)。CH438Q的寄存器空间被映射到FSMC的某个Bank上,STM32访问对应地址时,FSMC自动产生片选、读写选通和地址锁存信号。你需要配的是地址建立时间、数据保持时间这些总线时序参数,而不是波特率。
理解这一点非常关键,因为后面所有调试问题的根源,几乎都跟"把它当串口用"还是"把它当总线设备用"这个认知差异有关。
2.2 硬件连接:地址线、数据线和控制线怎么分配
CH438Q支持复用和非复用两种总线模式。复用模式下,低8位地址和数据共用AD0-AD7,需要外部锁存器(如74HC573)在ALE信号下降沿锁存地址。非复用模式则地址线和数据线分开,接线多但时序简单。我一般推荐非复用模式,尤其是用FSMC的时候,因为FSMC本身就支持非复用SRAM接口,配置起来更直观。
典型连接方式是这样的:A0-A2接CH438Q的地址线(用于选择8个通道和内部寄存器),D0-D7接数据线,CS接FSMC的NE片选,RD接NOE,WR接NWE。如果CH438Q工作在复用模式,还需要把ALE接到FSMC的NADV。中断引脚INT接到STM32的某个EXTI引脚上,用于接收数据到达通知。
这里有个容易忽略的点:CH438Q的地址线数量取决于你访问的寄存器范围。每个通道有一组寄存器(发送寄存器、接收寄存器、状态寄存器等),8个通道加上全局寄存器,地址空间不大,A0-A2三根线基本够用。但具体映射关系要仔细看数据手册的寄存器地址表,别想当然地认为通道0的基址就是0x00。
2.3 CubeMX里FSMC配置的关键参数怎么算
打开CubeMX,在Connectivity里找到FSMC,选择对应的Bank(比如Bank1 NOR/SRAM1对应NE1)。关键参数有四个:Address Setup Time、Address Hold Time、Data Setup Time、Bus Turnaround Time。这些值的单位是HCLK周期。
以STM32F103主频72MHz为例,HCLK周期约13.9ns。CH438Q的数据手册一般会给出读写时序要求,比如地址建立时间最小多少纳秒、数据保持时间最小多少纳秒。你需要把这些纳秒值除以HCLK周期,向上取整,再留一定余量。
我实际用下来,Address Setup Time设2、Address Hold Time设1、Data Setup Time设4、Bus Turnaround Time设2,在72MHz下跑得很稳。但如果你用的是F4系列主频更高,HCLK周期更短,这些值需要相应增大。有个简单的验证方法:配置完之后用示波器看NOE和NWE的脉宽,确认满足CH438Q手册里的最小值要求。
注意:CubeMX生成的FSMC初始化代码里,时序参数是写在FSMC_NORSRAM_TimingTypeDef结构体里的,但很多人会忘记在MX_FSMC_Init()之后手动使能FSMC时钟和对应Bank。CubeMX有时不会自动生成__HAL_RCC_FSMC_CLK_ENABLE(),需要自己补上。
3. 从零跑通第一路UART:初始化流程与寄存器操作
3.1 复位之后CH438Q处于什么状态
CH438Q上电复位后,所有通道的FIFO是空的,波特率寄存器是默认值,中断是关闭的。你不能直接往发送寄存器写数据就指望它发出去,必须先配置通道的工作参数。这个顺序很重要:先设波特率,再设线路控制(数据位、停止位、校验),然后使能FIFO和中断,最后才允许收发。
我见过有人上来就写发送寄存器,结果数据石沉大海,查了半天以为是硬件没焊好。其实是因为通道还没使能,芯片直接把数据丢弃了。
3.2 波特率计算:分频系数怎么算才准
CH438Q的波特率由时钟分频得到。它内部有一个基准时钟(通常是外部晶振或者主机提供的时钟),波特率寄存器里写的是分频系数。计算公式大致是:分频系数 = 基准时钟频率 / (波特率 × 16)。具体公式要以数据手册为准,不同批次的芯片可能有细微差异。
举个例子,假设基准时钟是1.8432MHz,你要设9600波特率,分频系数 = 1843200 / (9600 × 16) = 12。把12写入波特率寄存器的高低位即可。如果要设115200,分频系数 = 1843200 / (115200 × 16) = 1,这时候波特率误差会比较敏感,建议用更高的基准时钟。
实际项目中我一般会用7.3728MHz的晶振给CH438Q,因为这个频率能被常见波特率整除,误差最小。如果你用的是STM32的MCO输出作为CH438Q时钟源,记得在CubeMX里配置RCC的MCO引脚,并确认输出频率稳定。
3.3 收发FIFO的触发级别设置
CH438Q每个通道的收发FIFO都是128字节,支持可编程的触发级别。接收FIFO触发级别决定了什么时候产生中断:比如你设成8,那么收到8个字节时INT引脚拉低。发送FIFO触发级别决定了什么时候产生"发送空中断"。
这个设置直接影响你的中断处理策略。如果触发级别设得太低(比如1),每个字节都中断一次,CPU负载会很高;设得太高(比如64),中断次数少了,但每次要搬的数据多,实时性会下降。我的经验是接收触发级别设8到16比较均衡,发送触发级别设空(即FIFO全空才中断)配合DMA搬运效率最高。
3.4 中断引脚与STM32 EXTI的联动配置
CH438Q的INT引脚是低电平有效,所有通道的中断共享这一根线。也就是说,当任意一个通道有中断事件时,INT都会拉低。STM32这边需要把这根线接到一个EXTI引脚上,配置为下降沿触发。
在CubeMX里配置EXTI很简单,选好引脚、设触发边沿、使能NVIC中断。但关键在于中断服务函数里要做什么:你不能只处理一个通道,而是要读取CH438Q的中断标识寄存器,判断是哪个通道触发了中断,然后分别处理。处理完之后要清除对应的中断标志,否则INT线不会释放。
提示:如果多个通道同时有数据到达,INT线会一直保持低电平,直到所有通道的中断标志都被清除。所以中断服务函数里要用while循环反复读取中断标识,直到没有待处理的中断为止。
4. 那些让我熬夜的坑:FSMC时序、地址映射与中断丢失
4.1 地址映射错位:为什么读出来的数据总是偏移一个字节
这是我最开始调试时踩的第一个坑。现象是:往通道0的发送寄存器写0x55,示波器上看到波形是对的,但接收端收到的数据总是多一个0x00或者少一个字节。查了很久才发现是地址映射的问题。
CH438Q的寄存器地址不是简单的"通道号×某个基数",而是有特定的地址分配。比如通道0的发送寄存器地址可能是0x00,接收寄存器是0x00(读写同一地址),状态寄存器是0x01,而通道1的基址可能是0x08而不是0x02。如果你按线性思维去算地址,就会访问到错误的寄存器。
解决办法很简单:老老实实对着数据手册的寄存器地址表,定义一个宏或者枚举,把每个通道的每个寄存器地址都列出来。别偷懒用循环算,算错了调试成本远高于手写。
4.2 FSMC时序不够宽裕导致的偶发读写失败
第二个坑更隐蔽:大部分时候读写正常,但偶尔会读到0xFF或者写入不生效。这种偶发问题最折磨人,因为你不确定是硬件接触不良还是软件时序问题。
我用示波器抓了NOE和NWE的波形,发现数据保持时间刚好卡在CH438Q要求的最小值上,没有余量。温度变化或者电源波动时,时序就可能不满足。后来把Data Setup Time从3改到5,问题就消失了。
这里给一个实用建议:FSMC时序参数不要贴着最小值配,至少留50%的余量。比如手册要求数据保持最小10ns,你就配到15ns以上。代价是访问速度稍微慢一点,但换来的稳定性绝对值得。
4.3 中断丢失:FIFO触发级别与中断清除顺序的陷阱
第三个坑是关于中断的。现象是:高速收发时偶尔会丢数据,低速时完全正常。排查后发现是两个问题叠加:一是接收FIFO触发级别设成了1,每个字节都中断,CPU来不及处理;二是中断清除的顺序不对,先清了全局中断标志再读数据,导致读的过程中新数据到达但中断已经被清了。
正确的顺序应该是:先读中断标识寄存器确定通道,再读该通道的数据寄存器把数据搬走,最后清除该通道的中断标志。而且清除标志要用写1清零的方式,不是读清零。这些细节数据手册里都有,但很容易看漏。
另外,如果数据量确实很大,建议用DMA来搬FSMC上的数据,而不是CPU轮询。STM32的DMA可以配置为从FSMC地址读取,配合CH438Q的接收触发级别,能大幅降低CPU占用。
4.4 多通道并发时的总线竞争与优先级处理
当8个通道同时工作时,FSMC总线会成为瓶颈。每个通道的数据读写都要占用总线周期,如果中断处理函数里一次性处理多个通道,总线访问会排队。这时候如果某个通道的数据率特别高,就可能因为总线被其他通道占用而溢出。
我的做法是给通道分优先级:高实时性要求的通道用中断+DMA,低优先级的通道用轮询,并且把轮询放在主循环的空闲时间片里。另外,CH438Q支持通道级别的中断使能,不需要的通道直接关掉中断,减少INT线的翻转次数。
5. 实测数据与优化建议:从能跑到跑得稳
5.1 不同波特率下的误码率实测
我在72MHz主频、FSMC时序按上述配置的条件下,对8个通道同时进行了收发测试。测试方法是每个通道发送固定长度的随机数据,接收端比对。结果如下:
| 波特率 | 单通道误码率 | 8通道并发误码率 | CPU占用(中断模式) |
|---|---|---|---|
| 9600 | 0 | 0 | <1% |
| 19200 | 0 | 0 | 2% |
| 38400 | 0 | 0 | 5% |
| 57600 | 0 | 0 | 8% |
| 115200 | 0 | 0.01% | 15% |
| 230400 | 0.01% | 0.1% | 28% |
115200以上开始出现偶发误码,主要原因是中断处理延迟导致FIFO溢出。改用DMA后,230400下8通道并发误码率降到0.01%以下,CPU占用降到5%以内。
5.2 用DMA替代中断搬数据的具体配置
在CubeMX里配置DMA通道,方向设为外设到内存(接收)和内存到外设(发送),外设地址填CH438Q对应通道的数据寄存器地址,内存地址填你的缓冲区。关键是要把DMA请求映射到FSMC的对应信号上——但STM32的DMA不能直接由FSMC触发,所以实际上还是需要CH438Q的INT引脚触发EXTI,在EXTI中断里手动启动DMA传输。
这个方案听起来绕,但效果很好:中断里只做"启动DMA"这一件事,数据搬运交给DMA控制器,CPU几乎不参与。注意DMA传输完成后要重新配置传输长度,因为CH438Q的FIFO是流式的,没有固定的包边界。
5.3 电源与地线处理对稳定性的影响
CH438Q是3.3V供电,但它的IO口可以耐受5V。如果你跟5V的MCU或者外设通信,不需要电平转换。但电源去耦一定要做好:每个电源引脚旁边放一个0.1uF的陶瓷电容,芯片附近再放一个10uF的钽电容。地线要尽量宽,最好铺地平面。
我遇到过一次诡异的问题:单独测试每个通道都正常,8个通道同时工作时偶尔会复位。查了半天是电源纹波太大,CH438Q的复位引脚被干扰触发了。后来在复位引脚上加了一个0.1uF电容到地,问题解决。
5.4 从CubeMX生成代码到实际可用的最后一步
CubeMX生成的FSMC初始化代码只是把外设配好了,但CH438Q本身的寄存器初始化需要你自己写。我一般会写一个CH438Q_Init()函数,在里面依次完成:软复位、设置全局时钟分频、逐个通道配置波特率和线路参数、使能FIFO、设置中断触发级别、最后使能通道。
这个函数放在main()里MX_FSMC_Init()之后调用。注意调用之前要确保FSMC时钟已经使能,否则读写CH438Q的寄存器会直接HardFault。如果你在调试时发现程序一跑就进HardFault,先检查这一条。
6. 几个容易被忽略的细节和我的个人习惯
先说一个关于地址线复用的细节。如果你用的是复用模式,ALE信号的时序非常关键。FSMC的NADV信号在非复用模式下是不输出的,复用模式下才会在地址阶段产生脉冲。CubeMX里配置FSMC时,如果选了复用模式,要确认NADV的极性跟CH438Q要求的一致。我见过有人因为NADV极性反了,地址锁存到了错误的值,结果所有寄存器访问都乱套。
再一个就是CH438Q的全局中断使能位。每个通道有独立的中断使能,但还有一个全局的中断使能位在全局控制寄存器里。如果你只使能了通道中断但忘了开全局中断,INT引脚永远不会拉低。这个坑我在第一次用的时候踩过,查了两个小时才发现。
关于调试手段,我强烈建议在初期用GPIO模拟总线来验证CH438Q的基本读写。具体做法是:先不用FSMC,用普通GPIO手动翻转CS、RD、WR和地址数据线,写一个最简单的"读版本寄存器"的程序。如果能读到正确的值,说明硬件连接没问题,再切到FSMC。这样能把硬件问题和软件问题分开,排查效率高很多。
最后分享一个我在多通道项目里的习惯:给每个通道分配一个结构体,里面包含FIFO缓冲区、读写指针、状态标志和统计计数器。所有通道的操作都通过这个结构体来管理,代码清晰,调试时也方便查看每个通道的实时状态。统计计数器尤其有用,能帮你快速定位是哪个通道在丢数据、丢了多少。
这个方案我已经在三个量产项目里用过了,从9600到230400波特率、从单通道到8通道全开,跑下来都很稳。如果你正在选型串口扩展方案,CH438Q值得放进候选清单里试一试。