联系管理员

开通文章发布权限

扫码 添加微信
微信图片
电话: QQ:1602036736

泰山派 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 在第一帧之后永久卡死。机理链条:

__stop_tx() 在 !THRE 检查处提前返回
  → stop_tx 的 hrtimer 从未 arm
  → THRI 一直置位、em485->tx_stopped 一直 false
  → 下一次 __start_tx() 看见 THRI 已置位 → 什么都不做 → 永久静默

旁证:未改动的原厂内核、ttyS0/3/4/5、有没有 DE 脚都一样复现——是 em485 在此 SoC 上本身不可用,与方向控制无关。

⚠️ 移植到新 SoC / 新内核大版本时这条必须重验(连发 3 帧以上)。
em485 若好用就优先用上游机制。我们实测复现了卡死,才走方案 F。

2. 板厂自带方案错在哪(看到就整体替换)

板厂在 8250_port.c 里的"485 支持"长这样:

if (115 == up->port.rs485.rts_gpio || 116 == ... || 14 == ...) {
        gpio_set_value(up->port.rs485.rts_gpio, 1);
        udelay(2);
}
...
for (i = 0; i < 200; i++) { mdelay(3); if (TEMT) { printk(...); break; } }
gpio_set_value(up->port.rs485.rts_gpio, 0);

四宗罪:

  1. 脚号硬编码 115/116/14——换板/换引脚即失效,还会去拉一根不相干的 GPIO;

  2. 脚号放在 port.rs485.rts_gpio——uart_sanitize_serial_rs485() 在 SER_RS485_ENABLED
    未置位时会把整个结构 memset 清零,而且 TIOCSRS485 允许任何用户态进程整块覆写(可指挥内核拉任意 GPIO);

  3. mdelay(3)×200(上限 600 ms)——在持锁的 ISR 里,粒度比一整帧还粗,且每帧 printk;

  4. 只覆盖一个放手点——其余路径会让收发器一直占着总线。

3. 方案 F 设计(逐文件,按依赖顺序读)

3.1 include/linux/serial_8250.h(+17)

struct uart_8250_port 顶层加两个字段:

int  de_gpio;        /* -1 = 本口没有 DE 脚 */
bool de_active_low;

为什么不放 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——

if (p->fifosize && up->de_gpio < 0) {   /* 原来只有 p->fifosize */

因为 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 日志正常打印,发数据时完全不换向。

uart->de_gpio       = up->de_gpio;
uart->de_active_low = up->de_active_low;

3.4 drivers/tty/serial/8250/8250_port.c(+136,核心)

两个 static 函数 + 在 serial8250_tx_chars() 里布点 + 一处初始化:

static void serial8250_de_assert(struct uart_8250_port *up);  /* 灌 FIFO 前拉高 */
static void serial8250_de_release(struct uart_8250_port *up); /* 等 TEMT 再放低 */

release 的等待预算——不要 mdelay 死等(在持锁 ISR 里):

budget_us = DIV_ROUND_UP(up->port.frame_time, NSEC_PER_USEC) * (up->tx_loadsz + 2);
if (!budget_us || budget_us > 50000)   /* 0 = 还没 set_termios;50ms 兜底 */
        budget_us = 50000;
while (budget_us--) {
        if (serial_lsr_in(up) & UART_LSR_TEMT) break;
        udelay(1);
}
gpio_set_value(up->de_gpio, up->de_active_low ? 1 : 0);

注意 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() 里必须显式:

up->de_gpio = -1;
up->de_active_low = false;

serial8250_ports[] 是静态清零数组,而 0 是合法的 gpio 号——不写这两行,每个没有 DE
脚的串口发数据时都会去拉 gpio0。

3.5 8250_dwlib.c(+13,可选)

纯注释 + 一行 include,记录"为什么放着 em485 不用"。建议带上。

3.6 dts(本板专属)

&uart3 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&uart3m1_xfer>;
        485_ctrl_gpio = <&gpio3 RK_PA6 GPIO_ACTIVE_HIGH>;
};

绝对不要加 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. 验收(可证伪)

dmesg | grep -i 485
# 期望:dw-apb-uart fe670000.serial: RS485 direction on gpio 102 (active high)

cat /sys/kernel/debug/gpio | grep rs485
# 期望:gpio-102 (  |rs485-de  ) out lo     ← 已被驱动持有,空闲=接收态

# 连发 3 帧以上(唯一能区分"方案对了"和"撞上 em485 坑"的测试)
python3 dxl_probe.py ping --ids 200,10,20,30 --force   # 确认每帧都有回复
⚠️ 不要用 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 选择交叉编译器的逻辑是 "系统工具链优先":

if which aarch64-linux-gnu-gcc >/dev/null 2>&1; then
        notice "Using system toolchain for kernel: aarch64-linux-gnu-"
        export RK_KERNEL_TOOLCHAIN="aarch64-linux-gnu-"    # 无条件覆盖外部值!
else
        export RK_KERNEL_TOOLCHAIN="$(get_toolchain kernel "$RK_KERNEL_ARCH")"  # 走 prebuilts
fi

容器/宿主机里只要装了 Ubuntu 的 crossbuild-essential-arm64,无论你怎么 export
RK_KERNEL_TOOLCHAIN 都会被覆盖**,内核实际被 Ubuntu gcc 11.4 编译。
而 SDK 的 get_toolchain() 会精确匹配 **prebuilts 自带的官方工具链

(prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-*,与板厂出片用的完全同款)。

解法:卸掉容器里的 Ubuntu 交叉工具链,让 which 失败、走 get_toolchain 正道:

docker exec -u root <容器> bash -c 'apt-get purge -y crossbuild-essential-arm64 \
  gcc-11-aarch64-linux-gnu cpp-11-aarch64-linux-gnu binutils-aarch64-linux-gnu \
  linux-libc-dev-arm64-cross libc6-arm64-cross libgcc-11-dev-arm64-cross \
  libstdc++-11-dev-arm64-cross && apt-get autoremove -y'

验证手段:编完看 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 容器环境不进镜像的三样东西

  1. live-build 20230131:SDK 的 check-debian.sh 要求源码安装的版本(支持 bookworm),
    否则 rootfs 阶段直接失败;

  2. binfmt_misc 挂载/注册:内核态状态,容器重建后需
    mount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc && update-binfmts --enable qemu-aarch64;

  3. 容器内 apt 装的东西只在该容器可写层——docker commit 才能保住,重建容器就没了。

7.4 排查方法论:先做对照矩阵,再深挖内核

我们为"ping 舵机无应答"付出了巨大的内核排查成本(ftrace、寄存器轮询、pinctrl、tx/rx 计数
全部正常),最终靠换板对照矩阵才定位:

板子镜像结果
板 1B 原装✅ 通(后因插拔接触劣化变 ❌)
板 2新编镜像 ×2(不同编译器)❌
板 2B 原装镜像❌
板 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

7.5 芯片 WHO_AM_I 级别的自检(IMU 模块在 I2C 侧)

i2cdetect -y 3                       # 期望 6a 出现(AD0=GND)
i2ctransfer -f -y 3 w1@0x6a 0x0f r1  # 期望 0x70(LSM6DSV16X)

8. 一图流总结

dts 485_ctrl_gpio(GPIO3_A6=102, ACTIVE_HIGH, 无 boot-time 标志)
        ↓ dw8250_probe: of_get_named_gpio_flags + devm_request(接态=低) + 打印方向行
        ↓ register_8250_port: 显式拷贝 de_gpio/de_active_low(漏了=永不换向)
用户态普通 write(ttyS3)
        ↓ serial8250_tx_chars: de_assert → 灌FIFO → 三放手点 de_release(TEMT预算忙等)
        ↓ 有 DE 脚的口不挂 TX DMA
总线:NC7WZ241 DE=高 发 / =低 收,应答 500µs 后到,DE 已放低 ✅

评论

快捷导航

把好文章收藏到微信

打开微信,扫码查看

关闭

还没有账号?立即注册