ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RK3576嵌入式Linux启动链路详解与故障排查指南

RK3576嵌入式Linux启动链路详解与故障排查指南 做嵌入式Linux的兄弟应该都有这种经历手里拿到一块新板子焊好串口插上Type-C按下电源键的瞬间串口助手里面一片空白心里立刻咯噔一下。排查启动问题这件事懂行的人十分钟就能定位到具体阶段不懂的人可能卡上一周。我这两年一直在折腾Rockchip平台从RK3568到RK3588踩了不少坑最近又拿到一块基于RK3576的板子把这条启动链路从头到尾彻底理了一遍。今天这篇就把这套流程拆开讲清楚——从芯片上电那一刻的BootROM到SPL、U-Boot再到Linux内核和根文件系统每一步在干什么、日志怎么读、卡住怎么查一次说透。无论你是刚入行的嵌入式新人还是被启动问题折磨的老兵这篇文章都能给你一套可以直接上手的排查思路。1. RK3576 启动链路全景BootROM、SPL、U-Boot 各自负责什么1.1 为什么启动要拆成三段接力赛逻辑先说一个很多人刚接触时都会问的问题为什么启动不能像单片机那样上电直接跑一个完整的大程序非要搞BootROM、SPL、U-Boot这么麻烦根本原因是物理限制。RK3576是颗8核64位SoC跑Linux内核需要几十MB级别的镜像但芯片内部SRAM只有几百KB级别。开机瞬间DDR还没初始化不能当存储用所以芯片只能在极小的SRAM里执行代码。拿一个几百KB的SRAM去加载几十MB的内核显然不现实。于是整个启动过程被设计成接力赛BootROM是芯片出厂固化的第一棒它只负责把下一棒SPL从存储介质里读进来SPL负责把U-Boot主镜像加载到DDRU-Boot再加载内核和根文件系统。每一棒都只做自己那一小段事情干完就交棒绝不越权。这个设计和PC的BIOS引导很像但更纯粹。PC上有完整固件和BIOS而嵌入式SoC的BootROM代码量极小甚至连串口初始化都不一定做能用就行。这种最小必要原则贯穿了整个启动链路。1.2 RK3576 启动介质选择与三种启动模式RK3576和Rockchip其他平台一样启动介质非常灵活eMMC、SD卡、SPI NOR Flash、SPI NAND都可以作为启动盘。BootROM会根据芯片的BOOT引脚电平也就是板子上的拨码开关或电阻配置来决定从哪个介质读取下一级镜像。这里必须区分三个容易混淆的模式Normal模式正常启动BootROM从eMMC或SD等介质读取镜像进入系统。Loader模式BootROM运行后进入等待USB烧录的状态此时可以用烧录工具刷写固件。MaskRom模式当Loader镜像读取失败或者存储介质里根本没有有效数据时芯片会进入这个隐藏的底层模式专门用于紧急恢复烧录。实际调试中如果板子完全无法启动我会直接把板子强制进MaskRom模式重刷。这几乎是最后一道救命稻草前提是你得知道板子上的MaskRom引脚或者短接点在哪。1.3 一条启动链路的完整角色清单用一张表把整个流程的角色和职责列出来方便后面对照阶段运行位置主要职责典型产物BootROM芯片内部SRAM读取IDB头部、校验、加载SPL芯片出厂固化TPL芯片内部SRAMDDR初始化与内存训练u-boot.tplSPL芯片内部SRAM初始化存储介质、加载U-Boot主镜像u-boot.splU-Boot properDDR环境变量、驱动初始化、加载内核与dtbu-boot.bin / u-boot.itbLinux内核DDR硬件初始化、驱动加载、挂载rootfsboot.img / Image根文件系统eMMC/SD分区提供用户态环境和应用运行基础rootfs.ext4注意Rockchip的特殊之处普通ARM平台的直接顺序是BootROM → SPL → U-Boot但Rockchip在实际链路里多了一个TPL。TPL这个名字在很多SoC上并不存在但Rockchip的U-Boot分支里非常常见它的核心工作就是DDR训练和初始化。后面我会专门展开讲。2. BootROM 到 SPLDDR 初始化和第一行代码的启动细节2.1 BootROM 上电后到底干了什么RK3576上电后CPU首先执行的是芯片内部固化的BootROM代码。这段代码是芯片出厂时写死的用户没法修改也不需要修改。BootROM做的事情按顺序大概是初始化CPU最基本的状态、初始化内部SRAM、检测启动介质选择引脚、尝试从选中的介质里读取头部数据。这里有一个关键概念并不是把整个SPL镜像全部读进来BootROM只会读取存储介质最前面的若干扇区这些扇区里包含了所谓的IDB头。IDB头里带有镜像的标志信息、校验信息和下一级代码的加载地址。BootROM验证通过后才把后续代码搬运到SRAM并跳转执行。一旦BootROM发现介质里没有任何有效头部或者校验失败它会放弃该介质。如果所有介质都失败芯片进入MaskRom模式。这也是为什么很多人第一次给板子烧录时发现烧录工具能识别到设备就是因为芯片此时正处于MaskRom模式等待救援。实操中我总结了一个判断技巧如果串口从头到尾没有任何输出可以先试着用烧录工具连接能枚举到设备说明BootROM活着CPU时钟、电源、晶振大概率没问题问题出在启动介质或镜像适配。2.2 DDR训练这个环节最容易被忽视也最容易坑人上面提到Rockchip在SPL前面多塞了一个TPL这个TPL的核心任务就是初始化DDR并完成内存训练。为什么DDR需要训练因为DDR颗粒和控制器之间的时序会受到PCB布线长度、信号完整性、温度、电压波动等因素影响芯片需要在启动时测量并调整读写时序参数确保内存访问稳定。这个过程有点像两个人第一次合作得先互相试几次节奏找到彼此都能接受的语速。DDR训练就是在干这件事。不同板子的DDR走线、颗粒型号、频率配置都不同所以TPL里必须带上对应板子的DDR配置。很多RK平台的开发板会直接提供编译好的TPL二进制但如果自己画板子换了DDR颗粒就必须重新生成DDR配置。这里最容易出现的故障有板子上电后电流正常但串口没有任何输出因为TPL卡在DDR训练阶段。板子偶尔能启动偶尔不行温度影响DDR时序裕量。TPL反复复位循环打印部分日志后重启。我自己就有一次因为参考设计的DDR频率配置没仔细核对导致板子在冬季低温环境下频繁启动失败最后把DDR频率降了一档才稳定。2.3 第一次上电串口无输出时的排查顺序拿到新板子第一次上电串口什么都没有不要慌。我的排查顺序是固定的确认串口接线正确TX/RX别接反GND必须共地。确认串口软件配置的波特率正确。Rockchip平台常见波特率是115200或1500000两边不一样就会收到乱码或空白。我一般会在发送区随便敲几个字符如果软件有回显或能收到明显乱码至少说明线路通了。测量核心供电电压确认各路电源正常。示波器确认24MHz晶振起振没有晶振就没法跑CPU。尝试连接烧录工具能识别到设备说明BootROM已经跑起来问题大概率在介质或镜像。整套检查下来基本能把问题缩小到一个具体环节。很多新手一上来就怀疑焊接或芯片坏掉其实大部分情况就是波特率不对或者启动介质里没烧东西。3. SPL 到 U-BootMiniLoader、参数分区与启动日志解读3.1 TPL 与 SPL 的分工边界TPL完成DDR初始化后会加载SPL到SRAM执行。SPL的职责就不再碰DDR了它要做的是初始化存储介质驱动eMMC、SD、SPI等然后从介质里读取U-Boot主镜像加载到DDR指定地址。Rockchip SDK里经常听到一个词叫MiniLoader很多人会混淆。实际上MiniLoader就是整合了TPL和SPL的二进制产物烧录到固件前端的loader区域。烧录工具写入镜像时IDB区域放的就是MiniLoader。内核编译U-Boot时生成的u-boot.tpl、u-boot.spl最后会被Rockchip的工具链打包在一起。SPL和TPL的运行位置都是SRAM所以它们必须足够精简。SRAM空间极其有限SPL里不会放完整的文件系统驱动通常只支持最基础的块设备读取和FAT/ext4读取够用就行。说到底一切设计都是围绕空间换阶段的哲学内存不够那就分步执行。3.2 parameter.txt 与 Rockchip 分区表解析Rockchip平台有个很有特色的东西parameter.txt。它定义了整个固件的分区布局相当于给Flash划好了格子。我见过很多新手直接照着别人板子的parameter.txt乱套结果U-Boot和内核的分区编号对不上启动到一半就崩。一个典型的RK3576 GNU/Linux参数分区类似这样FIRMWARE_VER:1.0 MACHINE:rk3576 CMDLINE: mtdpartsrk29xxnand:0x000020000x00004000(uboot),0x000020000x00006000(boot),0x000200000x00008000(rootfs)每个分区的含义是大小起始位置(分区名)。这里的单位是扇区512字节。比如uboot分区起始于0x4000扇区即偏移8MB大小为0x2000扇区4MB。boot分区跟着ubootrootfs从0x8000扇区开始偏移16MB大小0x20000扇区64MB。U-Boot会根据parameter.txt来寻找boot分区的镜像文件Linux内核的root/dev/mmcblk0p3也是基于这个分区顺序。分区的编号是从0开始还是从1开始不同系统处理方式不一样这是导致rootfs挂载失败的一个经典坑。后面第5节会细讲。3.3 实战读日志从 SPL 到 U-Boot 的典型输出SPL阶段如果一切正常串口日志会快速滚过。我截取一段典型的输出具体版本和板子不同会有差异U-Boot SPL board init U-Boot SPL 2017.09-RK3576-... Trying to boot from MMC MMC: no card present spl: mmc boot failed Trying to boot from MMC1 ## Loading boot payload from MMC1... U-Boot 2017.09-RK3576-...注意看Trying to boot from MMC和boot from MMC1这两行它告诉你SPL正在尝试哪个介质。如果SD卡没插或者eMMC没识别到SPL会挨个介质尝试然后放弃或重启。U-Boot主镜像打印出的第一行通常会带日期和版本号看到这行说明你已经从SPL交接到了U-Boot properDDR和存储这一段已经通过了。我习惯于在SPL阶段打印更多调试信息可以在编译时打开DEBUG选项或者增加串口log level这样SPL会打印更详细的介质探测和加载地址信息问题定位会快很多。4. U-Boot 引导内核设备树、镜像格式与启动参数配置4.1 booti 还是 bootmAArch64 内核镜像加载方式U-Boot加载内核时有两个常用命令booti和bootm。RK3576是64位的ARMv8架构标准Linux内核编译产物是Image格式这时用booti加载最直接。bootm主要用于加载FIT镜像或者Android的boot.img这两者其实是把内核、设备树、ramdisk打包在一起的容器格式。我实际开发中更推荐用FIT镜像因为FIT有个巨大的优势把Image、dtb、ramdisk整合到同一个itb文件里U-Boot一次读取就能完成校验和加载。省去了单独管理多个文件的麻烦分发给别人也更不容易出现文件缺失或版本不匹配。无论是booti还是bootm核心都是把镜像读入DDR指定地址然后跳转执行。Rockchip的U-Boot里常用的镜像加载地址是0x02000000附近设备树加载在0x04000000或0x08300000。具体以你板子的printenv输出为准这个地址要避开U-Boot自身和内核镜像的地址区间不然加载时会互相覆盖表现为U-Boot卡死或内核崩溃而且毫无规律。4.2 设备树匹配dtb 不对的典型症状设备树DTB是U-Boot和内核之间传递硬件信息的桥梁。U-Boot加载dtb到内存并把dtb地址通过寄存器传给内核内核才能知道板子上有哪些外设、中断怎么分配、时钟怎么配。RK3576的官方设备树文件在内核源码中的路径通常是arch/arm64/boot/dts/rockchip/每个具体型号的板卡一个dts文件。设备树不匹配的症状非常典型内核启动日志卡在某个驱动的初始化部分或者某些设备完全没有probe甚至直接卡死在启动早期。比如U-Boot用的是开发板的dtb但实际板子的eMMC在另一个控制器上内核就会找不到根设备最终报VFS挂载错误。我建议每次重新编译内核后把dtb单独copy出来放到boot分区的独立文件里并在U-Boot中显式加载。不要过度依赖U-Boot内部预存的dtb源码更新后两者不同步是常有的事。4.3 U-Boot 环境变量实操bootcmd 和 bootargs 怎么填U-Boot启动时读环境变量其中最关键的是bootcmd和bootargs。bootcmd是一段命令列表U-Boot会自动执行它bootargs是传给内核的启动参数。一个在eMMC上启动GNU/Linux的典型配置setenv bootargs consolettyFIQ0,1500000 root/dev/mmcblk0p3 rw rootwait setenv bootcmd fatload mmc 0:2 0x02000000 boot.img; fatload mmc 0:2 0x04000000 rk3576.dtb; booti 0x02000000 - 0x04000000 saveenvsplit一下理解第一步把boot分区里的boot.img内核镜像读到0x02000000读入设备树rzk3576.dtb到0x04000000然后booti执行内核设备树参数用短横线表示不传ramdisk。bootargs里root/dev/mmcblk0p3表示根文件系统在eMMC的第3个分区rootwait告诉内核等设备就绪再挂载避免启动过早起设备。console参数也很关键Rockchip平台经常用ttyFIQ0或者ttyS0波特率有的是115200有的是1500000。写错console参数最直接的后果就是内核起来后串口没有登录提示但系统可能已经在正常运行了。5. 内核到根文件系统root 挂载解析与分区实战5.1 内核挂载 rootfs 前的必经之路U-Boot跳转到内核后内核首先解压自解压镜像然后进入start_kernel初始化流程。这个过程里内核要完成页表初始化、中断初始化、调度器初始化、时钟和定时器初始化然后才开始遍历平台设备逐个匹配驱动的probe。在内核挂载根文件系统之前它必须先把存储介质的驱动加载起来。对于eMMC/SD这种块设备对应的驱动是dw_mmc或sdhci相关驱动对于SPI Flash则要等spi-nor或spi-nand驱动初始化完成。这些驱动通常由设备树里的节点触发probe。如果设备树里没有对应节点或者probe失败内核根本不知道有这块存储设备rootfs自然挂不上。当内核尝试挂载rootfs失败时会有非常经典的报错Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)看到这个panic第一反应是查bootargs里的root参数第二反应是查存储介质驱动是否probe成功。5.2 root 参数的三种写法与常见坑root参数决定内核把哪个设备作为根文件系统挂载。常见的有三种写法直接设备节点root/dev/mmcblk0p3分区UUIDrootPARTUUIDxxxx卷标rootLABELrootfs第一种最简单直观但有风险内核的设备枚举顺序偶尔会变设备编号可能不稳定。第二种最可靠每个分区在格式化时都会生成唯一的PARTUUID只要分区表信息没变永远能精准找到目标分区。第三种适合可移动介质场景SD卡换机器也能通过卷标识别。我在RK3576板子上习惯用PARTUUID。但用PARTUUID之前一定要先在系统里确认分区ID可以用blkid命令blkid /dev/mmcblk0p3输出里会有一项PARTUUIDxxxx-xxxx把这个值写进bootargs的rootPARTUUID部分。用PARTUUID还有一个好处SD卡和eMMC启动时设备号可能不一样但PARTUUID完全不受影响。5.3 实战在板子上查看分区并用 fstab 固定挂载系统正常启动后可以用fdisk或者lsblk确认分区布局。我一般这样看lsblk输出会列出mmcblk0、mmcblk0p1、mmcblk0p2这样的节点对应分区大小。如果你想调整启动时的挂载点需要编辑/etc/fstab。这个文件告诉systemd哪些分区要在开机时挂到哪里。一个RK3576 Debian系统的fstab写起来大概是这样的/dev/mmcblk0p3 / ext4 defaults,noatime 0 1 /dev/mmcblk0p2 /boot ext4 defaults,noatime 0 2第一列是分区设备第二列是挂载点第三列是文件系统类型。如果fstab写错系统启动到一半可能会进入emergency mode需要输入root密码手动修复。这里有个技巧优先用PARTUUID而不是/dev/mmcblk0pX来写fstab因为在内核完全初始化后设备节点可能存在更名或顺序变化的情况。6. 启动排查实战卡死对照表与踩坑记录6.1 启动过程卡死阶段速查表实际调试中最需要的是快速定位卡死位置。我整理了一张速查表基本覆盖了我遇到过的情况现象所处阶段优先排查方向完全没有串口输出BootROM之前电源、晶振、BOOT模式、波特率串口输出乱码或不断重启BootROM/TPL波特率错误、DDR训练失败卡在U-Boot SPL循环尝试SPL存储介质未识别、SPL/TPL不匹配能进U-Boot但无法加载内核U-Bootbootcmd中路径错误、FAT分区不可读内核启动后马上崩溃内核早期设备树不匹配、内存参数错误卡在Starting kernel...U-Boot跳转镜像地址覆盖、dtb地址错误内核报VFS unable to mount内核挂载rootfsroot参数错误、存储驱动probe失败可以挂rootfs但落到emergencysystemd启动fstab错误、文件系统损坏这个表是我排查问题的第一参考。遇到启动故障先对照现象确定阶段然后只在那个阶段里找原因效率至少高一倍。6.2 三件套排查工具串口、烧录模式、日志对比排查启动问题我依赖三样东西串口日志、烧录工具、正常板卡的对照日志。串口工具在Linux上我常用minicom或picocom。minicom配置串口参数比较繁琐但功能全picocom轻量简单picocom -b 1500000 /dev/ttyUSB0如果不知道波特率可以先用烧录工具连接检查芯片状态。Rockchip的upgrade_tool在Linux下很好用sudo upgrade_tool ld能枚举到loader设备或者maskrom设备说明BootROM阶段是通的。这种诊断价值非常大能把故障范围直接劈成两半能枚举到设备问题在介质或镜像不能枚举到设备问题在硬件或BootROM。还有一招很实用拿一块能正常启动的同型号板子抓一份完整日志存下来。出问题时把坏板子的日志和好板子的日志做diff从第一个不一致的日志行开始查通常就是故障点。这个方法简单粗暴但实战效率极高。6.3 我踩过的三个坑希望你避开第一个坑是DDR频率配置。有一块RK3576板子为了追求性能把DDR频率调到最高档结果在高温拷机时频繁重启日志全部卡在DDR训练阶段。后来降低了DDR频率加入-20度到85度的温度循环测试才稳定。性能不是唯一目标稳定性才是底线。第二个坑是boot分区文件系统不匹配。我把boot分区格式化成ext4但U-Boot的bootcmd里用的是fatload命令两者不匹配导致镜像始终加载不出来。排查了半天才发现SPL和U-Boot内置的ext4读写支持是分开编译的没有开启相关配置。后来统一把boot分区做成FAT格式U-Boot读取兼容性最好。第三个坑是PARTUUID写错。我从blkid输出复制了磁盘的UUID而不是分区的PARTUUID然后启动时内核一直报找不到根分区。查了文档才发现rootPARTUUID需要的确实是分区的PARTUUID而不是整个磁盘的UUID。这个细节文档里有但实操时很容易忽略。说到底启动流程调试这个事情最忌讳的就是瞎猜。每次动手之前先对着日志确认当前卡在哪个阶段再针对那个阶段排查。链路是固定的代码是确定的唯一的变量就是你自己的经验和耐心。最后再分享一个我自己的习惯编译U-Boot和内核时永远把串口日志等级调到最高宁可在正常启动时多滚几行日志也不要等出了问题再重新编译只为了加打印。调启动问题信息就是一切。把这些常用排查方法记住下次再遇到RK3576或者其他Rockchip平台的启动故障你也能十分钟内定位到问题所在。
返回列表