ARTICLE DETAIL

资讯详情

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

单片机之后为何必须学u-boot?嵌入式Linux启动流程与QEMU实操

单片机之后为何必须学u-boot?嵌入式Linux启动流程与QEMU实操 1. 从单片机到 u-boot为什么我劝你尽早跨过这道分水岭如果你现在还在用 51 单片机点灯、用 STM32 跑裸机循环、用 DHT11 配 LCD1602 做温湿度显示那说明你已经把“单片机入门”这条路走得差不多了。再往下走如果还停留在“写个 while(1) 轮询 中断 串口打印”的层面技术视野会迅速收窄。我见过太多做了三五年单片机的朋友一碰到 Linux 内核、设备树、启动流程就卡住根本原因不是能力不行而是从来没有认真啃过u-boot。u-boot 是什么一句话说清楚它是嵌入式 Linux 系统里负责“把内核拉起来”的那段引导程序。你手里的 ARM64 开发板、工业控制板、路由器、车机、甚至部分国产化桌面终端上电后第一段真正干活的代码大概率就是 u-boot。它做的主要事情包括初始化 DDR、配置时钟、加载设备树、把 Linux 内核从 Flash 或网络搬到内存、设置启动参数最后跳转到内核入口。没有它内核连“第一行 C 代码”都跑不起来。这篇文章适合谁看三类人最合适。第一类已经玩过 51 或 STM32想往嵌入式 Linux 方向转的人第二类做过应用层开发但一直搞不清“应用层开发是不是嵌入式”的人第三类正在学嵌入式 Linux却被 u-boot 启动流程、环境变量、设备树、交叉编译搞得一头雾水的人。我会从实际项目角度出发把 u-boot 的核心逻辑、实操步骤、常见坑和排查方法讲透尽量让你看完就能在自己的 ARM64 板子或 QEMU 上跑一遍。提示本文不涉及任何网络访问工具、敏感系统或违规内容所有操作均基于公开开发板、QEMU 模拟器和通用嵌入式 Linux 技术栈。2. 为什么单片机之后必须补上 u-boot 这一课2.1 单片机思维和嵌入式 Linux 思维的根本差异单片机开发的核心是“我控制一切”。你写main函数初始化外设然后进死循环。中断来了就处理任务多了就自己写状态机。整个系统的启动流程非常短复位向量 - 启动文件 -main。你不需要关心“谁把代码搬到内存”因为代码通常就在 Flash 里原地执行或者由启动文件搬运少量数据段。嵌入式 Linux 完全不是这个逻辑。Linux 内核是一个庞大的、运行在虚拟内存保护模式下的操作系统。它需要 DDR 已经初始化好、需要知道内存布局、需要设备树描述硬件、需要根文件系统、需要启动参数。这些东西不可能由内核自己从零搞定所以必须有一个“前置程序”来完成这些脏活累活。u-boot 就是这个前置程序。我经常用一个类比单片机开发像自己盖一间平房你从打地基到装灯泡全包嵌入式 Linux 开发像接手一栋精装大楼u-boot 是物业交接流程内核是大楼运营系统根文件系统是里面的商户。你如果不懂交接流程大楼再漂亮也开不了门。2.2 u-boot 在典型 ARM64 启动链路中的位置以一块常见的 ARM64 开发板为例上电后的启动链路大致如下芯片内部 ROM 代码BootROM运行根据启动引脚选择从 eMMC、SD 卡、SPI Flash 或串口加载第一段程序。加载并运行二级程序加载器SPL 或 BL2完成最基础的 DDR 初始化和时钟配置。加载完整的 u-boot通常叫u-boot.bin或u-boot.itb运行在 DDR 中。u-boot 初始化串口、网口、存储、USB 等外设读取环境变量。根据启动命令把 Linux 内核镜像Image或Image.gz和设备树.dtb加载到内存指定地址。设置bootargs跳转到内核入口。内核接管挂载根文件系统启动 init 进程。这个链路里u-boot 是承上启下的关键。你如果只会写单片机程序看到bootargs、bootcmd、fdt、loadaddr这些词会非常陌生。但一旦理解你会发现它其实就是一个功能更强的“超级启动文件”。2.3 学 u-boot 能直接解决哪些实际问题很多人学 u-boot 是因为遇到了具体问题。我整理了几个最常见的场景板子启动卡在Starting kernel ...不知道是内核问题还是 u-boot 传参问题。想改启动参数比如控制台从ttyS0换成ttyS2但不知道在哪里改。需要从网络启动用 TFTP 加载内核但bootcmd不会写。设备树改了之后内核没反应不确定 u-boot 有没有把新的 dtb 传过去。产品需要双系统备份启动u-boot 环境变量和启动脚本不会设计。想裁剪启动时间去掉不必要的等待和探测。这些问题在单片机世界里根本不存在但在嵌入式 Linux 项目里天天出现。学会 u-boot你就有了排查启动问题的“第一现场”能力。3. u-boot 核心概念拆解别被术语吓住3.1 环境变量u-boot 的“配置中心”u-boot 最核心的概念之一就是环境变量。你可以把它理解成 u-boot 自己的“注册表”或“配置文件”。它保存在 Flash 的某个分区里启动时被读入内存你可以通过串口命令行查看和修改。常用命令printenv # 打印所有环境变量 printenv bootcmd # 打印某个变量 setenv myvar 123 # 设置变量 saveenv # 保存到存储介质几个关键变量必须认识变量名作用典型值示例bootcmd默认启动命令倒计时结束后自动执行run distro_bootcmdbootargs传给 Linux 内核的启动参数consolettyS0,115200 root/dev/mmcblk0p2 rwbootdelay自动启动倒计时秒数2baudrate串口波特率115200fdt_addr_r设备树加载到内存的地址0x40000000kernel_addr_r内核加载到内存的地址0x40400000serveripTFTP 服务器地址192.168.1.100ipaddr开发板 IP 地址192.168.1.200这些地址不是随便写的。ARM64 系统里DDR 起始地址通常是0x40000000或0x80000000具体要看芯片手册。内核、设备树、ramdisk 必须放在不重叠的区域否则会互相覆盖。我一般会留出至少 64MB 给内核32MB 给设备树ramdisk 看实际大小。3.2 启动流程从bootcmd到内核入口u-boot 启动后如果bootdelay大于 0会倒计时。期间你按任意键可以进入命令行。倒计时结束执行bootcmd。bootcmd通常是一个脚本比如setenv bootcmd fatload mmc 0:1 0x40400000 Image; fatload mmc 0:1 0x40000000 board.dtb; booti 0x40400000 - 0x40000000这条命令的意思是从 mmc 0 的第 1 个分区通常是 FAT 格式的 boot 分区读取Image到内存0x40400000读取board.dtb到0x40000000然后用booti命令启动 ARM64 内核。booti是 ARM64 专用的启动命令参数格式是booti kernel_addr ramdisk_addr fdt_addr。如果没有 ramdisk用-占位。注意bootm用于旧式 uImagebooti用于 ARM64 的 Image 格式bootz用于 ARM 的 zImage。用错命令会直接启动失败。3.3 设备树u-boot 和内核之间的“硬件说明书”设备树Device Tree是嵌入式 Linux 里另一个绕不开的概念。它用文本格式描述硬件有几个串口、I2C 上挂了什么、GPIO 怎么分配、内存多大、时钟怎么接。u-boot 负责把 dtb 加载到内存并传给内核。为什么需要设备树因为 ARM 生态太碎片化了。同样是 ARM64 芯片外设布局千差万别。如果把这些信息硬编码进内核内核会变得无比臃肿。设备树把硬件描述从内核代码里剥离出来一份内核可以支持多种板子只要换 dtb 就行。u-boot 里查看设备树fdt addr 0x40000000 # 设置当前 fdt 地址 fdt print / # 打印设备树根节点 fdt list /soc # 列出 soc 节点如果你修改了设备树但内核没反应第一件事就是在 u-boot 里确认 dtb 是否真的被加载到了正确地址以及bootargs里有没有传fdt_addr。3.4 交叉编译在 x86 上生成 ARM64 代码u-boot 和内核一样需要在 x86 主机上交叉编译成 ARM64 可执行文件。你需要一套 aarch64 工具链。Ubuntu 上可以这样装sudo apt install gcc-aarch64-linux-gnu然后编译 u-bootmake ARCHarm CROSS_COMPILEaarch64-linux-gnu- defconfig make ARCHarm CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)这里ARCHarm在 u-boot 里同时覆盖 32 位和 64 位 ARM具体由 defconfig 决定。编译完成后会生成u-boot.bin、u-bootELF 格式用于调试、spl/u-boot-spl.bin等文件。实操心得第一次编译 u-boot 不要急着上板子先在 QEMU 里跑通。QEMU 可以模拟 ARM64 的virt机器配合 u-boot 和内核能快速验证启动流程省去反复烧录的麻烦。4. 手把手实操在 QEMU 上跑通 u-boot Linux 内核4.1 环境准备与工具链安装我推荐用 Ubuntu 22.04 或 24.04 作为宿主系统。需要安装以下工具sudo apt update sudo apt install -y build-essential git bison flex libssl-dev \ gcc-aarch64-linux-gnu qemu-system-arm device-tree-compiler \ bc u-boot-tools各工具作用gcc-aarch64-linux-gnuARM64 交叉编译器。qemu-system-armQEMU 模拟器支持 ARM64 virt 机器。device-tree-compilerdtc用于编译和反编译设备树。u-boot-tools提供 mkimage 等工具。bison、flex、libssl-dev内核和 u-boot 编译依赖。4.2 获取 u-boot 源码并配置 QEMU 目标git clone https://source.denx.de/u-boot/u-boot.git cd u-boot make qemu_arm64_defconfig make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)qemu_arm64_defconfig是 u-boot 官方为 QEMU ARM64 virt 机器准备的配置。编译完成后当前目录会生成u-boot.bin。如果你想更贴近真实开发板可以选qemu_arm64_defconfig后手动调整环境变量比如设置bootcmd从 virtio 磁盘加载内核。4.3 准备 Linux 内核和设备树QEMU 需要内核和 dtb。可以用主线内核git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- defconfig make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后内核镜像在arch/arm64/boot/ImageQEMU virt 的设备树在arch/arm64/boot/dts/qemu/virt.dtb不同内核版本路径可能略有差异找不到就用find . -name virt.dtb。4.4 制作根文件系统并打包启动镜像最简单的方式是用 BusyBox 做一个 initramfs。步骤略长但核心是mkdir -p rootfs/{bin,sbin,etc,proc,sys,dev} # 编译 BusyBox 并安装到 rootfs # 写一个 init 脚本 # 打包成 cpio.gz find . | cpio -o -H newc | gzip ../rootfs.cpio.gz然后在 u-boot 里通过booti启动setenv bootargs consolettyAMA0 root/dev/ram rdinit/init booti 0x40400000 0x44000000 0x40000000其中0x40400000是内核地址0x44000000是 initramfs 地址0x40000000是 dtb 地址。4.5 QEMU 启动命令与参数详解完整启动命令qemu-system-aarch64 \ -M virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin \ -kernel arch/arm64/boot/Image \ -initrd rootfs.cpio.gz \ -append consolettyAMA0 root/dev/ram rdinit/init \ -dtb arch/arm64/boot/dts/qemu/virt.dtb参数说明-M virt模拟 ARM64 virt 通用机器。-cpu cortex-a57模拟 Cortex-A57 核心。-nographic串口直通终端不弹图形窗口。-bios u-boot.bin把 u-boot 当作 BIOS 加载。-kernel、-initrd、-dtbQEMU 会把这些加载到内存但 u-boot 也可以自己加载。-append内核命令行参数。实际调试时我通常只用-bios u-boot.bin然后在 u-boot 命令行里手动加载内核和 dtb这样更能理解启动流程。4.6 启动成功后的验证与调试启动后你应该能看到 u-boot 的串口输出类似U-Boot 2024.01 (Jan 01 2024 - 00:00:00 0000) DRAM: 128 MiB Core: 51 devices, 14 uclasses, devicetree: board Flash: 64 MiB In: serial Out: serial Err: serial Net: No ethernet found. Hit any key to stop autoboot: 2按回车进入命令行然后手动加载loadx 0x40400000 # 通过串口加载内核实际更常用 tftp 或 virtio在 QEMU 里更方便的是用-kernel让 QEMU 直接加载但为了学习 u-boot我建议用virtio磁盘或tftp。QEMU 用户网络模式支持内置 TFTP配置稍复杂但值得一试。5. 常见问题与排查技巧实录5.1 启动卡死与无输出问题现象上电后串口没有任何输出。排查思路确认串口线接对TX/RX 没有反。确认波特率是 115200u-boot 默认通常是 115200。确认启动介质选择正确比如 SD 卡启动引脚有没有拉对。确认 u-boot 是否真的被烧录到了正确偏移。有些芯片要求 u-boot 前面有头部不能直接 dd 裸镜像。用示波器或逻辑分析仪看串口 TX 是否有波形。常见坑有些开发板的串口是 RS232 电平不是 TTL直接接 USB 转串口会没输出甚至损坏接口。5.2 环境变量保存失败现象saveenv报错Saving Environment to MMC... Failed。原因u-boot 不知道环境变量该存到哪个分区。需要在配置里指定CONFIG_ENV_IS_IN_MMC、CONFIG_SYS_MMC_ENV_DEV、CONFIG_ENV_OFFSET等。解决在include/configs/你的板子.h或 defconfig 里确认环境变量存储配置。如果是新板子先用env default -a恢复默认再saveenv。5.3 内核启动后无根文件系统现象内核打印VFS: Cannot open root device或Kernel panic - not syncing: VFS: Unable to mount root fs。排查bootargs里root是否指向正确设备比如/dev/mmcblk0p2。根文件系统分区是否真的存在且格式正确。内核是否编译了对应的文件系统驱动比如 ext4、squashfs。如果用的是 initramfsrdinit路径是否正确。我遇到过最隐蔽的一次是rootwait没加eMMC 还没枚举完内核就去挂载根分区加上rootwait就好了。5.4 设备树不生效现象改了 dtb 里的 GPIO 或 I2C 配置内核行为没变化。排查在 u-boot 里fdt addr后fdt print确认加载的是新 dtb。确认booti的第三个参数是 dtb 地址。确认内核命令行没有覆盖 dtb 里的bootargs。确认 dtb 编译时没有语法错误dtc -I dtb -O dts反编译检查。5.5 常见问题速查表问题现象可能原因快速验证解决方案串口无输出波特率错、线序错、启动介质错换线、换波特率、查启动引脚逐项排除示波器确认saveenv失败环境变量分区未配置printenv看是否只读配置CONFIG_ENV_*内核 panic 无根文件系统root错、驱动缺失、未rootwait看内核打印修正 bootargs补驱动dtb 不生效地址错、未传参、编译错fdt print重新加载检查 booti 参数TFTP 下载失败网络不通、防火墙、IP 错ping服务器检查网线、IP、服务启动倒计时不停bootdelay为 -1 或按键卡住printenv bootdelaysetenv bootdelay 2实操心得调试 u-boot 最有效的方法是“二分法”。先确认串口有输出再确认能进命令行再确认能加载内核最后确认能挂根文件系统。每一步都独立验证不要跳步。6. 从会用到用好u-boot 进阶方向与学习路线6.1 深入 SPL 和 DDR 初始化SPLSecondary Program Loader是 u-boot 的第一阶段运行在芯片内部 SRAM 里主要任务是初始化 DDR 和时钟然后把完整的 u-boot 搬到 DDR。很多启动失败其实卡在 SPL 阶段但串口还没来得及初始化所以看不到输出。想深入 SPL需要看芯片手册里的 DDR 控制器寄存器、时钟树、引脚复用。这部分和单片机寄存器开发很像但复杂度高一个数量级。我建议先从现成板子的 SPL 配置入手用make menuconfig看SPL相关选项再对照原理图理解。6.2 理解 bootargs 与内核命令行bootargs是 u-boot 传给内核的“纸条”内核根据它决定控制台、根文件系统、内存大小、调试级别等。常用参数consolettyS0,115200串口控制台。root/dev/mmcblk0p2根文件系统位置。rootfstypeext4根文件系统类型。rw或ro读写或只读挂载。init/sbin/initinit 程序路径。loglevel7内核日志级别。mem512M限制内存大小。maxcpus2限制 CPU 核心数。这些参数在 u-boot 里用setenv bootargs ...设置然后booti启动。你也可以在 u-boot 命令行临时改方便调试。6.3 网络启动与 TFTP 调试网络启动是嵌入式开发里非常高效的方式。配置好 TFTP 服务器后每次改内核不用烧 Flash直接重启下载。u-boot 里配置setenv ipaddr 192.168.1.200 setenv serverip 192.168.1.100 setenv bootcmd tftp 0x40400000 Image; tftp 0x40000000 board.dtb; booti 0x40400000 - 0x40000000 saveenv宿主机上启动 TFTP 服务把Image和board.dtb放到 TFTP 根目录。注意防火墙和 SELinux 可能拦截先用tftp localhost自测。6.4 嵌入式学习路线的重新规划如果你还在纠结“应用层开发是不是嵌入式”我的看法是应用层开发是嵌入式的一部分但不是全部。嵌入式 Linux 的完整技能栈包括硬件基础原理图、寄存器、总线协议。引导程序u-boot、SPL、启动流程。内核驱动、设备树、裁剪、调试。根文件系统BusyBox、systemd、库依赖。应用层Qt、网络、数据库、GUI。工具链交叉编译、调试、性能分析。单片机是第 1 步的入门u-boot 是第 2 步的核心。跳过 u-boot 直接学应用层遇到启动问题就会束手无策。我的建议是单片机玩到能独立做小项目后立刻转 u-boot 内核 根文件系统这条线哪怕一开始只是跑通 QEMU也比在单片机里反复点灯强。6.5 推荐练手项目与验证方法在 QEMU ARM64 上跑通 u-boot 主线内核 BusyBox。给真实开发板编译 u-boot修改bootargs和bootcmd。用 TFTP 实现网络启动对比 Flash 启动的差异。修改设备树添加一个 GPIO 控制 LED在内核里验证。裁剪 u-boot去掉不需要的命令和驱动观察启动时间变化。实现双启动分区用环境变量切换 A/B 系统。每个项目都要有明确的验证标准比如“串口能进命令行”“内核能挂载根文件系统”“LED 能被 sysfs 控制”。不要只看编译通过要上板或上 QEMU 实测。我个人在实际操作中的体会是u-boot 最难的不是命令本身而是“启动链路思维”的建立。一旦你脑子里能画出从上电到 init 的完整流程图后面学内核驱动、系统裁剪、产品化都会顺很多。最后再分享一个小技巧每次改 u-boot 环境变量前先printenv备份一份到文本文件改坏了用env import恢复能省下大量重新烧录的时间。
返回列表