一块新板子上电后没有任何输出,或者卡在某个阶段不动,是嵌入式Linux开发中会遇到的问题。
启动链路全景
嵌入式Linux的启动是一条层层传递的链:硬件上电后执行片内BootROM,加载SPL/U-Boot,U-Boot加载内核和设备树,内核解压并启动,最后挂载根文件系统并执行init进程。
每一环都依赖上一环的正确输出。排查启动问题时,关键是从最靠近"有输出"的地方往前推——串口打印在哪里停了,问题就在那一环。
BootROM与SPL
片上ROM中的BootROM是芯片出厂固化的代码,负责最基础的初始化:检查启动介质选择(SD卡、eMMC、NAND、USB、串口等),从选定介质读取前几KB到内部SRAM,跳转到该代码执行。
这段初始代码就是SPL(Secondary Program Loader),也叫MLO(在AM335x等TI芯片上)或FSBL(Zynq上的First Stage Boot Loader)。SPL的任务很专一:初始化外部DDR内存,因为后续U-Boot和内核都太大,塞不进SRAM。
# 典型的SPL启动日志U-Boot SPL 2023.04 (Jul 15 2026 - 10:00:00)Trying to boot from MMC1spl: mmc boot mode: raw...
如果串口完全没有任何输出,问题在BootROM或之前:电源、时钟、复位电路、启动模式引脚配置。BootROM本身不打印,它的失败表现为"板子彻底没反应"。此时要量电源电压、检查晶振是否起振、确认BOOT引脚电平符合预期启动介质。
如果只看到SPL开头几行就停了,通常是DDR初始化失败——SPL里DDR时序参数和板子上的内存颗粒不匹配。这需要用芯片厂商提供的DDR压力测试工具(如i.MX的DDR Stress Test)来校准RPA(Register Programming Aid)参数。
U-Boot阶段
SPL把完整的U-Boot加载到DDR后跳转执行。U-Boot负责更完整的硬件初始化,并提供命令行环境,最终加载内核镜像和设备树到内存,把控制权交给内核。
# U-Boot启动日志(节选)U-Boot 2023.04 (Jul 15 2026 - 10:00:00)CPU: Freescale i.MX6UL rev1.2 at 396MHzDRAM: 512 MiBMMC: FSL_SDHC: 0Loading Environment from MMC... OKIn: serialOut: serialErr: serial# 倒计时结束后启动内核Hit any key to stop autoboot: 0
U-Boot最常见的启动方式是bootz(加载zImage+DTB)或bootm(加载uImage)。bootcmd环境变量定义了自动启动命令:
# 典型的bootcmdsetenv bootcmd 'mmc dev 0; fatload mmc 0:1 ${kernel_addr_r} zImage; \ fatload mmc 0:1 ${fdt_addr_r} myboard.dtb; \ bootz ${kernel_addr_r} - ${fdt_addr_r}'# 启动参数(传递给内核)setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait rw'
常见卡点:
- •
Loading Environment from MMC... *** Warning - bad CRC:环境变量区未初始化,不影响启动但每次都会打印,用saveenv写入一次即可消除。 - • 卡在
Hit any key to stop autoboot之后无输出:内核没加载成功(fatload路径错)或加载了但无法启动(镜像格式错、地址重叠)。 - •
Wrong Image Format for bootz:用bootz加载了uImage格式,或反之。确认镜像类型和boot命令匹配。
内核解压与启动
U-Boot执行bootz后,内核镜像已被搬运到内存。ARM Linux内核的自解压代码(如果使用的是压缩的zImage)先自解压,再跳转到真正的内核入口。
# 内核启动早期日志Booting Linux on physical CPU 0x0Linux version 6.1.22 (gcc 12.2.0)CPU: ARMv7 Processor [410fc075] revision 5OF: fdt: Machine model: My Custom BoardMemory policy: Data cache writeallocKernel command line: console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait rw
内核启动阶段的排查重点是console=参数。console=ttymxc0,115200中的ttymxc0是串口设备名,不同SoC不同(i.MX是ttymxc,三星是ttySAC,全志是ttyS)。如果内核启动后串口没输出了,先确认console参数和设备树中串口节点aliases是否匹配。
内核解压失败的典型现象是U-Boot打印Starting kernel ...后完全没有内核输出。原因可能是:DTB地址和内核地址重叠、DTB格式损坏、内核编译时未选对应SoC的CONFIG_选项。
根文件系统挂载
内核完成自身初始化后,会尝试挂载根文件系统。根文件系统的位置和类型由bootargs中的root=指定。
# 常见root=配置root=/dev/mmcblk0p2 # eMMC/SD卡第2分区root=/dev/nfs # NFS网络根文件系统root=PARTUUID=xxxx-02 # 按分区UUID挂载root=/dev/ram0 # initramfs/initrd
挂载失败的内核会panic:
VFS: Unable to mount root fs on unknown-block(179,2)Please append a correct "root=" boot optionKernel panic - not syncing: VFS: Unable to mount root fs
排查思路:
- •
unknown-block(179,2):179是SD/MMC主设备号,2是分区号。确认SD卡确实有第2个分区,且分区格式是内核支持的(ext4需要CONFIG_EXT4_FS)。 - • 用了
rootwait但分区识别慢:加上rootwait让内核等待设备就绪,否则可能分区还没枚举完就panic。 - • 根文件系统本身不完整:用
root=/dev/nfs先通过NFS启动验证内核,排除根文件系统制作问题。NFS启动成功说明内核和bootargs没问题,问题在根文件系统镜像。
initramfs的处理有所不同。它是内核镜像内置的cpio归档,内核启动时解压到内存作为初始根文件系统,由其中的init脚本负责挂载真正的根文件系统(pivot_root)。很多发行版用initramfs来加载存储控制器驱动模块,因为根文件系统所在的磁盘驱动未必编进内核。
init进程与用户空间
内核挂载根文件系统后,执行/sbin/init(或bootargs中init=指定的程序)。init进程PID=1,是所有用户空间进程的祖先。
# 系统进入用户空间的标志[ 3.123456] Run /sbin/init as init process[ 3.145678] EXT4-fs (mmcblk0p2): mounted filesystemStarting logging: OKStarting network: OKWelcome to My Embedded Linuxmyboard login:
init进程的种类决定了后续的启动行为:
- • BusyBox init:最轻量,读取
/etc/inittab,适合资源紧张的嵌入式设备。 - • systemd:功能全面但资源占用大,常用于需要服务管理的设备。
- • OpenRC / SysVinit:介于两者之间。
卡在Run /sbin/init之后没有继续,通常是init程序或它的依赖库缺失/不兼容。用init=/bin/sh作为bootargs可以跳过init直接进shell,验证根文件系统基本可用。
# 绕过init直接进shell,排查根文件系统setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rootwait rw init=/bin/sh'
进入shell后手动执行init的脚步,观察哪一步卡住。BusyBox init会解析/etc/inittab:
# 典型的inittab::sysinit:/etc/init.d/rcS # 系统初始化脚本::respawn:-/bin/sh # 串口终端(respawn表示退出后重启)::ctrlaltdel:/sbin/reboot::shutdown:/sbin/swapoff -a::shutdown:/bin/umount -a -r
rcS脚本负责挂载proc/sysfs、配置网络、启动守护进程。如果rcS中某条命令卡死(比如等待一个不存在的网络接口),系统就会停在那里。把rcS里命令逐条注释掉排查,能定位到具体哪一步出问题。
调试手段
早期串口输出
内核启动早期(console初始化之前)的日志看不到,因为串口驱动还没加载。需要开启CONFIG_EARLY_PRINTK并配置earlycon参数:
# 在bootargs中加入earlyconsetenv bootargs 'console=ttymxc0,115200 earlycon root=/dev/mmcblk0p2 rootwait rw'# 或指定具体earlycon(i.MX6)earlycon=ec_imx6q,0x02020000
earlycon能在console初始化前就输出内核日志,对排查"Starting kernel后无输出"这类问题很有用。
用NFS根文件系统隔离问题
# bootargs使用NFS根文件系统setenv bootargs 'console=ttymxc0,115200 \ root=/dev/nfs \ nfsroot=192.168.1.100:/nfs/rootfs,v3,tcp \ ip=192.168.1.200:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off \ rw'
NFS根文件系统让根文件系统在开发主机上,修改不需要重新烧写SD卡。当怀疑根文件系统有问题时,先NFS启动验证,能大幅缩短调试周期。
内核调试选项
# 有用的内核调试配置CONFIG_DEBUG_KERNEL=y # 总开关CONFIG_DEBUG_INFO=y # 保留调试符号,配合gdbCONFIG_EARLY_PRINTK=y # 早期打印CONFIG_PANIC_ON_OOPS=y # oops时直接panic,便于捕获CONFIG_KALLSYMS=y # 显示函数名而非地址
CONFIG_KALLSYMS让内核panic/oops时打印函数名而非十六进制地址,配合addr2line工具可以把地址转成源码行号,定位崩溃位置。
# 把panic地址转成源码位置arm-linux-gnueabihf-addr2line -e vmlinux 0xc0123456# 输出: /path/to/kernel/source/mm/slab.c:1234