ARTICLE DETAIL

资讯详情

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

从单片机到嵌入式Linux:QEMU+ARM64实战u-boot启动链路

从单片机到嵌入式Linux:QEMU+ARM64实战u-boot启动链路 1. 从单片机到 u-boot为什么我劝你尽早跨过这道坎玩过 51 单片机、STM32 的朋友大多经历过点灯、按键、串口打印、PWM 调速这一整套流程。这些内容确实能帮你建立对硬件的基本感知知道寄存器怎么配、中断怎么进、时钟树怎么算。但如果你在这个阶段停留太久很容易陷入一种舒适区陷阱——用着 Keil、点着下载按钮、看着串口助手输出一行Hello World就以为自己已经摸到了嵌入式的门槛。实际情况是单片机开发和嵌入式 Linux 开发之间隔着一道相当宽的鸿沟。这道鸿沟的核心标志之一就是u-boot。你可以把 u-boot 理解成嵌入式 Linux 世界的BIOS 引导菜单 硬件初始化管家三合一角色。它在你按下复位键之后、Linux 内核启动之前接管整个系统负责把 DDR 初始化好、把时钟配好、把内核从存储介质里搬进内存、把参数传给内核最后把控制权交出去。没有它内核连启动的机会都没有。我见过太多人卡在这一步单片机玩得挺溜一上手嵌入式 Linux 就懵了因为整个开发范式完全变了。单片机时代你写的是裸机程序或者 RTOS 任务代码直接跑在物理地址上而 u-boot 阶段你要面对的是多阶段启动、重定位、设备树、交叉编译工具链、链接脚本这一整套东西。这些概念在 51 单片机的世界里根本不存在所以第一次接触时那种每个字都认识但连起来看不懂的感觉非常正常。这篇内容就是写给这类人的有单片机基础、想往嵌入式 Linux 方向走、但被 u-boot 挡在门外。我会从整体设计思路讲到具体实操用 QEMU 模拟 ARM64 环境把整个流程跑通让你不用买开发板也能把 u-boot 的启动链路吃透。涉及的关键词包括 u-boot、嵌入式、ARM64、QEMU这些都会在后面的实操里反复出现。2. 整体设计思路为什么选 QEMU ARM64 这条路线2.1 先搞清楚 u-boot 到底解决什么问题在动手之前得先明白 u-boot 存在的意义。很多人学 u-boot 的方式是直接背命令比如printenv、setenv、bootm、tftp背完发现还是不知道它在干嘛。正确的切入角度是问自己如果我是芯片上电之后第一件事要做什么上电瞬间CPU 从固定的复位向量取第一条指令。这时候 DDR 还没初始化内存不能用能用的只有芯片内部一小块 SRAM通常几十到几百 KB。u-boot 的第一阶段SPLSecondary Program Loader就运行在这块小内存里它的任务极其克制初始化 DDR 控制器、配置基本时钟、然后把完整的 u-boot 从存储介质加载到 DDR 里。第二阶段也就是我们平时说的 u-boot proper跑在 DDR 里空间大了才能做那些复杂的事——驱动网络、USB、文件系统、命令行交互。这个两阶段设计不是 u-boot 故意搞复杂而是被硬件逼出来的。SRAM 太小装不下完整 u-boot所以必须分两步走。理解了这一点你再看 SPL 和 u-boot proper 的代码分工就不会觉得莫名其妙了。2.2 为什么用 QEMU 而不是真开发板学 u-boot 最劝退的地方在于一旦启动失败你面对的可能是一片漆黑的串口没有任何输出。这时候排查问题需要示波器、需要 JTAG、需要反复烧录门槛极高。而 QEMU 的价值在于它把整个硬件环境软件化了你可以随时改代码、重新编译、重新运行不用等烧录用-d参数导出 CPU 执行日志看到每一条指令配合 GDB 单步调试 u-boot 源码不花一分钱就能玩 ARM64 这种高端架构ARM64也叫 AArch64是当前服务器、手机、开发板的主流架构和 32 位 ARM 相比它的寄存器宽度、异常模型、内存布局都有本质区别。用 QEMU 模拟 ARM64 跑 u-boot既能学到 u-boot 的通用逻辑又能顺带把 ARM64 的启动流程摸清楚性价比很高。2.3 整体路线图我把整个学习路径拆成这么几层从下往上依次是层级内容对应工具环境层交叉编译工具链、QEMU 安装aarch64-linux-gnu-gcc、qemu-system-aarch64源码层u-boot 源码获取、配置、编译make、defconfig运行层QEMU 启动 u-boot、串口交互qemu-system-aarch64 -nographic调试层GDB 连接、日志分析gdb-multiarch、-d 参数扩展层加载内核、设备树、根文件系统booti、bootm这个顺序很重要不要跳步。我见过有人一上来就想让 u-boot 加载 Linux 内核结果连 u-boot 自己都没跑起来纯属浪费时间。先把 u-boot 单独跑通、能进命令行再考虑后面的事。3. 核心细节解析u-boot 启动链路里的关键概念3.1 交叉编译工具链的选择逻辑在 x86 电脑上编译 ARM64 程序必须用交叉编译工具链。这里有个常见误区很多人以为随便装个gcc-aarch64-linux-gnu就行实际上工具链的版本和 u-boot 版本之间有兼容性要求。u-boot 对工具链的要求主要体现在两点一是 GCC 版本不能太老一般要求 6.0 以上二是要支持目标架构的浮点 ABI。我实测下来Ubuntu 22.04 自带的gcc-aarch64-linux-gnu版本是 11.x配 u-boot 2023 之后的版本没问题。如果你用的是比较老的发行版建议手动装 Linaro 或者 ARM 官方发布的工具链。提示工具链前缀要和CROSS_COMPILE变量严格对应。ARM64 的前缀是aarch64-linux-gnu-注意结尾那个短横线不能少少了会报command not found。3.2 defconfig 配置文件的含义u-boot 支持几百种开发板每种板子的配置都放在configs/目录下文件名形如xxx_defconfig。对于 QEMU 模拟的 ARM64 环境官方提供了一个通用的配置文件qemu_arm64_defconfig。这个配置文件里定义了什么呢主要是这几类架构相关CONFIG_ARM64y、CONFIG_TARGET_QEMU_ARM_64BITy驱动相关串口驱动、时钟驱动、存储驱动功能相关是否支持命令行、是否支持网络、是否支持文件系统内存布局CONFIG_SYS_TEXT_BASE定义了 u-boot 被加载到的地址理解这些配置项的意义比死记配置命令重要得多。比如CONFIG_SYS_TEXT_BASE决定了 u-boot 重定位前的运行地址如果这个地址和 QEMU 加载地址对不上u-boot 一启动就会跑飞。3.3 重定位u-boot 最容易被忽略的一环重定位relocation是 u-boot 里一个非常核心但经常被跳过的概念。简单说u-boot 编译时链接地址是 A但实际运行时可能被加载到地址 B这时候所有绝对地址引用都需要修正这个过程就叫重定位。为什么需要重定位因为 u-boot 要支持从各种介质启动加载地址不固定。为了让代码在任何地址都能跑u-boot 在启动早期会把自己从当前地址复制到CONFIG_SYS_TEXT_BASE指定的地址然后修正 GOT 表和全局变量指针。在 QEMU 环境下重定位的日志会打印在串口上你会看到类似这样的输出U-Boot 2023.10 (Jan 01 2024 - 00:00:00 0000) DRAM: 128 MiB Core: 51 devices, 14 uclasses, devicetree: separate Flash: 64 MiB Loading Environment from Flash... OK In: serial Out: serial Err: serial Net: eth0: virtio-net#0 Hit any key to stop autoboot: 0看到DRAM: 128 MiB这行说明 DDR 初始化已经完成u-boot 已经成功跑起来了。4. 实操过程从零把 u-boot 跑在 QEMU 上4.1 环境准备与依赖安装先确认你的系统是 Ubuntu 20.04 或 22.04其他发行版命令略有差异。安装必要的工具sudo apt update sudo apt install -y gcc-aarch64-linux-gnu \ qemu-system-arm \ gdb-multiarch \ bison flex \ libssl-dev \ device-tree-compiler \ bc \ git这里每一项都有用gcc-aarch64-linux-gnu是交叉编译器qemu-system-arm提供 ARM64 模拟gdb-multiarch用于调试bison和flex是 u-boot 编译时需要的语法分析工具libssl-dev用于签名相关功能device-tree-compiler处理设备树bc是编译脚本里用到的计算器。装完之后验证一下aarch64-linux-gnu-gcc --version qemu-system-aarch64 --version两条命令都能输出版本号说明环境 OK。4.2 获取 u-boot 源码从官方仓库克隆建议用浅克隆加快速度git clone --depth 1 https://source.denx.de/u-boot/u-boot.git cd u-boot如果你网络环境访问官方仓库慢也可以用 GitHub 的镜像。克隆完成后看一下当前版本git log -1 --oneline记下这个版本号后面排查问题时有用。4.3 配置与编译QEMU ARM64 的配置非常直接export CROSS_COMPILEaarch64-linux-gnu- make qemu_arm64_defconfig make -j$(nproc)编译完成后在源码根目录会生成u-boot.bin这是我们要用的镜像文件。同时还会生成u-bootELF 格式用于调试和u-boot.map链接映射表排查地址问题必备。编译过程中如果报错最常见的原因是工具链版本不匹配或者缺少依赖。我踩过的坑是libssl-dev没装编译到签名模块时直接失败报错信息还特别隐晦找了好久才发现。4.4 用 QEMU 启动 u-boot启动命令如下qemu-system-aarch64 \ -M virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin \ -m 128M逐个参数解释-M virt使用 QEMU 的通用虚拟平台这个平台模拟了 ARM64 的基本硬件-cpu cortex-a57模拟 Cortex-A57 处理器这是 ARM64 的经典核心-nographic不开图形界面串口直接连到终端-bios u-boot.bin把 u-boot 当作固件加载-m 128M给 128MB 内存启动后你会看到 u-boot 的启动日志最后停在命令行提示符。这时候你已经进入了 u-boot 的交互界面。4.5 常用命令实操在提示符下可以试试这些命令 version printenv bdinfo helpversion显示 u-boot 版本和编译时间printenv打印所有环境变量bdinfo显示板级信息内存布局、启动参数等help列出所有可用命令。想退出 QEMU按CtrlA然后按X。这个组合键是 QEMU 的退出快捷键-nographic模式下必须用它直接CtrlC是杀不掉 QEMU 的。注意-nographic模式下QEMU 会接管你的终端。如果误操作导致终端显示混乱输入reset命令可以恢复。5. 常见问题与排查技巧实录5.1 启动无输出怎么办这是最常见的问题。u-boot 启动后串口一片空白什么日志都没有。排查思路按这个顺序来第一确认-bios参数指向的文件存在且非空。用ls -lh u-boot.bin看一下大小正常应该在几百 KB 到 1MB 之间。第二确认 QEMU 版本支持-M virt。老版本 QEMU 可能不支持这个平台用qemu-system-aarch64 -M help查看支持的机器列表。第三检查 u-boot 的CONFIG_SYS_TEXT_BASE是否和 QEMU 加载地址匹配。QEMU virt 平台默认把-bios加载到0x40000000如果 u-boot 配置的链接地址不是这个就会跑飞。5.2 编译报错的典型原因我把常见的编译错误整理成一张表错误信息原因解决方法aarch64-linux-gnu-gcc: command not found工具链未安装或 PATH 不对重装工具链检查 PATHfatal error: openssl/ssl.h: No such file缺少 libssl-devapt install libssl-devbison: command not found缺少语法分析工具apt install bison flexundefined reference to xxx链接脚本或配置不匹配检查 defconfig清理后重编Error: unrecognized opcode工具链版本过老升级工具链遇到编译错误第一反应应该是make distclean然后重新配置编译很多问题都是残留的中间文件导致的。5.3 用 GDB 调试 u-boot如果启动日志停在某个奇怪的地方或者你想单步跟踪启动流程GDB 是最强工具。启动 QEMU 时加上-s -S参数qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic \ -bios u-boot.bin -m 128M -s -S-s表示在 1234 端口开启 GDB server-S表示启动时暂停等待 GDB 连接。然后另开一个终端gdb-multiarch u-boot (gdb) target remote :1234 (gdb) break board_init_f (gdb) continue这样就能在board_init_f函数处断下来单步跟踪 u-boot 的早期初始化流程。这个技巧在排查重定位问题时特别有用。5.4 导出执行日志分析QEMU 支持把 CPU 执行的每条指令导出到文件qemu-system-aarch64 -M virt -cpu cortex-a57 -nographic \ -bios u-boot.bin -m 128M \ -d in_asm -D qemu.log生成的qemu.log会记录所有翻译执行的指令块。文件可能很大建议配合grep定位关键地址。比如你想知道 u-boot 在0x40000000处执行了什么可以grep -A 20 0x0000000040000000 qemu.log这个日志对理解 u-boot 的启动流程帮助极大尤其是当串口没有输出时它是唯一的线索。5.5 环境变量保存失败在 u-boot 里用setenv改了变量saveenv时可能报错提示找不到存储设备。这是因为 QEMU virt 平台默认没有持久化存储环境变量只能存在内存里。想持久化的话需要给 QEMU 挂一个虚拟 flashqemu-system-aarch64 -M virt -cpu cortex-a57 -nographic \ -bios u-boot.bin -m 128M \ -drive ifpflash,formatraw,fileflash.img然后 u-boot 配置里要启用对应的 flash 驱动。这个属于进阶内容初学阶段可以先不管用内存环境变量就够了。6. 从 u-boot 到内核下一步该往哪走u-boot 跑通之后很多人会问接下来呢我的建议是先把 u-boot 的命令行玩熟然后尝试让它加载一个真实的 Linux 内核。具体做法是用 QEMU 直接启动一个 ARM64 的 Linux 内核镜像然后在 u-boot 里用booti命令手动加载。这需要你准备三样东西内核镜像Image、设备树dtb、根文件系统rootfs。QEMU 官方提供了一些预编译的镜像可以直接下载使用。加载命令大致是这样 load virtio 0:0 0x41000000 Image load virtio 0:0 0x42000000 virt.dtb booti 0x41000000 - 0x42000000load命令从 virtio 设备读取文件到指定内存地址booti启动内核。-表示 initrd 地址为空0x42000000是设备树地址。这个过程会遇到不少问题比如设备树不匹配、内核地址冲突、根文件系统挂载失败等等。但正是这些问题能让你真正理解 u-boot 和内核之间的交接协议——内核期望 u-boot 传给它什么、以什么格式传、传到哪里。我个人在实际操作中的体会是u-boot 这东西光看文档学不会必须动手跑。跑通一次 QEMU 启动比看十篇教程都管用。而且 QEMU 环境的好处是你可以随便折腾搞坏了删掉重来就行没有任何硬件成本。等你把 QEMU 上的流程摸熟了再上真开发板会发现大部分知识都能直接迁移过去剩下的只是驱动适配和硬件差异。最后再分享一个小技巧u-boot 源码里的doc/目录是个宝库尤其是doc/board/下面针对各个平台的文档写得比很多第三方教程都清楚。遇到不懂的配置项直接去doc/里搜往往能找到官方解释。
返回列表