
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的通信模型再动手配CubeMX2.1 主机侧看到的是什么寄存器映射而非串口设备很多人第一次接触CH438Q会有一个思维惯性以为它像USB转串口芯片那样插上就能在系统里看到8个COM口。不是的。CH438Q对STM32来说本质上就是一片挂在并行总线上的外设SRAM主机通过地址线选中不同的寄存器通过数据线读写内容。它没有自动上报的能力所有收发动作都需要主机主动去读状态寄存器、搬数据。这个模型决定了你在CubeMX里配置的不是UART外设而是FSMCFlexible 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接NOEWR接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占用中断模式9600001%19200002%38400005%57600008%11520000.01%15%2304000.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值得放进候选清单里试一试。