news 2026/9/29 15:35:06

从单片机到u-boot:ARM64嵌入式Linux启动实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从单片机到u-boot:ARM64嵌入式Linux启动实战指南

1. 从单片机到 u-boot:为什么我劝你尽早跨过这道坎

如果你现在还在用 51 单片机点灯、用 STM32 跑裸机循环,觉得嵌入式也就那么回事,那我接下来要说的话可能会让你有点不舒服:你目前接触的,只是嵌入式世界里最表层的那一小块。真正让嵌入式工程师拉开差距的,不是你会不会写GPIO_SetBits,而是你能不能把一颗 ARM64 芯片从加电那一刻开始,一步步引导到操作系统跑起来。这中间的核心角色,就是u-boot。

我大概带了不下二十个从单片机转过来的兄弟,几乎所有人第一次面对 u-boot 源码时的反应都是一样的:目录一大堆、配置文件看不懂、编译报错、烧进去串口没输出。但一旦你啃下第一块板子,后面的事情就会变得非常顺。u-boot 不只是一个 bootloader,它是你理解嵌入式 Linux 启动链路、ARM64 体系结构、设备树、内存布局、驱动模型的最佳入口。你搞懂了 u-boot,回头再看内核源码,会发现很多设计思路是一脉相承的。

这篇文章我打算把从单片机思维切换到 u-boot 开发的完整路径讲清楚。包括为什么要学、学之前要补哪些课、怎么用QEMU 模拟 ARM64环境零成本上手、u-boot 的启动流程到底分几个阶段、编译配置怎么改、常见问题怎么排查。适合有 C 语言基础、玩过单片机、想往嵌入式 Linux 方向走的同学。不需要你手上有开发板,一台能跑 QEMU 的电脑就够了。

2. 单片机思维和 u-boot 思维,差在哪里

2.1 单片机开发到底在做什么

先把你熟悉的东西捋一遍。51 单片机也好,STM32 也好,本质上你做的事情是:上电后 CPU 从固定地址取第一条指令,这条指令通常在你的启动文件里,然后跳到main函数,你在main里初始化时钟、配置外设、写业务逻辑。整个系统只有你一个程序在跑,内存是你直接操作的物理地址,没有虚拟内存,没有进程调度,没有文件系统。

这种模式的好处是确定性极强。你写P1 = 0xFE,P1 端口的电平立刻变化,中间没有任何抽象层。但问题也很明显:代码规模一大就难以维护,想跑网络协议栈、想接文件系统、想同时处理多个任务,裸机就会变得非常吃力。这就是为什么复杂嵌入式系统最终都要上 Linux。

2.2 u-boot 在系统里扮演什么角色

u-boot 全称 Universal Boot Loader,它做的事情可以类比成 PC 上的 BIOS 加上 GRUB 的组合。芯片上电后,最先运行的是一小段固化在芯片内部的 ROM 代码(BootROM),它负责从启动介质(eMMC、SD 卡、SPI Flash、网络等)加载一小段代码到片内 SRAM 执行。这段代码就是 u-boot 的SPL(Secondary Program Loader)阶段。SPL 的任务很单一:初始化 DDR 控制器,把完整的 u-boot 从存储介质搬到 DDR 里,然后跳过去执行。

完整的 u-boot 跑起来之后,它会初始化串口、网口、存储、USB 等外设,提供命令行交互界面,最后根据环境变量加载 Linux 内核和设备树,把控制权交给内核。所以 u-boot 的核心职责就三件事:初始化硬件、加载内核、传递参数。

这里有个关键点很多人一开始不理解:为什么不能直接上电就跑 Linux 内核?因为内核镜像通常好几 MB,而芯片内部的 SRAM 只有几十到几百 KB,根本放不下。所以必须有一个中间人先把 DDR 初始化好,再把内核搬到大内存里。这个中间人就是 u-boot。

2.3 从单片机转过来需要补的三块知识

第一块是ARM64 体系结构基础。你不需要把 ARM Architecture Reference Manual 几千页全看完,但至少要搞清楚异常级别 EL0 到 EL3 的区别、AArch64 的寄存器布局、MMU 页表的基本概念。这些在 u-boot 的启动汇编代码里到处都会出现。

第二块是设备树(Device Tree)。单片机时代你是在代码里硬编码硬件信息,ARM Linux 体系下硬件描述被抽离成了设备树文件。u-boot 自己也用设备树来描述板级硬件,编译时会用到.dts文件。你得能看懂一个简单的 dts 文件,知道compatible、reg、status这些属性是什么意思。

第三块是交叉编译工具链。单片机你可能用 Keil 或者 IAR 一键编译,u-boot 是在 Linux 环境下用aarch64-linux-gnu-gcc这类交叉编译器构建的。你得熟悉 Makefile 的基本规则、环境变量的设置、链接脚本的作用。

提示:不要试图先把这三块知识全部学完再动手。正确的做法是边做边学,先用 QEMU 把 u-boot 跑起来,遇到不懂的概念再回头查。这样学习效率比啃书高十倍。

3. 用 QEMU 搭建 ARM64 的 u-boot 实验环境

3.1 为什么选 QEMU 而不是买开发板

我见过太多人一腔热血买了块 ARM64 开发板,结果卡在串口驱动装不上、烧录工具不兼容、板子根本起不来,热情三天就耗光了。QEMU 的好处是零成本、可重复、随时快照。你编译出来的 u-boot 二进制直接丢给 QEMU 就能跑,串口输出直接打印在终端里,不需要任何硬件连线。

而且 QEMU 支持-d参数导出 CPU 执行日志,支持 GDB 远程调试,这些在真实硬件上要么做不到,要么需要昂贵的仿真器。对于学习 u-boot 启动流程来说,QEMU 是最合适的工具,没有之一。

3.2 环境准备的具体步骤

我用的环境是 Ubuntu 22.04,其他 Linux 发行版操作类似。先装必要的工具:

sudo apt update sudo apt install -y git build-essential flex bison \ libssl-dev libncurses-dev device-tree-compiler \ gcc-aarch64-linux-gnu qemu-system-arm gdb-multiarch

这里每个包都有用:flex和bison是 u-boot 构建系统需要的词法语法分析工具,libssl-dev用于签名相关功能,device-tree-compiler提供dtc命令来编译设备树,gcc-aarch64-linux-gnu是 ARM64 交叉编译器,qemu-system-arm是模拟器本体,gdb-multiarch用于后续调试。

装完之后验证一下交叉编译器:

aarch64-linux-gnu-gcc --version

能打印出版本号就说明工具链没问题。接下来拉 u-boot 源码:

git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01

我建议 checkout 一个稳定的 tag,不要直接用 master,因为 master 上的代码随时在变,可能引入你排查不了的编译问题。v2024.01 是我实测比较稳定的版本。

3.3 QEMU 的 ARM64 虚拟平台选哪个

QEMU 支持好几种 ARM64 虚拟板子,最常用的是qemu_arm64_defconfig对应的virt机器。这个虚拟平台模拟了一颗通用的 ARM64 CPU、一段内存、一个 PL011 串口、一个 GIC 中断控制器。u-boot 官方对它有完整的支持,编译出来直接能跑。

配置和编译命令如下:

make qemu_arm64_defconfig make CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)

编译完成后,在源码根目录下会生成u-boot.bin,这就是我们要交给 QEMU 的文件。整个编译过程大概两三分钟,取决于你的机器性能。

启动命令:

qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -nographic -bios u-boot.bin

-nographic把串口重定向到当前终端,-bios指定固件文件。执行后你应该能看到 u-boot 的启动日志,最后停在一个=>提示符前面。恭喜你,你已经成功让 u-boot 在 ARM64 上跑起来了。

4. u-boot 启动流程的深度拆解

4.1 从第一条指令到 board_init_r

u-boot 的启动流程可以分成两个大阶段:SPL 阶段和完整 u-boot 阶段。在 QEMU 环境下,因为不需要真正初始化 DDR,SPL 阶段被简化了,但流程逻辑是一样的。

完整 u-boot 的入口在arch/arm/lib/vectors.S里的_start标号。第一条指令是跳转到reset向量,然后依次执行:设置 CPU 到 EL2 或 EL1、关闭 MMU 和 Cache、设置栈指针、清零 BSS 段、调用board_init_f。

board_init_f是在 DDR 可用之前运行的,它只能使用全局数据区(gd),不能使用全局变量。这个函数的主要工作是初始化串口(让你能看到输出)、计算内存布局、填充 gd 结构体。做完这些之后,它会调用relocate_code把 u-boot 自身重定位到 DDR 的高端地址,然后跳转到board_init_r。

board_init_r运行在 DDR 里,可以使用完整的 C 运行环境。它负责初始化所有外设驱动、扫描存储设备、设置环境变量、最后进入主循环等待用户输入。你在串口里看到的=>提示符,就是主循环的产物。

4.2 关键数据结构 gd 和 bd

u-boot 里有两个非常重要的全局数据结构:gd(global data)和bd(board data)。gd存放的是 u-boot 运行时的全局状态,比如串口设备指针、环境变量地址、重定位偏移量等。bd存放的是板级信息,比如内存起始地址、内存大小、CPU 频率等。

这两个结构体在board_init_f阶段被填充,在board_init_r阶段被大量使用。你如果要在 u-boot 里加自己的调试代码,打印gd->ram_size或者gd->relocaddr是最常用的手段。

4.3 环境变量的加载与保存

u-boot 的环境变量默认保存在存储介质的一个固定偏移处,格式是一个 CRC32 校验头加上一堆key=value字符串。启动时 u-boot 会尝试从CONFIG_ENV_OFFSET指定的位置读取环境变量,如果 CRC 校验失败就加载编译时内置的默认环境变量。

在 QEMU 环境下,默认环境变量是编译进二进制的,你修改后用saveenv保存会提示找不到存储设备。这是正常的,因为虚拟平台没有配置持久化存储。如果你想练习环境变量操作,可以给 QEMU 挂一个虚拟 SD 卡或者用-drive参数挂一个文件作为存储后端。

5. 实操:从零编译到串口交互的完整过程

5.1 编译配置的定制化修改

默认的qemu_arm64_defconfig已经够用了,但如果你想体验真实的开发流程,可以试着改一些配置。比如打开CONFIG_CMD_BOOTI支持直接启动 Linux 内核镜像,打开CONFIG_CMD_FDT支持设备树操作。

修改配置有两种方式:直接编辑configs/qemu_arm64_defconfig文件,或者用make menuconfig图形界面。我推荐后者,因为菜单界面能看到每个选项的依赖关系和帮助说明,对理解配置项很有帮助。

make CROSS_COMPILE=aarch64-linux-gnu- menuconfig

进去之后你可以看到 u-boot 的配置系统有多庞大,从 CPU 架构到具体驱动,从命令支持到调试选项,几千个配置项。不要被吓到,你只需要关注Command line interface和Device Drivers这两大类就够了。

5.2 启动日志的逐行解读

当你执行 QEMU 启动命令后,串口会打印出一大段日志。我挑几个关键行解释一下:

U-Boot 2024.01 (Jan 15 2024 - 10:30:00 +0800)

这是版本和编译时间。如果你改了代码重新编译,时间会更新,可以用来确认你烧进去的是不是最新版本。

DRAM: 128 MiB

这是 QEMU virt 平台默认的内存大小。这个值来自设备树里的 memory 节点,u-boot 解析设备树后填充到 gd 结构体里。

Core: 12 devices, 8 uclasses, devicetree: separate

这行说明 u-boot 的驱动模型扫描到了 12 个设备,归入 8 个 uclass。uclass 是 u-boot 驱动模型的核心概念,类似 Linux 的设备类,把功能相似的驱动归在一起统一管理。

Loading Environment from nowhere... OK

前面说过,QEMU 环境没有持久化存储,所以环境变量是从内置的默认值加载的。

In: serial Out: serial Err: serial

标准输入输出都绑定到了串口,这就是为什么你能在终端里和 u-boot 交互。

5.3 常用命令的实操演示

进入=>提示符后,你可以试试这些命令:

=> bdinfo

打印板级信息,包括内存起始地址、大小、重定位后的 u-boot 地址、环境变量地址等。这个命令是排查启动问题的第一选择。

=> printenv

打印所有环境变量。你会看到baudrate、bootcmd、bootargs等关键变量。bootcmd是自动启动时执行的命令序列,bootargs是传给 Linux 内核的启动参数。

=> md 0x40000000 0x10

从内存地址 0x40000000 开始显示 16 个字。md是 memory display 的缩写,调试时用来查看内存内容非常方便。

=> mm 0x40000000

交互式修改内存。输入地址后会提示你输入新值,回车确认,输入q退出。这个命令在验证寄存器写入时很有用。

=> help

列出所有可用命令。不同配置下命令数量不同,默认的 qemu_arm64 配置大概有七八十条命令。

6. 常见问题排查与避坑经验

6.1 编译报错的高频原因

报错一:aarch64-linux-gnu-gcc: command not found

原因是你没装交叉编译器,或者装了但没在 PATH 里。用which aarch64-linux-gnu-gcc确认一下。如果确实装了但找不到,检查你的~/.bashrc里有没有把工具链路径加进去。

报错二:fatal error: openssl/ssl.h: No such file or directory

缺少libssl-dev。u-boot 的签名和加密功能依赖 OpenSSL 头文件。装上就好。

报错三:ERROR: You must install dtc

缺少设备树编译器。sudo apt install device-tree-compiler解决。

报错四:编译到一半提示multiple definition of xxx

这种通常是你的编译环境里残留了上次编译的目标文件,而源码结构发生了变化。执行make distclean彻底清理后重新配置编译。

6.2 QEMU 启动无输出的排查思路

这是新手最常遇到的问题:命令敲下去,终端一片空白,没有任何输出。排查顺序如下:

第一步,确认u-boot.bin文件存在且大小合理。正常的 ARM64 u-boot 二进制大概几百 KB 到 1 MB 左右。如果只有几 KB,说明编译没成功。

第二步,确认 QEMU 命令参数正确。-machine virt和-cpu cortex-a57是必须的,-nographic也是必须的,否则串口输出不会重定向到终端。

第三步,加-d in_asm参数看 QEMU 是否在执行指令。如果日志里全是 0 地址的指令,说明 u-boot 的入口地址和 QEMU 期望的不一致。

第四步,用 GDB 连上去看 PC 指针停在哪里。启动 QEMU 时加-s -S参数,然后用gdb-multiarch连接localhost:1234,执行info registers看 CPU 状态。

6.3 环境变量保存失败的解决

在 QEMU 里执行saveenv大概率会报错,因为默认没有配置可写的存储后端。解决办法是给 QEMU 挂一个虚拟磁盘:

qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -nographic -bios u-boot.bin \ -drive file=env.img,format=raw,if=none,id=envdisk \ -device virtio-blk-device,drive=envdisk

然后需要在 u-boot 配置里打开CONFIG_ENV_IS_IN_MMC或CONFIG_ENV_IS_IN_VIRTIO,并设置正确的偏移量。这个操作稍微复杂一点,但走通一次之后你就理解了环境变量持久化的完整链路。

6.4 常见问题速查表

问题现象可能原因排查方法
编译报错找不到编译器工具链未安装或 PATH 未配置which aarch64-linux-gnu-gcc
编译报错缺头文件依赖库未安装根据报错信息安装对应 dev 包
QEMU 启动无输出参数错误或二进制损坏检查文件大小,加-d in_asm
串口输出乱码波特率不匹配QEMU 默认 115200,检查配置
saveenv 失败无持久化存储挂载虚拟磁盘并配置环境变量存储
GDB 连不上未加-s -S参数启动命令加上调试参数

注意:每次修改 u-boot 配置后,一定要先make distclean再重新make xxx_defconfig,否则旧的配置缓存会导致各种诡异问题。这个坑我踩过不止一次。

7. 从 u-boot 出发,下一步该往哪走

7.1 用 u-boot 加载 Linux 内核

u-boot 跑通之后,最自然的下一步是让它加载一个真实的 Linux 内核。你需要准备三个东西:内核镜像Image、设备树文件qemu-arm64.dtb、以及一个根文件系统。QEMU 环境下可以用-kernel参数直接加载内核,但那样就绕过了 u-boot。正确的做法是在 u-boot 命令行里用booti命令手动加载。

具体操作是先用tftp或者load命令把内核镜像和设备树加载到内存,然后执行:

=> booti 0x40080000 - 0x44000000

第一个参数是内核镜像地址,第二个参数是 initrd 地址(没有就写-),第三个参数是设备树地址。执行后 u-boot 会把控制权交给内核,你会看到内核启动日志刷屏。

7.2 阅读 u-boot 源码的正确姿势

u-boot 源码有几十万行,不要试图从头读到尾。正确的做法是跟着启动流程读。从_start开始,一路跟到board_init_r,把这条主线上调用的每个关键函数都看一遍。遇到不认识的函数就跳进去看实现,看完再回来继续。

我建议重点读这几个文件:arch/arm/lib/vectors.S(入口汇编)、common/board_f.c(board_init_f 实现)、common/board_r.c(board_init_r 实现)、drivers/serial/serial-uclass.c(串口驱动模型)。把这四个文件吃透,u-boot 的骨架你就掌握了。

7.3 驱动模型和 Linux 的异同

u-boot 的驱动模型(DM)和 Linux 的设备模型在设计理念上很像,都是通过设备树匹配驱动、通过 uclass/class 统一管理同类设备。但 u-boot 的 DM 更轻量,没有电源管理、没有热插拔、没有复杂的引用计数。

如果你以后要写 Linux 驱动,先在 u-boot 里写一个简单的 DM 驱动是很好的练手。u-boot 的驱动代码量小、逻辑清晰,调试也方便,适合用来理解设备树匹配、probe 流程、uclass 操作这些核心概念。

7.4 进阶方向:安全启动与固件更新

u-boot 还支持安全启动(Verified Boot)和固件更新(DFU、fastboot)。安全启动的核心是对内核镜像做签名校验,防止被篡改。固件更新则是通过 USB 或者网络把新固件写入存储介质。这两个方向在实际产品中非常重要,但学习曲线也比较陡,建议先把基础启动流程搞扎实再碰。

我个人在实际操作中的体会是,u-boot 这个东西入门确实有门槛,但它的知识密度极高。你花两周时间啃下来的东西,可能比你在单片机上折腾两个月学到的还多。而且这些知识是通用的,换任何一块 ARM64 板子,启动流程、驱动模型、环境变量机制都是一样的。最后再分享一个小技巧:把 QEMU 的启动命令写成一个 shell 脚本,每次改完代码一键编译加启动,能省下大量重复劳动的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 15:34:28

编译链接原理与Makefile核心价值:从零构建C/C++自动化工程

【Makefile 专家之路 | 基础篇】01. 万物起源:编译链接原理与 Makefile 的核心价值搞了十几年C/C项目,从最开始在命令行里手敲gcc,到后来维护几万行代码的自动化构建系统,踩过的坑比写过的代码还多。很多人问我Makefile到底怎么学…

作者头像 李华
网站建设 2026/9/29 15:34:00

ACA真题反推云计算实操能力图谱:从刷题到真实运维

简介:本资源为2025年阿里云ACA(助理工程师)云计算认证官方题型模拟试卷及详解答案,面向云计算初学者、备考ACA认证的技术人员及企业云运维入门者,旨在系统梳理核心服务操作规范与典型考点辨析。文档以单选、多选题形式…

作者头像 李华
网站建设 2026/9/29 15:33:30

动态SQL与MyBatis Generator:从标签解析到代码生成实战

1. 为什么我建议你把动态SQL当一门“小语言”来学 很长一段时间里,我对 MyBatis 的印象停留在“写 SQL 比 JDBC 舒服一点”这个层面。直到我接手一个老合同管理系统,里面每个列表查询都手写了一堆重复的条件判断,改一个字段要连着改三四个 XM…

作者头像 李华
网站建设 2026/9/29 15:32:59

Elasticsearch 8.x 性能调优实战:从磁盘判断到查询改写复盘

Elasticsearch 8.x 性能调优,我的实战复盘前阵子接手了一个 Elasticsearch 8.x 集群,业务方反馈:“写入偶尔会堵,查询有时候卡到一两秒。”注意,这里是“偶尔会堵”,不是“一直很慢”,也不是“某…

作者头像 李华
网站建设 2026/9/29 15:32:59

10kV架空电缆敷设全流程:从选型到验收的实战指南

简介:架空电缆敷设是通信线路工程中常见的布线方式,尤其适合离局较远、用户较少且变动较大的区域。课件围绕施工全流程展开,适合通信工程专业学生、线路勘察设计人员及一线施工人员学习参考。内容覆盖吊线钢绞线选型,明确吊线夹板…

作者头像 李华
网站建设 2026/9/29 15:32:58

智能体AI工程化落地指南:从概念演示到生产实践

说实话,2026年再聊智能体AI,已经不应该停留在“能做什么”的层面了。过去一年,市面上的白皮书和峰会PPT几乎都在讲同一个故事:智能体是下一个平台级机会。但机会归机会,真正动手的人才知道坑有多深。我自己过去一年帮几…

作者头像 李华