RK3399 SPI转串口实战:wk2114驱动移植与RS485自动收发

发布时间:2026/10/13 3:31:32
RK3399 SPI转串口实战:wk2114驱动移植与RS485自动收发 简介这份资源是面向嵌入式Linux驱动开发者的WK2xxx串口扩展驱动代码基于RK3399平台通过SPI总线将wk2114芯片扩展出4路串口解决主控原生UART接口不足的问题。适合从事瑞芯微平台底层开发、需要为工控或网关设备增加串口通道的工程师参考移植。压缩包内共1个文件为C语言源码体积约12KB属于轻量级驱动实现便于直接阅读与集成到内核源码树中。目前已有230人学习下载说明该方案在RK3399串口扩展场景中具有一定参考价值。读者可从中获取SPI转串口的寄存器配置、中断处理与tty驱动注册等关键实现思路理解wk2xxx芯片在Linux下的驱动框架并据此完成多串口设备的适配与调试减少从零编写驱动的时间成本。1. 从一颗 wk2114 说起为什么 RK3399 上还要折腾 SPI 转串口手上有一块 RK3399 的板子系统跑起来之后发现原生 UART 不够用——调试口占了一个GPS 模块要一个还有个工控传感器走 RS485 也得占一个。翻遍原理图能引出来的串口就俩剩下的全被复用成 I2S 和 SPI 了。这时候 wk2114 这类 SPI 转串口芯片就成了救命稻草一颗芯片扩出两路独立 UARTSPI 接口挂到 RK3399 的 SPI 控制器上Linux 侧注册成标准 ttyS 设备应用层完全无感。但事情没这么简单。wk2114 不是那种“上电即用”的透明桥它需要驱动去配置内部寄存器、管理收发 FIFO、处理中断上报。RK3399 跑的是 Linux 4.4 或 5.10 内核wk2114 的驱动要么找原厂要要么自己照着数据手册撸一个。这篇笔记就把从硬件连线到驱动加载、从设备树配置到回环测试的完整路径拆开讲适合手里有 RK3399 板子、需要扩串口但不想换主控的嵌入式工程师。踩过的坑、调过的参数、翻过的车都会写清楚。2. wk2114 驱动模型与 RK3399 SPI 控制器的对接逻辑2.1 wk2114 内部结构它到底把什么暴露给了 Linuxwk2114 的本质是一颗带 SPI 从机接口的双通道 UART 桥接芯片。内部有配置寄存器组、收发 FIFO、波特率发生器和中断控制逻辑。SPI 侧只支持模式 0CPOL0CPHA0最大时钟频率标称 10MHz但实际跑下来 5MHz 以上就容易丢包后面避坑章节会细说。从驱动视角看wk2114 对外呈现为一组寄存器寄存器偏移名称功能0x00GENA全局使能bit0 开通道 Abit1 开通道 B0x01GRST软复位写 1 后需延时再清0x02GMUXSPI 中断引脚复用配置0x10SPAGE通道 A 页选择0x11SCR通道 A 控制寄存器0x12SIER通道 A 中断使能0x13SFCR通道 A FIFO 控制0x14SLCR通道 A 线路控制0x15SDLL通道 A 波特率低字节0x16SDLM通道 A 波特率高字节0x17SSCR通道 A 波特率预分频0x20~0x2F—通道 B 对应寄存器偏移加 0x10驱动要做的事很明确通过 SPI 读写这些寄存器把 wk2114 的两个通道注册成两个ttyS设备实现uart_ops里的startup、shutdown、set_termios、send等回调。中断方面wk2114 有一根 IRQ 引脚收到数据或发送 FIFO 空时会拉低RK3399 侧配置成下降沿触发驱动在中断处理里读中断状态寄存器判断是哪个通道的事件。2.2 RK3399 SPI 控制器在 Linux 下的设备树写法RK3399 有三个 SPI 控制器SPI0 通常被保留给板载 FlashSPI1 和 SPI2 可以引出来用。以 SPI1 为例设备树里要配置引脚复用、时钟、DMA然后在控制器节点下挂 wk2114 子节点。spi1 { status okay; max-freq 5000000; /* 先降到 5MHz稳定后再试更高 */ pinctrl-names default; pinctrl-0 spi1_clk spi1_cs0 spi1_rx spi1_tx; num-cs 1; wk2114: wk21140 { compatible wk,wk2114; reg 0; spi-max-frequency 5000000; interrupt-parent gpio4; interrupts RK_PD2 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio4 RK_PD3 GPIO_ACTIVE_LOW; status okay; }; };max-freq和spi-max-frequency都设成 5MHz 是保守做法。interrupts里的 GPIO 编号要根据实际原理图改别照抄。reset-gpios是可选的如果硬件上复位引脚直接拉高驱动里可以不做复位控制但建议保留方便调试时软复位。2.3 驱动 probe 流程从 SPI 设备到 tty 注册驱动入口是spi_driver的probe函数。核心步骤static int wk2114_probe(struct spi_device *spi) { struct wk2114_port *port; int ret; /* 1. 软复位芯片 */ wk2114_write_reg(spi, WK2114_GRST, 0x01); msleep(10); wk2114_write_reg(spi, WK2114_GRST, 0x00); msleep(10); /* 2. 全局使能两个通道 */ wk2114_write_reg(spi, WK2114_GENA, 0x03); /* 3. 为每个通道分配 uart_port 并注册 */ for (i 0; i 2; i) { port devm_kzalloc(spi-dev, sizeof(*port), GFP_KERNEL); port-uart.ops wk2114_uart_ops; port-uart.line i; port-uart.dev spi-dev; port-uart.irq spi-irq; port-uart.iotype UPIO_MEM; port-uart.flags UPF_BOOT_AUTOCONF; port-uart.fifosize 128; ret uart_add_one_port(wk2114_uart_driver, port-uart); if (ret) { dev_err(spi-dev, add port %d failed\n, i); return ret; } } /* 4. 申请中断 */ ret devm_request_irq(spi-dev, spi-irq, wk2114_irq, IRQF_TRIGGER_FALLING, wk2114, spi); if (ret) { dev_err(spi-dev, request irq failed\n); return ret; } spi_set_drvdata(spi, port); return 0; }uart_add_one_port是核心它把uart_port挂到uart_driver上内核就会在/dev/下生成ttyS*节点。fifosize设成 128 是 wk2114 的实际 FIFO 深度写大了没用写小了会频繁触发中断。中断处理里要读SIER和SIIR判断事件类型收到数据就调tty_flip_buffer_push把数据推给上层。3. 从零编译 wk2114 驱动并挂到 RK3399 内核3.1 准备内核源码与交叉编译链RK3399 的 Linux 内核源码一般由板卡厂商提供解压后先确认版本tar -xzf linux-4.4-rk3399.tar.gz cd linux-4.4-rk3399 make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig交叉编译链用aarch64-linux-gnu-前缀如果板卡厂商有专用工具链优先用厂商的。defconfig之后要确认CONFIG_SPI、CONFIG_SERIAL_CORE、CONFIG_TTY都是y或m。3.2 把 wk2114 驱动编进内核的两种方式方式一作为模块编译。在drivers/spi/下新建wk2114.c修改Makefile和Kconfigobj-$(CONFIG_SPI_WK2114) wk2114.oconfig SPI_WK2114 tristate WK2114 SPI to UART bridge depends on SPI help Say Y here to support WK2114 dual UART bridge.然后make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules生成的wk2114.ko推到板子上insmod。方式二直接编进内核。在defconfig里加CONFIG_SPI_WK2114y重新编译完整内核镜像。量产固件建议用这种方式省得每次启动都要手动加载模块。3.3 设备树覆盖与启动验证如果不想重新编译整个设备树可以用dtc把 wk2114 节点编译成dtbo在uEnv.txt里用overlays加载。但 RK3399 的 U-Boot 对 overlay 支持参差不齐稳妥做法还是直接改arch/arm64/boot/dts/rockchip/rk3399-xxx.dts重新编译dtb。启动后检查dmesg | grep wk2114 # 期望输出wk2114 spi1.0: wk2114 probe success, 2 ports added ls /dev/ttyS* # 应该多出 ttyS3 和 ttyS4具体编号取决于已有串口如果dmesg里出现probe failed或request irq failed先查 GPIO 编号和中断触发方式再查 SPI 通信是否正常——可以用逻辑分析仪抓 CLK 和 MOSI看有没有波形。4. 参数调优与回环测试让两路串口真正跑起来4.1 波特率配置wk2114 的分频计算与常见偏差wk2114 的波特率由SDLL、SDLM、SSCR三个寄存器决定。公式是baud 14.7456MHz / (16 * (SDLM:SDLL 1) * (SSCR 1))14.7456MHz 是 wk2114 的典型晶振频率但有些板子用的是 12MHz 或 24MHz必须根据实际晶振改。以 115200 为例如果晶振是 14.7456MHzSSCR0则SDLM:SDLL 7实际波特率 14745600 / (16 * 8) 115200零误差。如果晶振是 12MHz算出来会有 2% 以上偏差高波特率下会丢包。驱动里set_termios回调要根据termios里的c_ospeed反算寄存器值static void wk2114_set_baud(struct wk2114_port *port, unsigned int baud) { unsigned int divisor; unsigned char scr 0; /* 先尝试 SSCR0 */ divisor 14745600 / (16 * baud); if (divisor 0 divisor 65535) { scr 0; } else { /* 分频不够就加大 SSCR */ for (scr 1; scr 256; scr) { divisor 14745600 / (16 * baud * scr); if (divisor 0 divisor 65535) break; } } wk2114_write_reg(port-spi, port-reg_base WK2114_SDLL, divisor 0xFF); wk2114_write_reg(port-spi, port-reg_base WK2114_SDLM, (divisor 8) 0xFF); wk2114_write_reg(port-spi, port-reg_base WK2114_SSCR, scr); }divisor算出来要减 1 再写寄存器上面代码为了简洁没减实际写的时候注意。SSCR是预分频值越大波特率越低但精度也越差。4.2 用 Python 脚本做双通道回环压力测试板子上跑一个 Python 脚本把 ttyS3 和 ttyS4 短接TX 接 RX然后互发数据import serial import threading import time port_a serial.Serial(/dev/ttyS3, 115200, timeout1) port_b serial.Serial(/dev/ttyS4, 115200, timeout1) errors 0 total 0 def sender(): global total for i in range(10000): data fPKT{i:05d}.encode() port_a.write(data) total 1 time.sleep(0.001) def receiver(): global errors while total 10000: data port_b.read(8) if len(data) ! 8: errors 1 continue # 校验序号是否连续 try: idx int(data[3:].decode()) except: errors 1 t1 threading.Thread(targetsender) t2 threading.Thread(targetreceiver) t1.start() t2.start() t1.join() t2.join() print(ftotal{total}, errors{errors}, loss_rate{errors/total*100:.2f}%)跑下来如果loss_rate超过 0.1%就要查 SPI 时钟是不是太高、FIFO 中断有没有丢、或者波特率偏差是不是太大。我一般会把 SPI 时钟从 5MHz 降到 2MHz 再跑一遍对比丢包率。4.3 中断合并与 FIFO 阈值减少 CPU 占用的两个旋钮wk2114 的SFCR寄存器可以设 FIFO 触发阈值。默认是收到 1 个字节就中断CPU 会被频繁打断。改成收到 8 个字节或超时再中断能显著降低 CPU 占用/* 设置通道 A 的 FIFO 触发阈值为 8 字节 */ wk2114_write_reg(spi, WK2114_SFCR, 0x08);但阈值设大了会增加延迟工控场景下如果对实时性要求高还是保持 1 字节触发。折中方案是开超时中断SFCR的 bit7 控制超时使能超时时间由SSCR的高 4 位决定。5. 避坑与排查wk2114 在 RK3399 上最容易翻车的五个点5.1 现象SPI 通信时好时坏dmesg 里报 timeout原因RK3399 的 SPI 控制器默认用 DMA 模式wk2114 的 SPI 从机对时序要求比较死DMA 突发传输时 CS 拉高拉低的时机可能不满足芯片手册里的最小间隔。解决在设备树里把rockchip,polling-mode打开强制用轮询模式发数据。代价是 CPU 占用高一点但稳定性大幅提升。或者把 SPI 时钟降到 1MHz 以下给时序留余量。5.2 现象两路串口同时收发时其中一路丢数据原因wk2114 的 SPI 接口是单线程的两个通道的寄存器读写共用一条 SPI 总线。如果驱动里没有加互斥锁两个通道的中断处理同时发起 SPI 传输就会冲突。解决在驱动里加一个struct mutex所有 SPI 读写操作前先mutex_lock操作完mutex_unlock。注意不能在中断上下文里用可能睡眠的锁如果中断处理里要读寄存器用spin_lock_irqsave或者把寄存器读操作放到工作队列里。5.3 现象波特率设成 921600 后收到的全是乱码原因wk2114 在 14.7456MHz 晶振下921600 波特率的分频值算出来是 1实际波特率 14745600 / (16 * 1) 921600理论零误差。但 SPI 时钟如果只有 5MHz读一个寄存器要 8 个时钟周期加上 CS 建立保持时间实际吞吐跟不上 921600 的字节率。解决把 SPI 时钟提到 10MHz 以上或者降低波特率到 460800。另外检查SFCR的 FIFO 阈值高波特率下建议设成 1 字节触发避免 FIFO 溢出。5.4 现象insmod 之后 /dev/ttyS* 没多出来原因uart_add_one_port返回失败但驱动里没打印错误码。常见原因是line编号和已有串口冲突或者uart_driver的nr设小了。解决在probe里把uart_add_one_port的返回值打出来如果是-EBUSY说明 line 被占了改port-uart.line的值。uart_driver.nr要大于等于line 1。5.5 现象系统休眠唤醒后串口收不到数据原因wk2114 在休眠时如果 SPI 控制器断电芯片内部寄存器会丢失。唤醒后驱动没有重新初始化。解决实现pm_ops的resume回调在里面重新执行一遍probe里的初始化流程——软复位、全局使能、波特率配置。如果硬件上 wk2114 的供电和 SPI 控制器是同一路电休眠时都断了那唤醒后必须完整重配。6. 进阶把 wk2114 驱动改造成支持 RS485 自动收发的实战技巧wk2114 本身只出 TTL 电平要接 RS485 得外挂一颗收发器比如 MAX485。MAX485 的 DE/RE 引脚需要 GPIO 控制方向发送时拉高 DE接收时拉低。如果每次write都手动控制 GPIO应用层会很啰嗦。更好的做法是在驱动里根据termios的TIOCSRS485标志自动翻转。先看内核里serial_rs485结构体的关键字段字段含义flags使能 RS485 的标志位delay_rts_before_send发送前 RTS 延时毫秒delay_rts_after_send发送后 RTS 延时毫秒padding保留驱动里在set_termios里判断port-uart.rs485.flags SER_RS485_ENABLED如果使能了就在start_tx里拉高 DE在发送完成中断里拉低 DE。关键代码static void wk2114_start_tx(struct uart_port *uport) { struct wk2114_port *port container_of(uport, struct wk2114_port, uart); if (uport-rs485.flags SER_RS485_ENABLED) { gpiod_set_value(port-de_gpio, 1); /* 根据 delay_rts_before_send 做延时 */ if (uport-rs485.delay_rts_before_send 0) mdelay(uport-rs485.delay_rts_before_send); } /* 把数据写入 wk2114 的发送 FIFO */ wk2114_write_fifo(port, ...); } static void wk2114_tx_complete(struct wk2114_port *port) { if (port-uart.rs485.flags SER_RS485_ENABLED) { if (port-uart.rs485.delay_rts_after_send 0) mdelay(port-uart.rs485.delay_rts_after_send); gpiod_set_value(port-de_gpio, 0); } }delay_rts_before_send和delay_rts_after_send这两个参数很关键。RS485 总线在方向切换时如果收发器还没稳定就发数据第一字节会丢。我一般会设before_send1、after_send1跑 115200 没问题。如果波特率更高比如 460800after_send要加到 2 甚至 3确保最后一个字节完全移出移位寄存器再拉低 DE。设备树里要加 DE GPIOwk2114: wk21140 { ... rs485-de-gpios gpio4 RK_PD4 GPIO_ACTIVE_HIGH; };驱动probe里用devm_gpiod_get拿到这个 GPIO初始化为低电平。验证 RS485 是否正常最简单的办法是拿两个板子A 板发 B 板收B 板收到后原样发回。用rs485test这类工具跑一晚上看有没有丢包或方向切换错误。如果发现偶尔丢第一字节优先加大delay_rts_before_send如果丢最后一字节加大delay_rts_after_send。最后说个血泪教训RS485 的 DE 引脚千万别和 wk2114 的复位引脚共用 GPIO我这么干过一次结果每次发送数据都会把芯片复位查了一整天才发现是引脚冲突。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询