把 openFyde 移植到 LubanCat-2:一次 RK3568 的完整踩坑记录


自用记录。目标是半年后的自己看到这篇能直接重建整套东西,而不是重新推一遍。 所有数字、commit hash、扇区偏移、寄存器值都是实测值,可以直接拿去对。

0. 这是什么

把 openFyde(FydeOS 的开源版,本质是 ChromiumOS)移植到野火 LubanCat-2(瑞芯微 RK3568)。

上游官方只支持 RK3588。RK3568 是从零开始,没有任何现成的板级支持。整个工作分两段:7 月打通“能开机进桌面”,8 月打通“镜像能独立启动 + 全部固化进构建系统”。

基线(记在 patches/BASELINE):

openFyde 版本   R132-16093.91.0
Chrome          M132
内核            6.1.75  (openFyde/kernel-rockchip_6, 分支 rk6.1-openfyde)
内核上游基线    705e94619

板上固件版本(全部实测,排查时经常要对):

SPL     2017.09-g606f72bd97a-240527 #lxh (May 30 2024)
DDR     2d653b3476 typ 24/01/20-15:04:19, fwver v1.21
BL31    v2.3-948-g0a207bf3c, fwver v1.46
OP-TEE  3.13.0-1018-g3864e29ae, fwver v2.16

1. 现在能干什么,不能干什么

能:

  • 完整 ChromeOS 桌面,从 SD 卡独立启动(不借 eMMC 上厂商的任何东西)
  • GPU 硬件加速:Panfrost + Mesa 25,Chrome 走 ANGLE → GLES
  • 串口 root shell(UART2 @ 1500000 baud,不是 115200)
  • 百兆有线网

不能:

  • Android 应用 —— ARC 需要 Google 授权
  • Crostini / Linux 容器 —— vm_concierge 收 TRAP 崩溃、crosdns 退出码 74,无限重启。需要 KVM,这条没通
  • 在线登录 —— 没有 Google/FydeOS API key。本地账号可以建、可以登
  • NPU —— 被主动禁用(见第 6 节)
  • 千兆网 —— 直连链路 1Gbps 下 93% 丢包,必须降到 100Mbps 全双工

2. 先建立地图:启动是分层的

调试嵌入式启动问题,第一步永远是判断“报错来自哪一层”。

BootROM          芯片里烧死的代码,改不了
  ↓
SPL              跑在 SoC 内部 SRAM 里(几百 KB),初始化 DDR、把完整 U-Boot 读进内存
  ↓
ATF (BL31)       ARM Trusted Firmware,特权层
OP-TEE (BL32)    安全世界
  ↓
U-Boot proper    跑在 DDR 里,扫描分区、执行 boot.scr、加载内核
  ↓
Linux 内核
  ↓
init (upstart)   ChromeOS 用 upstart,不是 systemd
  ↓
Chrome / UI

各层在串口里的特征输出:

层 特征
SPL DDR 2d653b3476 typ ...、the read training result:、U-Boot SPL 2017.09-...
ATF NOTICE: BL31: v2.3...
OP-TEE I/TC: OP-TEE version: 3.13.0-...
U-Boot U-Boot next-dev-...、Hit key to stop autoboot
内核 [ 0.000000] Booting Linux on physical CPU
init init: xxx main process ... respawning

还有一层是 Portage overlay,决定“哪些包进镜像”:

chromiumos-overlay          上游 ChromiumOS
  ↓
overlay-inaugural-openfyde  openFyde 通用层
  ↓
foundation-rk3568           芯片层(内核 dts、rockchip-mpp)
  ↓
overlay-lubancat2-openfyde  板级层(我们的东西)

越靠下优先级越高,但跨 overlay 同名包是比版本号,不是比层级 —— 这个坑后面会踩到。


第一部分:七月 —— 让它开机

3. 7 个 Bug:本质是“RK3588 配置串味”

foundation-rk3568 这一层是从 RK3588 抄过来的,内核 kconfig 里全是 RK3588 的开关。前 5 个 bug 都是这么来的。

# Bug 类型 现象 根因 解法
1 CRU 时钟驱动缺失 kconfig 卡 Waiting for root device CLK_RK3588=y 但无 CLK_RK3568 CONFIG_CLK_RK3568=y + CPU_RK3568=y
2 GPU CSF 错配 kconfig GPU 寄存器映射失败 -5 CSF 是 RK3588 Valhall 的特性 关 MALI_CSF_SUPPORT
3 GPU devfreq 死锁 kconfig GPU probe 后硬死 devfreq 触发 DDR DVFS 经 ATF 死锁 关 MALI_BIFROST_DEVFREQ
4 clk_disable_unused 锁死 bootargs 启动后期 SoC 死 内核关掉了无人认领的关键时钟 clk_ignore_unused
5 DTB chosen/fiq 劫持 dts VFS panic 重启循环 vendor dtsi 硬编码 eMMC root /delete-node/ chosen; fiq-debugger
6 Mali 闭源死局 驱动架构 Chrome GPU 起不来 内核是孤儿版 g21p0 DDK 改用 Panfrost + Mesa
7 vfat 内核 bug 内核 bug 89 秒必崩重启循环 fat_fill_super NULL deref 关 CONFIG_VFAT_FS

Bug 1:CRU 时钟驱动缺失

现象:内核卡在 Waiting for root device PARTUUID=...,rootfs 永远挂不上。

诊断:串口日志里所有外设都是 EPROBE_DEFER (-517) —— mmc、i2c、serial、usb、gpu 全部“延迟探测”。顺着 defer 链往上追:

rockchip-pm-domain: failed to get clk at index 0: -517

PM 域控制器拿不到时钟 → CRU(主时钟控制器)没绑定 → 谁要时钟谁 defer。

根因:kconfig 抄自 RK3588,有 CONFIG_CLK_RK3588=y 但没有 CONFIG_CLK_RK3568。clk-rk3568.c 根本没编进内核。

隐藏陷阱(这个很值得记):加了 CONFIG_CLK_RK3568=y 还不够。olddefconfig 会静默丢弃它 —— 因为 Kconfig 里 CLK_RK3568 depends on CPU_RK3568 || COMPILE_TEST,而配置里是 CPU_RK3588=y + # CONFIG_CPU_RK3568 is not set。

两个都要开:

CONFIG_CLK_RK3568=y
CONFIG_CPU_RK3568=y

教训:kconfig 选项被静默丢弃是常态。改完一定要回头 grep 生成的 .config 确认它真的在。

Bug 2:GPU CSF 错配

现象:Register map failed error = -5、Insufficient register space。

根因:CONFIG_MALI_CSF_SUPPORT=y。CSF(Command Stream Frontend)是 Valhall 架构的特性(RK3588 的 Mali-G610)。RK3568 的 Mali-G52 是 Bifrost 架构,用 JM(Job Manager)。

CSF 驱动会把 DT 里的 reg_size 覆盖成 CSF doorbell 的大小,在 G52 较小的寄存器区上越界,request_mem_region 失败。

Bug 3:GPU devfreq 死锁

现象:关掉 CSF 后 GPU probe 成功了,但启动约 0.74 秒硬死 —— 串口冻结,冷启动必死、热重启正常。

诊断关键:冷热差异 = 经典的 regulator/DVFS 问题。冷启动时 PMIC 电压从默认值开始,GPU/NPU 的 OPP 电压设置经 ATF 触发 DDR 频率切换,然后死锁。

根因:CONFIG_MALI_BIFROST_DEVFREQ=y 让 GPU 参与 devfreq,经 rockchip-dmc 触发 DDR DVFS。

这个“冷热行为不同 → 怀疑电源/时序”的判断方式,8 月排查 NPU panic 时又用上了一次。

Bug 4:clk_disable_unused 锁死 SoC

现象:前面都修好后,启动后期 SoC 死,而且假象很多 —— 死点看着在 GPU/NPU/蓝牙附近,实际都无关。

诊断手段(很通用):bootargs 加 initcall_debug ignore_loglevel,逐个 initcall 追踪,看到:

calling clk_disable_unused+0x0/0xe4 @ 1     ← 进去了就没出来

clk_disable_unused() 是 late_initcall,会关掉所有“没有驱动认领”的时钟。RK3568 上某个关键但无认领的时钟被关掉 → SoC 锁死。

解法:bootargs 加 clk_ignore_unused。这是 boot.scr 层的改动,不用重编内核。

Bug 5:DTB chosen/fiq 劫持

现象:换上正确的 vendor dtb 之后,VFS panic 重启循环。

根因:vendor 的 rk3568-lubancat-2.dtsi 里硬编码了:

chosen {
    bootargs = "...console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";
};

614e0000 是厂商 eMMC 系统的 root PARTUUID。我们从 SD 卡启动,PARTUUID 完全不同。这个 chosen 节点劫持了内核 cmdline,我们从 boot.scr 传的参数根本没生效。

解法:dts 里删掉:

/ { /delete-node/ chosen; /delete-node/ fiq-debugger; };

副作用(8 月才发现):删掉 fiq-debugger 之后 UART2 变成无人认领,/dev/ttyS2 从未注册,内核控制台接管不了 earlycon,串口彻底没了。所以还要补一句 &uart2 { status = "okay"; }; —— vendor dtsi 里 uart2 是 disabled 的,因为它本来被 fiq-debugger 占着。

Bug 7:vfat 内核 bug

现象:桌面起来后约 89 秒必崩,重启循环:

Internal error: Oops: 0000000096000005
pc : fat_fill_super+0x10c/0x1030 [fat]
Comm: mount
Kernel panic - not syncing: Oops: Fatal exception

诊断过程(排除法教科书):

  1. 关掉 eMMC(sdhci disabled)→ 仍崩 → 不是 eMMC
  2. 系统里唯一的 vfat 是 SD 卡的 EFI 分区(p12),但 fsck 完全正常
  3. 崩溃前 1.5 秒出现 loop6(14336 扇区 ≈ 7MB)→ 某个组件在用 loop 挂载一个 vfat 镜像
  4. fat_fill_super+0x10c 是 NULL deref(偏移 0x1c)→ fat 驱动自己的 bug(6.1.75-rockchip)

解法:桌面根本不需要 vfat(SD 的 EFI 分区是给 U-Boot 读的,内核不用挂),从内核里拔掉:

# CONFIG_FAT_FS is not set
# CONFIG_VFAT_FS is not set

临时验证时是删 vfat.ko.gz / fat.ko.gz 模块(因为原本是 =m)。

4. Bug 6 单独成章:GPU 的闭源死局与开源破局

这是整个移植最硬的一关。

闭源死局

板子的 GPU 是 Mali-G52。要用闭源栈,需要内核态 DDK(kbase)和用户态 libmali 版本严格匹配。

问题在于:厂商内核带的是一个孤儿版 g21p0 DDK,而世界上不存在与之匹配的 G52 libmali 二进制。ARM 只把 libmali 发给 SoC 厂商,Rockchip 公开的 libmali 仓库里没有这个组合。

这不是配置问题,是这个东西不存在。 试过让内核谎报 DDK 版本(openfyde: report DDK version g24p0-00eac0 to match libmali)、试过放宽 UK 握手(openfyde: permissive mali UK handshake),都是死路 —— 握手能糊弄过去,但 ABI 对不上,EGL 初始化照样失败。

开源破局

改用 Panfrost(内核态)+ Mesa(用户态)。这是一条完全独立的实现,不依赖任何闭源库。

make.conf:

USE="${USE} -mali panfrost"

-mali 是必须的 —— 上游 overlay-inaugural 会继承下来一个 mali 的 USE flag,不显式关掉会打架。

怎么证明 GPU 真的在加速

不用进桌面、不用开 chrome://gpu。 板子上跑:

ls -l /proc/*/fd 2>/dev/null | grep -o 'renderD[0-9]*' | sort | uniq -c
for n in 128 129 130; do echo -n "renderD$n -> "; basename $(readlink -f /sys/class/drm/renderD$n/device/driver); done

实测:

4 renderD129        ← 4 个 fd 正持有
renderD128 -> rockchip-drm   (显示控制器)
renderD129 -> panfrost       ★ GPU
renderD130 -> RKNPU

判据为什么成立:libgallium-25.0.0.so 里 panfrost(硬件)和 swrast(软件)都在,所以“库被加载了”区分不了两者。但只有硬件加速会去打开 DRM render node,软件渲染不会。renderD129 被实际占用 = Panfrost 在干活。

DRM minor 编号从内核 log 里对:[drm] Initialized panfrost 1.2.0 ... on minor 1 → minor 1 → renderD129。

Chrome 沙箱

Panfrost 是运行时 dlopen 加载 libgallium-25.0.0.so 的,而 Chrome 的 GPU minijail 沙箱会拦住这个动作 —— GPU 进程 Permission denied 直接死,Chrome 报 Software compositing fallback is unavailable。

bring-up 阶段先加 --disable-gpu-sandbox。这是临时手段,正路是修沙箱的路径白名单。


第二部分:八月 —— 让镜像独立可启动

7 月的成果有个前提:用 eMMC 上厂商的 U-Boot 去加载 SD 卡上的内核和 rootfs。镜像本身“能不能自己启动”从没验证过。8 月这一段就是解决这个,顺便把所有手工修补固化进构建系统。

5. SPL 堆耗尽(主戏,卡了两天)

5.1 现象

Trying to boot from MMC2
No misc partition
sys malloc pool space exhausted
alloc_read_gpt_entries: ERROR: Can't allocate lX bytes for GPT Entries
GPT: Failed to allocate memory for PTE
part_get_info_efi: *** ERROR: Invalid GPT ***
part_get_info_efi: ***        Using Backup GPT ***
spl: partition error
Trying fit image at 0x4000 sector
## Checking atf-1 ... OK
## Checking uboot ... sha256(...) + Error: inflateInit2() returned -4
uboot: decompress error, ret=-1

inflateInit2() 返回 -4 = Z_MEM_ERROR,zlib 申请不到解压窗口。注意 sha256 校验是 OK 的 —— 镜像内容无损,ATF 和 FDT 也都加载成功,唯独 U-Boot 需要解压所以挂了。

5.2 三条弯路(都只是把死亡点挪位置)

弯路一:把 FIT 里的 U-Boot 存成未压缩。

拆开 FIT,用 compression="none" 重新打包。结果 inflateInit2 那个错确实没了,但死点后移到加载 loadables:

## Checking uboot 0x00a00000 ... OK
## Checking fdt ... OK
sys malloc pool space exhausted
## Checking atf-2 0xfdcc1000 ... OK   ← 还能继续,但最终还是挂

弯路二:抹掉 SD 卡的 idblock(扇区 0x40 的 RKNS 魔数),想让 BootROM 转去用 eMMC 的 SPL。

结果:BootROM 确实转向 eMMC 了,但没用。SPL 被载入之后,会按自己编译时写死的设备顺序找完整 U-Boot,而 MMC2(SD)排第一,于是照样跑去读 SD。

BootROM 从哪里载入 SPL、和 SPL 去哪里找 U-Boot,是两件完全独立的事。

弯路三:连 SD 上的 FIT 一起抹掉(0x4000 主副本 + MB 12 / MB 14 的备份副本,都在 RWFW 分区里,不伤数据)。

结果 —— 这条是决定性证据:

Trying fit image at 0x4000 sector
Not fit magic                    ← 抹除生效
Trying to boot from MMC1         ← 成功退到 eMMC
...GPT 报错 × 8 轮...
## Checking uboot ... sha256(851953c210...) + OK   ← sha 不同,确认是 eMMC 自己的 FIT
sys malloc pool space exhausted
## Checking atf-2 ... [复位]

它真的用上了 eMMC 的厂商 U-Boot,然后死在完全相同的地方。

5.3 根因

看弯路三那段:SPL 转到 MMC1 之后,又打了 8 轮 GPT 报错。对上 U-Boot 源码:

  • SPL 用的是 malloc_simple —— 只分配、从不释放
  • ChromeOS 标准 GPT 是 128 项 × 128 字节 = 16KB,一次申请就超过整个堆
  • SPL 会逐个 MMC 设备枚举分区表,主表失败试备份表,每个设备好几轮

结论:只要 SD 卡插着、卡上带着那张 16KB 的 GPT,SPL 的堆就必然被啃穿,之后从哪个设备加载 U-Boot 都会失败。

厂商系统为什么没事?eMMC 上是 512 字节的 MBR,解析代价几乎为零。

我一度以为 eMMC 上有 Rockchip 私有的 parameter 分区(log 里 No misc partition 就是 SPL 在找它),实测前 32MB 里一个 PARM 都没有。这条假设是错的,别再往这个方向查。

5.4 解法:GPT 分区项 128 → 32

16KB → 4KB,SPL 就吃得下了。

我们只定义 12 个分区,全在前 32 个槽位,所以分区项本身、起止扇区、PARTUUID 一律不动,只改三个字段:

偏移 字段 改法
80 NumberOfPartitionEntries 128 → 32
88 分区项数组 CRC32 按 32×128 = 4096 字节重算
16 头部 CRC32 先把该字段清零,再算整个头

主表和备份表都要改。核心就这几行:

struct.pack_into("<I", hdr, 80, 32)
struct.pack_into("<I", hdr, 88, zlib.crc32(arr) & 0xffffffff)
struct.pack_into("<I", hdr, 16, 0)
struct.pack_into("<I", hdr, 16, zlib.crc32(bytes(hdr[:hsize])) & 0xffffffff)

效果立竿见影:alloc_read_gpt_entries 和 sys malloc pool space exhausted 全部消失,所有 loadables 加载成功,顺利跳进 U-Boot。

困了两天的问题,靠改一张分区表解决,没有重编 U-Boot。

5.5 一个无法调和的冲突

把缩项做进构建流程(board_make_image_bootable())之后,构建炸了:

WARNING: Primary GPT header is invalid
ERROR: GptValidityCheck() returned 2: GPT_ERROR_INVALID_HEADERS
mount: wrong fs type, bad option, bad superblock on /dev/loop16

原因:vboot 的 cgpt 硬性要求分区项数组总大小恰好 16KB(校验 number_of_entries * size_of_entry 是否等于写死的常量)。

于是:

  • SPL 要求 ≤ 4KB
  • cgpt 要求 = 16KB

没有哪个值能同时满足。 所以缩项只能作用在最终产物上,等 ChromeOS 的工具全部操作完镜像之后再做 —— 落在 scripts/flash_sd.sh 里。

sgdisk 和 parted 都接受 32 项的 GPT,只有 vboot 的校验更严。

6. 两颗 U-Boot 各会一半,与 NPU panic

SPL 打通后暴露的下一层问题。

U-Boot 出处 执行 boot.scr NPU 电源域
厂商 2017.09-231011 #jiawen eMMC 扇区 0x4000 ✗ SCRIPT FAILED(太老,没有 part uuid) ✓ 正常
自编 next-dev-gaeec6f2-250929 #ty overlay 打包的 ✓ 正常 ✗ panic

一个重要发现:overlay 里 sys-boot/rk-uboot-lubancat2/files/uboot.img 装的其实是自编那颗。以前从没暴露问题,是因为每次都是 eMMC 的厂商 U-Boot 抢先干活 —— SD 能独立启动之后,才第一次真正用上它。

NPU panic

rockchip-pm-domain fdd90000.power-management:power-controller:
  failed to get ack on domain 'npu', target_idle = 0, target_ack = 0, val=0x6
Kernel panic - not syncing: panic_on_set_idle set ...
  rockchip_pmu_set_idle_request → rockchip_pd_power → rk_iommu_driver_init

三个要点:

  1. panic() 是 vendor 代码里无条件的(drivers/soc/rockchip/pm_domains.c:636),拿不到 ack 就死,没有开关可关
  2. 触发者是 IOMMU,不是 NPU 本身(看调用栈 rk_iommu_driver_init),所以 rknpu 和 rknpu_mmu 两个节点都要禁。label 在 rk356x.dtsi:1100 和 :1242
  3. 曾出现过厂商 4.19 内核也同样 panic,冷断电(拔电源等 30 秒)后恢复 —— 说明存在可被冷断电清除的 PMU 闩锁成分。但我们的内核冷断电后仍 panic,所以不能只靠这个

解法:

&rknpu { status = "disabled"; };
&rknpu_mmu { status = "disabled"; };

openFyde 用不到 NPU(GPU 走 panfrost),为一个用不上的外设去补自编 U-Boot 的板级初始化不划算。

差点走错的方向

一度怀疑是 dtb 变了 —— 手工版 113188 字节 vs 构建版 112696 字节,NPU 恰好在同期开始 panic,看起来因果关系很强。

实测:把两份 dtb 反编译成 dts 做 diff,实质差异只有一处 —— serial@fe660000(UART2)从 disabled 变 okay,正是我们自己要的改动,跟 NPU 毫无关系。

时间上相邻 ≠ 有因果。能 diff 就别推理。

7. 固化进构建系统:portage 的那些坑

这一关代价最大。有三样东西一直是“手工刷上去就好使”但从没进版本控制:boot.scr、串口 shell job、未压缩 U-Boot。结果一张全新卡写上去,三个坑一次性全炸。

7.1 boot.scr

镜像里的 boot-A.scr.uimg 只有 110 字节(上游 chromeos-base/u-boot-scripts 那个两行的通用模板),而能启动的手工版是 612 字节。

不用自己造轮子 —— FydeOS 的 sys-boot/rk3588-uboot-script 早留了口子:用 files/boot-A.cmd 模板 + sed 替换 #ROCKCHIP_DTS# 和 #EXTRA_BOOT_ARGS#。所以只要在 make.conf 里:

EXTRA_BOOT_ARGS="cros_debug cros_secure console=ttyS2,1500000n8 clk_ignore_unused earlycon=uart8250,mmio32,0xfe660000 loglevel=7"

三项的作用:

  • console=ttyS2,1500000n8 —— UART2 是调试口,1.5M 波特率
  • clk_ignore_unused —— Bug 4 的解法,不加会在启动后期锁死
  • earlycon=uart8250,mmio32,0xfe660000 —— UART2 的 MMIO 基址,让内核最早期就能出字

但有个包冲突要一并修:上游 chromeos-base/u-boot-scripts 也往同一路径装它那个两行模板,谁后装谁赢。修法是放一个 rk3588-uboot-script-0.0.1-r4.ebuild 覆盖 baseboard 的 -r2,加上:

RDEPEND="chromeos-base/u-boot-scripts"

逼 portage 后装我们。构建日志里验证顺序:

Installing (350 of 767) chromeos-base/u-boot-scripts-0.0.1-r10     ← 上游先
Installing (425 of 767) sys-boot/rk3588-uboot-script-0.0.1-r4      ← 我们后

判据:产物 612 字节 = 对,110 字节 = 被盖了。

7.2 串口 shell

原来是就地改 console-ttyFIQ0.conf,而那文件是 chromeos-base/tty 构建时生成的,改了必然留不住。

tty 包是 USE flag 驱动的(tty_console_ttyS2、tty-baud-1500000),但光开 USE 不够:

  • 模板里的 if crossystem "cros_debug?1" 在本板会失败(没有可用的 VBoot NVRAM)
  • agetty 不带 -n -l /bin/bash 会要求登录,而 bring-up 阶段没有可用账号

所以自带一份 console-ttyS2.conf:

env TTY_BAUD_RATE=1500000
script
  exec agetty -n -l /bin/bash "${TTY_BAUD_RATE}" ttyS2 linux
end script

★ 这个文件必须放在独立包里,不能塞进 chromeos-bsp-lubancat2-openfyde。

因为 bsp inherit chrome-dev-flag,而 chrome-dev-flag.eclass 里的 src_install() 是裸函数定义、没有 EXPORT_FUNCTIONS —— 在 ebuild 里再定义一个同名函数会直接覆盖掉它,六个 Chrome GPU flag 全部消失且毫无报错。

验收时务必回头看一眼 /etc/init/ui.override 里六个 flag 还在不在。

7.3 ttyFIQ0 无限重启

我原以为“stock 的 ttyFIQ0 job 因为设备不存在会自己 stop,无害”。错的:

init: console-ttyFIQ0 main process (4520) terminated with status 1
init: console-ttyFIQ0 main process ended, respawning

那个 job 声明了 respawn,agetty 退出码 1,upstart 每 10 秒重启一次、永远循环,日志直接刷在串口上 —— 而串口是这块板子唯一的调试通道。

解法是装 /etc/init/console-ttyFIQ0.override,内容一行:

manual

不要去改那个 .conf,它是生成的。

7.4 Chrome GPU flags 进不了镜像:四层原因

症状:flags 在 sysroot 的 chrome_dev.conf 里有,镜像里没有。

  1. 依赖链断裂(根本原因):overlay-inaugural-openfyde 提供的 virtual/fydeos-board-spec-0.0.1-r2 的 RDEPEND 里没有 virtual/openfyde-board-spec,于是 chromeos-bsp-lubancat2-openfyde 从来没被装进任何镜像。补一个同名同版本的盖掉它(版本必须匹配才盖得住,0.0.1 会输)
  2. virtual/u-boot 版本输了:我们的是 0.0.1,chromiumos-overlay 的是 1-r3,portage 跨 overlay 比版本号。改名为 u-boot-1-r4
  3. 上游 profile 主动拉黑:CHROME_REMOVE_FLAGS="${CHROME_REMOVE_FLAGS} --disable-gpu-sandbox",eclass 按名字过滤,精准剔掉这一个而放行其余五个,现象极具迷惑性
  4. ebuild 改了但版本没变 → portage 复用旧 binpkg

关键认知:真正让 Chrome 吃到 flag 的是 /etc/init/ui.override(env CHROME_COMMAND_FLAG="..."),不是 chrome_dev.conf。而 eclass 是 grep -e "^#" 旧文件 > 新文件 再追加,后装的包整个覆盖而非合并 —— 所以既要 RDEPEND fydeos-default-chromedev-flags 保证我们后装,又要把它那三个 flag 一并抄进我们的 CHROME_DEV_FLAGS。

最终六个:

--use-gl=angle --use-angle=gl --disable-gpu-sandbox
--disable-features=CrostiniUseDlc --disable-buffer-bw-compression --enable-features=QuickUnlockFingerprint

7.5 构建系统会骗你

cros-workon 用本地树还是克隆,取决于本地 HEAD 是否等于 CROS_WORKON_COMMIT:

  • 相等 → 直接用 /mnt/host/source/src/third_party/kernel/v6.1-rockchip,改本地文件立即生效
  • 不等 → 克隆到 work/,改本地文件完全无效

日志里 >>> Compiling source in <路径> 那行是判断依据,排查时必看。改内核后必须同步 bump CROS_WORKON_COMMIT。

dts 有没有被编译,看日志里 DTC 的行数:339 = 没编我们的,340 = 编了。

Linux 的 dts 是白名单制:光把 .dts 放进目录等于不存在,必须在 arch/arm64/boot/dts/rockchip/Makefile 里登记。而且 make -k 会吞掉 dtc 的失败继续跑,导致 emerge 报 rc=1 但错误要往回翻一千多行。

RK_FUNC_1/2 宏在上游 6.1 被删了(dt-bindings/pinctrl/rockchip.h 只剩 RK_FUNC_GPIO),vendor dtsi 还在用。dtc 报一句没头没尾的 rk3568-lubancat-2.dtsi:1082.10-11 syntax error。补回 RK_FUNC_1..4 即可。

8. 刷卡:Windows 会篡改 GPT

全新卡写好、上板,秒炸:

GUID Partition Table Entry Array CRC is wrong: 0x791f26b4 != 0xc71c0011
part_get_info_efi: *** ERROR: Invalid GPT ***

镜像本身是好的 —— 在 VM 上验证过:头里存的 CRC 0x791f26b4 正好等于 4096 字节数组的实际 CRC,sgdisk 也不报错。

把卡上的 GPT 头逐字段打印:

字段 卡上的值 说明
n 32 项数没被改
ecrc 0x791f26b4 还是我们写的原值
entries_lba 26 原本是 2 —— 被挪了
LBA 26 处实际 CRC 0xc71c0011 那里根本不是我们的数组
头 CRC 匹配 说明头被重新计算过
alt_lba 120881151 备份 GPT 被挪到 57.6G 盘尾

根因:我们镜像的备份 GPT 在 8.4G 处,而卡是 57.6G —— 备份 GPT 不在盘尾。Windows 认为这是损坏,于是“修复”:重写 GPT 头、把备份挪到盘尾、把分区项数组指针改成 LBA 26,却没把数组数据搬过去,也没重算数组 CRC。

我们的数组从头到尾完好地躺在 LBA 2(CRC 对得上,第一个分区名是 STATE)。被破坏的只有一个指针。

修法:把镜像的前 34 扇区 + 备份区(skip=16564348 count=33)dd 回卡上,几十 KB 的事。

结论:别用 Windows 写卡,走 Linux 侧的 scripts/flash_sd.sh。

诊断要点:读 GPT 头偏移 72 处的 entries_lba,是 2 就正常,是 26 就是被 Windows 动过。

9. 分区布局(备查)

Number  Start(sector)   End         Size       Name
   11        64          65599      32.0 MiB   RWFW        ← idblock@0x40, FIT@0x4000 在这里面
    6      65600         65600      512 B      KERN-C
    7      65601         65601      512 B      ROOT-C
    9      65602         65602      512 B      reserved
   10      65603         65603      512 B      reserved
    2      69632        135167      32.0 MiB   KERN-A
    4     135168        200703      32.0 MiB   KERN-B
    8     200704        233471      16.0 MiB   OEM
   12     364544        495615      64.0 MiB   EFI-SYSTEM  ← boot.scr 在 /boot/boot.scr.uimg
    5     495616        499711       2.0 MiB   ROOT-B
    3     499712       6078463       2.7 GiB   ROOT-A
    1    6078464      16564332       5.0 GiB   STATE

裸扇区:

  • 0x40 (64):idblock,魔数 RKNS = 524b4e53
  • 0x4000 (16384):U-Boot FIT,魔数 d00dfeed
  • MB 12 / MB 14:FIT 的备份副本

都在 RWFW 区内,这个分区 ChromeOS 本身不用,往里写引导器是安全的。

10. 复现步骤

# 1. repo sync 之后先打树外 patch(这些文件不在 overlay 里,sync 会 revert)
git -C chromite apply .../patches/chromite/*.patch
git -C src/scripts apply .../patches/scripts/*.patch

# 2. 内核:apply patches/kernel/*,然后把 CROS_WORKON_COMMIT 指向结果

# 3. 构建
cros build-image --board=lubancat2-openfyde --noenable_rootfs_verification base

# 4. 刷卡(一定从 Linux 侧)
scripts/flash_sd.sh .../chromiumos_base_image.bin /dev/sdX

只改了 rootfs 的话可以只刷 ROOT-A(启动走 EFI 分区里的 boot.scr,不动它就还能起来)。不落盘的流式刷法:

# 编译机侧切出来
dd if=<image> bs=1M skip=244 count=2724 | gzip -1 > /tmp/roota.gz
# PC 只做管道中转
plink ... "cat /tmp/roota.gz" | ssh board "gunzip -c | sudo dd of=/dev/mmcblk1p3 bs=1M conv=fsync"

实测 87 秒写完 2.7G,等效 32.8 MB/s。

11. 方法论:真正学到的

① 调试的第一步是定位层级,不是改代码。 整个过程本质就是把失败点一层层往下推:SPL 堆耗尽 → U-Boot 解压失败 → 内核 NPU panic → init 起来 → 桌面。

② 越早期的阶段,资源约束越离谱。 SPL 的堆只有几 KB,而 ChromeOS 的分区表要 16KB。这不是谁写错了代码 —— 是两套上游各自合理的假设撞在了一起。移植工作的本质就是在中间找可行解。

③ 同一份二进制,喂不同的数据就是活和死的区别。 eMMC 和 SD 上跑的是同一颗 SPL、同一个 git hash,一个活一个死,差别只在盘头那张表。

④ 设计能证伪的实验,而不是连续试修法。 抹掉 SD 的 FIT 逼 SPL 退到 eMMC —— 还是死。这一下同时排除了“设备顺序”和“压缩”两个假设。

⑤ 能 diff 就别推理。 dtb 字节数变了 + NPU 开始 panic,看起来因果很强。反编译一 diff,实质差异只有一个 UART2。

⑥ 非确定性 = 先怀疑物理层。 Bug 3 的“冷启动必死、热重启正常”指向 DVFS/regulator;8 月 NPU panic 靠拔电源等 30 秒清掉 PMU 闩锁。这类症状不要去查逻辑。

⑦ 手工修补就是债,一定会被追讨。 boot.scr、串口 job、未压缩 U-Boot 三样都是“手工刷上去就好使”但没进版本控制,全新卡一写三个坑一起炸。每次手工修好一个东西,当场就问:这进 overlay 了吗?

⑧ 配置项会被静默丢弃。 CONFIG_CLK_RK3568=y 被 olddefconfig 悄悄扔掉,因为依赖没满足。改完一定回头 grep 生成的 .config。

⑨ “应该没事”这四个字要禁用。 “ttyFIQ0 设备不存在会自己停,无害” —— 它带 respawn,每 10 秒刷一次屏。每一个“应该”都该换成一条实测。

12. 遗留问题

  • -kvm_host:Crostini 三个服务(vm_concierge/crosdns/vhost_user_starter)在疯狂重启,白烧 CPU
  • 自己编 U-Boot:现在的 idblock.bin 是从厂商 eMMC 扒的。正路是用 Rockchip 公开的 rkbin + 开源 U-Boot 源码自己编。这一件事能同时解决三个问题:blob 来源合法性、SPL 堆(直接改大 CONFIG_SPL_SYS_MALLOC_F_LEN 就不用缩 GPT)、NPU 初始化
  • Chrome GPU 沙箱:现在靠 --disable-gpu-sandbox 绕过,正路是修路径白名单
  • 千兆网:直连链路 1Gbps 下 93% 丢包,锁在 100Mbps 全双工
  • STATE 分区扩容:57.7G 的卡只用了 8.4G
  • 硬件视频解码:overlay 里有 rockchip-mpp,没确认接进 Chrome
  • MIPI 摄像头:7 月做过 IMX 传感器移植,没验证

附:关键提交

overlay:
  3ae0215  boot.scr 的 bootargs + 串口 console job
  d474826  GPT 缩到 32 项
  1d32de3  改为构建后执行(cgpt 要求 16KB,冲突不可调和)
  7bb2b5c  压制 ttyFIQ0 的 respawn
  562caf2  U-Boot 存未压缩
  a5625a7  flash_sd.sh
  babd702  整理成板级包(patches/ + README)

kernel (rk6.1-openfyde):
  c2eb46c  add EmbedFire LubanCat-2 (RK3568)
  263bf65  restore RK_FUNC_<n> aliases
  1b0dce1  disable the NPU