开头先交代一下背景:我最近在做一套低功耗数据采集节点,主控用的是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,引脚分配如下:
- SCK:PB13
- MOSI:PB15
- MISO:PB14
- CS:PB12(软件控制)
- INT:PA0(外部中断输入,下降沿触发)
- RST:PA1(推挽输出,默认高)
选PA0和PA1之前,我特意查了引脚复用功能表,确认这两个引脚不与其他关键外设冲突。L151的SPI2与USART3、TIMER有引脚复用,如果你的板子同时用了这些外设,一定要核一遍重映射表。我第一次打样时没注意,把SPI2的SCK放在了和SWD调试口复用的引脚上,导致调试器连不上,后来紧急飞线才解决。
CS引脚用普通GPIO控制而不是硬件NSS,原因有两个:一是硬件NSS在SPI异常时容易出现片选状态错乱,二是软件控制片选可以让我在每次访问前主动拉低、访问结束拉高,时序上更可控。WK2124对CS的要求是:在传输命令和数据期间必须保持低电平,不能中途释放,软件控制正好满足。
2.2 SPI时钟极性相位:主从双方必须匹配
这是SPI转串口最容易翻车的第一关。WK2124的数据手册上通常会写明支持的SPI模式,一般支持模式0(CPOL=0,CPHA=0)或者模式3(CPOL=1,CPHA=1)。我在初始化时选择模式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。主频16MHz,SPI时钟设为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 主控休眠前如何处理WK2124
STM32L151最吸引人的就是低功耗特性,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接口断开,因为调试器本身会灌电流进来,导致测出来的数值偏大。我第一次测的时候把调试器插着,读数怎么都降不下来,拔掉调试器后数据才正常。希望这些实际经验能帮你少走弯路。