
上个月我把一块 RK3588 开发板从包装盒里拿出来第一件事不是找SD卡而是先把网线插上。用 TFTP 引导内核、NFS 挂载 openEuler 24.03 根文件系统这套网络化部署流程是我在 RK3588 平台上用下来最省心的系统迭代方式——不用反复烧录存储介质也不用担心 rootfs 被改坏之后要拆板子恢复。今天这篇就把从 TFTP 启动到系统完美运行的全过程写清楚服务器端怎么搭、openEuler 24.03 的 rootfs 怎么造、U-Boot 怎么配、常见的坑有哪些以及最后如何把系统从网络盘平滑迁移到 eMMC 或 NVMe。1. 为什么非要折腾网络化部署把“反复烧卡”换成“重新上电”1.1 传统烧录模式的时间账很多人拿到 RK3588 开发板的第一反应是做一张 SD 启动卡或者用 rkdeveloptool 往 eMMC 里烧镜像。这个流程本身没问题但只要你进入“改代码-烧系统-看日志-再改代码”的循环就会发现时间全浪费在存储介质上。我举个例子一张 32G 的 TF 卡写一次完整镜像快的也要五到十分钟。改一次内核配置整个镜像就得重做系统启动到一半把 rootfs 写坏了又得把卡拔下来重新写。如果同时调三块板子每块板子都要重复这套物理操作还要小心别把 A 板的卡插到 B 板上。更别提 RK3588 有些板卡 eMMC 烧录时还要进 maskrom 模式手一抖就识别不到设备整个下午就耗进去了。1.2 网络化部署到底改了什么网络化部署的核心思路是开发板本地只放一个能启动的 U-Boot内核、设备树、根文件系统全部从服务器获取。TFTP 负责传输内核和 dtb 这种几 MB 到几十 MB 的小文件局域网环境下几秒就完成根文件系统通过 NFS 挂载到服务器上的一个共享目录开发板上运行的就是这个“远程目录”。这意味着你改完代码后不需要写卡、不需要插拔只需要把新的内核放到 TFTP 目录、把新的 rootfs 更新到 NFS 导出目录然后开发板重新上电或者 reboot就会以全新状态启动。板子崩溃了重启就行反正 rootfs 是服务器上的一个目录随时可以回滚。我还特别喜欢它的多板管理能力。调试 RK3588 时几块板子同时跑不同版本的内核或者 rootfs传统方式要烧好几张卡网络化部署只需要在服务器上建对应目录每块板子通过 U-Boot 环境变量选择加载哪一份重启就能切换。1.3 适用边界它是什么不是什么这套方案适合的开发场景非常明确内核调试和驱动开发需要频繁更换内核镜像rootfs 定制需要反复增删软件包同时调试多块 RK3588 板卡需要验证“干净的 rootfs 能不能启动”这类基础问题但它不是拿来当量产方案的。正式产品最终肯定要把系统落进 eMMC、NVMe 或者 SD 卡因为生产环境不能依赖一台服务器和一根网线。我更愿意把它定义为开发调试阶段的基础设施加速器不是终点。前置条件有三个板子上的 U-Boot 能正常启动并驱动网卡服务器和开发板在同一个二层网络内服务器上能跑 TFTP/DHCP/NFS 服务。这三样满足之后剩下的就是配置问题。2. RK3588 启动链拆解TFTP 到底顶替了哪一环2.1 上电到 Linux 的五级跳转很多人一听到“TFTP 启动”就以为网络引导是从芯片上电开始接管一切其实不是。RK3588 的启动链路是一条很固定的流水线芯片固化 BootROM上电后先初始化 DDR然后从 eMMC、SD、SPI NOR 等介质中按优先级寻找 idbloader。idbloader ATF负责 DDR 初始化、Trusted Firmware 运行环境建立之后跳转到 SPL。U-Boot proper真正完整的 U-Boot 在这里运行网卡驱动、文件系统驱动、网络协议栈都可用。内核U-Boot 从内存地址加载 Image 并跳转。根文件系统内核挂载 rootfs进入 userspace。TFTP 介入的位置是第 4 步之前。U-Boot 启动后执行 dhcp 拿到 IP然后通过 TFTP 从服务器拉取 Image 和 dtb再通过 booti 启动内核。换句话说本地介质上的 idbloader 和 U-Boot 一个都不能少TFTP 只是替掉了“本地存储中放内核和 rootfs”这一层。2.2 U-Boot 在 RK3588 网络引导里的三个隐性要求第一网卡驱动必须编译进 U-Boot。RK3588 集成的是 DesignWare 系列的 GMAC但不同板卡的 PHY 芯片不一样常见的有裕太微 YT8531、瑞昱 RTL8211F 这些。如果你拿到的 U-Boot 没有适配对应 PHYdhcp 拿不到 IP后面全是白搭。第二U-Boot 里要有正确的加载地址环境变量。RK3588 是 64 位 ARM内核 Image 一般加载到内存高地址默认通常是kernel_addr_r0x10000000、fdt_addr_r0x0f000000这一带。不同 BSP 可能不一样进 U-Boot 后务必先printenv确认。第三要用对启动命令。RK3588 跑的是 aarch64 内核文件格式是 Image启动命令是booti不是 32 位平台常用的bootz。2.3 openEuler 24.03 对 RK3588 的支持边界openEuler 24.03 是基于 Linux 6.6 内核的 LTS 发行版本身对 AArch64 支持很成熟内核里也有 RK3588 的基础平台代码。但这里有个现实问题openEuler 的目标场景主要面向服务器和云基础设施它不会为某块具体的 RK3588 开发板去调整内核 config更不会预编译好板级固件。所以我一般建议采用“组合方案”自己编译一个针对 RK3588 板卡的内核rootfs 使用 openEuler 24.03 的包管理体系。这样既能享受 dnf、systemd、openEuler 软件源这些发行版优势又能保证网卡、PCIe、USB 等驱动适配板子。下面是两个方案的对比方案优势风险openEuler 自带内核包管理统一更新方便config 可能缺板级驱动NFS/网卡需额外确认自编内核 openEuler rootfs驱动可控外设支持好内核需要自己维护模块路径要手动放置如果你坚持用 openEuler 仓库里的内核至少得确认这几个 config 是开启的CONFIG_ROOT_NFS、CONFIG_NFS_FS、CONFIG_NFS_V3、CONFIG_STMMAC_ETH以及对应 PHY 驱动。自编内核虽然前期麻烦但后面调外设时会省下大量时间。3. 服务器端准备TFTP、DHCP、NFS 三件套3.1 TFTP 服务搭建与文件摆放服务器以 Ubuntu/Debian 为例安装三个服务只需要一条命令sudo apt install tftpd-hpa nfs-kernel-server dnsmasqTFTP 的配置在/etc/default/tftpd-hpa我习惯把根目录固定到/srv/tftp# /etc/default/tftpd-hpa TFTP_USERNAMEtftp TFTP_DIRECTORY/srv/tftp TFTP_ADDRESS0.0.0.0:69 TFTP_OPTIONS--secure然后在这个目录里放好启动需要的文件/srv/tftp/ ├── Image # aarch64 内核 ├── rk3588-evb1-v10.dtb └── boot.scr # 可选重启服务后先用本机自测一下sudo systemctl restart tftpd-hpa tftp 127.0.0.1 -c get Image能成功下载说明 TFTP 这环通了。3.2 DHCP 如何告诉板子“去哪个服务器拿文件”开发板执行dhcp命令后需要从 DHCP 服务器拿到 IP 地址。如果只做手动引导DHCP 只要分配 IP 就够了如果想实现更自动化的引导可以在 DHCP 响应里带上 bootfile 选项让 U-Boot 自动去 TFTP 拉取指定文件。dnsmasq 的配置相当精简一个服务同时承担 DHCP、TFTP、DNS# /etc/dnsmasq.conf interfaceeth0 bind-interfaces dhcp-range192.168.1.100,192.168.1.200,12h dhcp-option3,192.168.1.1 dhcp-bootImage,192.168.1.10这里dhcp-bootImage,192.168.1.10的意思是告诉客户端去 192.168.1.10 这台服务器的 TFTP 服务下载名为Image的文件。需要说明的是U-Boot 里的dhcp命令拿到 bootfile 后会自动发起 TFTP 请求并加载到$kernel_addr_r如果你后面还执行了手动tftp命令注意别把文件加载到同一个内存地址造成覆盖。3.3 NFS 根文件系统导出在/etc/exports里加一行把 openEuler 的 rootfs 目录共享出去/srv/nfs/openeuler 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check,no_wdelay)然后执行sudo exportfs -rav生效。这里no_root_squash是重点如果不加开发板上的 root 用户写文件时会被映射成 nobodysystemd 启动时大量服务会因为权限问题失败而且错误信息非常隐蔽。内核启动参数里对应的写法是root/dev/nfs nfsroot192.168.1.10:/srv/nfs/openeuler,v3,tcp rw ipdhcp我推荐在nfsroot参数里强制指定v3。原因后面排障部分会详细讲嵌入式环境用 NFSv3 比 NFSv4 少很多 idmap 和权限相关的坑。3.4 防火墙与权限最容易翻车的三个细节第一端口要放行。TFTP 是 UDP 69NFS 涉及 TCP 2049 和 rpcbind 的 111mountd 还会动态分配端口。省事的做法是直接对开发板所在网段放行sudo ufw allow from 192.168.1.0/24如果是 CentOS/Rocky 这类带 firewalld 的系统记得把tftp、nfs、rpc-bind服务加入信任区。第二TFTP 目录权限。tftpd-hpa 的工作用户是 tftp文件至少要是 644 权限目录要有读和执行权限。我见过太多人把整个/srv/tftp设成 777结果服务反而因为安全策略拒绝读取。第三NFS 导出目录的父目录权限不能太开放。比如/srv的权限是 777NFS 客户端可能无法进入子目录。建议/srv保持 755/srv/nfs也保持 755导出目录本身可写即可。4. openEuler 24.03 根文件系统制作从 dnf 命令到可启动目录4.1 在 x86 服务器上构建 aarch64 rootfsopenEuler 没有像 debootstrap 那样的一键 rootfs 工具但可以用 dnf 的--installroot功能完成。在 x86 服务器上构建 aarch64 rootfs 时最关键的一点是安装并注册 qemu-user-static否则 rpm 脚本无法执行sudo apt install qemu-user-static binfmt-support sudo systemctl restart binfmt-support然后写一个 openEuler 24.03 的 repo 文件放到/etc/yum.repos.d/oe.repo[openEuler-24.03] nameopenEuler-24.03 baseurlhttps://repo.openeuler.org/openEuler-24.03-LTS/everything/aarch64/ enabled1 gpgcheck0执行安装命令。基础包列表我建议先保守一点装到能启动、能用 dnf、能 ssh 登录即可sudo dnf --installroot/srv/nfs/openeuler \ --releasever24.03 \ --forcearchaarch64 \ install systemd bash coreutils dnf util-linux \ openssh-server iproute2 procps-ng vim \ nfs-utils networkmanager构建完成后检查关键文件是否存在ls /srv/nfs/openeuler/sbin/init ls /srv/nfs/openeuler/lib/systemd/systemd cat /srv/nfs/openeuler/etc/os-release如果能通过 qemu-user-static 直接 chroot 进去执行命令说明 rootfs 基本可用。用官方 container 镜像导出 rootfs 也是一种省事做法但容易漏掉 systemd 所需的服务单元我建议还是用 dnf 构建一次后续增删包都统一走 dnf。4.2 内核、模块与固件的放置规则内核我推荐用 Rockchip 的 BSP 内核或者 Linux 6.6 稳定版自己编。编译命令大致如下make ARCHarm64 rockchip_linux_defconfig make ARCHarm64 Image dtbs -j$(nproc) make ARCHarm64 modules_install INSTALL_MOD_PATH/srv/nfs/openeuler编译产物中arch/arm64/boot/Image和arch/arm64/boot/dts/rockchip/rk3588-*.dtb放到 TFTP 目录modules_install会把模块装到 rootfs 的/lib/modules/kernel-version下。这一步千万别省很多板子启动后eth0找不到就是因为网卡驱动模块没装进 rootfs而内核把对应驱动编成了模块而不是编进内核。固件文件同样重要RK3588 的 Mali GPU、NPU、WiFi/BT 都需要/lib/firmware里有对应固件。可以先从 Rockchip SDK 的 hardware/rockchip 目录里把 firmware 拷贝过来这样后续验证外设时能少踩很多坑。4.3 首启前必须处理的 systemd 与 fstab新 rootfs 的/etc/fstab里默认可能只有根文件系统一行。因为 NFS root 是由内核挂载的fstab 里再写根挂载反而会出问题我建议把这一行注释掉其他本地分区也全部注释避免 systemd 在 startup 阶段等待不存在的块设备。串口登录必须显式启用否则内核起来了也看不到登录提示符ln -s /usr/lib/systemd/system/serial-getty.service \ /etc/systemd/system/getty.target.wants/serial-gettyttyFIQ0.serviceRK3588 的 BSP 默认串口控制台是ttyFIQ0波特率 1500000。如果你不确定可以先用 U-Boot 的printenv console查看它传给内核的 console 参数保持一致。还有一个非常容易被忽略的点openEuler 默认启用 SELinux。在 NFS 根文件系统上第一次启动时所有文件的安全上下文都不对会出现满屏的 AVC denial服务起不来。最简单的处理是先关掉sed -i s/SELINUXenforcing/SELINUXpermissive/ /srv/nfs/openeuler/etc/selinux/config或者在内核启动参数里追加selinux0等系统完全跑通之后再决定要不要重新开启。5. U-Boot 引导配置从手敲命令到一键自动化5.1 手动 TFTP 流程先跑通第一次不要急着固化脚本先手动敲命令把链路走通。进 U-Boot 后依次执行setenv autoload no dhcp tftp ${kernel_addr_r} Image tftp ${fdt_addr_r} rk3588-evb1-v10.dtb setenv bootargs consolettyFIQ0,1500000 root/dev/nfs nfsroot192.168.1.10:/srv/nfs/openeuler,v3,tcp rw ipdhcp booti ${kernel_addr_r} - ${fdt_addr_r}逐条解释一下autoload no防止dhcp拿到 bootfile 后立刻自动加载避免和你后续手动 TFTP 冲突。dhcp让板子拿到 IP如果网络正常会打印出 IP、服务器 IP。两条tftp分别下载内核和设备树到不同的内存地址。bootargs指定 console、rootfs 方式、NFS 服务器地址、导出路径、NFS 版本、网卡 IP 获取方式。booti启动 64 位 Image中间的-表示没有 initrd。如果一切正常串口会陆续出现内核日志最后弹出 openEuler 的登录提示符。5.2 把命令固化进 bootcmd手动流程跑通后就可以把整串命令保存为默认启动项setenv bootcmd dhcp; tftp ${kernel_addr_r} Image; tftp ${fdt_addr_r} rk3588-evb1-v10.dtb; setenv bootargs consolettyFIQ0,1500000 root/dev/nfs nfsroot192.168.1.10:/srv/nfs/openeuler,v3,tcp rw ipdhcp; booti ${kernel_addr_r} - ${fdt_addr_r} saveenv这样每次上电之后 U-Boot 会自动执行网络引导。注意 bootcmd 里的变量展开时机问题单引号包裹时${kernel_addr_r}会在执行时展开所以没问题。如果板子数量多不想每块都手工敲环境变量可以做一个boot.scr放到 TFTP 目录。先把启动脚本写到boot.cmdsetenv bootargs consolettyFIQ0,1500000 root/dev/nfs nfsroot192.168.1.10:/srv/nfs/openeuler,v3,tcp rw ipdhcp tftp ${kernel_addr_r} Image tftp ${fdt_addr_r} rk3588-evb1-v10.dtb booti ${kernel_addr_r} - ${fdt_addr_r}用 mkimage 打包mkimage -A arm64 -O linux -T script -C none -n netboot -d boot.cmd boot.scr只需要在 U-Boot 里执行dhcp; source ${kernel_addr_r}或者配合 PXE 流程让它自动加载系统分发就变得特别方便。5.3 进阶PXE 与多镜像选择新版 U-Boot 支持bootflow机制可以扫描 DHCP/PXE/EFI 等多种引导源。比如你希望同一块板子在开工时选择 openEuler、Ubuntu 或 Rockchip 官方系统可以在服务器上配好 PXE 菜单。bootflow scan -lb就会列出可用的引导项。PXE 的完整配置依赖 DHCP option 67 和 209把 bootfile 指向grubaa64.efi再用 GRUB 的 menuentry 做多系统选择。这套东西功能很强但对于 RK3588 开发场景属于“锦上添花”。我个人经验是先把手动流程和 bootcmd 固化搞定再考虑 PXE 菜单因为绝大多数情况下 bootcmd 已经够用了PXE 反而增加了一层排查复杂度。6. 首启排障实录四个阶段的常见故障与真实案例6.1 先用串口日志给故障分层网络引导一旦出问题先别急着怀疑系统看一眼串口输出停在哪一步故障位置表现优先排查方向DHCP/TFTP卡在 DHCP 请求反复重试网线、服务器防火墙、dnsmasq 状态内核早期启动Starting kernel 后无任何输出console 参数、dtb 匹配、内存加载地址rootfs 挂载卡在 Waiting for root deviceNFS 导出、CONFIG_ROOT_NFS、nfsverssystemd 阶段大量服务报错或反复重启SELinux、rootfs 权限、关键服务缺失6.2 案例一DHCP 成功但 TFTP 下载超时有次我在新装的服务器上配好所有服务板子dhcp能拿到 IP但tftp下载 Image 时一直卡住。从服务器上看TFTP 服务也起来了文件也在。最后用tcpdump抓到关键信息sudo tcpdump -i eth0 udp port 69发现 TFTP 请求到了服务器但响应报文根本没发回去。原因就是防火墙只放行了 TCP 端口UDP 69 没放行。这类问题用ufw allow from 192.168.1.0/24或者干脆在测试阶段停掉防火墙就能解决。另外TFTP 工具用--secure选项时路径必须是 TFTP 根目录内的相对路径如果 U-Boot 里写了绝对路径也会下载失败。6.3 案例二卡在 Waiting for root device这是网络引导最常见的拦路虎。内核日志停在Waiting for root device /dev/nfs...先确认几件事服务器上执行showmount -e能看到导出目录。内核 config 里CONFIG_ROOT_NFS是y不是m否则内核启动阶段挂不了 NFS。rootfs 目录下有 /sbin/init。no_root_squash确实加上了。我踩过最隐蔽的一个坑是 NFS 版本不匹配。服务器默认开了 NFSv4内核 nfsroot 也尝试用 vers4 挂载但客户端环境里没有 idmap 相关服务导致挂载一直超时。后来在nfsroot参数里强制指定v3,tcp一次就过了。这也是我在前面的配置里坚持用 NFSv3 的原因。如果确认配置没问题可以直接在开发板的 U-Boot 命令行下临时改成ip192.168.1.200这种静态 IP 方式先试排除 DHCP 分配的干扰。6.4 案例三内核有输出但 systemd 疯狂报 SELinuxopenEuler 默认 SELinux 策略在 NFS 上第一次启动时基本必炸。它会显示大量类似audit: type1400 audit(...): avc: denied { write } for pid1 commsystemd name/ devnfs ino...这属于安全上下文不匹配不是系统坏了。我建议第一次启动时直接在内核参数加selinux0跑通后再考虑如何给 NFS rootfs 打正确的 SELinux 标签。如果确实需要保留 SELinux得在 chroot 环境下执行restorecon -R /但 NFS 挂载环境下的标签管理很麻烦开发阶段没有必要在这个点上死磕。6.5 案例四Starting kernel 后完全无输出这种情况十有八九是 console 参数不对或者 dtb 选错了型号。RK3588 不同板卡的串口可能接在 ttyFIQ0 上也可能被 BSP 映射成 ttyS0同一颗 SoC 的不同开发板dtb 也不通用。比如你在 EVB 的 dtb 上跑香橙派或某个第三方板卡很可能内核都没跑到串口初始化。调试手段有两个一是对比 U-Boot 的console环境变量和 bootargs 里的 console二是加earlyconuart8250,mmio32,0xfeb50000强制早期串口输出看内核到底死在哪个位置。7. 从网络引导到完美运行落盘迁移与日常维护7.1 把系统从 NFS rootfs 迁移到 eMMC/NVMe开发调试阶段结束后总要把系统装到本地存储。这里有个很实用的迁移路径开发板还在 NFS root 模式运行时直接挂载一块新分区用 rsync 把整个 rootfs 同步过去。mkfs.ext4 /dev/nvme0n1p1 mount /dev/nvme0n1p1 /mnt/target rsync -aAXHx / /mnt/target/-aAXHx这几个参数别省-A保留 ACL-X保留 xattr-H保留硬链接-x不跨界到其他文件系统否则系统跑起来会出各种奇怪问题。迁移后修改/mnt/target/etc/fstab把根分区改成对应 UUIDU-Boot 那边也改成从本地启动。不过我建议保留一个“网络引导 fallback”setenv bootcmd if load mmc 0 ${kernel_addr_r} /Image; then run bootcmd_local; else run bootcmd_net; fi这样本地系统损坏时只要服务器还开着插上网线重启就能回到网络引导环境相当于给板子留了一条救援通道。7.2 驱动与外设的验证清单系统跑起来只是第一步RK3588 的外设才是真正的工作面。我习惯按下面这张表逐项验证外设验证命令常见问题网络ip addr、ethtool eth0PHY 驱动缺失速率协商失败NVMelspci、nvme listPCIe 驱动未启用链路不稳定USB3lsusb -tType-C 控制器配置错误显示cat /sys/class/drm/card0/status需要 DRM 驱动和对应固件NPUls /dev/rknpu*需要 Rockchip 的 rknpu 驱动模块openEuler 的软件源里不会有 Rockchip 的 NPU 驱动和 Mali 用户态库如果项目做 AI 推理建议从 Rockchip SDK 里把rknpu模块和rknn-toolkit装进来。GPU 桌面环境也会麻烦一些我目前的经验是优先把 RK3588 当无头服务器或者纯计算平台用把 GPU/NPU 相关问题当作独立专项处理这样整体部署的复杂度会低很多。7.3 长期维护rootfs 版本化与备份网络化部署真正舒服的地方在于 rootfs 可以像代码一样做版本管理。我在服务器上习惯这样组织目录/srv/nfs/ ├── openeuler-24.03-base ├── openeuler-24.03-app └── openeuler - openeuler-24.03-app需要切换版本时只改软链接几块开发板重启后自动切换到新 rootfs。修改 rootfs 之前先打一个 tar 包tar --numeric-owner -czf /srv/backup/openeuler-24.03-$(date %F).tar.gz /srv/nfs/openeuler真把系统搞挂了也能快速还原。最后再提醒一句这种 DHCP TFTP NFS 的网络引导服务最好只在隔离的实验室网段里跑。把它放到办公网段一旦 DHCP 和别人冲突或者 NFS 目录被内网其他机器访问到排查起来相当痛苦。我在实际维护中一直坚持“开发网段独立、服务器服务最小化”的原则这套网络化部署才真正变得又稳又省心。