
开头先交代一下背景我最近在做一套低功耗数据采集节点主控用的是STM32L151外设传感器模块全是UART接口——GPS、RS485总线、调试口、还有一路透传。一个板子上要四路串口同时跑但L151这款芯片内部最多就两三个UART资源明显不够。试过软件模拟串口也试过改板子少挂一路都折腾得不太理想。最后换成WK2124这款SPI转四路串口芯片整套驱动从选型到落地前后花了一周时间。这篇文章就把我整个决策过程、驱动实现和调试踩坑记录整理出来给同样在L151上做串口扩展的同行一个参考。我默认读者手里已经有STM32L151和WK2124的数据手册至少看过引脚图和寄存器表。如果还没看过建议先下载手册对着看我下面讲到的寄存器地址和命令格式全部以芯片手册为准。1. 串口不够用时为什么是WK2124而不是软件模拟1.1 四路UART同时工作的实际场景压力我的应用是一个分布式环境监测节点主控要同时挂在五六个传感器上。传感器大多走UART输出波特率不高但数据流很连续有的每秒出一包有的只在事件触发时突发几百字节。最初板载的USART1接了调试USART2接GPS剩下两路并没有合适的外设口。理论上可以用普通GPIO模拟UART接收端用外部中断配合定时器测量脉宽发送端用定时器翻转电平。但实测下来有两个难接受的问题第一CPU占用率极高波特率9600时中断频繁主循环几乎被占用掉一半时间第二接收长帧时误差会累积偶尔出现字节错位排查起来非常费劲。四路同时开着系统就直接不可用了。后来考虑过换更大的主控但项目对功耗和成本有硬约束板子已经定型只能在现有L151方案上想办法。SPI接口在主控上是现成的浪费一条SPI总线换来四路独立硬件UART这笔账怎么算都划算。1.2 串口扩展芯片选型对比市面上SPI转UART的方案不少常见的有两通道、四通道甚至八通道的设备。我做了一个横向对比核心看这几项通道数我需要四路四通道芯片刚刚好也能留一路备用。FIFO深度FIFO越大主控响应压力越小尤其突发传输不能丢数据。波特率范围要支持1200到115200最好能到更高因为有些传感器用460800。硬件流控RS485半双工会用到方向自动控制至少要有RTS/CTS控制能力。中断能力不能纯靠轮询必须有INT引脚通知主控数据到达。待机功耗电池供电项目芯片本身睡眠电流必须小于10微安级别。对比下来WK2124的优势比较明显四路UART每一路都有独立的收发FIFO发送和接收各256字节我使用的版本如此具体以手册为准支持硬件自动流控SPI从设备模式速率可以跑到10MHz以上。另一个同类产品某些版本只有64字节FIFO突发场景下容易溢出。单从SPI协议上讲这类芯片控制逻辑都类似无非是写寄存器、读状态、读写FIFO三件事。WK2124的寄存器布局比较规整命令格式简单内部还带睡眠模式和L151的低功耗设计思路很合拍。所以我最终选了它。1.3 SPI接口相比I2C接口的优势同样能挂多串口的芯片也有走I2C接口的。I2C的优势是引脚少但弱点也很明显速率上限受限标准模式100kHz、快速400kHz即便快模式也就1MHz一帧数据上百字节时I2C总线会被占住很长时间。SPI全双工、无地址开销时钟可以到几兆甚至十几兆最适合大批量FIFO搬运。而且L151的SPI外设支持DMA我可以把接收中断里逐字节读FIFO的操作换成DMA一次搬完腾出大量CPU时间。这在使用I2C的方案里就很难做到这么顺畅。总线上的CS片选还能决定当前总线属于哪个通道的FIFO逻辑上非常干净。2. 硬件连接和初始化开工前先确认这五个细节2.1 SPI引脚分配别和调试口以及JTAG打架WK2124是SPI从设备需要四根信号线SCK、MOSI、MISO、CS外加一根中断输出INT和一根复位输入RST。我在L151上选用了SPI2引脚分配如下SCKPB13MOSIPB15MISOPB14CSPB12软件控制INTPA0外部中断输入下降沿触发RSTPA1推挽输出默认高选PA0和PA1之前我特意查了引脚复用功能表确认这两个引脚不与其他关键外设冲突。L151的SPI2与USART3、TIMER有引脚复用如果你的板子同时用了这些外设一定要核一遍重映射表。我第一次打样时没注意把SPI2的SCK放在了和SWD调试口复用的引脚上导致调试器连不上后来紧急飞线才解决。CS引脚用普通GPIO控制而不是硬件NSS原因有两个一是硬件NSS在SPI异常时容易出现片选状态错乱二是软件控制片选可以让我在每次访问前主动拉低、访问结束拉高时序上更可控。WK2124对CS的要求是在传输命令和数据期间必须保持低电平不能中途释放软件控制正好满足。2.2 SPI时钟极性相位主从双方必须匹配这是SPI转串口最容易翻车的第一关。WK2124的数据手册上通常会写明支持的SPI模式一般支持模式0CPOL0CPHA0或者模式3CPOL1CPHA1。我在初始化时选择模式0也就是空闲时钟为低电平数据在第一个边沿采样。如果主从配置不一致表现是寄存器读回来全是0xFF或者写寄存器后芯片完全没有反应。遇到这种情况不用上来怀疑芯片坏先用示波器看SCK的极性然后确认主控SPI配置里的CPOL和CPHA。我建议把所有SPI访问都封装成一个独立函数统一维护CS和SPI发送接收操作这样即使以后要切换模式只需要改一个地方。封装函数里最好加一个超时保护防止芯片未就绪时死等SPI标志位。2.3 复位电路与电源去耦WK2124的RST引脚是低电平复位上电时要求RST至少保持低电平一段时间典型值几百微秒确保内部状态机初始化完毕。我的做法是在L151启动代码里先把RST引脚拉低延时1毫秒以上再拉高然后等待WK2124内部就绪。电源去耦方面WK2124内置了多路UART内部开关频率不低对电源噪声比较敏感。每个电源引脚旁边都放了0.1微法电容靠近芯片放置电源入口再加一颗10微法钽电容。如果板子上有RS485收发器WK2124的电源和RS485电源尽量分开否则收发器瞬间大电流会把WK2124的稳压拉出纹波表现为串口帧偶尔错一个字节。这里还有个容易被忽略的点WK2124的INT引脚是开漏输出需要外部上拉到3.3V。我第一次画原理图时默认它内部有上拉结果中断死活检测不到后来查手册发现必须外接4.7k到10k上拉电阻补上之后中断立刻正常工作。如果你的板子已经打样没有上拉电阻也没关系可以把INT配置成输入模式下拉检测但是稳定性远不如外部上拉。3. 寄存器手册里容易被忽略的通信细节3.1 SPI命令格式读和写的区分方式WK2124的SPI访问是命令字节数据字节的格式。命令字节由访问方向位和寄存器地址构成高一位表示方向1为读0为写低七位是地址。也就是说写寄存器发送0x00 | (寄存器地址 0x7F)随后紧跟着要写入的数据字节。读寄存器发送0x80 | (寄存器地址 0x7F)随后从总线上读回一个字节。传输过程中片选必须一直拉低不能中间拉高。这是个约定俗成的设计很多通用SPI转UART芯片都这样。第一次写驱动时我习惯性地按照先发地址再发数据、每个字节都单独拉低拉高CS的方式处理结果芯片完全没响应。仔细看手册才发现命令和紧随其后的数据必须是连续的一个事务CS在整个过程中保持低电平。这个问题很容易被忽视建议一开始就把事务封装到位。3.2 波特率寄存器的分频计算WK2124的每个通道都有一组分频寄存器通过两个字节配置分频系数用它来产生该通道的收发时钟。公式大致是输出波特率 输入时钟频率 / 分频系数以我用14.7456MHz的晶振输入为例想得到115200波特率分频系数 14.7456MHz / 115200 128所以分频系数的高字节设为0低字节设为128写入两个字节的分频寄存器。如果用12MHz晶振计算115200波特率时除不尽会产生误差长时间通信后可能积累错误。这也是为什么很多转串口芯片推荐使用14.7456MHz或18.432MHz晶振的原因。实际驱动里我封装了这样一个函数传入波特率目标值内部根据当前晶振频率算出分频数写入通道配置。不同芯片的分频公式可能存在偏移比如分频系数是2倍因此我调整波特率之后一定会用示波器或者逻辑分析仪实测TXD引脚上的波形确认实际波特率和目标一致。一个方便的办法是让该通道自发发送0x55在示波器上数一位的脉宽。3.3 中断状态与FIFO的协作机制WK2124有一个中断状态寄存器通过读取它来确定当前是什么事件触发了INT引脚。常见的事件有接收FIFO达到触发阈值、接收超时、发送FIFO空、线路错误等。处理逻辑一般是INT引脚变低说明有事件。主控通过SPI读取中断状态寄存器。根据状态位判断是哪类事件分别处理。处理完成后芯片会自动清除相应的中断标志INT引脚恢复高电平。如果你使用的是中断方式接收需要特别注意FIFO触发阈值的设置。FIFO深度是256字节在FIFO控制寄存器里可以设置触发点的值比如1、4、8、16、64。这样做的意义是当接收到的字节数达到阈值时芯片才拉低INT通知主控主控一次性把一批数据读走避免每个字节都打断一次。我在实际项目中把阈值设为16字节配合DMA搬运。如果阈值设得太小比如1字节CPU会被高频率中断淹没如果设得太大实时性会下降对于短帧数据包容易在FIFO里等很久。大家需要根据自己数据包的典型长度做权衡我用16是因为传感器数据包大多在几十字节左右16字节触发既不会太频繁响应也算及时。还有一个接收超时中断。长时间没有新数据到达但FIFO里还有残留数据时芯片会触发超时中断避免主控永远不知道还有数据在里面。这个特性在不定长协议里非常有用。我处理不定长GPS语句时就是靠这个超时中断来判断一帧接收完成。4. 驱动代码落地的完整路径4.1 SPI主机初始化与基础访问函数L151这边我用的是标准外设库先把SPI2初始化为mode 0、8位数据、MSB先行、软件NSS。主频16MHzSPI时钟设为4MHz这个速率对WK2124来说完全够也留足了裕量。void SPI2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB | RCC_AHBPeriph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }基础访问函数是核心把CS控制和SPI收发封装在一起uint8_t WK2124_ReadReg(uint8_t regAddr) { uint8_t val 0; GPIO_WriteBit(GPIOB, GPIO_Pin_12, Bit_RESET); // CS拉低 SPI2_SendByte(0x80 | (regAddr 0x7F)); // 读命令 val SPI2_ReceiveByte(); // 接收数据 GPIO_WriteBit(GPIOB, GPIO_Pin_12, Bit_SET); // CS拉高 return val; } void WK2124_WriteReg(uint8_t regAddr, uint8_t val) { GPIO_WriteBit(GPIOB, GPIO_Pin_12, Bit_RESET); SPI2_SendByte(regAddr 0x7F); // 写命令 SPI2_SendByte(val); GPIO_WriteBit(GPIOB, GPIO_Pin_12, Bit_SET); }SPI2_SendByte里要有一个等待发送完成的循环每次发送后必须读一次SPI状态寄存器来清除标志。实际操作中我发现如果这个基础函数里对接收到的一字节数据不及时读取再发下一个字节时SPI的接收缓冲区会被覆盖因此封装里我会先读取上次收到的数据再发下个命令。4.2 初始化WK2124的规范顺序初始化步骤不是简单地往寄存器填值顺序错了后面全乱。我的顺序是RST引脚先拉低延时2毫秒再拉高等待芯片完成内部复位。全局复位命令如果芯片支持再次确认状态。关闭所有通道的中断配置线路控制寄存器设置数据位、停止位、校验位。按每个通道的实际波特率需求写入分频寄存器。配置FIFO控制寄存器使能FIFO和中断触发阈值。配置Modem控制寄存器这里面包含RTS/CTS模式选择如果是RS485可能还需要配置自动方向控制。最后使能对应通道的接收中断、发送中断等。关键点在于先配置好线路参数和波特率再开中断防止芯片在参数未就绪时就上报中断导致主控去读到的是一堆无效状态。下面是通道初始化函数的简化示例void WK2124_ChannelInit(uint8_t ch, uint32_t baud, uint8_t lcr) { uint16_t div (uint16_t)(WK2124_CLK / baud); WK2124_WriteReg(WK2124_LCR(ch), lcr); WK2124_WriteReg(WK2124_DLL(ch), div 0xFF); WK2124_WriteReg(WK2124_DLH(ch), (div 8) 0xFF); WK2124_WriteReg(WK2124_FCR(ch), 0x10); // 使能FIFO触发阈值另行设置 WK2124_WriteReg(WK2124_IER(ch), 0x01); // 使能接收中断 }WK2124_CLK是外部晶振提供的参考时钟我用14.7456MHz所以对整数波特率都能得到精确分频。如果你的板子用了更便宜的12MHz晶振在115200下分频系数为104.1667取整后误差约0.16%对短帧影响不大但长时间大数据量传输时可能积累成帧错乱建议遇到问题先怀疑这里。4.3 发送路径和接收路径的实现发送比较简单不需要中断也能跑得很稳。我的做法是应用层调用发送函数先查询通道的线路状态寄存器判断发送FIFO是否为空如果空就直接写入待发送数据如果FIFO不满但有数据则循环等待写入直到所有数据送入FIFO。这种阻塞发送在波特率较高时耗时很短四路同时发也不会有问题。接收路径我用的是外部中断触发。GPIO配置上PA0设为下降沿触发的外部中断优先级设为中等。在中断服务函数里不直接逐字节读FIFO而是读取中断状态寄存器判断接收事件然后在一个循环里调用SPI读取函数把FIFO里的数据全部搬到软件环形缓冲区。循环读取次数应等于FIFO中可读字节数这个可以从接收状态寄存器获取。void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line0); uint8_t iir WK2124_ReadReg(WK2124_IIR_GLOBAL); if (iir WK2124_INT_RX_DATA) { uint8_t avail WK2124_ReadReg(WK2124_RX_COUNT(ch)); while (avail--) { uint8_t data WK2124_ReadReg(WK2124_RBR(ch)); RingBuffer_Push(rxBuf[ch], data); } } } }实际测试时发现如果中断服务函数里还做太长的事务比如把数据再转发到RS485发送会导致主循环卡顿。最好在中断里只做FIFO搬运到内存协议解析和应用逻辑留到主循环或低优先级任务中。我后来进一步用DMA读取WK2124的RX FIFO一次事务就能搬完但DMA和SPI的交互写起来稍复杂这里先不展开。5. 实测踩坑四路同发时的数据完整性问题5.1 中断丢失还是SPI超时一个完整的排查链路整机联调时遇到一个很奇怪的现象四路串口都在9600波特率下工作只要其中两路同时有持续数据流过一阵子就会出现丢字节。刚开始我怀疑是WK2124的FIFO溢出因为主控处理不及时。于是我用逻辑分析仪抓SPI总线和INT引脚的时序发现INT引脚确实频繁拉低但主控的中断处理并没有遗漏每次都能响应。继续排查发现丢字节不是发生在接收中断搬运的时候而是发生在应用层从环形缓冲区取数据的时候。我的主循环里用了while轮询环形缓冲区非空才取数据如果同一瞬间两路数据同时到达先处理完一路再去处理另一路时环形缓冲区已经满了新的数据直接被丢弃。问题最后定位在我的环形缓冲区开得太小只有64字节而触发阈值是16字节一路突发200字节数据时缓冲区根本放不下。最终方案是每路环形缓冲区扩大到512字节同时把WK2124的FIFO触发阈值从16改为32减少中断频率给主循环更多时间取数据。修改之后再没有出现过丢字节。这里分享一个排查技巧当你怀疑SPI通信问题时一定先用示波器看CS、SCK和数据线上的波形而不是直接改代码。高级一点的逻辑分析仪可以直接解码SPI命令字节一眼就能看出配置对不对。我那天就是用这种办法发现写寄存器命令的时序是正常的才把注意力从SPI层转移到上层缓冲区。5.2 波特率误差的坑整数分频之外不一定靠谱另一块板子没有贴14.7456MHz晶振临时用了8MHz。当我配置四个通道波特率不完全相同时出现了一个隐蔽问题通道0的115200正常通道1的4800也正常但是通道1和通道2同时通信时偶尔会出现字节错位。算出分频系数后找到了问题原因8MHz除以4800等于1666.666取整为1666实际波特率变成了4801.9误差只有0.04%单通道确实没问题但是当通道1的误差累积到多字节帧时UART的起始位采样不太敏感到了帧末尾就偶尔错位。这再一次验证了晶振选择的重要性。后来我统一改用14.7456MHz任何标准波特率的误差都是0问题直接消失。如果你的项目结构无法更换晶振还有一种补救办法通过软件校准分频系数选择最接近的分频数并且尽量把误差控制在1%以内。对于RS485这种多节点系统建议误差控制在0.5%以内否则总线上的其他设备也有可能采样失败。5.3 四通道同时收发时的SPI总线争用WK2124虽然是四路UART但SPI总线只有一个主控访问哪个通道的FIFO是通过写入不同通道的寄存器地址来区分的。所以当四路UART同时有接收中断时主控只能一个一个把它们的数据搬走CPU资源争用不可避免。为了提高效率我做了两个优化把INT中断服务里的SPI访问改为DMA方式减少CPU干预时间。使用批量读取功能如果能一次读取多个FIFO数据比如通过连续地址读取就尽量用一条SPI事务读完而不是每个字节都拉一次CS。优化后的实测效果四路都在115200下全速收发CPU占用率从原来的35%降到15%左右。这个结果说明只要把事务管理好L151跑WK2124完全没有压力。6. 低功耗场景下再谈WK2124驱动6.1 主控休眠前如何处理WK2124STM32L151最吸引人的就是低功耗特性Stop模式下电流可以做到微安级别。但如果你把WK2124一直保持全速运行整机功耗肯定高得离谱。所以进入低功耗前我需要先把WK2124也睡过去。芯片支持软件睡眠模式通过配置相应的睡眠控制寄存器使芯片进入低功耗状态。进入休眠前我的做法是关闭WK2124所有通道的中断防止休眠过程中INT引脚被意外拉低。将发送FIFO里的残留数据清空或者等待发送完毕。配置睡眠模式寄存器让WK2124进入睡眠。再把L151的EXTI中断使能关闭避免INT引脚上的毛刺唤醒主控。L151进入Stop模式。唤醒时我先通过外部唤醒源唤醒主控然后重新初始化WK2124。这里要注意一个细节从睡眠唤醒后WK2124的寄存器内容可能已经丢失部分芯片睡眠模式下寄存器会复位不能假设之前的配置还在。我的驱动里特意保留了一个WK2124_Init(initStruct)函数唤醒后直接调用它保证四个通道的波特率、线路参数、FIFO设置全部重建。6.2 INT引脚与唤醒引脚的冲突问题如果你的WK2124的INT引脚恰好连接在主控的唤醒源引脚上需要格外小心。正常情况下WK2124睡眠后INT应该保持高电平但如果在睡眠前有接收数据没有读取INT会被拉低主控一进Stop马上被自己唤醒形成反复唤醒。处理办法只有一个进入睡眠前反复读取WK2124的接收FIFO直到确认所有残留数据被清理干净并且中断状态寄存器显示没有未处理事件后再置睡眠模式。我在这里踩过一次坑明明睡了主控的电流却一直下不来最后发现是GPS模块在进入待机前最后发了一帧数据正好卡在INT引脚上。6.3 实测功耗数据整机在电池供电下WK2124睡眠L151 Stop模式外加几个传感器静态待机整机电流实测约8微安。这个成绩基本能达到我们项目的低功耗指标。如果WK2124不休眠光芯片本身就要吃几毫安电流整机就别谈电池续航了。最后再分享一个小技巧如果要精确测量低功耗电流要在电源回路里串万用表同时把调试器的SWD接口断开因为调试器本身会灌电流进来导致测出来的数值偏大。我第一次测的时候把调试器插着读数怎么都降不下来拔掉调试器后数据才正常。希望这些实际经验能帮你少走弯路。