做嵌入式开发这些年,瑞芯微平台一直是我手里的主力,从RK3288、RK3399到现在的RK3568、RK3588,前前后后折腾过不少板子。很多人拿到瑞芯微官方SDK之后,第一步不是看代码,而是想搞清楚一件事:这一堆源码到底怎么变成能烧进板子里的镜像。这个问题听起来挺基础,但真做起来会发现,里面牵扯到U-Boot、Kernel、Rootfs三套完全不同的构建体系,还夹着Loader模式、parameter分区表、update.img整包制作这些概念。这篇文章就把瑞芯微SDK里镜像生成与烧录这条链路完整拆开,讲清楚每个镜像从哪里来、怎么编出来、又该怎么烧进去。
文章面向的是正在用或准备用瑞芯微平台做产品的开发者,不管你是做BSP、驱动还是整机集成,只要需要自己编译SDK并且把固件刷到板子上,这篇文章都能给你一条很明确的主线。我会尽量按实际操作顺序来讲,顺便把那些容易踩的坑标出来,省得大家再走一遍我走过的弯路。
1. 先搞清楚SDK目录与镜像产物的对应关系
1.1 SDK顶层结构与编译入口
瑞芯微官方发布的Linux SDK,拿到手解压之后第一眼会觉得很乱,目录很多,但你只要抓住几个关键路径,心里就有底了。顶层目录里最重要的几个分别是u-boot、kernel、buildroot、debian、app、device、docs,另外还有几个脚本文件build.sh、mkimage.sh和envsetup.sh。
build.sh是整个SDK的编译入口,很多刚接触的人会直接执行./build.sh,结果发现在没有指定参数的情况下它会把U-Boot、Kernel、Rootfs从头到尾编一遍,耗时极长。实际上日常开发中更多用的是./build.sh uboot、./build.sh kernel、./build.sh rootfs这样带子命令的编译,分区编译完再统一打包。
device/rockchip目录下存放的是不同芯片平台和板型的配置文件,比如rk3568、rk3588、rv1126这些子目录,里面又有BoardConfig*.mk和Rockchip*.mk两种文件。前者定义了板级配置,比如RK_UBOOT_DEFCONFIG、RK_KERNEL_DEFCONFIG、RK_ROOTFS_TYPE等变量;后者更像是引用关系,告诉构建脚本在当前平台上该用哪份配置。
mkimage.sh是最后一环的打包脚本,它会把前面编译出来的U-Boot、Kernel、Rootfs各自处理成最终固件格式,然后存放到rockdev/目录。这个脚本就是“镜像生成”的核心,后面我会专门讲它的执行逻辑。
1.2 一次完整编译会生成哪些镜像
在瑞芯微平台上一套完整的固件通常不是单一文件,而是按分区拆开的多个镜像。编译完之后,rockdev/目录下你会看到这些文件:
| 镜像文件 | 对应分区 | 主要来源 | 作用 |
|---|---|---|---|
uboot.img | uboot | U-Boot编译产物 | U-Boot主体 |
trust.img | trust | 安全启动相关(ATF、OP-TEE) | 启动信任链与运行时可执行环境 |
boot.img | boot | Kernel、dtb、ramdisk | Linux内核启动镜像 |
rootfs.img | rootfs | Buildroot/Debian | 根文件系统 |
oem.img | oem | 板厂私有分区 | 预装应用或无关紧要数据 |
userdata.img | userdata | 初始数据 | 用户数据分区 |
parameter.txt | 分区表 | 配置文件 | 定义Flash分区布局 |
update.img | 整包 | 上述所有镜像打包 | 用于整包升级 |
这里最容易搞混的是uboot.img和trust.img的区别。在瑞芯微新平台的启动流程中,芯片上电后先运行BootROM,BootROM加载的是U-Boot SPL或者DDR初始化代码,而trust.img装的是ARM可信固件ATF和OP-TEE,它负责在U-Boot之前或与U-Boot配合完成DDR初始化、切换到安全世界、拉起U-Boot主程序。所以单独烧uboot.img而不烧trust.img,板子大概率起不来,这两者必须配套。
parameter.txt也容易被忽略,它虽然不是可执行镜像,但烧录时必须先写进去,因为升级工具要靠它来识别每个分区在Flash中的偏移和大小。如果分区间错位了,轻则文件系统挂不上,重则直接变砖返厂。
2. 从U-Boot到Kernel:两个Boot阶段的分工与编译细节
2.1 U-Boot侧到底在干什么
U-Boot在瑞芯微平台上承担的任务比一般嵌入式Linux项目要重一些。它不只是引导内核,还要负责DDR初始化、时钟初始化、存储设备驱动加载、显示初始化、启动logo显示等。这也是为什么RK平台的U-Boot编译出来会有多个二进制文件,而且它们的组织方式跟主线U-Boot不太一样。
在SDK里编译U-Boot其实特别简单,执行./build.sh uboot,脚本会自动读取BoardConfig*.mk里指定的RK_UBOOT_DEFCONFIG,然后进入u-boot/目录用这个配置去做编译。你在命令行里看到它执行了make rk3568_defconfig之类的动作,实际上就是在用平台默认配置生成.config,接着就是常规的make -j16。
编译结束之后,u-boot/目录里会得到很多文件,但最关键的是uboot.img和trust.img。这两个镜像不是U-Boot直接编出来的,而是通过瑞芯微提供的打包工具,把U-Boot二进制、ATF、DDR初始化代码、Miniloader等部件组合到一起。所以你如果在u-boot/目录里找不到一个叫作uboot.img的文件,别着急,它生成在rockdev/目录下。
有一个实际开发中经常遇到的需求是给U-Boot阶段加上开机动画或者自定义logo。瑞芯微平台在这方面做得比较友好,它支持在U-Boot阶段就显示logo,然后无缝过渡到Kernel,减少切换闪烁。具体做法是在U-Boot里配置CONFIG_LOGO相关选项,然后把图片放到u-boot/logo/目录下,注意图片格式和分辨率要匹配,不然编译能过但显示不出来。
编译U-Boot时最典型的坑是交叉编译工具链问题。SDK通常会自带一套工具链,放在prebuilts/gcc/linux-x86/目录下,但如果你用了自己安装的交叉编译器,版本太新反而会报错。我实测过用GCC 9、GCC 8编译RK3568的U-Boot都能过,但用GCC 10以上就会出现某些结构体对齐相关的警告,个别的还会直接编译失败。所以能不动SDK自带的工具链就尽量别动。
2.2 Kernel侧的关键产物与dtb匹配
Kernel的编译用的是./build.sh kernel,这个命令做的事情就是读取RK_KERNEL_DEFCONFIG对应的配置,进入kernel/目录执行编译,然后把boot.img生成出来。如果你只是改了驱动源码而没动配置,也可以用./build.sh kernel直接把boot.img编出来,不需要重新配置内核。
这里有个关键点很多人没注意:boot.img里面包含的不只是Image或zImage,还有一个叫resource.img的概念。在比较旧的SDK版本里,resource.img包含了多个dtb文件和logo图片,而在新版本SDK里,dtb文件已经直接打到boot.img里面去了。所以你在检查Kernel编译产物时,不要光找kernel.img,要看最终生成的boot.img是否包含了正确的dtb。
dtb匹配问题是我见过最多的启动失败原因。Kernel编译默认会把该平台所有dts都编译成dtb,然后在U-Boot阶段通过读取分区或环境变量来选择加载哪一个。如果你板子的网口、屏幕或者SD卡驱动不对,先不要怀疑内核驱动代码,先去确认boot.img里的dtb是不是跟你的板子匹配。最简单的确认方法是解包boot.img,或者用mkimage工具查看dtb列表。
瑞芯微SDK里Kernel还有一个特殊目录叫kernel/arch/arm64/boot/dts/rockchip/,板型对应的dts文件都放在这里。如果你自己画了板子,最常规的做法是复制一份官方相近板型的dts,比如rk3568-evb.dts,改好之后在Makefile里添加编译目标。路径和依赖关系只要加对,./build.sh kernel就能自动把它编成dtb并打包到boot.img。
内核启动参数中console=ttyFIQ0是瑞芯微平台的默认配置,很多人想用ttyS0或者ttyAMA0,改了内核cmdline之后发现串口没输出。原因在于瑞芯微使用了FIQ debugger机制,串口输出经过FIQ处理,直接指定普通串口设备不一定能拿到打印信息。如果你只是想看完整启动日志,不要折腾参数,直接用SDK默认的console配置就行。
3. Rootfs的组装与update.img打包流程
3.1 Rootfs的两种主流方案
Rootfs在瑞芯微SDK中有两种主流选择:Buildroot和Debian。Buildroot适合做体积小、功能固定的量产镜像,SDK里通过buildroot/目录维护;Debian适合做功能丰富、方便调试的开发镜像,SDK里通过debian/目录维护。
在BoardConfig*.mk里用RK_ROOTFS_TYPE来指定用哪一个。常见写法有RK_ROOTFS_TYPE=buildroot和RK_ROOTFS_TYPE=debian。如果你改了这个变量,最好执行一下./build.sh cleanall再重新编译,因为两种Rootfs的构建系统差异太大,共存时的残留文件会导致一些奇怪的编译报错。
Buildroot的构建过程是先把工具链、busybox、第三方库全部编译一遍,然后打包成rootfs镜像。第一次执行./build.sh rootfs时会非常久,我试过在8核16线程的机器上编一个功能完整的Buildroot也要一个多小时。这里要注意磁盘空间,Buildroot构建过程会产生大量临时文件,buildroot/output/目录轻松吃掉几十GB。建议给SDK所在的磁盘预留至少100GB空闲空间,否则编到一半磁盘满了会出各种令人崩溃的诡异问题。
Debian的Rootfs思路不一样,它不是从源码编译整个系统,而是用debootstrap从一个基础软件包集合开始,安装一个最小的Debian根文件系统,然后根据需要装上各种应用。这种方式在开发调试阶段更省心,因为直接用apt就能装软件。但是要注意,SDK里带的Debian根文件系统是预置好的,你没法直接往里面加apt源之外的包,想改就得用chroot进rootfs手动操作。
我先说Buildroot,因为它更容易出现“表面成功编译但实际跑不起来”的情况。很多人改完Buildroot配置,重新编译rootfs后发现rootfs.img体积一点没变,烧进板子也看不到自己的改动,原因就很可能是没执行./build.sh cleanall而只执行了./build.sh rootfs。Buildroot有缓存机制,某些配置项改动后不会触发对应软件包的重编,所以需要先把旧的编译产物清掉再重新编。
3.2 mkimage.sh与update.img的生成逻辑
编译完各个分区的镜像之后,最后一步就是打包update.img。这个过程由mkimage.sh控制,它在SDK顶层目录下执行,把rockdev/目录里各个分区镜像以及parameter分区表都打包成一个统一固件文件。
mkimage.sh的核心动作其实就两件事:一是根据parameter.txt把各个镜像放到正确的偏移位置,二是生成一个带校验信息的升级头,让烧录工具能识别并校验。瑞芯微的升级工具在烧录update.img时会先解析升级头,然后按里面的分区信息逐个烧录。
很多人会有个疑问:既然有了单独的uboot.img、boot.img、rootfs.img,为什么还要打一个update.img?原因有两方面,一方面update.img里包含完整的分区表和校验信息,用RKDevTool的“升级固件”功能一键烧录,不用手动选择每个分区文件;另一方面update.img在生产烧录和售后升级时更方便,一个文件搞定所有分区,不容易漏掉或选错。
在实际量产时,我通常不会让产线用update.img整包升级,而是用一个叫upgrade_tool的命令行工具配合单独的镜像文件做分区烧录。这样可以省去整包校验和解析的时间,也更方便定制烧录流程。比如只烧uboot.img和trust.img来更新Bootloader,或者只烧boot.img来更新内核,不用每次都带一个几百MB的rootfs。
这里要注意一个细节:update.img的打包参数RK_UPDATE_INI在Rockchip*.mk里配置,默认指向device/rockchip/rk3568/rockimg/rk3568.ini。如果你自己加了一个分区,就得同步修改这个ini文件,把分区镜像路径加进去,否则打包出来的update.img会缺少这个分区,烧录后系统依然能启动,但那个分区的数据是空的。
打包完成的update.img在rockdev/目录下,它的体积通常跟rootfs大小直接相关。Debug版本的Debian rootfs解压后可能超过2GB,整包固件也会跟着膨胀。如果发现update.img体积异常大,先检查rootfs是否包含了一些不该有的日志缓存和临时文件。
4. 烧录实操:从Loader模式到升级工具使用
4.1 进入Loader模式的几种方法
瑞芯微芯片内部有一个BootROM程序,它上电会检测USB或者SD卡设备,如果外部触发条件满足就进入下载模式。这个模式在RKDevTool里显示为“Loader模式”,是烧录的前提。
进入Loader模式最可靠的方法是按住板子上的RECOVERY键(有些板子叫UPDATE或MASKROM键)再上电。注意动作顺序很关键:先把RECOVERY键按住不松,然后再接USB线和电源。上电之后BootROM检测到RECOVERY引脚被拉低,就会停留在下载模式等待PC端工具连接。如果你的板子没有实体按键,也可以飞线短接对应的测试点,原理一样。
还有一种是软件层面的方式,就是在已经能启动的U-Boot或Linux里执行reboot loader命令。这条命令会让芯片直接进入Loader模式,适合远程维护场景。不过要注意,U-Boot环境里执行reboot loader和Linux系统里执行reboot loader,最终效果都是一样的,但前提是内核或U-Boot里对应驱动没有被动过。
我遇到过不少烧录失败的情况,最后排查出来都是USB连接问题。瑞芯微Loader模式依赖于USB枚举,PC端必须识别到一个Rockchip设备才算成功进入Loader模式。如果你插上USB线后设备管理器里没有任何变化,先确认RECOVERY引脚是否真的拉低了,再检查USB线是否支持数据通信,这个坑特别常见:很多USB线只能充电,插上去完全没反应。
4.2 RKDevTool与upgrade_tool两种烧录方式
PC端烧录工具常用的有两款:Windows下面用RKDevTool,Linux环境下用upgrade_tool。Windows版RKDevTool会把驱动和工具一起装好,图形界面操作直观,适合开发阶段单板调试。Linux下则更依赖命令行,适合产线和自动化集成。
RKDevTool的使用逻辑很简单。连接板子进入Loader模式后,工具界面上会显示一个“发现一个Loader设备”。此时有两个选择:如果你有update.img整包,就直接切到“升级固件”页,点击“升级固件”按钮选择文件,然后点“升级”;如果你只想单独刷某个分区,就切到“分区表”页,对照parameter的分区名,分别给每个分区指定镜像路径,再点“执行”。
upgrade_tool命令行工具在Linux下的用法更直接:
# 查看当前识别到的设备 sudo upgrade_tool ld # 烧录整包固件 sudo upgrade_tool uf update.img # 单独烧录分区镜像 sudo upgrade_tool di -b boot.img sudo upgrade_tool di -u uboot.img sudo upgrade_tool di -t trust.img # 烧录parameter分区表 sudo upgrade_tool di -p parameter.txt这里di参数表示download image,后面跟着分区标识符,-b对应boot,-u对应uboot,-t对应trust,-p对应parameter。分区标识符跟parameter.txt里定义的名字必须一致,所以在执行之前先cat parameter.txt确认分区名称是个好习惯。
烧录顺序上,如果是从空板或异常状态救砖,我建议严格按照“parameter → loader/uboot → trust → boot → rootfs → 其他分区”的顺序来。先烧parameter是为了让工具正确识别分区的物理布局;然后再烧uboot和trust,这两个是启动关键,顺序对了之后板子就能正常进入U-Boot;之后再烧kernel和rootfs才有意义。如果你跳过parameter直接烧boot,工具不知道往Flash哪个地址写,结果就是烧完重启什么都没变。
还有一个需要提醒的点,就是Linux下用upgrade_tool需要设备权限。默认情况下普通用户访问USB设备会提示权限不足,所以要么用sudo执行所有命令,要么给设备创建一个udev规则。如果不想每次输密码,我在/etc/udev/rules.d/下放了一个规则,把VID为2207的设备权限改成0666,插上就能直接操作。
4.3 分区表parameter.txt的调整思路
parameter.txt是烧录时最容易拖后腿但也最关键的文件。它定义了Flash芯片上每个分区的名字、起始偏移和大小,格式类似下面这样:
FIRMWARE_VER: 1.0 MACHINE_MODEL: RK3568 MACHINE_ID: 007 MANUFACTURER: Rockchip CMDLINE: mtdparts=rnand0:0x00002000@0x00004000(uboot),0x00002000@0x00006000(trust),...这里每个分区的偏移和大小都是用十六进制表示的,单位是扇区,一个扇区512字节。比如0x00002000就是8192个扇区,对应4MB空间。如果你要调整rootfs大小,就得数清楚后面的偏移地址,保证前一个分区结束的扇区号正好是后一个分区的起始扇区号,不能有重叠也不能有缝隙,否则后面分区数据会被错误覆盖。
修改parameter.txt之后一定要重新打包update.img,而不是只把parameter单独烧进去。原因很简单,update.img内部还会记录一份分区表信息,单独烧parameter只是改了Flash上的分区布局,但整包里的分区信息还是旧的,下次再用整包升级时又会把布局改回去。所以规范做法是:先改device/rockchip/rk3568/目录下的parameter文件,然后重新执行./build.sh updateimg,生成新的update.img。
我吃过一次亏,当时为了给用户数据区腾空间,把userdata分区扩大了一倍,只改了parameter烧进去,没重新打包整包。后来产线用整包升级,分区布局瞬间又被重置了,用户数据全部丢失。那次之后我只要是动了分区表,一定会连同整包一起重新生成,并且做一次完整的升级验证。
5. 常见问题速查与避坑经验
5.1 编译阶段的高频报错
编译阶段最常见的报错主要有三类:磁盘空间不足、依赖库缺失、工具链不匹配。磁盘空间不足通常出现在编Buildroot和Debian rootfs时,报错信息五花八门,一会儿是No space left on device,一会儿又是cmake编译中段。建议在编译前先用df -h检查一下空间。
依赖库缺失主要出现在Ubuntu宿主机的编译环境里。SDK编译某些组件需要libssl-dev、libncurses5-dev、python2等,不同Ubuntu版本带的包名还不一样。我常备的一行安装命令是sudo apt-get install -y libssl-dev libncurses5-dev git gnupg flex bison gperf build-essential zip curl liblz4-tool zlib1g-dev libc6-dev-i386 x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig,基本覆盖大多数SDK的依赖需求。
工具链不匹配这个坑在新手手里出现频率极高,尤其是那些之前在别的项目里装过交叉编译器的开发者。SDK在build.sh里会主动使用prebuilts/gcc/linux-x86/下自带的工具链,但如果你在~/.bashrc里自定义了PATH,而且自定义的路径排在SDK工具链之前,就可能用错编译器,导致U-Boot编译报Error: unknown pseudo-op: .arch这样的汇编错误。解决办法是编译前检查which aarch64-linux-gnu-gcc,确认用的确实是SDK自带的那个。
5.2 烧录与启动阶段的排查实录
烧录阶段最典型的失败现象是RKDevTool一直提示“等待设备”或者“设备连接失败”。遇到这个现象,我通常按以下顺序排查:第一,确认板子是否真的成功进入Loader模式;第二,换一个USB端口和USB线;第三,重新安装驱动程序,Windows下尤其常见驱动被安全软件拦截的情况;第四,确认是否使用了USB Hub,我遇到过几次是USB Hub供电不稳导致设备枚举失败,直连主板USB口就正常了。
启动阶段的问题就更多了,而且需要结合串口日志来判断。串口输出里如果卡在DDR V1.09这样的信息上,说明U-Boot的DDR初始化没过,要么是DDR频率配置不对,要么是硬件上DDR颗粒型号和参数对不上。如果卡在Loading firmware...之后没有输出,多半是trust.img和uboot.img版本不匹配,或者ATF找不到对应平台的BL31参数。
有一个经常被忽略的检查项是电源时序。RK3568、RK3588这些平台有多路电源轨,上电顺序有严格要求。如果你自研板卡烧录一切正常,但上电后就是起不来,先拿示波器抓一下各路电源的上电时序是否正确。我调试过一块板子,现象是偶尔能启动偶尔死机,折腾了好多天最后发现是某个DC-DC的使能引脚时序慢了十几毫秒,导致DDR初始化时供电还没稳定。
Kernel阶段起不来的话,优先看dtb匹配和console输出。我在RK平台遇到过最多次的现象是:内核打印到Starting kernel ...之后就没下文了。这种情况八成是dtb里的内存配置、串口配置跟实际硬件不符。你可以把boot.img里的dtb解包出来,用fdtdump对比一下chosen、memory和serial节点,基本能定位问题。
最后再分享一个实用习惯:每次编译新固件之前,先把rockdev/目录备份一下。瑞芯微SDK在编译过程中会覆盖rockdev/下的文件,如果你上一个版本能正常工作但新版本编译后想回退,没有备份就得重新编译旧代码,浪费时间不说,有时候旧分支可能已经被你改了。我现在都习惯在每次出固件之前执行一次tar czf rockdev_$(date +%Y%m%d).tar.gz rockdev/,一个命令,却能省掉很多不必要的麻烦。