资讯详情

资讯详情

STM32F2串口不够用?WK2114四通道UART拓展驱动全解析

简介这份资源面向嵌入式开发工程师与STM32学习者提供WK2114串口扩展芯片的驱动实现用于解决STM32F2系列微控制器UART接口不足、难以同时与多个外设进行串行通信的问题适用于物联网设备、工业自动化等需要多路串口通信的场景。压缩包共2个文件包含1个c源文件与1个h头文件整体约4KB其中源文件实现WK2114的初始化、波特率与数据格式设置、控制信号及收发数据等函数头文件则定义函数原型、常量与结构体便于其他模块调用与编译。资源已有1263人学习下载读者可据此掌握WK2114与STM32F2的UART1扩展配置方法理解HAL库或寄存器操作方式并借助发送与接收接口完成数据转发验证为多串口嵌入式项目提供可直接复用的驱动参考与调试思路。1. 当你的STM32F2只有两个串口却要接六个外设时去年调一个工控板子主控是STM32F2系列硬件同事一脸轻松地跟我说“串口够用”。结果一数一个接上位机协议一个接4G模块一个接扫码枪一个接温控器还有一个留给调试打印。五个外设芯片上只有两个USART加三个UART其中调试口还不想复用。当时第一反应是换主控但板子已经打样了换芯片等于整版重来。后来翻到WK2114这颗串口拓展芯片四通道UART通过SPI或者并口挂到主控上相当于给STM32F2外挂四个独立串口。这篇文章就把我当时从裸机驱动到中断收发的完整过程拆开讲包括寄存器配置、SPI时序、中断处理和几个让我调了两天的坑。如果你也在用STM32F2或者类似资源紧张的MCU被串口数量卡过脖子这份驱动代码应该能直接拿去改。2. WK2114驱动架构从SPI挂载到四路UART的数据通路2.1 芯片定位与STM32F2的接线方式WK2114是一颗四通道UART拓展芯片每个通道独立配置波特率、数据位、停止位和校验方式内置收发FIFO支持中断输出。它和主控之间有两种接口模式SPI模式和并口模式。我选的是SPI模式因为STM32F2的SPI2刚好空着而且SPI占用的引脚少PCB走线简单。接线逻辑是这样的WK2114的SPI接口接STM32F2的SPI2CS片选用一个普通GPIO控制INT中断输出接STM32F2的一个外部中断引脚。四个UART通道的TX/RX分别引出到板子上的接线端子。这里有个细节要注意WK2114的SPI时钟极性要和STM32F2的SPI2配置匹配我一开始CPOL和CPHA设反了读出来的寄存器值全是0xFF后面避坑章节会细说。从数据通路上看STM32F2通过SPI读写WK2114的内部寄存器每个通道有独立的寄存器组包括波特率分频寄存器、FIFO控制寄存器、中断使能寄存器和状态寄存器。发送数据时主控把数据写入对应通道的THR寄存器接收数据时主控从RHR寄存器读取。中断引脚会在任意通道有收发事件时拉低主控在外部中断服务函数里轮询WK2114的中断状态寄存器判断是哪个通道触发的再做相应处理。2.2 驱动分层设计与目录结构驱动代码我分成三层来写这样后面换主控或者换接口模式时不用大改。第一层是硬件抽象层封装SPI读写字节和片选控制第二层是WK2114寄存器操作层提供读写寄存器、配置通道、收发数据的函数第三层是应用接口层暴露给上层业务代码调用的串口收发API。目录结构大致如下wk2114_driver/ ├── inc/ │ ├── wk2114_hal.h // 硬件抽象层接口 │ ├── wk2114_reg.h // 寄存器地址与位定义 │ ├── wk2114_core.h // 核心驱动接口 │ └── wk2114_app.h // 应用层接口 ├── src/ │ ├── wk2114_hal.c // SPI读写与片选实现 │ ├── wk2114_core.c // 寄存器配置与通道管理 │ └── wk2114_app.c // 收发缓冲与中断处理 └── example/ └── main.c // 初始化与收发示例这种分层的好处是如果后面要把SPI模式换成并口模式只需要重写wk2114_hal.c里的几个函数上层代码完全不用动。寄存器定义单独放在一个头文件里方便对照数据手册核对地址和位域。2.3 关键寄存器配置与SPI时序WK2114的寄存器地址是7位SPI传输时第一个字节的最高位是读写标志位0表示写1表示读低7位是寄存器地址。第二个字节是数据。片选在传输期间保持低电平传输结束后拉高。先看寄存器读写的基础函数// wk2114_hal.c #include wk2114_hal.h // SPI2读写一个字节STM32F2标准外设库风格 static uint8_t spi2_transfer(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI2, data); while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI2); } // 写WK2114寄存器 void wk2114_write_reg(uint8_t addr, uint8_t value) { WK2114_CS_LOW(); // 片选拉低 spi2_transfer(addr 0x7F); // 最高位0表示写 spi2_transfer(value); // 写入数据 WK2114_CS_HIGH(); // 片选拉高 } // 读WK2114寄存器 uint8_t wk2114_read_reg(uint8_t addr) { uint8_t value; WK2114_CS_LOW(); spi2_transfer(0x80 | (addr 0x7F)); // 最高位1表示读 value spi2_transfer(0x00); // 发送哑数据接收返回值 WK2114_CS_HIGH(); return value; }这里spi2_transfer是阻塞式收发每次等TXE和RXNE标志。WK2114的SPI最高支持到几MHzSTM32F2的SPI2挂在APB1上时钟频率要算好分频系数别超过WK2114的SPI时序上限。片选控制宏WK2114_CS_LOW和WK2114_CS_HIGH在头文件里定义成GPIO操作具体引脚根据你的板子来。通道配置函数里波特率分频值的计算是关键。WK2114的波特率计算公式是分频值 时钟频率 / (16 × 目标波特率)。假设WK2114外部晶振是1.8432MHz要配115200波特率分频值就是1843200 / (16 × 115200) 1。这个值写入对应通道的DLL和DLM寄存器。// wk2114_core.c void wk2114_config_channel(uint8_t ch, uint32_t baudrate) { uint16_t divisor; uint8_t lcr; // 使能波特率分频寄存器访问 lcr wk2114_read_reg(WK2114_REG_LCR(ch)); wk2114_write_reg(WK2114_REG_LCR(ch), lcr | 0x80); // 计算分频值外部晶振1.8432MHz divisor 1843200 / (16 * baudrate); wk2114_write_reg(WK2114_REG_DLL(ch), divisor 0xFF); wk2114_write_reg(WK2114_REG_DLM(ch), (divisor 8) 0xFF); // 设置数据位8无校验1位停止位 wk2114_write_reg(WK2114_REG_LCR(ch), 0x03); // 使能FIFO清空收发FIFO wk2114_write_reg(WK2114_REG_FCR(ch), 0x07); }WK2114_REG_LCR(ch)这类宏在wk2114_reg.h里定义每个通道的寄存器基地址不同用通道号偏移计算。LCR寄存器的bit7是分频锁存访问位置1后才能写DLL和DLM。配置完波特率后要把bit7清掉同时设置数据位和停止位。FCR寄存器写0x07表示使能FIFO、清空发送FIFO、清空接收FIFO。2.4 中断处理与收发缓冲WK2114的中断是汇总输出所有通道的中断事件都通过同一个INT引脚通知主控。主控在外部中断服务函数里读取中断标识寄存器判断是哪个通道的什么事件。// wk2114_app.c void EXTI_IRQHandler(void) { if (EXTI_GetITStatus(WK2114_INT_EXTI_LINE) ! RESET) { uint8_t iir wk2114_read_reg(WK2114_REG_IIR); uint8_t ch (iir 4) 0x03; // 中断来自哪个通道 // 接收数据可用 if ((iir 0x0F) 0x04) { while (wk2114_read_reg(WK2114_REG_LSR(ch)) 0x01) { uint8_t data wk2114_read_reg(WK2114_REG_RHR(ch)); wk2114_rx_buffer_put(ch, data); // 存入环形缓冲 } } // 发送FIFO空 else if ((iir 0x0F) 0x02) { wk2114_tx_isr(ch); // 从发送缓冲取数据写入THR } EXTI_ClearITPendingBit(WK2114_INT_EXTI_LINE); } }中断标识寄存器IIR的低4位表示中断类型0x04是接收数据可用0x02是发送FIFO空。bit4和bit5是通道号。接收处理时循环读LSR寄存器的bit0判断数据就绪然后从RHR读取。这里用环形缓冲来解耦中断和上层业务避免在中断里做耗时操作。发送部分上层调用wk2114_send_data把数据放入发送环形缓冲然后手动触发一次发送中断或者直接写THR启动发送。后续的发送FIFO空中断会持续把缓冲里的数据搬进THR直到发完为止。3. 跑通第一个串口从零搭建WK2114驱动工程3.1 硬件连接与STM32F2外设初始化先确认硬件连接。WK2114的SPI接口需要接四根线SCK、MOSI、MISO、CS再加上INT中断线。我用的STM32F2引脚分配是SPI2_SCK接PB13SPI2_MISO接PB14SPI2_MOSI接PB15CS用PB12INT用PB11配置为下降沿触发的外部中断。STM32F2的SPI2初始化代码如下// main.c 中的SPI2初始化 void spi2_init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // PB13/14/15复用为SPI2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2); // PB12作为CS普通推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_12); // CS默认拉高 // SPI2配置主机模式CPOL0CPHA0 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_32; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }SPI2挂在APB1上STM32F2的APB1时钟一般是30MHz或者45MHz分频32之后SPI时钟大约1MHz左右对WK2114来说完全够用。CPOL和CPHA我设的是模式0WK2114支持模式0和模式3这两个最常用。如果你读寄存器一直返回0xFF或者0x00先检查这两个参数。3.2 WK2114初始化流程与通道自检SPI初始化完之后要对WK2114做一次完整的初始化。流程是软复位、配置全局控制寄存器、逐个配置四个通道、使能中断。// wk2114_core.c void wk2114_init(void) { uint8_t i; // 软复位 wk2114_write_reg(WK2114_REG_GCR, 0x01); delay_ms(10); wk2114_write_reg(WK2114_REG_GCR, 0x00); // 全局控制使能中断输出SPI模式 wk2114_write_reg(WK2114_REG_GCR, 0x08); // 配置四个通道波特率115200 for (i 0; i 4; i) { wk2114_config_channel(i, 115200); // 使能接收数据可用中断 wk2114_write_reg(WK2114_REG_IER(i), 0x01); } // 清空所有通道的接收FIFO for (i 0; i 4; i) { wk2114_write_reg(WK2114_REG_FCR(i), 0x07); } }初始化完成后做一个简单的自检读每个通道的LCR寄存器看是否等于0x03。如果读出来是0x00或者0xFF说明SPI通信有问题。还可以写一个测试值到暂存寄存器再读回来验证读写通路。自检函数uint8_t wk2114_self_test(void) { uint8_t i, val; for (i 0; i 4; i) { wk2114_write_reg(WK2114_REG_SCR(i), 0xA5); val wk2114_read_reg(WK2114_REG_SCR(i)); if (val ! 0xA5) { return i 1; // 返回出错的通道号 } } return 0; // 全部通过 }SCR是暂存寄存器没有实际功能专门用来做读写测试。如果某个通道返回错误先检查该通道的寄存器地址宏有没有写错再检查SPI时序。3.3 收发测试用中断方式跑通第一个通道初始化通过后先跑一个最简单的收发测试。把通道0的TX和RX短接发一个字节看能不能收回来。// main.c 中的测试代码 int main(void) { uint8_t test_data 0x55; uint8_t recv_data 0; system_init(); // 时钟、GPIO、NVIC初始化 spi2_init(); wk2114_init(); if (wk2114_self_test() ! 0) { printf(WK2114 self test failed!\r\n); while (1); } // 通道0发送一个字节 wk2114_send_byte(0, test_data); delay_ms(10); // 检查接收缓冲 if (wk2114_rx_buffer_get(0, recv_data)) { if (recv_data test_data) { printf(Loopback test passed!\r\n); } else { printf(Data mismatch: sent 0x%02X, recv 0x%02X\r\n, test_data, recv_data); } } else { printf(No data received!\r\n); } while (1); }wk2114_send_byte把数据写入THR寄存器如果发送FIFO没满就直接写满了就等。接收部分依赖中断把数据存入环形缓冲主循环里从缓冲取。短接TX和RX是最简单的验证方式能跑通说明SPI配置、寄存器配置、中断处理都没问题。如果收不到数据先确认中断有没有触发。可以在中断服务函数里翻转一个GPIO用示波器看有没有波形。如果没有检查WK2114的INT引脚有没有正确配置成外部中断NVIC里有没有使能对应的中断通道。3.4 四通道并发收发的资源分配四个通道同时收发时要注意中断处理的时间。WK2114的中断是汇总的一次中断可能同时有多个通道的事件。我在中断服务函数里用轮询方式处理所有通道而不是只处理IIR指示的那个通道。void EXTI_IRQHandler(void) { uint8_t ch; if (EXTI_GetITStatus(WK2114_INT_EXTI_LINE) ! RESET) { for (ch 0; ch 4; ch) { uint8_t lsr wk2114_read_reg(WK2114_REG_LSR(ch)); // 接收数据可用 if (lsr 0x01) { while (wk2114_read_reg(WK2114_REG_LSR(ch)) 0x01) { wk2114_rx_buffer_put(ch, wk2114_read_reg(WK2114_REG_RHR(ch))); } } // 发送FIFO空 if (lsr 0x20) { wk2114_tx_isr(ch); } } EXTI_ClearITPendingBit(WK2114_INT_EXTI_LINE); } }每个通道的接收缓冲大小根据业务定我一般给256字节。发送缓冲也是256字节。如果某个通道数据量大可以单独加大。中断里只做数据搬运协议解析放在主循环或者单独的任务里。4. 避坑记录WK2114驱动调试中遇到的五个问题4.1 读寄存器始终返回0xFF现象SPI初始化完成后读WK2114任意寄存器都返回0xFF写进去的值也读不回来。原因SPI的CPOL和CPHA配置与WK2114不匹配。WK2114支持SPI模式0CPOL0CPHA0和模式3CPOL1CPHA1我一开始配成了模式1导致采样边沿错位。解决把SPI_CPOL设为LowSPI_CPHA设为1Edge也就是模式0。如果不确定可以两个模式都试一下读暂存寄存器SCR写0xA5读回来对比。4.2 中断只触发一次就不再触发现象第一个字节接收正常中断也进了但后续数据来了中断不再触发。原因WK2114的中断是电平输出不是脉冲。如果中断服务函数里没有把WK2114的中断状态清除INT引脚会一直保持低电平STM32F2的外部中断是边沿触发的不会重复进入。解决在中断服务函数里处理完数据后要读一次IIR寄存器或者写一次中断清除寄存器让WK2114释放INT引脚。我在代码里是读IIR来清除但要注意读IIR之前先把数据读完否则可能丢数据。4.3 波特率误差导致数据错乱现象短接回环测试通过但接实际外设时数据偶尔出错尤其是波特率115200以上。原因WK2114的外部晶振频率和分频值计算有偏差。我一开始用的晶振是1.8432MHz分频值算出来是整数没问题。但后来换了一块板子晶振是12MHz分频值算出来有小数取整后误差偏大。解决优先选用1.8432MHz或者3.6864MHz这种能被常用波特率整除的晶振。如果只能用其他频率尽量选分频值误差小于2%的。WK2114的波特率误差容忍度比STM32F2内置USART要低这点要注意。4.4 发送FIFO满导致数据丢失现象连续发送大量数据时后面的数据发不出去THR写入被忽略。原因发送FIFO只有16字节如果上层发送速度超过UART实际发送速度FIFO很快满。我一开始在wk2114_send_byte里没有检查LSR的THRE位直接写THRFIFO满时写入无效。解决发送前检查LSR寄存器的bit5THRE或者用发送FIFO空中断来驱动发送。我后来改成环形缓冲加中断发送上层只管往缓冲里放中断里从缓冲取数据写THR彻底解决了丢数据的问题。4.5 多通道中断冲突导致响应延迟现象四个通道同时有数据时中断处理时间变长偶尔丢数据。原因中断服务函数里用轮询方式处理四个通道每个通道都要读LSR、读RHRSPI读写本身有开销四个通道轮一遍时间不短。如果某个通道数据量大会阻塞其他通道。解决把接收缓冲加大到512字节中断里只做数据搬运不做任何解析。另外把SPI时钟适当提高减少单次读写时间。如果还是不够可以考虑用DMA来搬SPI数据但WK2114的SPI时序对DMA支持不太友好需要额外处理片选信号。5. 二次开发改造把WK2114驱动塞进你的项目5.1 移植到其他主控平台的注意事项这套驱动虽然是在STM32F2上写的但分层设计让它移植到其他主控并不难。核心要改的是wk2114_hal.c里的三个函数SPI读写字节、片选控制、延时函数。如果你用的是STM32F4或者STM32H7SPI外设寄存器不同但HAL库或者LL库的API换一下就行。如果换到GD32或者APM32这类兼容芯片基本可以照搬。移植时注意两点一是SPI时钟频率别超过WK2114的上限具体值查数据手册二是中断引脚的外部中断配置不同主控的NVIC和EXTI配置方式不同但逻辑是一样的。5.2 用环形缓冲解耦中断与业务逻辑驱动里最值得复用的设计是环形缓冲。接收和发送各一个中断里只操作缓冲业务层从缓冲读写。这样中断处理时间可控业务层也不用关心底层时序。环形缓冲的实现很简单关键是处理好读写指针的边界。我一般用模运算缓冲大小取2的幂次这样可以用位与代替取模速度快一点。// 环形缓冲结构 typedef struct { uint8_t buffer[256]; volatile uint16_t head; volatile uint16_t tail; } ring_buffer_t; // 写入一个字节返回0成功-1失败 int ring_buffer_put(ring_buffer_t *rb, uint8_t data) { uint16_t next (rb-head 1) 0xFF; if (next rb-tail) { return -1; // 缓冲满 } rb-buffer[rb-head] data; rb-head next; return 0; } // 读取一个字节返回0成功-1失败 int ring_buffer_get(ring_buffer_t *rb, uint8_t *data) { if (rb-head rb-tail) { return -1; // 缓冲空 } *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) 0xFF; return 0; }head和tail用volatile修饰因为中断和主循环都会访问。缓冲大小256用0xFF做掩码。如果数据量大可以改成512或者1024掩码相应改成0x1FF或0x3FF。5.3 从轮询到中断再到DMA的演进路径如果你刚开始调WK2114建议先用轮询方式跑通基本收发确认SPI通信和寄存器配置没问题。然后加上中断把收发放到中断里处理。最后如果数据量确实大再考虑DMA。DMA方式下SPI的收发由DMA搬运主控只需要在DMA传输完成中断里处理片选和状态。但WK2114的SPI传输是变长的每次读写寄存器长度不固定用DMA反而麻烦。我的经验是如果四个通道波特率都不超过115200中断方式完全够用。如果某个通道跑到921600可以考虑单独给那个通道用DMA其他通道还是中断。5.4 一个验证驱动稳定性的土办法驱动写完之后怎么验证稳定性我一般会做一个压力测试四个通道同时以最高波特率收发发送端用PC串口工具循环发随机数据接收端校验数据完整性跑至少两个小时。如果中间出现任何丢包或者错包就停下来查。这个测试能暴露很多问题中断响应不及时、缓冲溢出、SPI时序在高温下不稳定等等。我那次跑压力测试前一个小时没问题一个半小时后开始偶尔丢包最后查出来是SPI时钟分频系数在高温下裕量不够降了一档分频就好了。从那以后我每次调新驱动不管功能多简单都强制跑一遍两小时压力测试再交付。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →