
简介这是基于STM32F103C8与WK2124芯片的SPI扩展四串口HAL库移植工程包面向需要扩展串口资源的嵌入式开发者适用于物联网设备、工业控制与数据采集等场景。压缩包共1187个文件以836个C源码和255个头文件为主辅以51个汇编启动文件、28个IAR链接配置文件和多个数学库文件同时包含IAR/Keil工程配置、CubeMX初始化文件.ioc、.mxproject及测试用例整体约18.33MB目录结构清晰便于查阅。工程已实际验证可成功运行读者可对照学习SPI主模式初始化、WK2124内部寄存器配置、中断服务编写及四路串口收发测试借助HAL库的硬件抽象层接口还能掌握一套与具体MCU无关的外设驱动移植方法。目前已有801人学习适合正在进行STM32串口扩展或HAL库开发的中级嵌入式工程师参考。1. 用 SPI 扩展串口WK2124 的 HAL 移植到底解决了什么问题做多串口设备时STM32 的 UART 外设数量经常不够用直接换大容量芯片又会打乱硬件和软件结构。WK2124 这类 SPI 扩展串口芯片用一组 SPI 引脚换四路全双工串口配合 STM32 HAL 库做 Cube HAL 版移植是工程里很实际的取舍。我这次把一套裸机 Demo 的驱动逻辑拆成了 HAL 风格的模块在测试板上跑通了四路收发。如果你是做物联网网关、工业采集或者多串口透传卡在「串口不够、驱动不会改」这类问题这份源码和笔记能直接省掉你对着数据手册翻寄存器的两天时间。下文从硬件时序讲起再给移植步骤和实测参数。2. WK2124 SPI 扩展串口的硬件基础寄存器模型、片选与 IRQ 设计2.1 引脚连接与寄存器模型决定驱动怎么分层WK2124 对外是一个 SPI 从机对内管理四路 UART。STM32 这边需要提供的信号很简单SCLK、MOSI、MISO、CS 和 IRQ。前三根接 SPI1 或 SPI2 对应引脚CS 不建议接到硬件 NSS因为硬件 NSS 的自动管理在连续读写时会提前切走片选导致寄存器访问错位。我一般会专门分配一个普通 GPIO 做 CS软件拉低开始一次事务拉高结束。IRQ 则要接到支持外部中断的引脚上不能只拿普通输入轮询。这个决定影响了驱动分层驱动只需要关心两件事一是 SPI 总线上怎么搬字节二是 IRQ 来了之后怎么处理事件业务层完全不接触 WK2124 的寄存器细节。寄存器模型上WK2124 的寄存器通过 SPI 命令帧访问。业务层不关心命令帧长什么样驱动里只需要把「读某个寄存器」「写某个寄存器」做成基础函数。表 1 是我整理的典型连接不同板子可以根据实际引脚调整。注意 CS 和 IRQ 对应的 STM32 引脚不是 SPI 外设的一部分而是普通 GPIO 和 EXTICube MX 配置时不要选错。功能WK2124 引脚STM32 引脚SPI 时钟SCLKPA5 (SPI1_SCK)SPI 数据输入MOSIPA7 (SPI1_MOSI)SPI 数据输出MISOPA6 (SPI1_MISO)片选CSPA4 (GPIO 输出)中断通知IRQPB0 (EXTI0)这里有个选型理由值得说用 SPI 扩展串口而不是 GPIO 模拟 UART核心原因是时序可控。GPIO 模拟串口在系统忙时会产生波特率抖动单片机主频稍微被中断抢占误码率就上来了。WK2124 把波特率生成和收发的底层逻辑做在芯片内部STM32 只负责高速读写数据接收侧靠 IRQ 通知CPU 占用比软件模拟低一个量级。很多开发者一开始觉得 GPIO 模拟省钱省片选但实际上后期排错的时间远超过一颗 WK2124 的成本。2.2 时钟、波特率与 IRQ 极性先确认边界再写代码WK2124 的 SPI 从机接口对时钟频率有上限要求。常见做法是把 STM32 SPI 时钟先压低到 10MHz 以内跑通再慢慢往上调。我在一块通用测试板上用 6MHz 起步读写寄存器稳定后才去动分频系数。如果一开始就按满速配置寄存器偶发读错时很难判断是硬件布线问题还是 SPI 模式问题。SPI 时钟这块我建议直接量 SCLK 而不是看 Cube MX 里的分频数字因为 APB 总线的实际频率不一定等于理论值尤其系统里还开着 USB 或定时器中断。波特率的设置不在 STM32 侧而在 WK2124 内部。每路 UART 的波特率、数据位、停止位都由寄存器控制驱动初始化时要按各通道用途分别配置。不同通道可以用不同波特率这一点比 STM32 自带的串口灵活。但要注意芯片内部有一个处理时钟四路串口同时在跑时通道数越多单字节的瞬时处理窗口越紧后面会讲到怎么测上限。我的经验是不要把四路都拉满到同一条总线极限至少给其中一路留出余量。IRQ 极性是另一个容易踩的边界。WK2124 的 IRQ 信号有明确的有效电平可能是高有效也可能是低有效具体要看数据手册。接线时我建议先把 IRQ 方向配成输入打开上拉或下拉再用示波器量一次有数据进来时的电平变化得到实际极性后再配置 EXTI 触发沿。下面的回调函数是常见的 IRQ 处理骨架触发后进入 WK2124 驱动统一处理。void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin WK2124_IRQ_PIN) { // 进入驱动层读取并清中断状态搬运 FIFO wk2124_irq_handler(hwk); } }这段代码看起来简单但逻辑上有两个关键点一是回调里只做数据搬运不做协议解析二是必须调用驱动层的 irq_handler 去读状态寄存器否则 IRQ 引脚可能一直保持有效电平导致 EXTI 反复触发。参数层面IRQ_PIN 要和 Cube MX 里配置的引脚一致回调里用 GPIO_Pin 判断是为了兼容多个 EXTI 共用同一个回调。初始化顺序也要养成固定习惯上电后先做一次软复位等待芯片内部时钟稳定再逐通道写波特率寄存器和控制寄存器最后使能中断。软复位的具体写法在 Demo 里是连续写复位寄存器两次和普通寄存器写不同。如果把这一步省了芯片可能处于未知状态后续配置不会生效。另外WK2124 的中断状态寄存器通常需要按数据手册要求清标志有些是读后自动清有些需要写 1 清。很多代码跑一段时间就收不到 IRQ就是因为状态寄存器里的标志位没有被清干净。移植时先读 ID 是最小的验证手段ID 能读回来再谈后续功能。3. Cube HAL 移植步骤从 Cube MX 工程到可用的 WK2124 驱动3.1 Cube MX 初始化配置SPI 参数与 GPIO 分工打开 Cube MX 后先把 SPI1 设为全双工主机模式。SCLK、MOSI、MISO 会自动分配CS 引脚要在 GPIO 标签下手动改成一个普通输出口命名成 WK2124_CSIRQ 引脚要启用 EXTI 并使能外部中断。SPI1 的参数页里我建议按下面的值起配Clock Speed 先选 6MHzCPOL0CPHA0数据宽度 8 位MSB first。这套参数对应的是最常见的 SPI Mode 0先跑通再优化。生成工程后主频配置会影响 SPI 分频。比如 STM32F103 的 APB2 时钟是 72MHzSPI1 预分频 16 就是 4.5MHz预分频 8 是 9MHz。我习惯在初始化代码里把 SPI 时钟显式算出来不要凭感觉填。下面是 Cube MX 生成的初始化代码注意 CS 和 IRQ 是普通 GPIO不参与 SPI 时序。void MX_SPI1_Init(void) { 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_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这段代码里最容易被忽略的是 NSS SPI_NSS_SOFT。WK2124 的 CS 必须由软件控制因此 SPI 外设内部 NSS 要禁用否则硬件会自动把 NSS 拉低拉高和 GPIO 片选互相打架。BaudRatePrescaler 具体值要根据主频换算表格里我会给出常见的对照但实际以示波器量到的 SCLK 为准。APB2 时钟预分频实际 SPI 时钟72MHz164.5MHz72MHz89MHz84MHz165.25MHz84MHz810.5MHzGPIO 配置也要同步检查。CS 引脚要设成推挽输出、初始化为高电平IRQ 引脚设为输入并根据实际电平决定上拉或下拉。Cube MX 默认的 GPIO 速度可能不高SPI 引脚建议把输出速度选到 High否则在较高 SPI 时钟下 MISO 采样会不稳定。这块我踩过一次SCLK 上升沿太缓导致第一字节经常错一位把 GPIO Speed 改成 High 后问题消失。3.2 HAL 寄存器读写封装把 SPI 总线变成寄存器接口裸机 Demo 里的寄存器读写多用软件延时控制时序移植到 HAL 后可以把它们统一封装成基于 HAL_SPI_TransmitReceive 的函数。读寄存器的典型流程是拉低 CS发送寄存器地址随后读取指定长度的数据最后拉高 CS。下面是我在工程里实际使用的读函数。int wk2124_reg_read(wk2124_t *dev, uint8_t reg, uint8_t *buf, uint16_t len) { uint8_t cmd reg; // 寄存器地址 uint8_t tmp 0xFF; HAL_GPIO_WritePin(dev-cs_port, dev-cs_pin, GPIO_PIN_RESET); if (HAL_SPI_TransmitReceive(dev-hspi, cmd, tmp, 1, 10) ! HAL_OK) goto error; if (HAL_SPI_TransmitReceive(dev-hspi, buf, buf, len, 10) ! HAL_OK) goto error; HAL_GPIO_WritePin(dev-cs_port, dev-cs_pin, GPIO_PIN_SET); return 0; error: HAL_GPIO_WritePin(dev-cs_port, dev-cs_pin, GPIO_PIN_SET); return -1; }逻辑说明第一步发送地址第二步用同一缓冲区既发送 dummy 又接收数据HAL_SPI_TransmitReceive 是全双工接口发送什么不关心只关心收进来的内容。超时参数 10 表示 10ms按 4.5MHz 传几十个字节只会占用几十微秒这个时间足够宽松。需要注意的是整个事务过程中 CS 必须持续拉低中途不能有别的 SPI 操作插进来。参数里 dev 结构体保存了 hspi、cs_port、cs_pin这样多块 WK2124 可以复用同一套函数。写寄存器的思路一样只是把命令和数据一起发出去。封装完成后初始化函数里就能用干净的方式逐个配置寄存器比如复位芯片、设置波特率、打开接收中断。驱动层有了 read/write 基础函数后后续加功能很少需要再动底层的 SPI 逻辑。这份资源里的代码把常见的寄存器操作都拆成了带注释的短函数直接套用比自己在数据手册里拼命令帧省事得多。建议你把寄存器地址集中放在一个头文件里用宏定义管理换芯片型号时只需要改这一处。4. 收发链路实测环形缓冲区、IRQ 搬运与不同波特率下的表现4.1 接收链路环形缓冲区与 ISR 只搬不解析接收侧最关键的设计是「中断服务程序里只搬数据不碰协议」。WK2124 的 IRQ 到来后芯片内部 FIFO 已经有数据如果不在 IRQ 里及时读走后续字节就可能覆盖。我在驱动里用一个环形缓冲区承接从芯片 FIFO 搬出来的数据业务代码随时可以从缓冲区里取。环形缓冲区结构体定义如下。#define WK2124_RX_BUF_SIZE 1024 typedef struct { uint8_t data[WK2124_RX_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } wk2124_ring_t; static void ring_write(wk2124_ring_t *r, uint8_t byte) { uint16_t next (r-head 1) % WK2124_RX_BUF_SIZE; if (next r-tail) { r-head 0xF0; // 置溢出标志业务层按需处理 return; } r-data[r-head] byte; r-head next; }head 是写入位置tail 是读取位置。缓冲区满的判断是 next 等于 tail说明没有空位。溢出时我没有直接丢数据而是把 head 写成一个非空标志这样应用层读到 0xF0 就知道丢帧了。之所以用取模而不是简单的自增是为了避免回绕时下标越界代价是缓冲区大小必须是 2 的整数倍时才能用位运算优化这一点在参数调整时要注意。ISR 里的搬运动作要做得快。标准流程是进入 irq_handler 后循环读芯片状态只要状态寄存器里还有事件就继续读对应通道的数据寄存器直到状态清空。下面是简化版本。void wk2124_irq_handler(wk2124_t *dev) { uint8_t status 0; while (1) { wk2124_reg_read(dev, REG_INT_STATUS, status, 1); if ((status RX_EVENT) 0) break; wk2124_reg_read(dev, REG_RX_DATA, dev-rx_byte, 1); ring_write(dev-rx_ring, dev-rx_byte); } }这里用 while 循环把同一 IRQ 周期内的所有数据都读干净避免中断退出后 IRQ 仍然有效造成 CPU 反复进中断。参数上 REG_INT_STATUS 和 REG_RX_DATA 是 WK2124 数据手册里的寄存器地址Demo 的驱动头文件里已经做了宏定义。缓冲区大小 1024 是按 115200 波特率估算的一次 50ms 的突发数据量约 576 字节留一倍余量足够。如果项目有更高的突发流量直接把宏改成 2048 即可代价是多占 1KB RAM。4.2 发送链路与回环测试不同波特率下的边界在哪发送相对简单阻塞发送时先读一下芯片的发送状态寄存器确认发送 FIFO 不忙再把数据写进去。超时参数不能省否则一旦芯片异常程序会卡死在轮询循环里。我建议把超时做成宏不同波特率下用同一套逻辑只是时间窗不同。int wk2124_uart_send(wk2124_t *dev, uint8_t ch, uint8_t *buf, uint16_t len) { uint32_t start HAL_GetTick(); for (uint16_t i 0; i len; i) { while (wk2124_tx_busy(dev, ch)) { if (HAL_GetTick() - start WK2124_SEND_TIMEOUT) return -1; } wk2124_reg_write(dev, REG_TX_DATA(ch), buf[i], 1); } return 0; }wk2124_tx_busy 内部读的是该通道的发送状态位如果一直为忙说明上一字节还没移出。超时时间我一般设 20ms在正常波特率下远远足够设置太短反而会在系统高负载时误报错误。实测部分我在一块测试板上把四路 TX 分别短接到对应 RX用随机字节流发送PC 端通过另一个串口观察接收。表的数字是从某次连续 10 万字节测试里截取的能反映大体趋势。波特率SPI 时钟发送字节错误数结论96004.5MHz1000000稳定1152004.5MHz1000000稳定4608004.5MHz1000001偶发错位9216004.5MHz10000012不建议错位问题集中出现在波特率超过 460800 之后原因后面会展开。轮询模式在 9600 下没问题但四路同时收发时 CPU 占用明显上升中断模式四路 115200 跑满CPU 占用低很多。如果项目要求四路同时以 115200 收发IRQ 模式比轮询模式稳定的原因也很直接轮询模式必须自己安排固定查询周期周期短了 CPU 占用高周期长了又错过 FIFO 中段。IRQ 模式让芯片主动通知CPU 只在有数据的时候参与。5. 避坑与常见问题排查移植中遇到的五类典型问题5.1 读寄存器全是 0xFFCS 与 SPI 模式不匹配现象初始化函数调用读 ID 寄存器返回永远是 0xFF或者偶发读出 0x00。驱动其余代码没有任何问题。原因最常见有两点。一是 SPI 的 CPOL、CPHA 和芯片要求不一致比如配成了 Mode 3时钟空闲电平和采样边沿错位MISO 上采到的都是无效电平。二是 CS 不是由 GPIO 软件控制而是把 NSS 设成了硬件自动模式SPI 外设会在每次传输时自动切 CS导致事务在芯片看来不完整。解决先把 SPI 参数按 Mode 0 配置即 CPOL0、CPHA0CS 改成 GPIO 软件控制。在读写函数前后加入短延时用示波器确认 CS 低电平持续到整个事务结束。改完还不行就对比数据手册里的 SPI 时序图重点看数据在时钟上升沿还是下降沿有效。5.2 IRQ 收不到事件极性与上拉配置错误现象回环测试时发送方一直在发接收方业务层等不到数据但用调试器查看芯片 FIFO 已经满了。原因IRQ 引脚配置成了不带上下拉的输入或者 EXTI 触发沿和芯片实际极性相反。芯片输出的是高有效电平代码却配了下降沿触发导致 IRQ 拉高时进不了中断拉低时芯片又已经清了事件。解决先用万用表或示波器量 IRQ 在空闲和数据到达时的电平变化确定有效极性。然后配置 GPIO 为上拉模式EXTI 触发边沿跟随实际极性。我在工程里会把 IRQ 极性写成宏方便不同批次芯片调整避免改完硬件还要翻代码。5.3 四路通道相互干扰中断服务里没有清干净状态现象第 1 路串口收发正常第 2、3、4 路偶尔丢字节并且丢字节的时机都在第 1 路收到数据之后。原因WK2124 的中断状态寄存器是多个通道公用的。ISR 里只读一次状态、只处理一个通道就退出导致同一中断周期内其他通道的事件没有被处理而且芯片 IRQ 可能还处于有效状态触发方式异常。解决ISR 里用 while 循环读状态直到状态寄存器里没有任何待处理事件。每一路通道的数据寄存器地址要按偏移量计算Demo 里 REG_TX_DATA(ch)、REG_RX_DATA(ch) 这种带通道号的宏就是干这个用的。改完再用四路同时回环测试观察是否还有丢失。5.4 HAL_SPI_TransmitReceive 卡死或超时错误恢复路径缺失现象系统跑一段时间后运行日志里出现 HAL_TIMEOUT之后驱动一直无法恢复看起来像死锁。原因HAL 的 SPI 接收在检测到总线错误后会把外设置忙如果代码里超时返回后没有清理错误标志下一次调用会一直卡在 HAL_BUSY 判断。另一个原因是超时参数设置过短系统中断密集时 SPI 传输被频繁打断。解决在每次调用 HAL_SPI_TransmitReceive 后检查返回值非 OK 时先调用 __HAL_SPI_CLEAR_OVRFLAG 清理溢出标志再执行 HAL_SPI_DeInit 加 HAL_SPI_Init 复位外设。超时时间按最大传输长度和实际 SPI 时钟计算留出 10 倍余量不要拍脑袋写 10ms。5.5 高波特率偶发错位芯片内部处理速度与 SPI 读出速度不匹配现象9600 和 115200 下零错误460800 开始出现字节错位收到的数据偶尔变成两个字符合并或单字节重复。原因SPI 侧读 FIFO 的速度太快芯片内部还没把串口数据整理好主机就把不完整的数据取走了。速率越高单位时间需要处理的数据量越大这个窗口就越明显。解决先降低 SPI 时钟重测观察错误是否消失然后在读数据寄存器之间增加微延时给芯片留出处理窗口。如果项目必须跑高波特率优先用 IRQ 方式慢速读取不要再加大缓冲区在循环里猛读。事后我重新测了一遍把 SPI 时钟从 4.5MHz 降到 3MHz460800 的错误数降到了 0。6. 进阶技巧把 WK2124 驱动接进 RTOS 收发流程到这里驱动已经能跑但真正放到产品里还会面临多任务竞争。我的习惯是给每一路串口建一个接收线程线程阻塞在信号量上IRQ 里只负责把数据丢进环形缓冲区并释放信号量。这样应用层可以用阻塞读取代替满屏的 while 轮询代码更容易读。void uart_task(void *arg) { wk2124_chan_t *ch (wk2124_chan_t *)arg; uint8_t byte; for (;;) { osSemaphoreAcquire(ch-sem, osWaitForever); while (ring_pop(ch-rx_ring, byte) 0) { // 业务代码按帧处理 } } }这种结构的信号量只表示「缓冲区里有数据」真实数据量由环形缓冲区的读取循环兜底。发送线程较多时可以再增加互斥锁保护发送函数避免两个任务同时占用 SPI 总线。移植完成后的验证建议是四路串口同时接入上位机用 PC 端随机数据连续发 8 小时统计丢字节和错误帧数。这套流程与 Demo 里的驱动代码一一对应拿到资源后建议先按第 2 章的表查一遍接线再直接跑读 ID 示例验证环境。从那以后我每次移植完都强制走一遍检查清单SPI 模式、CS 时序、IRQ 极性、超时阈值挨个确认过才敢上产线。希望帮到你。本文还有配套的精品资源点击获取