资讯详情

串口不够用?STM32L151+WK2124 SPI转4路串口驱动全记录

📅 2026/10/21 19:32:38 | 华诺云谱 👁 阅读
串口不够用?STM32L151+WK2124 SPI转4路串口驱动全记录
串口不够用这个事做嵌入式的人迟早会撞上。最近我在维护一台低功耗仪表主控就是标题里的STM32L151板子上要接的设备却不少两个传感器、一个显示屏、一个调试口后端还想留一路备用。这颗芯片原生只带两个UART根本不够分。最后定的方案就是标题里写的外挂WK2124用SPI转出4路独立串口。这篇记录从SPI初始化到驱动调稳的完整过程里面有我做下来比较顺手的代码结构也把遇到的坑一个个列出来给后面同样在折腾这颗芯片的人省点时间。1. 项目背景与方案选型为什么是STM32L151加WK21241.1 串口不够用时的几个选择串口资源紧张其实很常见但解决思路差别很大。有人会用定时器加GPIO模拟一个UART出来有人直接换一颗更多串口的MCU还有人选择外挂一颗SPI转串口芯片。这几条路我都想过先说结论在这个项目里外挂WK2124是最省事、最稳的选择。方案串口数量MCU资源占用优点缺点软件模拟串口两三个就顶天了定时器中断加CPU零额外成本波特率不准CPU占用高数据处理一忙就丢字节更换更多UART的MCU看型号一般4到6路换芯片、改板子、重写外设配置集成度高性能也强成本上升PCB要重新设计电池供电的场合功耗未必合适I2C转串口芯片常见1到2路也有多路的一条I2C总线线路简单占用引脚少速率和扩展路数都比较有限多路芯片选择少SPI转串口芯片WK21244路独立串口1路SPI加1个片选IO扩展路数多驱动成熟收发速率足够多一颗芯片需要写驱动软件模拟串口第一轮就被排除了原因是仪表项目里接收端的数据流根本断不得MCU还得实时处理传感器数据软件串口稍微被中断打断就会出现帧错误。换MCU也想过但实际一算为了多两个串口连PCB都要大改功耗和成本都划不来。WK2124这种方案的好处是MCU这边只多占用一组SPI和一个IO就能换来4路硬件串口数据格式、波特率、FIFO、流控全靠芯片自己处理主控的压力小很多。1.2 WK2124这颗芯片到底能干什么WK2124是一颗SPI从机芯片通过SPI接口接收主控的命令把数据分配到4路UART上。每个串口都支持独立配置波特率、数据位、校验位、停止位都可以单独设置互不影响。这意味着你可以在第1路跑9600的传感器第2路跑115200的调试口两边各玩各的。它的收发路径里带了FIFO缓冲这在高速数据流场景下非常关键。MCU有空的时候往FIFO里塞一批数据芯片自己慢慢往UART发送接收方向也一样串口收到的数据先进FIFO主控过来取就行。相比那种一个字节一个字节靠查询硬扛的串口扩展方案FIFO能把主控的等待时间明显降下来。另外芯片还支持硬件流控需要用RTS/CTS对接外部设备时也能直接支持不用自己拿IO去模拟。选STM32L151配WK2124比较顺手的另一个理由是电平匹配。两者都是3.3V供电SPI和UART引脚直接连就行不用加电平转换电路。STM32L151本身是低功耗系列SPI外设齐全休眠模式配合外部中断唤醒也很成熟和外挂芯片的搭配不会有先天障碍。1.3 选芯片和找资料的几个建议拿到WK2124之后第一步不是写代码而是把数据手册从头到尾翻一遍。我建议重点看几处SPI帧格式、寄存器映射表、复位时序、INT引脚极性、FIFO控制方式。这些地方影响全局设计比后面函数怎么写重要得多。买样片也要留心渠道。这类芯片市面上货源鱼龙混杂如果买到翻新片或者不同批次的散片调试的时候会冒出各种奇怪的异常而且极难复现。正规渠道申请样片最好没有的话也要找口碑好的代理商。封装方面同样要提前确认不同封装的引脚间距和散热要求都不一样PCB打样前核对好封装库别拿到实物发现引脚对不上。如果实际只需要两路串口也可以找同系列的两路版本寄存器和驱动基本通用改动量很小。这块后面细说。2. 硬件连接把线连对就已经成功了一半2.1 核心引脚连接参考WK2124和STM32L151之间的物理连接不算复杂核心就是一组SPI信号加几个控制脚。我用一个真实项目的连接方式举个例子具体引脚你可以按自己板子的布局调整但功能对应关系不要改。STM32L151引脚功能配置WK2124引脚PA5SPI1_SCKSCLKPA7SPI1_MOSISI主发从收PA6SPI1_MISOSO从发主收PA4GPIO输出片选SCSPA3GPIO输入外部中断INTPA2GPIO输出软复位RST#VDD 3.3V电源VDDGND地GNDSPI为什么要有片选脚而不直接用SPI的硬件NSS因为WK2124这种芯片的片选通常用普通GPIO控制更灵活。硬件NSS有时会在多字节传输时自动变化时机不好控制用软件拉高拉低反而干净。2.2 电源、复位、中断的细节别偷懒电源去耦是很多调试问题的根源。WK2124的VDD引脚旁边至少要放一个0.1uF陶瓷电容最好再在稍远一点的位置加一个10uF的储能电容保证芯片在突发收发时电压不会跌下去。板子如果空间允许电源和地之间多打几个过孔对稳定性有帮助。RST#引脚我强烈建议用GPIO控制而不是简单接一个上电RC复位电路。用GPIO控制的优点是驱动程序随时可以把芯片复位一遍调试时非常管用。上电后正确的操作顺序是先把RST#拉低保持一段足够的时间这个时间以手册要求为准再拉高释放然后等芯片进入就绪状态再开始配置。我在实际项目里遇到过因为复位时间不够导致芯片偶尔配置不进去的问题后面改成GPIO软复位并加了延时问题就消失了。INT引脚是WK2124向主控报告中断事件的关键信号。要特别留意的是中断极性不同批次的芯片或者同系列不同型号INT的有效电平和触发方式可能不一样。不要只看手册上画的波形就下结论上电后拿示波器实测一下最可靠。实测的时候可以先让某个串口收到一字节数据看INT引脚到底是拉高还是拉低再去配置STM32的外部中断触发沿。这一步做错了中断会一直进不来或者一直误触发。2.3 晶振频率和波特率误差算完再选WK2124的UART波特率由它的输入时钟经过分频得到计算方式和传统UART控制器类似。公式是分频系数 输入时钟频率 / (16 × 目标波特率)这里最大的坑是时钟频率选了一颗和常用波特率没有整数倍关系的晶振。我用一组实际数字说明。假设WK2124的输入时钟是7.3728MHz想跑115200波特率分频系数 7372800 / (16 × 115200) 4.0分频寄存器写4就精确命中串口波形非常干净。但如果你图省事直接用了12MHz的晶振同样跑115200分频系数 12000000 / (16 × 115200) ≈ 6.51。分频寄存器只能写整数写到6实际波特率就变成了12000000 / (16 × 6) 125000误差超过8%。这个误差在串口通信里已经大到没法用了两端设备互相收发就是乱码。所以选晶振时会优先考虑7.3728MHz或者3.6864MHz这种频率它们对9600、19200、38400、115200这些常见波特率都有整数倍关系。如果项目里被迫用了非标频率建议把波特率降下来分频数变大了取整带来的相对误差会小很多对通信的容忍度也更高。2.4 SPI线速别一上来就拉满WK2124虽然标称SPI速率不低但实际调试时我建议先把SCLK降到1MHz以下跑通整个流程再逐步往上提。SPI时钟太快加上线缆寄生电容、板子走线阻抗不均等因素很容易出现偶发读错数的问题而这种问题在低速下完全不复现排查起来非常磨人。调通后的目标速率大概在几MHz以内比较稳。做板子的时候SPI走线尽量短一点别绕远路MOSI和MISO分开一点距离减少串扰。我用杜邦线临时飞线调试时SPI线稍微长一点就能明显感觉到稳定性下降后来换成PCB走线之后就没有类似问题了。3. 驱动实现从SPI底层到串口收发3.1 SPI外设初始化先把脚下站稳我用的是STM32的HAL库先把SPI1的引脚复用和参数配置好。这里简化部分代码重点逻辑都在。void WK2124_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // SCK、MOSI、MISO 复用为 SPI1 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // CS 用普通 GPIO GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // CS 默认拉高 // INT 外部中断引脚 GPIO_InitStruct.Pin GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_IT_FALLING; // 极性以实测为准 GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI3_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI3_IRQn); hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_32; // 先低速 hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); }这里面两个参数要特别说一下。CLKPolarity和CLKPhase决定了SPI工作在Mode 0还是Mode 1还是其他模式WK2124到底支持哪种以手册为准。我一般先用模式0如果读回来的数据不对先别急着怀疑时序问题把模式换一遍试试这是调试SPI芯片最快的手段之一。BaudRatePrescaler就是前面强调的降速策略先32分频把速度压下来。3.2 寄存器读写掌握SPI通信的帧节奏WK2124的SPI通信帧节奏其实很固定就是片选拉低之后先发一个命令字节再读或者写一个数据字节。命令字节里包含了读写方向、通道号、寄存器地址这三段信息具体位分配以芯片手册为准。我这里用一套比较通用的示例格式你自己实现的时候一定要对照着手册把这几段重新确认一遍。uint8_t WK2124_CS_PIN GPIO_PIN_4; GPIO_TypeDef *WK2124_CS_PORT GPIOA; static uint8_t WK2124_CmdBuild(uint8_t ch, uint8_t reg, uint8_t isWrite) { // 高位为读写标志中间为通道号低位为寄存器地址 return (isWrite ? 0x80 : 0x00) | ((ch 0x03) 5) | (reg 0x1F); } static uint8_t WK2124_Xfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; } void WK2124_RegWrite(uint8_t ch, uint8_t reg, uint8_t val) { HAL_GPIO_WritePin(WK2124_CS_PORT, WK2124_CS_PIN, GPIO_PIN_RESET); WK2124_Xfer(WK2124_CmdBuild(ch, reg, 1)); WK2124_Xfer(val); HAL_GPIO_WritePin(WK2124_CS_PORT, WK2124_CS_PIN, GPIO_PIN_SET); } uint8_t WK2124_RegRead(uint8_t ch, uint8_t reg) { uint8_t rx 0; HAL_GPIO_WritePin(WK2124_CS_PORT, WK2124_CS_PIN, GPIO_PIN_RESET); WK2124_Xfer(WK2124_CmdBuild(ch, reg, 0)); rx WK2124_Xfer(0xFF); // 读数据同时提供SCLK时钟 HAL_GPIO_WritePin(WK2124_CS_PORT, WK2124_CS_PIN, GPIO_PIN_SET); return rx; }读操作里为什么要发一个0xFF这是很多新手最容易想不通的地方。SPI是全双工协议从机只有在主机提供SCLK时钟时才能把MISO上的数据推出来所以读数据这个动作其实必须伴随着一次写操作那个写出去的内容是什么不重要关键是时钟要动起来。这个0xFF就是用来制造时钟的哑字节。片选的时序也可以做得更简。每次访问寄存器都固定拉低、传两个字节、拉高简单可靠。不要想当然地认为可以把片选一直拉低连续读很多个寄存器WK2124支不支持这种burst模式要翻手册确认不支持的话连续读会导致数据错位。3.3 配置串口参数DLAB这个坑必须绕开WK2124的寄存器风格类似传统UART控制器波特率寄存器非常容易掉坑里。它的分频寄存器和数据寄存器有时共用地址怎么区分靠线路控制寄存器LCR的最高位DLAB。只有当DLAB置1的时候你写的DLL、DLH才是波特率分频值DLAB清零的时候这个地址才被解释为发送/接收数据。很多人配置了半天串口波特率没变原因就是忘了开DLAB。#define WK2124_REG_LCR 0x03 #define WK2124_REG_DLL 0x00 #define WK2124_REG_DLH 0x01 #define WK2124_CLK_FREQ 7372800UL // 实际用到的时钟频率 void WK2124_SetBaud(uint8_t ch, uint32_t baud) { uint16_t div WK2124_CLK_FREQ / (16UL * baud); uint8_t lcr WK2124_RegRead(ch, WK2124_REG_LCR); // 置DLAB位允许写波特率分频寄存器 WK2124_RegWrite(ch, WK2124_REG_LCR, lcr | 0x80); WK2124_RegWrite(ch, WK2124_REG_DLL, div 0xFF); WK2124_RegWrite(ch, WK2124_REG_DLH, (div 8) 0xFF); // 恢复原来的LCR同时配置数据格式 WK2124_RegWrite(ch, WK2124_REG_LCR, lcr); }学会了DLAB这招之后配置8N18个数据位、无校验、1个停止位就很简单了往LCR里写对应配置值就行。每次配置UART参数的时候我习惯先读一下当前LCR再改需要改的位而不是直接覆盖整个寄存器这样不会把原有配置弄丢。3.4 数据收发查询方式快速打通中断方式用于实战先把最简单的一字节发送和接收函数写出来。发送前一定要查询线路状态寄存器LSR确认发送FIFO或保持寄存器已经空了再写否则数据会被冲掉。#define WK2124_REG_LSR 0x05 #define WK2124_REG_THR 0x00 #define WK2124_REG_RBR 0x00 void WK2124_UartPutChar(uint8_t ch, uint8_t data) { // 等待THR/fifo空闲bit5为1表示空 while ((WK2124_RegRead(ch, WK2124_REG_LSR) 0x20) 0); WK2124_RegWrite(ch, WK2124_REG_THR, data); } uint8_t WK2124_UartGetChar(uint8_t ch) { // 等待数据到达bit0为1表示有数据 while ((WK2124_RegRead(ch, WK2124_REG_LSR) 0x01) 0); return WK2124_RegRead(ch, WK2124_REG_RBR); }这套查询方式拿来验证硬件、做简单测试完全没有问题但如果在实际项目里用长数据流MCU会被这个while循环掐死一边收数据一边处理其他任务的场景根本跑不动。正确做法是用中断。WK2124的INT引脚拉低时主控进入外部中断服务函数然后通过读芯片的中断识别寄存器IIR判断中断类型是接收数据到了、发送FIFO空了还是线路出错再分别处理。需要注意的是中断服务函数里尽量只做标志记录和轻量级的寄存器读写具体数据搬运放到主循环里做。我在工程里会为每个通道准备一个环形队列接收中断只负责把数据从WK2124揽进队列主循环再从队列里取出来处理。这样既能保证数据不丢又不会让中断服务函数拖慢整个系统。4. 调试实录从确认芯片活着到常见问题速查4.1 上电后的第一件事回读一个寄存器拿到新板子驱动还没写全的时候先别急着配置串口第一件事是确认SPI链路到底通没通。最简单的方法是找一个可读可写的寄存器先写进去一个特征值再读出来比对。如果能正确回读说明SPI物理链路和帧格式都没问题如果读回来的值不对再往下调串口参数就是白费功夫。uint8_t testVal 0xA5; WK2124_RegWrite(0, 0x0E, testVal); // 0x0E仅作示例找手册里的空闲寄存器测试 uint8_t ret WK2124_RegRead(0, 0x0E); if (ret ! testVal) { // 通信链路有问题先查硬件 }这个自检函数建议在初始化流程的最开始调用它能在5秒钟之内告诉你问题出在SPI还是出在串口侧把调试范围一下子缩小一半。4.2 常见问题速查表调试过程中我积累了一张问题排查表遇到异常直接对着查效率高很多。现象可能原因排查顺序回读寄存器全是0xFF片选没拉到位、SCK空闲电平不对、SPI模式不匹配逻辑分析仪抓SPI帧检查CS和SCK波形回读寄存器全是0x00芯片没上电、RST#没释放、片选一直拉低量VDD量RST#电平检查复位时序部分寄存器能读部分不对命令字节拼装错误、寄存器地址位不对对照手册逐位解析命令字节串口输出乱码波特率分频不对、晶振频率和波特率没整数比按分频公式重新计算降波特率测试只能发不能收MISO没接好、GPIO复用配置错、对端TX没接过来示波器看WK2124的RX引脚有没有波形中断一直触发INT极性配反、中断状态没清示波器量INT引脚读IIR确认中断原因偶发丢字节FIFO已满还在写、查询和中断混用导致时序乱发送前检查FIFO状态接收用中断加队列芯片发烫供电超压或引脚短路量各路电源和相邻引脚检查焊接4.3 几个我踩过的坑写在最前面第一个坑是SPI时钟贪快。一开始我把分频设得很小想跑高速结果读寄存器偶尔会错多读几次才正常。排查了半天最后把SPI时钟降到1MHz左右问题当场消失。后来我在所有项目里都保留了“低速调通、再提速度”的调试习惯。第二个坑就是DLAB。第一次配置UART波特率寄存器写了无数遍示波器上一看波形波特率纹丝不动。后来翻手册才注意到波特率寄存器和数据寄存器共用地址必须先打开DLAB位。这个坑在WK2124这类芯片上太典型了强烈建议写驱动的时候把DLAB操作封装成一个独立函数避免到处踩。第三个坑是中断极性。我那批芯片的INT引脚实测是低有效但第一版驱动按手册画的波形配成了下降沿触发结果中断完全不进。后来用示波器量了INT引脚发现芯片拉低表示有中断把STM32的外部中断触发方式改成下降沿问题就解决了。所以一定要实测不要假设。第四个坑是长数据流丢字节。查询方式下每读一个字节之前都要查LSR如果读LSR和读RBR之间来了一个更高优先级的中断数据就会被覆盖。后来改成中断加环形队列同时在操作临界区加了保护丢数据的问题才彻底解决。5. 低功耗适配与后续扩展思路5.1 STM32L151和WK2124如何一起省电STM32L151的低功耗能力很强但外挂了WK2124之后不能指望芯片自己闲着就自动省电。SPI从机在没时钟、没传输的时候静态功耗其实不高但如果WK2124的某路UART还开着接收那一路的接收电路就会一直工作电流会一直存在。我在低功耗项目里的做法是这样所有数据交互完成之后先把片选拉高关闭SPI外设时钟让SPI相关的IO进入确定的电平状态避免浮空输入造成的额外电流。然后把不用的串口通道通过控制寄存器关掉只保留需要唤醒接收的那一路。WK2124的INT引脚接在STM32L151支持外部中断唤醒的GPIO上当串口有数据到达时WK2124把INT拉低STM32L151从STOP模式唤醒再恢复SPI和WK2124的配置。如果项目对功耗的要求非常苛刻更狠的做法是给WK2124单独加一个MOS管或负载开关进入低功耗模式时直接给它断电。这种方式省电最彻底但代价是唤醒后必须重新初始化WK2124上电时序和复位时序都要在代码里处理好。我建议非极端情况不要轻易这么做保持WK2124常供电但让串口进休眠一般就够用了。5.2 如果以后还需要更多串口WK2124这个方案的扩展性其实很好。一颗芯片搞定4路串口后如果某个项目需要8路甚至更多可以再加一颗WK2124两片共用同一个SPI总线只需要各自独立的片选引脚。中断引脚如果不够用可以把多颗芯片的中断输出接到同一个GPIO上MCU收到中断后逐个去读每颗芯片的中断状态寄存器判断到底是哪一片在“喊话”。这个思路在工程上很常用虽然中断处理稍微绕一点但IO占用非常省。还有一种思路是把SPI总线的价值最大化。同样是SPI除了转串口还能挂LCD、传感器、Flash等外设只要总线带宽算得过来一组SPI就能撑起整个系统的外部设备扩展。我后面做其他项目时会先在纸上画一张SPI设备拓扑把每个外设的片选和中断分配清楚再动手画PCB这样可以避免引脚冲突。最后说点个人体会。这套方案调通之后我后续做项目但凡遇到串口不够第一反应都会先算算需不需要外挂WK2124。它最大的价值不是某个参数有多么出彩而是把“引脚不够、串口不够”这种系统级问题变成了一个驱动问题。驱动写好了后面做多路通信就真的省心。踩过几次坑之后我现在拿到新板子的第一件事都是先把复位时序和INT极性量清楚再开始写驱动这个习惯能让人少熬好几天的夜。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑