泰山派 1F 串口半双工方向自动切换
泰山派 1F(RK3566)串口半双工方向自动切换 · 方案 F 适配实录
一句话:让 RK3566 的 UART3 在内核驱动里自动切换单线半双工总线的收发方向,
用户态就是普通串口读写——不 pyserial、不 ioctl、不碰 GPIO。
本文记录方案设计、移植步骤、验收方法,以及我们踩过的全部坑(含一次"三块板对照"才定位的硬件故障)。
0. 硬件与背景
| 项 | 值 |
|---|---|
| 主控 | 泰山派 1F 核心板(RK3566,Debian 12,内核 6.1.141) |
| 总线 | Dynamixel 舵机单线半双工 TTL(15 个 XL330),1 Mbps |
| 串口 | UART3(fe670000)→ /dev/ttyS3,引脚复用 uart3m1:TX=GPIO3_B7(pin111)、RX=GPIO3_C0(pin112) |
| 方向控制 | 收发器(如 NC7WZ241)的 DE 脚 ← GPIO3_A6(gpio102),拉高=本机驱动总线(发),拉低=松手(收) |
| 上游 | 立创泰山派 1F SDK(rk3566_linux_sdk_20260414),内核树 kernel-6.1 |
命名陷阱:dts 属性叫 485_ctrl_gpio、日志打 RS485——只是沿用既有命名。
实际是单线 TTL 半双工,不是差分 RS485,不要去找 RS485 收发器。
半双工的本质约束:发完必须等最后一个 bit 真的移位出去才能松手。
早了截断帧,晚了把从机应答自己吃掉。1 Mbps 下一帧 10 字节约 100 µs,容错就这么宽。
1. 为什么不用内核自带的 em485
Linux 8250 自带 em485(SER_RS485_ENABLED + hrtimer)本来是干这个的。在这颗 SoC 上实测不可用:
启用后(TIOCSRS485 ioctl 或 dts linux,rs485-enabled-at-boot-time 均可触发)TX 在第一帧之后永久卡死。机理链条:
旁证:未改动的原厂内核、ttyS0/3/4/5、有没有 DE 脚都一样复现——是 em485 在此 SoC 上本身不可用,与方向控制无关。
⚠️ 移植到新 SoC / 新内核大版本时这条必须重验(连发 3 帧以上)。
em485 若好用就优先用上游机制。我们实测复现了卡死,才走方案 F。
2. 板厂自带方案错在哪(看到就整体替换)
板厂在 8250_port.c 里的"485 支持"长这样:
四宗罪:
脚号硬编码 115/116/14——换板/换引脚即失效,还会去拉一根不相干的 GPIO;
脚号放在
port.rs485.rts_gpio——uart_sanitize_serial_rs485()在SER_RS485_ENABLED
未置位时会把整个结构 memset 清零,而且TIOCSRS485允许任何用户态进程整块覆写(可指挥内核拉任意 GPIO);mdelay(3)×200(上限 600 ms)——在持锁的 ISR 里,粒度比一整帧还粗,且每帧 printk;只覆盖一个放手点——其余路径会让收发器一直占着总线。
3. 方案 F 设计(逐文件,按依赖顺序读)
3.1 include/linux/serial_8250.h(+17)
struct uart_8250_port 顶层加两个字段:
为什么不放 port.rs485:见上节第 2 条(memset + 用户态可写)。极性同样从 dts gpio flags cell 读,不从 port.rs485.flags 读。
3.2 drivers/tty/serial/8250/8250_dw.c(+69)
dw8250_probe() 里:
of_get_named_gpio_flags()取脚号和极性(原厂of_get_named_gpio()拿不到极性);-EPROBE_DEFER原样返回,不要吞——gpio_is_valid()把它当普通错误,吞掉的后果是de_gpio永远 -1、总线永不换向,而日志一个字都没有;devm_gpio_request_one()以接态初始化(active-high →GPIOF_OUT_INIT_LOW,即空闲=接收态);打印
RS485 direction on gpio %d (active %s)—— 这是验收要抓的一行;同文件靠后一处:有 DE 脚的口不挂 TX DMA——
因为 TX DMA 绕开 serial8250_tx_chars(),__dma_tx_complete() 里没有 DE 处理——
一次大写会在收发器还在接收态时把数据发出去。1 Mbps 短帧场景 DMA 也没有收益。
3.3 drivers/tty/serial/8250/8250_core.c(+6,最容易漏)
serial8250_register_8250_port() 只搬运显式列出的字段——probe 里探到的新字段不显式拷一遍
就到不了真正跑起来的那个 port。症状:probe 日志正常打印,发数据时完全不换向。
3.4 drivers/tty/serial/8250/8250_port.c(+136,核心)
两个 static 函数 + 在 serial8250_tx_chars() 里布点 + 一处初始化:
release 的等待预算——不要 mdelay 死等(在持锁 ISR 里):
注意 frame_time 是每字符 ns,先换算成 µs 再乘(先每字符、再乘批大小),1 Mbps 下
48 字节约 480 µs < 660 µs 预算,帧不会被截断。
三个放手点都要布,漏任何一个都会偶发性占死总线:
| 位置 | 为什么也要放手 |
|---|---|
uart_tx_stopped(port) 分支 | stop_tx 只屏蔽中断,FIFO 仍在排空 |
进函数即 uart_circ_empty(xmit) 分支 | 上批恰好填满 FIFO 时 DE 还是高的 |
| 一批发完且队列空 | 正常路径 |
队列非空时保持 DE 拉高(下一批 TX 中断接着发)。port->x_char(XON/XOFF)不处理——半双工总线上不存在软流控。
最后在 serial8250_init_port() 里必须显式:
serial8250_ports[] 是静态清零数组,而 0 是合法的 gpio 号——不写这两行,每个没有 DE
脚的串口发数据时都会去拉 gpio0。
3.5 8250_dwlib.c(+13,可选)
纯注释 + 一行 include,记录"为什么放着 em485 不用"。建议带上。
3.6 dts(本板专属)
绝对不要加 linux,rs485-enabled-at-boot-time ——那会把 em485 打开,撞回第 1 节的坑。
GPIO 编号换算(RK 系):bank*32 + group*8 + index,A=0/B=1/C=2/D=3。gpio3 RK_PA6 = 3×32+0×8+6 = 102;GPIO3_B7 = 111(TX);GPIO3_C0 = 112(RX)。
用户态普通串口读写即可(termios + read/write),方向切换全程内核完成。
4. 移植检查清单
§3.1–3.5 五个文件(dwlib 可选)
跳过补丁里顺手改的散热风扇 dts 节点、defconfig 的
PWM_FAN、8250_dwlib.h的权限位dts 按 §3.6 填本板 UART/引脚/极性,用公式验算 gpio 编号
编译烧写后跑第 5 节验收
遵守第 6 节禁令
5. 验收(可证伪)
⚠️ 不要用TIOCGRS485的SER_RS485_ENABLED当判据——本方案刻意不置该位,
读出来enabled=False是正常态。误判之后想"补使能"去写TIOCSRS485,
在 1F 板上实测会持久化设置并弄死总线约 32 分钟。
6. 禁止事项
❌ 写
TIOCSRS485❌ dts 加
linux,rs485-enabled-at-boot-time❌ 用户态直接操作 DE 引脚(已被驱动认领;和驱动抢时序=随机丢帧)
❌ 脚号放
port.rs485.rts_gpio❌
serial8250_tx_chars()里 printk(ISR 内每帧一行,1 Mbps 下是持续串行输出)❌
mdelay等 TEMT
7. 实战排错实录(本次移植的真实踩坑,比方案本身更耗时)
7.1 编译器陷阱:你的 RK_KERNEL_TOOLCHAIN 会被静默覆盖
SDK 的 kernel-helper 选择交叉编译器的逻辑是 "系统工具链优先":
容器/宿主机里只要装了 Ubuntu 的 crossbuild-essential-arm64,无论你怎么 exportRK_KERNEL_TOOLCHAIN 都会被覆盖**,内核实际被 Ubuntu gcc 11.4 编译。
而 SDK 的 get_toolchain() 会精确匹配 **prebuilts 自带的官方工具链
(prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-*,与板厂出片用的完全同款)。
解法:卸掉容器里的 Ubuntu 交叉工具链,让 which 失败、走 get_toolchain 正道:
验证手段:编完看 kernel-6.1/.config 的 CONFIG_CC_VERSION_TEXT——必须是aarch64-none-linux-gnu-gcc ... 10.3.1。日志里 Toolchain pattern for kernel: aarch64-none-linux-[^-]*-gcc
表示走了 get_toolchain(B 板当年成功构建的日志里正是这一行)。
我们为此付出的代价:gcc 11.4 编出的内核"软件行为完全正常"(ftrace 实测 DE 时序
132 µs @1 Mbps、tx 计数正常),但总线上就是收不到从机应答——烧了官方工具链镜像才通。
同一源码两个编译器,行为差异真实存在。
7.2 file 命令被连坐删除
卸载交叉工具链时 apt autoremove 会把基础工具 file 一起删掉,Buildroot recovery 阶段
报 You must install '/usr/bin/file'。装回来即可:apt-get install -y file。
7.3 容器环境不进镜像的三样东西
live-build 20230131:SDK 的 check-debian.sh 要求源码安装的版本(支持 bookworm),
否则 rootfs 阶段直接失败;binfmt_misc 挂载/注册:内核态状态,容器重建后需
mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc && update-binfmts --enable qemu-aarch64;容器内 apt 装的东西只在该容器可写层——
docker commit才能保住,重建容器就没了。
7.4 排查方法论:先做对照矩阵,再深挖内核
我们为"ping 舵机无应答"付出了巨大的内核排查成本(ftrace、寄存器轮询、pinctrl、tx/rx 计数
全部正常),最终靠换板对照矩阵才定位:
| 板子 | 镜像 | 结果 |
|---|---|---|
| 板 1 | B 原装 | ✅ 通(后因插拔接触劣化变 ❌) |
| 板 2 | 新编镜像 ×2(不同编译器) | ❌ |
| 板 2 | B 原装镜像 | ❌ |
| 板 3(全新) | B 原装镜像 | ✅ 15/15 |
结论:板 2 的 UART3 信号链硬件故障(三种镜像全不通),板 1 是换板插拔导致接触劣化。
教训:同一个"不通",先花 10 分钟换板/换镜像做矩阵,再决定要不要读内核代码。
内核侧排查的实用武器(都曾正确地证明"软件无罪"):
# DE 切换的函数级时序(function tracer,无采样损失)
cd /sys/kernel/tracing
echo 'gpio_to_desc gpiod_set_raw_value' > set_ftrace_filter
echo function > current_tracer; echo > trace; echo 1 > tracing_on
python3 dxl_probe.py ping --ids 200 --force # 发帧
echo 0 > tracing_on; grep -E 'gpio_' trace
# 期望:assert 与 release 成对出现,间隔 ≈ 帧时间(1Mbps 10B ≈ 132µs)
# 串口收发计数(rx=0 = 应答根本没进串口)
grep fe670000 /proc/tty/driver/serial
# 引脚复用归属
grep 'pin 111 \|pin 112 \|pin 102 ' /sys/kernel/debug/pinctrl/*/pinmux-pins
# GPIO 物理电平(RK3566 bank 基址是 0xFE74/75/76/770000,不是老 SoC 的 fdd6xxxx!)
busybox devmem 0xFE760000 # GPIO3 A 口数据寄存器,bit6 = GPIO3_A6


评论