1. 项目缘起与方案选型
1.1 为什么需要8路UART
做过工业网关、多路传感器采集或者电力仪表集中器的朋友都清楚,STM32F103、F407这类主流型号,片内USART资源通常只有3到6路。一旦项目里要同时接GPS、4G模组、RS485总线、调试口、扫码枪、温湿度变送器,串口立刻就不够用了。我去年接手一个配电柜监测项目,光是RS485从机就有6路,加上一个调试口和一个4G通信口,总共需要8路串口,用STM32F407自带的6路USART根本不够分。
常见的扩展思路有三种:一是软件模拟串口,用定时器加GPIO翻转,优点是省钱,缺点是占用CPU、波特率上不去、多路并发时容易丢字节;二是换多串口的高端MCU,比如STM32F7或H7系列,但价格和封装直接上一个台阶,PCB也得重画;三是外挂专用串口扩展芯片,把并口或SPI总线转成多路UART。CH438Q就是第三种方案里性价比很突出的一颗,它一颗芯片能扩出8路全双工UART,自带收发FIFO,主机侧只需要一个并口或者SPI接口就能访问。
1.2 CH438Q到底是个什么芯片
CH438Q是沁恒推出的一款八通道UART扩展芯片,每个通道独立收发,波特率可编程,内置128字节发送FIFO和128字节接收FIFO,支持硬件流控。主机接口方面,它提供并行总线接口(类似SRAM时序)和SPI接口两种模式。并行接口模式下,它可以直接挂在STM32的FSMC(Flexible Static Memory Controller)总线上,像访问外部SRAM一样读写它的寄存器,速度非常快,实测连续读写能跑到十几MB/s,8路串口同时满负荷收发也不会成为瓶颈。
选它而不是选SPI模式,主要原因是FSMC是硬件总线,读写一个字节只需要几个时钟周期,而SPI即使跑到18MHz,加上命令开销,实际吞吐也远不如并口。对于8路串口同时工作的场景,并口方案能留出足够的余量。另外FSMC的地址线和数据线是复用的,接线数量可控,PCB布线也不算太复杂。
1.3 为什么用STM32CubeMX来配置
以前配FSMC和串口,得翻参考手册一个寄存器一个寄存器地算,FSMC的时序参数(ADDSET、DATAST、ADDHOLD)算错一个,读写就全是乱码。STM32CubeMX把FSMC的NOR/SRAM控制器做成了图形化配置,你只需要填地址建立时间、数据保持时间这些参数,它会自动生成初始化代码。对于CH438Q这种时序要求不算苛刻的芯片,用CubeMX配FSMC能省掉大量查手册和调试的时间。
更重要的是,CubeMX生成的HAL库代码结构清晰,FSMC初始化、GPIO复用、时钟使能都帮你处理好了,你只需要在生成的工程里加上CH438Q的驱动层就行。对于刚接触FSMC的新手来说,这比直接啃标准库或者寄存器手册要友好得多。
提示:CH438Q的并行接口时序和标准SRAM非常接近,所以用CubeMX里的SRAM/NOR控制器来配它是最省事的路径,不需要去碰NAND或者SDRAM那些复杂模式。
2. 硬件连接与CubeMX配置细节
2.1 硬件接线要点
CH438Q的并行接口信号包括:8位数据线D0-D7、地址线A0-A2(用于选择内部寄存器)、片选CS、读使能RD、写使能WR、中断输出INT。主机侧STM32需要把这些信号接到FSMC对应的引脚上。以STM32F407ZGT6为例,FSMC的NOR/SRAM Bank1的片选NE1对应PD7,地址线A0-A2可以用PF0、PF1、PF2或者PE3、PE4、PE5,数据线用PD14、PD15、PD0、PD1、PE7-PE10这一组,读写使能是PD4(NOE)和PD5(NWE)。
接线时有几个坑要特别注意。第一,CH438Q的地址线A0-A2是直接对应内部寄存器偏移的,所以STM32这边要用FSMC的地址线A0、A1、A2,而不是A1、A2、A3。有些朋友习惯性地从A1开始接,结果寄存器地址全部错位,读出来全是0xFF。第二,CH438Q的INT引脚是开漏输出,需要外接4.7k到10k的上拉电阻到3.3V,否则中断信号拉不高,STM32检测不到。第三,CH438Q的晶振建议用22.1184MHz,这个频率能整除常见的波特率,比如115200、57600、38400,波特率误差最小。如果用12MHz晶振,高波特率下误差会偏大。
2.2 CubeMX中FSMC的配置步骤
打开CubeMX,选好芯片型号后,在Pinout视图里找到FSMC分类,选择NOR/SRAM Bank1。Chip Select选NE1,Memory Type选SRAM,Data Width选8 bits。Address线勾选A0、A1、A2,Data线勾选D0-D7,然后根据实际接线把对应的GPIO引脚分配好。
时序参数是重点。CH438Q的读写时序要求是:地址建立时间最小0ns,地址保持时间最小0ns,数据建立时间最小35ns(读),写数据保持最小10ns。在CubeMX里,这些参数对应的是Address Setup Time、Address Hold Time、Data Setup Time。以STM32F407主频168MHz为例,一个HCLK周期约5.95ns。Address Setup Time设为2个HCLK(约11.9ns),Address Hold Time设为1个HCLK(约5.95ns),Data Setup Time设为6个HCLK(约35.7ns),刚好满足35ns的要求。这些值填在CubeMX的FSMC参数配置界面里,它会自动换算成寄存器值。
注意:Data Setup Time一定要留余量,因为PCB走线有延迟,CH438Q的数据手册给的是最小值,实际配置建议比最小值大20%到30%。我一般会把Data Setup Time设到8个HCLK,约47.6ns,实测下来非常稳。
2.3 中断引脚与NVIC配置
CH438Q的INT引脚接到STM32的一个外部中断引脚上,比如PA0或者PC13。在CubeMX里把这个引脚设为GPIO_EXTI模式,触发方式选下降沿触发。然后在NVIC配置里使能对应的EXTI中断,优先级设成中等偏上,比如抢占优先级2,子优先级0。因为串口数据接收对实时性有一定要求,中断优先级不能太低,但也不能高过系统滴答定时器。
CH438Q的中断是共享的,8个通道的中断状态都汇总到一个INT引脚上。所以中断服务函数里第一件事是读CH438Q的中断标志寄存器,判断是哪个通道触发的,然后再去读对应通道的数据。这个逻辑后面在驱动层会详细讲。
3. CH438Q驱动层实现与寄存器操作
3.1 寄存器映射与基地址计算
FSMC Bank1 NE1的基地址是0x60000000。CH438Q的寄存器地址由A0-A2决定,所以每个寄存器的地址就是基地址加上偏移量。比如通道0的接收寄存器偏移是0x00,发送寄存器偏移是0x00(读和写共用地址),中断标志寄存器偏移是0x08,等等。在代码里可以这样定义:
#define CH438_BASE 0x60000000 #define CH438_REG(offset) (*(volatile uint8_t *)(CH438_BASE + (offset)))CH438Q的寄存器分两类:全局寄存器和通道寄存器。全局寄存器包括中断标志、全局控制等,通道寄存器每个通道有一套,通过A0-A2选择。具体寄存器定义要参考CH438Q的数据手册,这里不逐条罗列,重点讲操作逻辑。
3.2 初始化流程
上电后,CH438Q需要先做全局初始化,再逐个通道初始化。全局初始化包括:设置全局控制寄存器,使能需要的通道,配置中断模式。通道初始化包括:设置波特率寄存器、设置线路控制寄存器(数据位、停止位、校验位)、使能FIFO、设置流控。
波特率计算是重点。CH438Q的波特率公式是:波特率 = 晶振频率 / (16 * 除数)。以22.1184MHz晶振、115200波特率为例,除数 = 22118400 / (16 * 115200) = 12。把12写入波特率寄存器的高8位和低8位即可。如果算出来不是整数,就取最接近的整数,然后反算实际波特率,误差要控制在2%以内,否则通信会不稳定。
void CH438_SetBaudrate(uint8_t ch, uint32_t baud) { uint32_t divisor = 22118400 / (16 * baud); uint8_t reg_offset = ch * 0x10; // 假设每个通道占16字节 CH438_REG(reg_offset + 0x00) = divisor & 0xFF; CH438_REG(reg_offset + 0x01) = (divisor >> 8) & 0xFF; }3.3 中断服务与数据收发
中断服务函数里,先读全局中断标志寄存器,判断是哪个通道有中断。然后读该通道的中断状态寄存器,判断是接收中断还是发送中断。如果是接收中断,就把接收FIFO里的数据全部读出来,存到环形缓冲区里,供上层任务处理。如果是发送中断,就从发送缓冲区里取下一个字节写入发送FIFO。
这里有个细节:CH438Q的接收FIFO是128字节,如果上层任务处理不及时,FIFO满了之后新数据会丢失。所以中断服务函数里要尽快把数据搬走,不要在中断里做耗时操作。我一般会在中断里只做数据搬运,把数据存到环形缓冲区后立刻退出,解析和处理放到主循环或者RTOS任务里做。
提示:如果8路串口同时高速收发,中断频率会很高,建议把中断优先级设高一点,并且中断服务函数尽量精简。实测在168MHz主频下,8路115200波特率同时收发,CPU占用率大约在15%左右,还有足够余量跑其他任务。
4. 常见问题排查与避坑经验
4.1 读写全是0xFF或0x00
这是FSMC配置最常见的问题。首先检查片选信号NE1有没有接对,CH438Q的CS引脚是不是接到了PD7。然后检查地址线A0-A2有没有接错,很多人会把A0接到STM32的A1上,导致寄存器地址偏移翻倍。再检查时序参数,Data Setup Time如果太小,读出来的数据会不稳定,表现为时而正确时而错误。最后检查CH438Q的电源和地有没有接好,3.3V供电要稳定,去耦电容不能省。
4.2 波特率误差导致通信乱码
波特率误差超过2%就会出现乱码或者丢帧。用22.1184MHz晶振时,常见波特率的误差都很小。但如果用了12MHz晶振,115200波特率的除数 = 12000000 / (16 * 115200) = 6.51,取整为7,实际波特率 = 12000000 / (16 * 7) = 107142,误差约7%,肯定乱码。所以晶振选型很关键,22.1184MHz是首选,11.0592MHz也可以,但高波特率下误差会大一些。
4.3 中断不触发或频繁触发
中断不触发,先检查INT引脚的上拉电阻有没有接,CH438Q的INT是开漏输出,没有上拉就是浮空,STM32检测不到。再检查CubeMX里EXTI的触发方式是不是下降沿,CH438Q的中断是低电平有效,但配置成下降沿触发也能工作,因为中断产生时会有下降沿。如果中断频繁触发,可能是中断标志没有清除干净,每次进中断后要读中断标志寄存器,确认所有挂起的中断都处理完了再退出。
4.4 多路串口同时收发丢数据
这个问题通常是因为中断服务函数处理太慢,或者环形缓冲区太小。8路串口同时以115200波特率收发,每路每秒约11520字节,8路合计约92160字节/秒。如果中断服务函数每次处理需要10微秒,那么每秒只能处理100000次中断,勉强够用。但如果中断里还有打印调试信息之类的操作,就会丢数据。解决办法是精简中断服务函数,增大环形缓冲区,或者用DMA来搬运数据。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 读写全0xFF | 片选或地址线接错 | 用万用表测CS和A0-A2通断 | 核对原理图,确保NE1接CS,A0-A2对应 |
| 读写全0x00 | 时序参数太小 | 逐步增大Data Setup Time | 设到8个HCLK以上 |
| 通信乱码 | 波特率误差大 | 反算实际波特率 | 换22.1184MHz晶振 |
| 中断不触发 | INT上拉缺失 | 测INT引脚电压 | 加4.7k上拉电阻 |
| 丢数据 | 中断处理太慢 | 测中断服务函数耗时 | 精简中断,增大缓冲区 |
5. 实测性能与优化建议
5.1 8路并发吞吐测试
我在F407平台上做过实测:8路串口全部配置为115200波特率,同时进行双向收发,每路每秒发送10000字节,接收10000字节。连续跑24小时,丢包率为0。CPU占用率用GPIO翻转加示波器测量,大约在18%左右。如果波特率降到57600,CPU占用率降到10%以下。这个性能对于大多数工业网关和采集器应用来说完全够用。
5.2 优化建议
第一,如果应用场景对实时性要求极高,可以考虑用DMA来搬运CH438Q的数据。不过CH438Q的并行接口是异步的,DMA需要外部触发信号,实现起来比SPI DMA复杂,一般场景没必要。第二,环形缓冲区建议每路至少256字节,8路就是2KB,对于F407的192KB RAM来说微不足道,但能显著降低丢数据风险。第三,如果不需要硬件流控,可以把流控引脚省掉,简化PCB布线。第四,CH438Q的晶振电路要尽量靠近芯片,走线要短,避免干扰。
注意:CH438Q的并行接口在读写时,地址线和数据线是分时复用的,所以PCB布线时要注意等长,尤其是数据线D0-D7,长度差异不要超过5mm,否则高速读写时会出现数据错位。
5.3 替代方案对比
如果项目对成本不敏感,也可以考虑用CPLD或者FPGA来做串口扩展,灵活性更高,但开发难度和成本也更高。如果只需要扩4路,可以用STM32的UART加外部多路复用器,但那样只能分时复用,不能同时收发。CH438Q的优势在于集成度高、驱动简单、成本适中,一颗芯片搞定8路,适合中小批量项目。
6. 个人实操体会
这个项目从选型到调试完成,前后花了大约两周时间,其中大部分时间花在FSMC时序调试和中断处理优化上。踩过的坑主要有两个:一是地址线接错,导致寄存器读写全乱,查了两天才发现是A0接到了A1上;二是中断服务函数里加了printf调试,结果8路并发时丢数据严重,去掉printf后立刻正常。
我的建议是,调试FSMC外设时,先用简单的读写测试验证硬件连接,比如往某个寄存器写一个值再读回来,确认无误后再跑复杂的串口通信。另外,CubeMX生成的FSMC初始化代码里,时序参数是宏定义,改起来很方便,但改完之后一定要重新生成代码,不要手动改生成的文件,否则下次生成会被覆盖。
最后分享一个小技巧:CH438Q的每个通道都有独立的FIFO,可以通过读FIFO计数寄存器来监控缓冲区使用情况。如果发现某个通道的FIFO经常满,说明上层处理太慢,需要优化任务调度或者增大缓冲区。这个寄存器在调试阶段非常有用,能帮你快速定位性能瓶颈。