TECHNICAL GUIDE
串口扩展芯片驱动移植指南_Linux与Android下多路UART设备节点与调试方法 | 上海耀罡电子
扩展芯片出来四个串口,在应用层就是四个 ttyS/ttyUSB。本文把设备节点规划、驱动结构、权限与调试手段讲透,让你少走两周的弯路。
先明确目标:让扩展口变成标准 tty
最终效果
驱动的核心目标不是「把数据搬过去」,而是让扩展出来的每一路串口,在操作系统眼里就是一个普通的串口设备。
具体来说,系统里应该出现这样的设备节点:
/dev/ttyS0 ← 主控自己的 UART
/dev/ttyS1 ← 扩展通道 1
/dev/ttyS2 ← 扩展通道 2
/dev/ttyS3 ← 扩展通道 3
/dev/ttyS4 ← 扩展通道 4
注意后缀是 ttyS 而不是 ttyUSB。这是刻意的:ttyUSB / tftdi 那一类是 USB 转串口芯片的驱动,走 USB 枚举、热插拔、CDC 协议这一整套机制;而扩展出来的串口在硬件上就是主控 UART 的延伸,根本没有 USB 这层,套用 USB 驱动模型只会带来不必要的复杂度。用标准 TTY 模型的直接好处是:应用程序完全不需要改代码,原本打开 ttyS0 的程序,把参数改成 ttyS1 就能用。
设备节点规划:为什么顺序会变
内核版本带来的差异
Linux 3.1 以下与 3.1 以上使用的是两套完全不同的架构。这个分界点几乎所有做串口扩展的工程师都踩过。
| 系统 | 串口架构 | 开发方式 |
|---|---|---|
| Linux 3.1 以上 | Device Tree 架构(DTS) | 在设备树中描述硬件,驱动代码尽量通用 |
| Linux 3.1 以下 | 平台设备 + 私有结构体 | 每个平台一份私有驱动,移植成本高 |
| Android 5.0 – 8.0 | 基于 Linux 3.x 内核 | 沿用内核串口框架 |
为什么用 DTS 更省事
在 Device Tree 架构下,硬件描述和驱动代码是分离的。设备树文件负责声明「有几路串口、每路的寄存器地址和中断号是多少、时钟频率多少」,驱动代码只负责按标准框架注册这些端口。
这样带来一个非常实际的收益:同一份驱动代码可以跨多个平台复用。你在 A 平台(RK3568)上调试好的驱动,换到 B 平台(STM32MP1)通常只需要改设备树,不用重写驱动。对于同时做多款产品的方案公司,这个差别是很大的。
设备编号为什么会跳号
很多开发者会遇到一个问题:明明只接了一个扩展芯片,为什么系统里冒出 ttyS3 到 ttyS8 一串节点,而不是 ttyS1 到 ttyS4?
原因是内核在枚举串口时会给所有可能的端口编号分配节点,包括那些硬件上不存在、但驱动注册时声明了编号的口。主控自身占用了若干编号,扩展芯片再往后排。
这带来一个实际影响:不能假设扩展口一定是连续的 ttyS1–ttyS4。正确做法是在设备树里显式指定编号,或者在应用层通过设备特征(VID/PID、序列号)来识别,而不是写死节点名。
驱动结构与子口配置方式
一层主控口 + 多个子口
扩展芯片的典型结构是:一个主串口连到主控,芯片内部再分出多个子串口。主控实际上只占用一个 UART 外设。
这带来一个直接的好处:主控的引脚资源压力被大幅缓解。一个原本只有 2~3 路串口的 MCU,接上扩展芯片后能同时管理十几台串口设备。对 POS 机、网关这类设备密度高的产品,价值非常直接。
子口参数独立配置
WK2168 与 WK2124 的每个子口都支持独立配置,包括波特率、数据位、停止位、校验方式,以及独立的 256 级收发 FIFO。
「独立」两个字是关键。很多 MCU 的硬件 UART 有波特率分频器但不够用,导致两路设备波特率不同时必须软件模拟;而扩展芯片每个子口都有自己的分频与 FIFO,四路设备波特率互不相同也可以同时稳定工作。这对接不同厂商设备的现场集成非常实用。
寄存器的组织方式
典型的扩展芯片寄存器空间分三块:公共寄存器(中断使能、电源管理、全局配置)、通道寄存器(每通道的波特率分频、FIFO 控制、工作模式)、以及 FIFO 数据区。
驱动的寄存器定义通常用结构体把位域拆开,这样上层调用时是可读的:``c
typedef struct {
volatile uint32_t CTRL; /* 全局控制 */
volatile uint32_t INT_EN; /* 中断使能 */
volatile uint32_t INT_STATUS;/* 中断状态 */
/* 每通道寄存器基址,循环访问 */
volatile uint32_t CH_REG[CH_NUM];
} wk_dev_t;
``
这种位域拆分的写法比裸位运算好维护得多——调试时可以直接打印字段值定位问题。
流控配置:硬件流控 vs 软件流控
两种流控的差别
| 方式 | 机制 | 优点 | 代价 |
|---|---|---|---|
| 软件流控 | 用 XON/XOFF 字符在数据流里插入控制码 | 不占额外引脚 | 控制码会与二进制数据冲突,占带宽 |
| 硬件流控 | RTS/CTS 独立信号线 | 不干扰数据,可靠性高 | 每个子口要占用一对 GPIO |
什么时候必须用硬件流控
在以下场景里,软件流控会出问题:
数据是二进制的,可能包含 0x11(XON)或 0x13(XOFF)字节,接收端会把它误当成流控指令而停等;波特率较高且数据量大,控制码插入会占用有效带宽;干扰环境复杂,靠字符级握手不如硬件信号可靠。
WK2168-IQPG 支持硬件流控,在需要接高速或二进制协议的设备时(如某些扫码枪、带加密握手协议的读卡器),这是选它而不是 WK2124 的实际理由。
半双工 485 的方向控制
前面提到过 RS485 半双工需要 DE/RE 控制。有硬件流控能力的芯片可以把方向控制纳入芯片内部时序,主控侧不需要额外管 GPIO。
即使主控要自己控制方向,也要遵循一个硬性规则:必须等移位寄存器发送完成(TC/TXE 标志置位)才能切换方向,不能等发送缓冲区空。这是「最后一个字节丢失」这类偶发故障的根本原因。
权限:为什么普通程序打不开设备
两个层次的权限
Linux 下访问串口设备要通过两道关:
第一层是设备节点的属主与权限位(-rw-rw---- 1 root dialout),决定「哪些用户组可以读写这个节点」;第二层是 SELinux / AppArmor 等安全模块的策略,在 Android 上尤其严格。
Android 5.0 到 8.0 的 SELinux 策略相对宽松,但从 Android 9 开始,未在策略中声明的设备节点,即使是 root 身份也可能被拒绝访问。这一点在把方案从 8.0 往上迁移时经常成为阻碍。
实用做法
| 场景 | 做法 |
|---|---|
| Linux 应用 | 把运行账号加入 dialout 组,或 udev 规则里设置 MODE=0666 |
| Android 应用 | 在 sepolicy 中为设备节点声明类型与权限 |
| 临时调试 | chmod 666 设备节点,或 setenforce 0(仅调试用) |
udev 规则的写法
如果需要按 USB 路径或序列号做稳定匹配,可以写一条 udev 规则把设备节点固定成软链接,这样即使枚举顺序变了,应用层也不用改:
# /etc/udev/rules.d/99-wk-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="04d8", \
SYMLINK+="wk_serial0", MODE="0666"
这样 /dev/wk_serial0 就稳定指向扩展出来的第一路,彻底摆脱了「为什么这次编号变了」的困扰。强烈建议在量产方案里这么处理——设备枚举顺序在不同批次、不同启动时序下都可能变化,靠写死 ttyS 编号的代码早晚会在客户现场出问题。
调试方法与故障排查表
先分清是哪一层的问题
串口不通时,正确的排查顺序是从物理层往上一层走:
物理层 → 线路/电平/接线/终端电阻
↓
驱动层 → 设备节点是否出现、能否 open
↓
协议层 → 波特率/数据位/校验/流控配置
↓
应用层 → 应用逻辑与解析
最常见的错误是一上来就怀疑应用代码。实际上如果设备节点根本没出现,说明驱动或设备树就没配好,跟应用层毫无关系。
常用调试手段
| 工具 | 用途 |
|---|---|
ls /dev/ttyS* |
确认节点是否创建,编号是否与预期一致 |
dmesg | grep -i tty |
看驱动初始化日志,有无探测到硬件 |
stty -F /dev/ttyS1 -a |
读取当前串口参数,核对波特率与流控 |
cat /dev/ttyS1 |
直接读原始数据,判断是硬件没发还是解析错 |
| 串口调试助手 | PC 端模拟主机,验证从机行为 |
| 示波器 / 逻辑分析仪 | 看实际波形,判断有无数据、极性对不对 |
故障排查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 设备节点根本没出现 | 设备树配置错误 / 驱动未编译 | dmesg 看探测日志 |
| 节点存在但 open 失败 | 权限不足 / 节点被占用 | ls -l 看权限;lsof 查占用 |
| 能 open 但收不到数据 | 接线错 / RX TX 接反 / 无共地 | 交换 TX RX 试一次 |
| 数据乱码 | 波特率或校验位不匹配 | stty -a 核对两端参数 |
| 数据偶尔丢最后一字节 | 485 方向切换过早 | 等 TC 标志再切方向 |
| 高负载时丢包 | FIFO 溢出 / 中断处理不及时 | 减小单次读写长度,提高中断优先级 |
常见问题
串口扩展芯片的驱动是通用的吗?
在 Linux 3.1 以上的 Device Tree 架构下,驱动代码可以做到基本通用,不同平台主要改设备树。Linux 3.1 以下是平台设备架构,基本每个平台都要一份私有驱动。WK 系列支持 Linux 2.6 至 4.4 以及 Android 5.0 至 8.0 多个内核版本,具体适配情况可联系我们确认。
扩展出来的串口为什么不是 ttyUSB?
因为扩展口在硬件上是主控 UART 的延伸,不存在 USB 枚举与 CDC 协议这一层。用标准 TTY 模型(ttyS)可以省掉热插拔、VENDOR/PID 匹配、驱动重绑定等一整套 USB 设备管理机制,而且应用程序无需任何改动——原本打开 ttyS0 的程序换个节点名即可。
四路扩展的波特率可以各不相同吗?
可以。WK2168 与 WK2124 的每个子口都有独立的波特率分频器和独立的 256 级收发 FIFO,四路设备波特率不同也能同时稳定工作。这是扩展芯片相比 MCU 硬件 UART 的一个实际优势——很多 MCU 的 UART 分频器不够用,两路不同波特率就必须软件模拟。
驱动移植最常卡在哪一步?
绝大多数情况卡在两个地方:一是设备树里设备节点号写死,换平台或换启动顺序后编号变了,应用代码全错;二是 Android 高版本的 SELinux 策略没有声明对应设备节点类型,权限配置对但依然访问被拒。建议从一开始就按 udev 规则或 sepolicy 的方式做稳定标识,避免后期返工。
硬件流控和软件流控该选哪个?
数据是二进制格式、波特率较高、或现场干扰较大时,选硬件流控。WK2168-IQPG 支持硬件流控,同时主接口内置 RS485 自动收发控制,可以一并解决半双工方向切换的时序问题。如果四路都是低速的短距单端设备,软件流控足够,还能省下 GPIO。