RK3566 开发板Debug串口不打印拯救记录
一块 RK3566 开发板的串口救赎 —— 三层故障叠加的排查日记
一句话总结:同一根串口线,U-Boot 有输出、一进内核就彻底静默。
原因不是一个,是三个问题叠在一起互相掩盖。而排查过程中我犯的三个误判,恰好也互相掩护 —— 其中一次"验证通过"的回环测试,验的其实是另一根线。最终通过往 dtbo 分区合并一个设备树覆盖层 + 修补 boot 镜像的 cmdline,把内核日志、Android shell、root 权限全部恢复到你原本就焊好的那一个调试口上。
0. 缘起
起因很朴素:手上有块 RK3566 开发板,跑 Android,想用 adb 连上去,但 adb devices 一直是空列表。
"是不是 Android 里 USB 调试开关没打开?"——这是最自然的猜测,也是错的。后来的事实证明,这块板子上 USB adb 在物理层面就不可能实现。但在搞清楚这一点之前,我们先在串口上兜了一大圈,而那一圈才是真正有价值的部分。
一个背景信息很重要:这块板子的固件是厂商定制过的,而且有不少地方没配置对。 下面你会看到,我们最终修好了两处真实存在的固件缺陷。
1. 第零步:先建立观测能力
排查硬件问题,第一步永远不是猜,而是建立可靠的观测通道。
1.1 主机侧
先确认 PC 上的工具链。这里立刻发现两个问题:
# PATH 里的 adb 是老古董
C:\Android\adb.exe → Android Debug Bridge version 1.0.32
机器上其实有 4 个不同版本的 adb,最新的一个是:
D:\...\platform-tools_r33.0.3-windows\platform-tools\adb.exe
→ Android Debug Bridge version 1.0.41 (33.0.3-8952112)
换掉。但换完 adb devices 依然是空的。
1.2 关键判断:这不是"设置问题"
我们做了一次完整的 USB 设备枚举,把机器上所有 USB\VID_* 节点列出来对照:
USB\VID_8087&PID_0026 Intel 蓝牙
USB\VID_04F2&PID_B725 内置摄像头
USB\VID_046D&PID_C52B 罗技键鼠
USB\VID_1A86&PID_55D3 CH343 USB-TTL → COM5 ← 我焊的调试线
USB\VID_1366&PID_0105 Segger J-Link
USB\VID_05E3&PID_0626 Genesys Hub
...
没有 VID_2207(Rockchip),没有 VID_18D1(Google/ADB),连一个"未知设备"都没有。
这个观察很重要,因为它排除了一整类假设:
| 现象 | 说明 |
|---|---|
adb devices 显示 unauthorized |
是授权问题(需要在板子上点确认) |
显示 offline |
是 adbd 状态问题 |
| 显示空列表,设备管理器也不闪 | 物理层没建立,设备根本没上总线 |
| 显示"未知设备" | 是驱动问题 |
我们是最后一种情况的更极端版本:连枚举都没发生。
后来用 900 秒、每 700ms 一次的 USB 到达事件监视器反复验证,结论一致 —— 零 ADD 事件。板子从来没有拉高 USB 的 D+ 上拉电阻。
结论:这不是 adb 配置问题,是板子侧的问题。需要更底层的观测手段。 于是转向串口。
2. 串口:从"不知道波特率"开始
板子上焊了一个 USB-TTL 到调试口,出 COM5。问题是:上电时有数据输出,但不知道波特率。
2.1 用"零字节"作为诊断信号
写了个抓取脚本,扫一遍常见波特率:
[ 115200] bytes=0 printable=0% resp=
[1500000] bytes=0 printable=0% resp=
[ 921600] bytes=0 printable=0% resp=
[ 460800] bytes=0 printable=0% resp=
[ 230400] bytes=0 printable=0% resp=
[ 57600] bytes=0 printable=0% resp=
[ 38400] bytes=0 printable=0% resp=
[ 9600] bytes=0 printable=0% resp=
这个"全 0"其实信息量很大:如果波特率只是猜错了,你会读到乱码字节(byte count > 0,printable 比例极低)。读到 0 字节意味着这一瞬间串口上真的没有电平翻转。
于是我们改成"监听窗口 + 让用户上电"的模式,直接在 1500000 上抓:
U-Boot SPL 2017.09-ge4e124926e-230922 #lxh (Sep 25 2023 - 11:07:45), fwver: v1.13
...
U-Boot 2017.09 (Nov 06 2025 - 11:37:50 +0800)
Model: Rockchip RK3568 Evaluation Board
PreSerial: 2, raw, 0xfe660000
DRAM: 2 GiB
波特率 = 1500000,一次命中。(Rockchip 调试串口的标准波特率,这个"惯例"这次是对的。)
2.2 抢下 U-Boot 提示符
拿到日志只是第一步,我们想要的是能敲命令的提示符。但 printenv 显示:
bootdelay=0
autoboot 延时是 0 —— 正常手段根本来不及按键。解决方案是在上电瞬间持续高频发 CTRL+C:
while ($sw.Elapsed.TotalSeconds -lt $WaitSeconds) {
$sp.Write([byte[]]@(0x03), 0, 1) # CTRL+C
Drain -Ms 150 -sp $sp # 每 150ms 一次
if ($accumulated -match "=>\s*$") { break } # 看到提示符就停
}
每 150ms 一次的频率足以在 U-Boot 检查按键的窗口里"占住" UART FIFO。成功拿到提示符,这是整个排查的转折点 —— 从此我们有了一个完整的、可以任意敲命令的 U-Boot shell。
⚠️ 但这个方法有个副作用:只要这个脚本在跑,板子一上电就被拦在 U-Boot 里,永远进不了系统。重启后"蓝灯不亮、进不去系统",就是这个原因。排查工具本身会改变系统行为
3. 情报搜集:U-Boot 里的底牌
有了提示符,先把家底摸清:
3.1 U-Boot 环境
board=evb_rk3568
board_name=evb_rk3568
soc=rockchip
serial#=0012509008
baudrate=1500000
bootdelay=0
bootargs=storagemedia=emmc androidboot.storagemedia=emmc androidboot.mode=normal androidboot.dtb_idx=0 androidboot.dtbo_idx=0
stdin=serial,usbkbd
stdout=serial,vidconsole
3.2 分区表
Part.Start LBA.End LBA Name
1. 0x00002000 0x00003fff security
2. 0x00004000 0x00005fff uboot
3. 0x00006000 0x00007fff trust
4. 0x00008000 0x00009fff misc
5. 0x0000a000 0x0000bfff dtbo ← 后面大有用处
6. 0x0000c000 0x0000c7ff vbmeta
7. 0x0000c800 0x000207ff boot ← 内核 + DTB + cmdline
8. 0x00020800 0x00068577 recovery
9. 0x00068578 0x00128577 backup
10. 0x00128578 0x001e8577 cache
11. 0x001e8578 0x001f0577 metadata
12. 0x001f0578 0x001f0d77 baseparameter
13. 0x001f0d78 0x00804d77 super ← Android 动态分区
14. 0x00804d78 0x03a3dfbf userdata
顺带解掉一个虚惊:启动日志里的 Could not find "system" partition 不是故障。这是 Android 11 的动态分区布局 —— system/vendor/product 都装在 super(3.26GB)里面。U-Boot 那句是老代码路径在找传统的 system 分区。
3.3 从 boot 镜像里挖出 cmdline
这一步很关键。用 mmc read 把 boot 分区头部读到内存再 dump:
mmc read 0x10000000 0xc800 0x4
md 0x10000000 0x100
10000000: 52444e41 2144494f 01ecb008 10008000 ANDROID!........
10000010: 000c8c1a 11000000 000f0c00 10f00000 ................
10000020: 10000100 00000800 00000002 1600015a ............Z...
10000040: 736e6f63 3d656c6f 46797474 20305149 console=ttyFIQ0 ..
按 Android boot image 格式解码(little-endian):
| 偏移 | 字段 | 值 |
|---|---|---|
| 0x00 | magic | ANDROID! |
| 0x08 | kernel_size | 0x01ecb008 = 32,285,704 |
| 0x10 | ramdisk_size | 0x000c8c1a = 823,322 |
| 0x18 | second_size | 0x000f0c00 = 985,088 |
| 0x24 | page_size | 2048 |
| 0x28 | header_version | 2 |
| 0x2C | os_version | 0x1600015a(→ Android 11) |
| 0x40 | cmdline[512] | (下面) |
cmdline 全文:
console=ttyFIQ0 androidboot.baseband=N/A androidboot.wificountrycode=CN
androidboot.veritymode=enforcing androidboot.hardware=rk30board
androidboot.console=ttyFIQ0 androidboot.verifiedbootstate=orange
firmware_class.path=/vendor/etc/firmware init=/init rootwait ro
loop.max_part=7 androidboot.selinux=permissive buildvariant=userdebug
这里已经有两条重要情报:
buildvariant=userdebug+androidboot.selinux=permissive→ 这是调试版,正常情况 adbd 一定可用,而且androidboot.console=ttyFIQ0会让 Android 的init在控制台上开一个 shell。console=ttyFIQ0—— 内核被要求把输出写到ttyFIQ0。
4. 迷航:一系列"自洽但错误"的方向
从这一步开始,我们进入了本次排查最长的弯路。关键在于:每一个误判看起来都有充分证据支持。
4.1 现象:内核完全静默
不管怎么引导,串口上的输出永远停在同一个位置:
Starting kernel ...
(然后什么都没有)
我们尝试了:
| 手段 | 预期 | 结果 |
|---|---|---|
| 什么都不改,纯冷启动 | 看到内核日志 | ❌ 0 字节 |
bootargs 加 console=ttyFIQ0 androidboot.console=ttyFIQ0 |
看到内核日志 | ❌ 0 字节 |
再加 earlycon=uart8250,mmio32,0xfe660000,1500000n8 ignore_loglevel |
绕过所有控制台驱动直接怼寄存器 | ❌ 0 字节 |
改成 console=ttyS2,1500000 |
换一个 UART 试 | ❌ 0 字节 |
程序化检查整份日志:没有任何 [ 0.000000] 形式的内核时间戳行。
earlycon 都打不出东西,这是个非常强的信号。但当时我们对它的解读是错的 —— 我们以为"内核挂了"。
4.2 误判一:以为固件刷错了
U-Boot 报 board=evb_rk3568、Model: Rockchip RK3568 Evaluation Board,而板子是 RK3566。
再加上启动日志里出现 Net: eth1: ethernet@fe010000、环境变量里同时有 ethaddr 和 eth1addr 两个 MAC —— 双 GMAC 是 RK3568 的特征,RK3566 只有一路。
于是我们推断:"有人把 RK3568 EVB 的镜像刷到了一块 RK3566 板子上,DTB 不匹配导致内核早期崩死。" 这个推断完美解释了"U-Boot 正常、内核静默"。
它是错的。 后来一条 getprop 就推翻了:
ro.product.model = k3566pb
ro.build.display.id = K3566PB-000_v11.0.09
固件本身就是为 k3566pb 编译的。 U-Boot 的型号字符串和双 GMAC 只是厂商沿用了 RK3568 EVB 的 U-Boot 配置带来的假象 —— 他们改了内核和 Android,但没改 U-Boot 的板级标识。
教训一:看到"不匹配"的证据时,先确认它描述的是哪个组件。 U-Boot 的 DT 和内核的 DT 是两套东西。
4.3 误判二:以为 OTG 的 VBUS 配置错了
转向 USB 问题本身。我们拿到了 U-Boot 的设备模型树:
nop |-- usbdrd * dwc3-generic-wrapper
usb | `-- dwc3@fcc00000 dwc3-generic-host ← USB3 OTG 控制器
nop |-- usbhost *
usb | `-- dwc3@fd000000 dwc3-generic-host ← USB3 host
phy |-- usb2-phy@fe8a0000 host-port / otg-port
phy |-- usb2-phy@fe8b0000 host-port / otg-port
两个 dwc3 都绑成 dwc3-generic-host,没有任何 peripheral 实例 —— 这正好对应启动日志里出现两次的:
Warn: can't find connect driver
结论:这版 U-Boot 没有 USB device 能力,所以 fastboot/rockusb/ums 全都不可用。
然后在系统内部读到 extcon 状态:
--- extcon0 (fe8a0000.usb2-phy) ---
USB=0 ← 没检测到"作为设备"的连接
USB-HOST=1 ← 当前角色 = HOST
USB_VBUS_EN=1 ← 正在往外输出 VBUS 5V
SDP=0 CDP=0 DCP=0 ← 也没检测到充电器/主机
于是推断:"板子的 OTG 口一直停在 host 角色往外供电,所以永远不可能切成 device"。
查设备树,vbus-supply 的 phandle 确实指向 vcc5v0_otg(0xff000000 对得上),也没有 regulator-always-on:
usb2-phy@fe8a0000/otg-port/vbus-supply = 0xff000000
vcc5v0-otg-regulator/phandle = 0xff000000 ✅ 匹配
所以 DT 是"对"的,"厂商把 VBUS 标成常开"这个假设不成立。
真正的答案要朴素得多 —— 见 4.5。
4.4 误判三(最致命):验证通过了,但验错了对象
这一节是我最想记录下来的一段。
已知 U-Boot 用 0xfe660000 做调试口。我们在内核里发现 /dev/ttyS2 存在、可用。基于 Rockchip 的通用惯例,我假设 ttyS2 = 0xfe660000(因为 RK 平台通常 uart2 是调试口)。
于是让用户把 USB-TTL 焊到 "ttyS2" 对应的口上,然后跑了一个闭环回环测试:
- 通过 adb 在板子上执行
stty -F /dev/ttyS2 1500000 cs8 -cstopb -parenb - PC 打开 COM5,同样波特率
- 通过 adb 执行
echo TTYS2PROBE@1500000#OK > /dev/ttyS2 - PC 侧读回
结果:
[1500000] bytes=23 resp=TTYS2PROBE@1500000#OK.\n <== MATCH!
[ 115200] bytes=22 resp=TTYS2PROBE@115200#OK.\n <== MATCH!
[ 9600] bytes=20 resp=TTYS2PROBE@9600#OK.\n <== MATCH!
三个波特率全部回环成功。 按常理,这"证明"了焊接、接线、波特率都没问题。
但它是自洽的谎言 —— 这个测试验证的是"PC 能否通过这根线跟 /dev/ttyS2 通信",而 /dev/ttyS2 根本不是我们的目标。测试完全正确,只是测的对象不是我们以为的那个。
于是接下来的引导依然全静默,因为:
- U-Boot 打印在
0xfe660000(线已经不在这儿了) - 内核(如果它能打印)会打印到
ttyS2,但内核压根不驱动0xfe660000
两边的线都断了,而测试只证明了其中一根是通的。
4.5 破局:把"惯例"换成"事实"
真正解决问题的,是停止依赖惯例,直接从运行中的系统读出真实的映射表:
$ cat /proc/tty/driver/serial
serinfo:1.0 driver revision:
1: uart:16550A mmio:0xFE6C0000 irq:58
2: uart:16550A mmio:0xFE6D0000 irq:59
3: uart:16550A mmio:0xFE690000 irq:57
5: uart:16550A mmio:0xFE650000 irq:56
$ for a in /proc/device-tree/aliases/serial*; do echo -n "$(basename $a)="; cat $a; echo; done
serial0=/serial@fe660000 ← ★★★
serial1=/serial@fe6c0000
serial2=/serial@fe6d0000 ← /dev/ttyS2 在这里!
serial3=/serial@fe690000
serial4=/serial@fe680000
serial5=/serial@fe650000
serial6=/serial@fe6a0000
serial7=/serial@fe6b0000
serial8=/serial@fe670000
serial9=/serial@fdd50000
厂商把 serialN 别名重映射了,完全是乱的。
| 硬件地址 | DT alias | 内核设备 | 用途 |
|---|---|---|---|
0xfe660000 |
serial0 |
ttyS0 |
U-Boot 调试口 |
0xfe6c0000 |
serial1 |
ttyS1 |
空闲 |
0xfe6d0000 |
serial2 |
ttyS2 |
蓝牙 ← 刚焊上去的就是这个 |
0xfe690000 |
serial3 |
ttyS3 |
空闲 |
真相是:/dev/ttyS2 = 0xfe6d0000,是蓝牙的口。 而 U-Boot 的调试口是 0xfe660000,内核里叫 ttyS0。
教训二:任何基于"平台惯例"的编号推断都必须实测验证。 厂商重映射别名是合法的,而且很常见。
教训三:闭环测试通过只能证明"这条路径通",不能证明"这是对的那条路径"。 测试的自洽性会主动掩盖方向性错误 —— 这比测试失败更危险。
5. 顺带解开的另一个谜:网络 adb
在串口方向上兜圈的同时,用户在 Android 设置里找到了板子的 IP:192.168.137.114。
adb connect 192.168.137.114:5555
adb devices -l
192.168.137.114:5555 device product:k3566pb model:k3566pb device:k3566pb transport_id:1
一次成功,状态直接是 device。 原因是厂商预先设了:
service.adb.tcp.port = 5555
这块板子开箱就支持网络 adb,根本不需要 USB。 最初的需求到这里其实已经解决了,但是后续刷机考虑到风险性,这里还是要打通串口
而有了 adb root 之后,我们才得以从系统内部读设备树、读分区、读 extcon —— 这个通道成了后续所有取证工作的基础。
6. 真相:三层故障叠加
把所有线索拼起来,最终锁定三个独立的问题:
问题 A:console= 指向一个不存在的设备
$ cat /proc/device-tree/fiq-debugger/status
disabled ← fiq-debugger 被禁用
$ ls /dev/ttyFIQ0
ls: /dev/ttyFIQ0: No such file or directory ← 设备从未创建
$ cat /proc/cmdline
... console=ttyFIQ0 androidboot.console=ttyFIQ0 ... ← cmdline 却引用它
内核被要求把所有输出写到 ttyFIQ0,而这个设备不存在。输出就这么被静默丢弃了。
这也解释了为什么连 earlycon 都救不回来 —— earlycon 我当时的参数写法有问题(指定了波特率 1500000n8,导致驱动用了错误的输入时钟去算分频),加上 console= 指向虚空,双重静默。
问题 B:调试口在内核里被禁用
$ for n in fe650000 fe660000 fe670000 fe680000 fe690000 fe6a0000 fe6b0000 fe6c0000 fe6d0000; do
echo -n "serial@$n status="; cat /proc/device-tree/serial@$n/status; echo; done
serial@fe650000 status=okay
serial@fe660000 status=disabled ← ★ U-Boot 的调试口,内核禁用!
serial@fe670000 status=disabled
serial@fe680000 status=disabled
serial@fe690000 status=okay
serial@fe6a0000 status=disabled
serial@fe6b0000 status=disabled
serial@fe6c0000 status=okay
serial@fe6d0000 status=okay
serial@fe660000 是 disabled → 内核根本不驱动 U-Boot 用的那个 UART。 即使 console= 改对了,只要这个节点是关的,也打不出东西。
问题 C:USB device 口没有焊接座子
最后回到 USB。从 U-Boot 的 dm tree 拿到两个控制器的 PHY 引用,再从内核设备树解析 phandle:
### USB2 PHY 子端口 phandle
fe8a0000 otg-port = 0x23000000
fe8a0000 host-port = 0x25000000
fe8b0000 otg-port = 0x27000000
fe8b0000 host-port = 0x28000000
### 控制器 → PHY
[OTG ] usbdrd/dwc3@fcc00000 dr_mode=otg phys=0x23000000 → fe8a0000/otg-port
[HOST] usbhost/dwc3@fd000000 dr_mode=host phys=0x25000000 → fe8a0000/host-port
### USB2 主控(对应空焊盘)
usb@fd800000 phys=0x27000000 usb@fd840000 phys=0x27000000
usb@fd880000 phys=0x28000000 usb@fd8c0000 phys=0x28000000
板子上 USB1~3 是未焊接的焊盘,只有一个座子焊了 —— 就是现在插鼠标那个 A 型母座,用 A 转 C 线接到电脑的 C 口。
看内核日志,鼠标挂在哪儿:
[ 188.363061] usb 5-1: new low-speed USB device number 2 using xhci-hcd
[ 188.508130] usb 5-1: Product: Lenovo Precision USB Mouse
↑ 走的是 usbhost/fd000000.dwc3
矛盾解开了:
- 唯一焊了的座子 →
usbhost/dwc3@fd000000→dr_mode = host,在设备树层面就被钉死 → 永远不可能当 device - 能当 device 的
usbdrd/dwc3@fcc00000(dr_mode=otg)→ 对应的物理口是 USB1~3 三个空焊盘之一
而且用户当前的接法是 板子的 A 型母座 → 电脑的 C 口,这是主机对主机,永远不可能枚举。更麻烦的是板子 vcc5v0_host/vcc5v0_otg 都在对外供电,而 A 转 C 线里的 Rp 电阻会让电脑认为"对面是主机"从而不供 VBUS —— 这是 VBUS 对打,有损坏风险,建议立刻拔掉。
顺带从设备树里挖出了 VBUS 使能脚,可用于定位焊盘:
vcc5v0-otg-regulator gpio = phandle 0x36000000, pin 5 → GPIO0_A5
vcc5v0-host-regulator gpio = phandle 0x36000000, pin 6 → GPIO0_A6
(phandle 0x36000000 = /pinctrl/gpio@fdd60000 = GPIO0)
7. 修复
目标明确:让你原本就焊好的那一根线(0xfe660000),同时拿到 U-Boot + 内核日志 + Android shell。
需要改两处,都在分区层面,重启后持久生效。
7.1 关键决策:用 DTBO 覆盖层,而不是改主 DTB
改主 DTB 需要先从 boot 分区里把 123KB 的 DTB 抠出来 —— 但我们在 boot 分区里搜不到 FDT 魔数 d00dfeed(Rockchip 的布局和 AOSP 标准不一致,second 区是高熵数据)。这条路成本高、风险大。
更好的办法:U-Boot 在引导时会应用 dtbo 分区里的设备树覆盖层(日志里的 ANDROID: fdt overlay OK)。我们追加一个覆盖层来启用那个节点即可,完全不用碰主 DTB。
先把原厂 dtbo 解析出来:
00000000: d7b7ab1e 0000026f 00000020 00000020 ← Android DTBO 头(big-endian)
00000010: 00000001 00000020 00000800 00000000 ← 1 个条目, page=2048
00000020: 0000022f 00000040 ... ← size=559, offset=64
00000040: d00dfeed 0000022f 00000038 000001b8 ← FDT 魔数 ✅
解析出它的内容,发现和串口毫无关系,是标准的 Rockchip 重启模式覆盖层:
fragment@0 { target = <&chosen>; __overlay__ { bootargs_ext = "..."; }; };
fragment@1 { target = <&reboot_mode>; __overlay__ {
mode-bootloader; mode-charge; mode-fastboot;
mode-loader; mode-normal; mode-recovery;
}; };
(target 通过 __fixups__ 解析,chosen = /fragment@0:target:0、reboot_mode = /fragment@1:target:0。)
7.2 一个必须绕开的坑:dtbo_idx
U-Boot 的 bootargs 里有:
androidboot.dtbo_idx=0
这表示只应用第 0 个覆盖层。 如果我把新覆盖层放在 entry 1,它会被直接忽略。
改成 dtbo_idx=0,1 就得再动 U-Boot 环境变量(多一处写操作、多一份风险)。
更优雅的方案:把我们的 fragment 直接合并进原厂那个覆盖层,作为它的 fragment@2。这样:
- 仍然只有 1 个 entry,
dtbo_idx=0照样生效 ✅ - 原厂的
chosen/reboot_mode功能完整保留 ✅ - 不需要动 U-Boot 环境 ✅
- 总改动从 3 处降到 2 处 ✅
用 Python 手写 FDT 二进制操作(解析原覆盖层 → 在 struct block 里插入新 fragment → 追加 strings → 修正 header 偏移):
fragment@2 {
target-path = "/serial@fe660000";
__overlay__ { status = "okay"; };
};
用 target-path 而不是 target(phandle),因为 libfdt 的覆盖层实现原生支持 target-path,省掉 phandle 解析这一步。
结果:559 B → 670 B,dtbo 镜像 734 B,1 entry。
7.3 修补 boot 镜像的 cmdline
console=ttyFIQ0 → console=ttyS0,1500000
androidboot.console=ttyFIQ0 → androidboot.console=ttyS0
这里藏着一个很容易踩的字符串替换陷阱:
"androidboot.console=ttyFIQ0"
└──────────────┘
含有 "console=ttyFIQ0" 子串!
如果先替换 console=ttyFIQ0,会把它误伤成 androidboot.console=ttyS0,1500000 —— androidboot.console 只接受 tty 设备名,带上波特率就废了。
必须先替换长的那条:
new = cur.replace("androidboot.console=ttyFIQ0", "androidboot.console=ttyS0")
new = new.replace("console=ttyFIQ0", "console=ttyS0,1500000")
老 cmdline 334 字符 → 新 338 字符,512 字节字段内余量充足。
另外确认了一个安全性问题:boot 镜像的 id 字段是 kernel/ramdisk/second 三段的 SHA1,不包含 cmdline —— 所以改 cmdline 不会让这个 id 失效。
8. 验证
8.1 写入安全流程
每一步都遵循同一个模式:
1. 板子侧备份原始数据(dd 到 /data/local/tmp)
2. PC 侧也存一份 hex
3. push 新数据 → 先读回确认内容正确
4. dd 写入
5. 从分区读回,逐字节比对
dtbo 写入后:
1+1 records out — 734 bytes copied
read-back MATCH = True
tail (700..733) = ...reboot_mode\0target-path\0status\0 ← 我们的 strings block 落位
bytes 734+ = all zeros ← 无残留旧数据
8.2 覆盖层生效的硬件级证据
重启后抓取 U-Boot 日志:
ANDROID: fdt overlay OK
Using Device Tree in place at 0x0a100000, end 0x0a12156c
↑ 之前是 0x0a1214fd
差值 = 0x6F = 111 字节
DTB 实际长大了 111 字节 —— 正好是新增 fragment 的体积。 这是覆盖层被真正应用进内核 DTB 的硬证据。
8.3 软件验证
$ cat /proc/tty/driver/serial
0: uart:16550A mmio:0xFE660000 irq:57 ← ★ 出现了!
1: uart:16550A mmio:0xFE6C0000 irq:59
2: uart:16550A mmio:0xFE6D0000 irq:60
3: uart:16550A mmio:0xFE690000 irq:58
5: uart:16550A mmio:0xFE650000 irq:56
$ ls -l /dev/ttyS0
crw-rw---- 1 root root 4, 64 /dev/ttyS0
$ cat /proc/device-tree/serial@fe660000/status
okay ← 从 disabled 变 okay
8.4 物理链路回环
su 0 stty -F /dev/ttyS0 1500000 cs8 -cstopb -parenb
su 0 sh -c 'echo TTYS0_ROUNDTRIP_OK > /dev/ttyS0'
PC 侧 COM5:
TTYS0_ROUNDTRIP_OK.\n
==> MATCH! ttyS0 physical path CONFIRMED on the debug port
这次测的是对的那根线了。
8.5 最终验收
写完 cmdline 补丁重启后:
### /proc/cmdline
... console=ttyS0,1500000 ... androidboot.console=ttyS0 ...
### 串口上的实时输出(带内核时间戳)
[ 159.840284] RTW: rtw_set_ps_mode(wlan0) Enter 802.11 power save - WIFI-TRAFFIC_IDLE
[ 161.226979] RTW: rtw_sctx_wait timeout: dump_mgntframe_and_wait_ack_timeout
### 发一个回车,出现 shell
console:/ $
### 执行命令
>>> id
uid=2000(shell) gid=2000(shell) ... context=u:r:shell:s0
>>> su 0 id
uid=0(root) gid=0(root) groups=0(root) ... context=u:r:su:s0 ← ★ root!
>>> uname -a
Linux localhost 4.19.232 #405 SMP PREEMPT Thu Nov 6 11:41:03 CST 2025 aarch64
### 降噪(WiFi 驱动 RTW 很吵)
>>> su 0 sh -c 'echo 1 4 1 7 > /proc/sys/kernel/printk'
>>> cat /proc/sys/kernel/printk
1417
>>> echo CONSOLE_TEST_OK
CONSOLE_TEST_OK
console:/ $ ← 干净、完全可交互
内核日志 + Android shell + root 权限,全部跑在你最初就焊好的那一根线上。
9. 成果与回滚
最终可用通道
| 通道 | 状态 | 能力 |
|---|---|---|
串口 0xfe660000 @1500000 8N1 |
✅ 本次打通 | 内核日志、Android shell、su 0 → root |
网络 adb 192.168.137.114:5555 |
✅ 一直可用 | 厂商预设 service.adb.tcp.port=5555 |
| USB adb | ❌ 物理不可能 | 能当 device 的座子未焊接 |
串口是最有价值的一条 —— 它是唯一在"系统挂了、网络断了、USB 认不到"时仍然能碰到板子的通道,而且能直接进 U-Boot。
回滚(全部就绪)
su 0 dd if=/data/local/tmp/dtbo.img of=/dev/block/by-name/dtbo
su 0 dd if=/data/local/tmp/bootpage0-orig.bin of=/dev/block/by-name/boot
U-Boot 分区全程未被触碰 —— 这是最后的安全网:无论前面改出什么问题,串口永远能抢回控制权。
10. 复盘:十条经验
关于证据
-
"零字节"和"乱码"是完全不同的信息。 串口读到 0 字节说明线上没有电平翻转;读到乱码说明波特率错了。二者指向完全不同的排查方向。
-
自洽的测试可能测错对象 —— 这比测试失败更危险。 我们的回环测试三个波特率全过,但它验证的是"PC 与
/dev/ttyS2通",而/dev/ttyS2不是目标。测试通过会主动压制你对方向的怀疑。 做闭环验证时,一定要问一句:"这个测试通过,究竟证明了什么、没证明什么?" -
不要相信平台惯例,要读实际配置。 厂商重映射
serialN别名是合法操作。/proc/tty/driver/serial+/proc/device-tree/aliases/十秒钟就能给出事实,别省这一步。 -
交叉验证不同来源的证据。 U-Boot 说
board=evb_rk3568,Android 说ro.product.model=k3566pb—— 两个都对,它们描述的是不同组件。看到"矛盾"时,先问"这两条证据各自描述什么"。
关于方法
-
先建立观测通道,再开始猜。 这次的核心突破全靠拿到 U-Boot 提示符和网络 adb —— 有了能任意敲命令的 shell,问题就从"猜"变成了"读"。
-
抢
bootdelay=0的 autoboot 可以靠上电瞬间持续发 CTRL+C(实践中 150ms 间隔足够)。 -
从运行中的系统反查设备树是最直接的取证方式。
/proc/device-tree和/proc/tty/driver/serial是事实,比任何数据手册和惯例都可靠。 -
排查工具本身会改变系统行为。 我的
uboot-console.ps1持续发 CTRL+C,导致用户有一次重启后"蓝灯不亮、进不去系统",一度怀疑是排查把板子搞坏了。必须清楚知道自己的工具在做什么,并在不需要时立刻停掉。
关于动手改
-
每一处写入都要有可执行的回滚。 备份存两份(板子 + PC),写入前后逐字节比对。并且永远保留一条不依赖待修改组件的恢复通道 —— 这次是 U-Boot 分区全程没动。
-
改动前先算清"常量"和"陷阱"。 这次两个典型:
androidboot.console=ttyFIQ0里含有console=ttyFIQ0子串,替换必须从长到短- boot 镜像
id字段是 kernel/ramdisk/second 的 SHA1,不含 cmdline,所以改 cmdline 是安全的 androidboot.dtbo_idx=0意味着只应用第 0 个覆盖层 → 决定了"合并进原覆盖层"而不是"追加新条目"
附录 A:本次用到的工具
| 工具 | 用途 |
|---|---|
uart-capture.ps1 |
被动抓串口,输出 ASCII + HEX dump |
uboot-console.ps1 |
抢 bootdelay=0 窗口(每 150ms 发 CTRL+C)并自动下发命令序列 |
usb-watch.ps1 |
轮询 pnputil 做快照 diff,捕获 USB 枚举事件 |
uart-verify.ps1 |
串口闭环回环测试(设置板子波特率 → 回写 → PC 读回) |
uart-probe.ps1 |
多波特率扫描找 shell |
adb-board.ps1 |
adb 包装(自动 connect,因 server 会被回收) |
build_patches.py |
FDT/DTBO 构造器 + cmdline 补丁,含解析与自校验 |
附录 B:关键环境坑
adb pull在特定环境下会挂住(40MB 和 4MB 都超时),但adb push正常。小数据用xxd -p取回、xxd -r -p送下去更可靠。- adb server 在独立进程之间会被回收,
adb connect的状态存在 server 里,每个新终端都要重新 connect。 - adb 路径含中文时偶发编码破坏 → 复制到纯 ASCII 路径下使用。
- toybox 的
xxd不支持-c 2048(上限较小);用默认宽度分行取再拼接。 - 板子上的
su是 toybox 风格:su [WHO [COMMAND...]],不是su -c。 正确写法su 0 id、su 0 sh -c '...'。 - 该内核 未启用
CONFIG_OF_OVERLAY,不支持运行时热加载覆盖层,只能靠 U-Boot 在引导时应用。 - cmdline 加
loglevel=N与运行时echo C D M B > /proc/sys/kernel/printk(4 个字段,第一个是 console_loglevel)是两套写法。
尾声
从"adb 连不上"到"串口 root shell",中间隔了三个互相掩盖的固件缺陷、三次方向性误判、以及一次自洽却测错对象的验证。
最值得记住的一条:当证据看起来完美地支持某个结论时,恰恰要停下来问一句"它可能没证明什么"。
那块板子现在状态很好 —— 一根线,U-Boot、内核日志、Android shell、root,全都有。
浙公网安备 33010602011771号