RK3588固件打包全流程解析:从build.sh到update.img
1. 固件打包这件事到底在打什么第一次接触RK3588固件打包的人最容易犯的错就是把它当成一个“点一下按钮”的活儿。实际上从你敲下编译命令到拿到一个能烧进板子的update.img中间要经过编译产物收集、分区镜像生成、打包配置、签名校验好几个环节。任何一个环节的路径写错、分区名对不上、权限没给够最后都会卡在烧录工具报错上而且报错信息往往含糊得让人抓狂。RK3588是瑞芯微目前主流的高性能八核处理器用在大屏设备、边缘计算盒子、工业控制板、AI推理终端上的项目非常多。它的固件结构比早期的RK3288、RK3399复杂不少分区数量多、镜像类型多尤其是引入了AB分区、安全启动、动态分区这些机制之后打包流程不再是简单的“把几个img拼在一起”。你如果去看Rockchip官方SDK里的build.sh和mkimage.sh会发现里面嵌套了好几层脚本调用参数传递链路很长不把这条链路理清楚出了问题根本不知道从哪查。这篇文章面向的是正在用RK3588做产品开发、需要自己出固件的工程师也适合刚接手RK3588项目、对打包流程还比较模糊的开发者。我会把从脚本调用到update.img生成的完整链路拆开讲包括每个环节在做什么、为什么这么做、参数怎么配、踩过的坑怎么绕。内容基于Rockchip Linux SDK的常见实践不同SDK版本Android 12、Ubuntu、Buildroot在细节上会有差异但核心逻辑是通的。提示本文讨论的是基于Rockchip官方Linux SDK的打包流程Android SDK的打包机制类似但脚本入口不同我会在相关章节标注差异点。2. 打包前的准备工作别急着敲命令2.1 确认SDK版本和目录结构RK3588的SDK目录结构在不同版本之间有变化但大框架是一致的。你拿到SDK之后先别急着编译花十分钟把目录结构看清楚后面能省好几个小时。典型的Rockchip Linux SDK根目录长这样sdk/ ├── build.sh # 顶层编译入口脚本 ├── device/rockchip/ # 板级配置 │ └── rk3588/ │ ├── BoardConfig.mk # 板级编译配置 │ └── ... ├── u-boot/ # U-Boot源码 ├── kernel/ # 内核源码 ├── buildroot/ # Buildroot根文件系统 ├── debian/ # Debian根文件系统 ├── external/ # 外部组件 ├── tools/ # 打包工具 │ └── linux/ │ ├── mkimage.sh # 镜像打包脚本 │ └── ... ├── rockdev/ # 编译产物输出目录 │ ├── MiniLoaderAll.bin │ ├── parameter.txt │ ├── uboot.img │ ├── boot.img │ ├── rootfs.img │ └── update.img └── output/ # 部分版本用这个目录关键点在于device/rockchip/rk3588/下面的BoardConfig.mk这个文件决定了你编译哪个板型、用哪个defconfig、分区表怎么配。很多人编译出来的固件烧进去起不来十有八九是这个文件里的配置和实际硬件不匹配。2.2 板级配置文件的几个关键项打开BoardConfig.mk你需要重点关注这几项# 目标板型 RK_KERNEL_DTS ? rk3588s-evb1-lp4x-v10 # 根文件系统类型 RK_ROOTFS_TYPE ? ext4 # 根文件系统系统类型 RK_ROOTFS_SYSTEM ? buildroot # 分区表文件 RK_PARAMETER ? parameter-buildroot-fit.txt # 是否启用AB分区 RK_AB_UPDATE ? falseRK_KERNEL_DTS指定了设备树文件这个必须和你的硬件一致。我见过有人拿EVB板的配置直接编译烧到自己的定制板上网口不通、HDMI没输出查了半天以为是驱动问题结果就是DTS用错了。RK_PARAMETER指定分区表文件这个文件在device/rockchip/rk3588/目录下。分区表决定了各个分区的偏移地址和大小改错了会导致分区重叠或者空间不够。AB分区模式下分区表结构完全不同这个后面单独讲。2.3 编译环境的依赖检查RK3588的SDK编译对主机环境有要求Ubuntu 20.04或22.04是比较稳妥的选择。需要的依赖包不少官方文档里列了一长串但实际最容易缺的是这几个sudo apt-get install repo git ssh make gcc libssl-dev \ liblz4-tool expect g patchelf chrpath gawk texinfo \ chrpath diffstat binfmt-support qemu-user-static \ live-build bison flex fakeroot cmake gcc-multilib \ g-multilib unzip device-tree-compiler ncurses-dev \ libgucharmap-2-90-dev bzip2 expat gpgv2 cpp-aarch64-linux-gnu \ libncurses5-dev libncursesw5-devdevice-tree-compiler这个包特别容易漏缺了它编译DTS的时候会报dtc not found。qemu-user-static在做多架构根文件系统的时候会用到如果你只编译aarch64的Buildroot可能暂时用不上但一旦涉及混合架构就会卡住。注意不要在编译过程中切换用户或者用sudo编译SDK里很多脚本依赖当前用户的环境变量和权限用sudo跑会出现文件属主混乱的问题后面打包的时候权限检查过不去。3. 从build.sh到mkimage.sh脚本调用链路拆解3.1 build.sh的入口逻辑build.sh是整个编译打包的顶层入口它的核心逻辑其实不复杂就是解析参数、加载板级配置、依次调用各个子模块的编译脚本最后调用打包脚本。但它的参数分支很多第一次看容易晕。常用的调用方式# 完整编译U-Boot Kernel Rootfs 打包 ./build.sh # 只编译内核 ./build.sh kernel # 只编译U-Boot ./build.sh uboot # 只打包不重新编译用已有产物 ./build.sh firmware # 清理后完整编译 ./build.sh cleanall ./build.shbuild.sh执行的时候第一步是source板级配置文件把BoardConfig.mk里的变量导入环境。然后根据你传的参数决定执行哪些步骤。如果不传参数默认走完整流程。这里有个细节build.sh会把编译产物统一拷贝到rockdev/目录下这个目录是打包脚本的输入源。如果你单独编译了内核但没有执行拷贝步骤rockdev/里的boot.img还是旧的。所以单独编译某个模块之后要么手动拷贝要么执行./build.sh firmware让脚本帮你同步。3.2 mkimage.sh的核心工作mkimage.sh在tools/linux/目录下是真正干打包活儿的脚本。它的工作可以概括为三步第一步收集rockdev/目录下的各个分区镜像检查文件是否存在、大小是否超出分区限制。第二步根据parameter.txt生成分区表信息写入parameter分区。第三步调用afptool和img_maker两个工具把所有分区镜像打包成update.img。afptool负责把各个分区镜像按照分区表打包成一个中间格式img_maker负责加上Rockchip的固件头信息生成最终的update.img。这两个工具是闭源的二进制在tools/linux/目录下直接调用即可。打包命令的核心形式# 打包 afptool -pack ./ rockdev/Image/update.img # 或者分步执行 afptool -pack ./ update.tmp img_maker -rk3588 update.tmp update.img-rk3588这个参数指定芯片平台不同平台的固件头格式不一样写错了烧录工具识别不了。3.3 脚本调用中的参数传递陷阱build.sh调用mkimage.sh的时候会传递几个关键环境变量这些变量决定了打包的行为变量名作用常见值RK_CHIP芯片平台rk3588RK_ROOTFS_TYPE根文件系统类型ext4 / squashfsRK_AB_UPDATE是否启用AB分区true / falseRK_PARAMETER分区表文件名parameter-buildroot-fit.txtRK_UPDATE_SIGN是否签名true / false这些变量如果在BoardConfig.mk里没配好或者被环境变量覆盖了打包出来的固件可能分区不对、签名缺失。我遇到过一次RK_AB_UPDATE在BoardConfig.mk里设的是true但环境里有个同名的旧变量是false结果打包出来的固件没有AB分区烧进去之后OTA升级直接失败。排查了半天才发现是环境变量污染。实操心得在跑build.sh之前先执行env | grep RK_看一下当前环境里有没有残留的RK相关变量有的话先unset掉避免干扰。4. 分区表与镜像生成打包的骨架4.1 parameter.txt的格式与含义parameter.txt是RK3588固件的分区表文件它定义了每个分区的名字、起始地址、大小。这个文件的格式看起来简单但每一项都有讲究。一个典型的parameter-buildroot-fit.txt内容FIRMWARE_VER: 8.1 MACHINE_MODEL: RK3588 MACHINE_ID: 007 MANUFACTURER: RK3588 MAGIC: 0x5041524B ATAG: 0x00200800 MACHINE: 0xffffffff CHECK_MASK: 0x80 PWR_HLD: 0,0,A,0,1 TYPE: GPT CMDLINE: mtdpartsrk29xxnand:0x000020000x00004000(uboot),0x000020000x00006000(misc),0x000100000x00008000(boot),0x000200000x00018000(recovery),0x000380000x00038000(backup),0x0000C0000x00070000(oem),0x004000000x0007C000(rootfs),-0x0047C000(userdata:grow)CMDLINE那一行是核心mtdparts后面的格式是大小起始地址(分区名)。大小和地址的单位是扇区每个扇区512字节。比如0x000020000x00004000(uboot)表示uboot分区从0x4000扇区开始大小是0x2000个扇区换算成字节就是0x2000 * 512 4MB。-0x0047C000(userdata:grow)里的-表示这个分区占用剩余所有空间grow表示可扩展。userdata分区通常这么配让系统第一次启动的时候根据存储容量自动扩展。4.2 分区大小计算与调整分区大小不是随便写的要根据实际镜像大小来定。比如boot.img包含了内核和设备树RK3588的内核镜像通常在30MB到50MB之间加上设备树和资源文件boot分区给64MB0x10000扇区是比较稳妥的。计算方式分区大小(扇区) 镜像大小(字节) / 512假设你的boot.img是40MB那就是40 * 1024 * 1024 / 512 81920扇区 0x14000。但你不能刚好卡着这个数要留20%到30%的余量因为后续内核更新可能会变大。所以给0x1000064MB是合理的。rootfs分区的大小取决于你用的根文件系统。Buildroot最小可以做到几十MBDebian或者Ubuntu通常在1GB到4GB之间。如果你用的是ext4格式的根文件系统打包的时候mkimage.sh会检查镜像大小是否超出分区限制超了会直接报错。注意修改分区表之后一定要同步检查BoardConfig.mk里的RK_PARAMETER是否指向了正确的文件。我见过有人改了parameter.txt但BoardConfig.mk还指向旧文件打包出来的固件分区还是旧的。4.3 AB分区模式下的分区表差异AB分区也叫A/B无缝升级是RK3588上比较常用的机制它把boot、rootfs等关键分区各做两份系统运行时从A分区启动升级的时候写入B分区升级完成后切换启动标志。这样即使升级失败也能回滚到A分区不会变砖。AB分区模式下的parameter.txt会长这样CMDLINE: mtdpartsrk29xxnand:0x000020000x00004000(uboot),0x000020000x00006000(misc),0x000100000x00008000(boot_a),0x000100000x00018000(boot_b),0x004000000x00028000(rootfs_a),0x004000000x00428000(rootfs_b),-0x00828000(userdata:grow)可以看到boot变成了boot_a和boot_brootfs变成了rootfs_a和rootfs_b。分区数量翻倍对存储容量的要求也翻倍。如果你的板子eMMC只有8GB用AB分区就要仔细算一下空间够不够。启用AB分区需要在BoardConfig.mk里设置RK_AB_UPDATE true同时RK_PARAMETER要指向AB分区版本的parameter.txt。打包脚本会根据这个配置决定生成哪些分区镜像。5. 完整打包实操从编译到update.img5.1 完整编译流程假设你已经配好了BoardConfig.mk硬件也确认没问题下面是完整的编译打包步骤# 1. 进入SDK根目录 cd /path/to/rk3588_sdk # 2. 选择板级配置如果SDK支持多板型 ./build.sh lunch # 根据提示选择对应的板型编号 # 3. 清理之前的编译产物可选首次编译建议执行 ./build.sh cleanall # 4. 完整编译 ./build.sh./build.sh执行的时间取决于主机性能RK3588的完整编译在16核32线程的机器上大概需要30到60分钟。编译过程中会在终端输出大量日志重点关注有没有error和warning。编译完成后rockdev/目录下会生成所有分区镜像和update.img。你可以用ls -lh rockdev/看一下产物-rw-r--r-- 1 user user 4.0M update.img -rw-r--r-- 1 user user 4.0M uboot.img -rw-r--r-- 1 user user 64M boot.img -rw-r--r-- 1 user user 1.2G rootfs.img -rw-r--r-- 1 user user 256K parameter.txt -rw-r--r-- 1 user user 4.0M MiniLoaderAll.binMiniLoaderAll.bin是引导加载器烧录的时候必须先烧这个再烧update.img。有些烧录工具会把两者合并但分开烧更稳妥。5.2 单独打包已有产物如果你已经编译过了只是改了分区表或者想重新打包不需要重新编译整个SDK直接执行./build.sh firmware这个命令会跳过编译步骤直接调用mkimage.sh用rockdev/里现有的镜像重新打包。速度快很多通常几十秒就能完成。但要注意./build.sh firmware不会自动同步各个模块的编译产物到rockdev/。如果你单独编译了内核需要先手动拷贝# 编译内核 ./build.sh kernel # 拷贝内核产物到rockdev cp kernel/arch/arm64/boot/Image rockdev/boot.img # 或者用SDK提供的脚本 ./build.sh kernel-update不同SDK版本的拷贝方式不一样有的版本build.sh kernel会自动拷贝有的需要手动。建议编译完之后ls -lh rockdev/boot.img看一下时间戳确认是不是最新的。5.3 打包过程中的关键日志解读mkimage.sh执行的时候会输出一些关键信息这些信息能帮你判断打包是否正常Pack update.img... parameter.txt: OK uboot.img: OK (4.0M) boot.img: OK (64M) rootfs.img: OK (1.2G) userdata: grow Pack OK如果某个分区显示FAIL或者size overflow说明镜像大小超出了分区限制需要调整分区表或者精简镜像。还有一种常见情况是parameter.txt: not found这是因为rockdev/目录下没有parameter.txt。这个文件是build.sh从device/rockchip/rk3588/目录拷贝过来的如果拷贝步骤失败了就会缺这个文件。手动拷贝一下即可cp device/rockchip/rk3588/parameter-buildroot-fit.txt rockdev/parameter.txt5.4 签名固件的打包如果产品需要安全启动固件需要签名。RK3588支持安全启动签名后的固件只有对应的密钥才能烧录和启动。签名打包的流程# 生成密钥首次使用 ./build.sh sign-key # 签名打包 ./build.sh firmware SIGN1签名后的update.img会包含签名信息烧录工具在烧录的时候会校验签名。如果密钥不匹配烧录会失败。实操心得签名密钥一定要备份好丢了就没办法再给同一个设备升级固件了。我见过一个团队把密钥放在CI服务器的临时目录里服务器重装之后密钥没了所有已出货的设备都没法OTA升级只能召回。6. 常见问题与排查技巧实录6.1 烧录失败update.img识别不了烧录工具提示Invalid firmware或者Check firmware failed通常有这几个原因第一update.img没有生成完整。检查文件大小正常的update.img应该和所有分区镜像的总和差不多。如果只有几KB说明打包过程中断了。第二固件头信息不对。img_maker的-rk3588参数写错了或者用了其他平台的img_maker。确认tools/linux/目录下的工具是RK3588对应的版本。第三签名不匹配。如果设备启用了安全启动但固件没有签名或者签名密钥不对烧录工具会拒绝。排查步骤# 检查update.img文件头 hexdump -C update.img | head -5 # 正常的RK3588固件头应该包含RKFW或者RKAF标识6.2 启动失败分区表不对导致找不到根文件系统固件烧录成功但启动卡在Kernel panic - not syncing: VFS: Unable to mount root fs大概率是分区表里的rootfs分区地址和实际烧录的不一致。检查方法进入U-Boot命令行执行part list查看分区信息对比parameter.txt里的定义。如果不一致说明打包的时候用的分区表和烧录的时候用的不是同一个。还有一种情况是rootfs镜像格式和内核支持的格式不匹配。比如内核只支持ext4但你打包的是squashfs就会挂载失败。检查BoardConfig.mk里的RK_ROOTFS_TYPE和内核配置里的文件系统支持。6.3 打包报错size overflowmkimage.sh报rootfs.img size overflow说明根文件系统镜像超出了分区表里定义的大小。解决方法有两种一是增大分区表里rootfs分区的大小二是精简根文件系统。增大分区大小需要同步调整后续分区的起始地址比较麻烦。精简根文件系统更简单删掉不必要的包和文件即可。如果你用的是Buildroot可以通过make menuconfig去掉不需要的包。如果是Debian或Ubuntu可以用debootstrap的--variantminbase选项只安装最小系统。6.4 常见问题速查表问题现象可能原因排查方法解决方案烧录工具识别不了update.img固件头错误/文件不完整hexdump检查文件头重新打包确认img_maker参数启动卡在VFS mount分区表不匹配U-Boot执行part list同步parameter.txt和实际分区打包报size overflow镜像超出分区限制对比镜像大小和分区大小增大分区或精简镜像单独编译后打包无效产物未同步到rockdev检查rockdev文件时间戳手动拷贝或执行完整编译AB分区OTA失败分区表未启用AB检查RK_AB_UPDATE变量修改BoardConfig.mk并重新打包签名固件烧录失败密钥不匹配检查签名密钥和设备密钥使用正确的密钥重新签名6.5 几个容易忽略的细节第一个细节rockdev/目录下的文件权限。如果之前用sudo执行过编译rockdev/里的文件属主可能是root后续用普通用户打包的时候会报权限错误。解决方法sudo chown -R $USER:$USER rockdev/。第二个细节parameter.txt里的MAGIC字段。这个字段必须是0x5041524B对应ASCII的PARK。如果这个值不对烧录工具会认为分区表无效。有些人在手动编辑parameter.txt的时候不小心改了这个值导致烧录失败。第三个细节MiniLoaderAll.bin和uboot.img的区别。MiniLoaderAll.bin是第一阶段引导负责初始化DDR和加载uboot.img。烧录的时候两个都要烧只烧update.img是不够的。有些烧录工具会把MiniLoaderAll.bin包含在update.img里但Rockchip的Linux SDK默认是分开的。第四个细节eMMC和SD卡的启动顺序。RK3588支持从eMMC、SD卡、USB等多种介质启动启动顺序由BOOT引脚或者efuse配置决定。如果你烧录到SD卡但板子还是从eMMC启动就会看不到新固件。检查板子的启动模式配置。7. 进阶话题定制化打包与自动化7.1 添加自定义分区有些产品需要在固件里加入自定义分区比如存放配置文件的config分区、存放日志的log分区。添加自定义分区的步骤第一步在parameter.txt的CMDLINE里添加分区定义。比如添加一个64MB的config分区0x000200000x00C00000(config)第二步在mkimage.sh里添加对应的镜像拷贝逻辑。默认情况下mkimage.sh只处理已知的分区名自定义分区需要手动添加。第三步在根文件系统的fstab里添加挂载项让系统启动的时候自动挂载。7.2 自动化打包脚本产品化项目通常需要自动化打包比如CI流水线里每次代码提交都自动出固件。自动化打包的关键是把所有配置都固化到脚本里避免手动操作。一个简单的自动化打包脚本框架#!/bin/bash set -e SDK_DIR/path/to/sdk OUTPUT_DIR/path/to/output BOARDrk3588s-evb1-lp4x-v10 cd $SDK_DIR # 选择板级配置 echo $BOARD | ./build.sh lunch # 完整编译 ./build.sh # 拷贝产物 mkdir -p $OUTPUT_DIR cp rockdev/update.img $OUTPUT_DIR/update-$(date %Y%m%d-%H%M%S).img echo Build done: $OUTPUT_DIRset -e保证任何一步失败都会终止脚本避免用旧的产物打包出错误的固件。7.3 固件版本管理每次打包出来的update.img应该带上版本号方便追溯。版本号可以写在parameter.txt的FIRMWARE_VER字段里也可以单独用一个文件记录。我通常的做法是在BoardConfig.mk里定义一个RK_FIRMWARE_VERSION变量打包的时候通过环境变量传给mkimage.sh最终写入固件的某个只读分区。系统启动后可以通过cat /proc/device-tree/firmware/version或者读取特定文件来获取版本号。实操心得固件版本号一定要和代码仓库的commit hash关联起来。我见过一个项目固件版本号是手动填的结果两个不同的固件用了同一个版本号现场升级的时候搞混了排查了一整天。7.4 打包产物的校验固件打包完成后建议做一次校验确认update.img的完整性。Rockchip的烧录工具在烧录的时候会校验但提前校验能节省时间。校验方法# 计算MD5 md5sum update.img # 用afptool解包检查 afptool -unpack update.img output_dir/ ls output_dir/afptool -unpack能把update.img解包成各个分区镜像如果解包失败或者缺少分区说明打包有问题。这个操作在排查烧录失败的时候特别有用。8. 关于RK3588打包的一些个人体会RK3588的固件打包流程看起来复杂但核心逻辑就是“编译产物收集、分区表定义、镜像打包”这三件事。把这三件事的链路理清楚大部分问题都能定位到具体的环节。我自己的习惯是每次打包前先跑一遍./build.sh cleanall虽然多花几十分钟但能避免很多因为残留产物导致的诡异问题。另外rockdev/目录我会定期清理只保留最近一次的产物避免旧文件干扰。还有一点RK3588的SDK更新比较频繁不同版本的脚本行为可能有差异。遇到问题的时候先确认SDK版本然后去Rockchip的官方文档或者开发者社区搜一下大概率有人遇到过同样的问题。自己硬啃脚本虽然也能解决但效率低很多。最后分享一个小技巧如果你需要频繁打包测试可以把mkimage.sh里的打包命令单独提取出来写成一个快速打包脚本跳过编译步骤只重新打包。这样改一次分区表或者替换一个镜像几秒钟就能出新固件调试效率高很多。