ARTICLE DETAIL

资讯详情

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

从单片机到u-boot:QEMU ARM64实战入门与启动流程解析

从单片机到u-boot:QEMU ARM64实战入门与启动流程解析 1. 从单片机到 u-boot为什么我劝你尽早跨过这道坎玩过 51 或者 STM32 的朋友大概都有这种体会点个灯、跑个 OLED、调个串口成就感来得很快。但当你真正想往嵌入式 Linux 方向走的时候会发现前面横着一堵墙——Bootloader。而 u-boot就是这堵墙上最厚的那块砖。我自己是从 STC89C52 一路玩上来的中间经历过 STM32F103 的寄存器开发、HAL 库开发后来因为一个项目需要跑 Linux 系统被迫啃 u-boot。说实话第一次看 u-boot 源码的时候满屏的宏定义和链接脚本头都大了。但熬过那两三个月之后回头看单片机那套东西和 u-boot 比起来真的只是玩具级的复杂度。这篇内容我想聊的不是u-boot 是什么这种教科书问题而是一个从单片机转过来的开发者怎么用最短的时间把 u-boot 跑起来、看懂、改得动。核心关键词就几个u-boot、嵌入式、ARM64、QEMU。我会用 QEMU 模拟 ARM64 环境来演示因为这样你不需要买任何开发板一台电脑就能把整个流程走通。适合谁看有 C 语言基础、玩过单片机、想往嵌入式 Linux 方向转的兄弟。如果你连指针和结构体都还没搞明白建议先把 C 语言补一补再来。整个思路是这样的先用 QEMU 把 ARM64 的 u-boot 编译、运行、调试跑通建立感性认识然后拆解 u-boot 的启动流程和关键数据结构最后动手改一个自己的命令理解 u-boot 的驱动模型和命令行框架。每一步我都会给出具体的命令和参数你照着敲就行。2. 为什么是 u-boot 而不是继续玩单片机2.1 单片机与 u-boot 的本质差异很多人觉得单片机玩到 STM32H7 这个级别主频都 480MHz 了和嵌入式 Linux 应该差不多吧差得远。这个差异不在于主频而在于软件栈的层次。单片机开发你写的代码直接跑在裸机上最多加个 RTOS。中断向量表是你自己填的堆栈是你自己设的内存布局是你自己在链接脚本里定的。整个系统就你一个人在管简单直接。u-boot 不一样。它本身是一个裸机程序但它要负责的事情多得多初始化 DDR 控制器、配置时钟树、加载设备树、从各种介质eMMC、NAND、TFTP、USB读取内核镜像、校验镜像完整性、传递启动参数给内核。做完这些之后它还要把自己让出去把 CPU 控制权交给 Linux 内核。这个过程涉及的东西比单片机的中断处理复杂一个数量级。我个人的感受是单片机教会你怎么控制硬件u-boot 教会你怎么管理一个完整的启动链路。前者是点后者是线。2.2 从单片机转 u-boot 需要补哪些课从我的经验来看从单片机转到 u-boot需要补的课主要有这几块ARM64 架构基础异常等级EL0-EL3、MMU 页表、GIC 中断控制器。这些在单片机上基本不涉及但 u-boot 在 ARM64 上跑的时候必须处理。设备树Device Treeu-boot 和 Linux 内核都靠设备树来描述硬件。你得会看.dts和.dtsi文件知道compatible属性怎么匹配驱动。链接脚本与内存布局u-boot 的u-boot.lds决定了代码段、数据段、BSS 段放在哪里。这个比单片机的分散加载文件复杂得多。命令行框架与驱动模型u-boot 有一套自己的 U_BOOT_CMD 宏和 DMDriver Model框架理解这套东西才能改得动代码。好消息是这些概念你不需要一次性全部搞懂。我的建议是先跑起来再逐步深入。下面我就用 QEMU 带你走一遍。2.3 QEMU 为什么是学习 u-boot 的最佳搭档学 u-boot 最大的障碍是什么是烧录和调试的成本。你要是用真实开发板每次改代码都要重新编译、烧录、上电、看串口输出一轮下来少说五分钟。而且一旦 u-boot 跑飞了串口什么都不输出你根本不知道死在哪里。QEMU 解决了这个问题。它可以在你的 x86 电脑上模拟一颗 ARM64 的 CPU配合 GDB 可以单步调试 u-boot 的每一条指令。编译完直接运行不需要烧录不需要硬件。而且 QEMU 支持-d参数导出各种调试信息配合-S -s可以让你在 u-boot 的第一条指令处就停下来。我实测下来用 QEMU 学习 u-boot 的效率至少是真实开发板的三倍。等你把 QEMU 上的流程跑通了再上手真实开发板会发现只是换了个加载方式而已。3. 环境搭建从零把 ARM64 u-boot 跑起来3.1 工具链与依赖安装先说环境。我用的是 Ubuntu 22.04其他发行版大同小异。需要装的东西如下sudo apt update sudo apt install -y git build-essential bison flex \ libssl-dev libncurses-dev python3 python3-pip \ gcc-aarch64-linux-gnu gdb-multiarch \ qemu-system-arm device-tree-compiler这里解释一下几个关键包的作用gcc-aarch64-linux-gnuARM64 的交叉编译器。你的电脑是 x86_64要编译出 ARM64 能跑的代码必须用交叉编译器。qemu-system-armQEMU 的 ARM 模拟器。注意包名是qemu-system-arm但它同时支持 ARM32 和 ARM64。gdb-multiarch支持多架构的 GDB用来调试 ARM64 程序。device-tree-compiler设备树编译器把.dts编译成.dtb。注意不要用gcc-arm-linux-gnueabihf那是 ARM32 的编译器。ARM64 必须用aarch64前缀的。3.2 获取 u-boot 源码并配置源码直接从官方仓库拉git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01我选 v2024.01 这个版本因为它对 QEMU ARM64 的支持比较完善而且相对稳定。你如果拉最新版也行但可能会遇到一些新引入的编译问题。配置 QEMU ARM64 的默认配置make qemu_arm64_defconfig这个defconfig文件在configs/qemu_arm64_defconfig里面定义了 QEMU 虚拟机的默认配置。你可以打开看看里面指定了CONFIG_TARGET_QEMU_ARM_64BITy、CONFIG_SYS_TEXT_BASE0x40200000等关键参数。3.3 编译与产物说明编译命令make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后在根目录下会生成几个关键文件文件名作用u-bootELF 格式的 u-boot带符号信息用于 GDB 调试u-boot.bin纯二进制格式用于烧录到开发板u-boot.map链接映射文件可以看到每个函数和变量在内存中的地址u-boot.srecS-record 格式某些烧录工具需要.config当前配置和内核的.config类似u-boot.map这个文件非常重要。当你调试的时候想知道某个函数在哪个地址直接在这个文件里搜就行。我经常用它来确认board_init_f、board_init_r这些关键函数的地址。3.4 用 QEMU 启动 u-boot启动命令qemu-system-aarch64 \ -machine virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin参数解释-machine virtQEMU 的虚拟开发板ARM64 上最常用的虚拟平台。-cpu cortex-a57模拟 Cortex-A57 核心这是 ARMv8 的 64 位核心。-nographic不用图形界面串口输出直接打到终端。-bios u-boot.bin把 u-boot.bin 当作固件加载。启动后你应该能看到类似这样的输出U-Boot 2024.01 (Jan 15 2024 - 10:30:00 0800) 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: 0 看到提示符说明 u-boot 已经跑起来了。你可以敲help看看支持哪些命令敲bdinfo看看板级信息敲printenv看看环境变量。提示如果卡在 Hit any key to stop autoboot直接按回车就能进入命令行。4. 深入 u-boot 启动流程从第一条指令到命令行4.1 ARM64 的启动入口与异常等级u-boot 在 ARM64 上的入口是arch/arm/cpu/armv8/start.S里的_start。这个文件的第一条指令就是整个 u-boot 的起点。ARM64 有四个异常等级EL0用户态、EL1内核态、EL2虚拟化、EL3安全监控。u-boot 通常运行在 EL2 或 EL1。QEMU 的virt机器默认从 EL2 开始执行所以 u-boot 启动后会先判断当前异常等级然后做相应的初始化。_start里做的事情按顺序是关闭 MMU 和 D-Cache因为此时还没有建立页表设置异常向量表基地址判断当前 EL 等级如果是 EL3 或 EL2做相应的降级或初始化设置栈指针跳转到_main这个流程和单片机的启动文件比如 STM32 的startup_stm32f103.s思路是一样的只是 ARM64 多了异常等级的处理。4.2 board_init_f 与 board_init_r 的分工_main在arch/arm/lib/crt0_64.S里它最终会调用两个关键函数board_init_f和board_init_r。board_init_f运行在重定位之前此时 u-boot 还在只读内存QEMU 里是 ROM 或 Flash中运行。它做的事情包括初始化串口让你能看到输出初始化定时器计算重定位目标地址填充gdglobal data结构体调用board_init_f的初始化序列board_init_r运行在重定位之后此时 u-boot 已经被拷贝到 DDR 中运行。它做的事情包括初始化 DMDriver Model框架扫描设备树绑定驱动初始化各种外设MMC、USB、网络等进入主循环等待用户输入命令这个两阶段设计和单片机的启动流程有本质区别。单片机通常只有一次初始化而 u-boot 必须分两阶段因为它在重定位之前不能写全局变量还在只读内存里。4.3 重定位u-boot 为什么要搬家重定位是 u-boot 里一个非常重要的概念。简单说u-boot 编译出来的代码链接地址是CONFIG_SYS_TEXT_BASEQEMU ARM64 上是0x40200000但实际运行时它需要把自己拷贝到 DDR 的高地址区域通常是内存顶端往下若干 MB。为什么要搬家因为 u-boot 最终要加载 Linux 内核内核需要一块连续的低地址内存。如果 u-boot 一直占着低地址不放内核就没地方放了。所以 u-boot 在初始化完 DDR 之后会把自己拷贝到内存高端把低端内存让出来给内核。重定位的核心代码在common/board_f.c的relocate_code函数里。它会计算源地址、目标地址、代码长度然后调用memcpy完成拷贝。拷贝完成后通过修改gd-relocaddr和调整栈指针让后续代码在新地址上运行。我踩过的一个坑是如果你在board_init_f阶段用了一个全局变量重定位之后这个变量的值会丢失因为全局变量在重定位时被拷贝了一份但指针还指向旧地址。正确的做法是用gd结构体来传递数据因为gd的地址在重定位时会被更新。4.4 设备树在 u-boot 中的角色u-boot 从 v2015.07 开始引入 DMDriver Model设备树成为描述硬件的核心机制。在 QEMU ARM64 上u-boot 会从arch/arm/dts/qemu-arm64.dts编译出设备树并在启动时解析它。设备树里每个节点都有一个compatible属性u-boot 用这个属性来匹配驱动。比如串口节点uart0: serial9000000 { compatible arm,pl011; reg 0x0 0x9000000 0x0 0x1000; clocks uartclk; };u-boot 启动时会扫描设备树找到compatible arm,pl011的节点然后调用对应的 PL011 串口驱动来初始化它。这个机制和 Linux 内核的设备树匹配完全一样。理解设备树是理解 u-boot 驱动模型的关键。如果你之前只玩过单片机建议花点时间看看设备树的语法至少要知道reg、compatible、status这几个属性的含义。5. 动手改一个 u-boot 命令从看懂到改得动5.1 u-boot 命令行框架解析u-boot 的命令行框架核心是U_BOOT_CMD宏。每个命令都是通过这个宏注册的。以mdmemory display命令为例U_BOOT_CMD( md, 5, 1, do_mem_md, memory display, [.b, .w, .l, .q] address [# of objects] );参数含义第一个参数命令名第二个参数最大参数个数第三个参数是否可重复1 表示按回车重复执行第四个参数命令处理函数第五个参数简短描述第六个参数用法说明这个宏展开后会生成一个cmd_tbl_t结构体放在.u_boot_list段里。u-boot 启动时会遍历这个段把所有命令注册到命令表里。5.2 编写一个自定义命令我们来写一个简单的命令myinfo功能是打印当前 CPU 的异常等级和 u-boot 的重定位地址。在cmd/目录下新建myinfo.c#include common.h #include command.h #include asm/system.h static int do_myinfo(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) { unsigned long el; el current_el(); printf(Current EL: %lu\n, el); printf(U-Boot relocaddr: 0x%lx\n, gd-relocaddr); printf(U-Boot text base: 0x%lx\n, gd-ram_base); return CMD_RET_SUCCESS; } U_BOOT_CMD( myinfo, 1, 1, do_myinfo, print my custom info, );然后在cmd/Makefile里加上obj-$(CONFIG_CMD_MYINFO) myinfo.o在cmd/Kconfig里加上配置项config CMD_MYINFO bool myinfo help Print custom information about the current U-Boot.最后在configs/qemu_arm64_defconfig里加上CONFIG_CMD_MYINFOy重新编译。5.3 编译、运行与验证重新编译make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)启动 QEMUqemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -bios u-boot.bin在 u-boot 命令行里敲myinfo你应该能看到 myinfo Current EL: 2 U-Boot relocaddr: 0x7fe00000 U-Boot text base: 0x40000000这说明你的自定义命令已经成功注册并运行了。Current EL: 2表示 u-boot 运行在 EL2relocaddr是重定位后的地址ram_base是 DDR 的起始地址。提示如果你敲myinfo提示 Unknown command检查一下.config里CONFIG_CMD_MYINFO是否真的被设成了y。有时候defconfig的修改不会自动生效需要手动跑一次make qemu_arm64_defconfig再编译。5.4 用 GDB 单步调试 u-bootQEMU 配合 GDB 调试 u-boot 非常方便。启动 QEMU 时加上-S -sqemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -bios u-boot.bin -S -s-S表示启动时暂停-s表示在 1234 端口开启 GDB server。另开一个终端启动 GDBgdb-multiarch u-boot在 GDB 里连接 QEMU(gdb) target remote :1234 (gdb) b board_init_f (gdb) c这样就能在board_init_f处停下来然后单步执行观察寄存器和内存的变化。我经常用这种方式来确认重定位前后的地址变化比看代码直观得多。6. 常见问题与排查技巧实录6.1 QEMU 启动后没有任何输出这是最常见的问题。排查思路如下现象可能原因解决方法完全无输出-bios参数路径错误确认u-boot.bin在当前目录完全无输出编译目标不是 ARM64检查CROSS_COMPILE是否为aarch64-linux-gnu-输出乱码串口波特率不匹配QEMU 默认 115200检查CONFIG_BAUDRATE输出后卡死设备树不匹配确认用的是qemu_arm64_defconfig我遇到过一次编译出来的u-boot.bin只有几 KB明显不对。后来发现是make的时候没加CROSS_COMPILE用的是本机 x86 编译器编译出来的东西根本跑不了。所以每次编译前一定要确认交叉编译器前缀。6.2 重定位后代码跑飞如果你在board_init_f里加了自定义代码重定位后跑飞了大概率是全局变量的问题。前面说过重定位前全局变量在只读内存里重定位后会被拷贝到新地址但如果你在重定位前修改了全局变量重定位时会被覆盖掉。正确的做法是用gd结构体。gd是一个指向global_data的指针重定位时 u-boot 会更新这个指针的值保证它始终指向正确的位置。6.3 自定义命令不生效检查清单.config里对应的CONFIG_CMD_XXX是否为ycmd/Makefile里是否加了obj-$(CONFIG_CMD_XXX) xxx.ocmd/Kconfig里是否定义了配置项编译时是否有报错被忽略我踩过的坑是在Kconfig里定义了配置项但忘了在defconfig里打开结果编译出来的 u-boot 里根本没有这个命令。后来养成习惯每次加新命令都用grep CONFIG_CMD_MYINFO .config确认一下。6.4 GDB 连接不上 QEMU如果 GDB 报 Connection refused检查QEMU 是否加了-s参数1234 端口是否被占用lsof -i :1234防火墙是否拦截了本地回环还有一个容易忽略的点gdb-multiarch需要指定架构。如果连接后提示 Remote g packet reply is too long在 GDB 里执行set architecture aarch64再连接。6.5 编译报错 No rule to make target这种错误通常是defconfig文件损坏或者版本不匹配。解决办法make distclean make qemu_arm64_defconfig make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)make distclean会清除所有生成的文件包括.config然后重新配置。这个操作我每次切换 u-boot 版本的时候都会做一遍能避免很多莫名其妙的编译问题。7. 从 QEMU 到真实开发板的迁移思路在 QEMU 上把 u-boot 跑通之后下一步就是上真实开发板。迁移的核心思路是换配置、换设备树、换加载方式。换配置把qemu_arm64_defconfig换成你开发板对应的defconfig比如orangepi_pc2_defconfig、rock64-rk3328_defconfig等。每个开发板的配置里定义了 DDR 初始化参数、时钟频率、串口引脚等硬件相关的东西。换设备树QEMU 用的是qemu-arm64.dts你的开发板有自己的.dts文件通常在arch/arm/dts/目录下。设备树里描述了开发板上的所有外设包括 eMMC、网口、USB、GPIO 等。换加载方式QEMU 用-bios直接加载 u-boot.bin真实开发板通常通过 SD 卡、eMMC 或串口加载。SD 卡启动的话需要把 u-boot 写到特定的偏移地址比如 RK3328 是 0x8000这个偏移量在芯片手册里有说明。我个人的建议是先在 QEMU 上把 u-boot 的启动流程和命令框架搞明白再上开发板。因为开发板上如果 u-boot 跑不起来你连串口输出都看不到排查起来非常痛苦。QEMU 至少能保证你有一个可工作的参考环境。8. 学习路径与资源推荐从单片机转到 u-boot我建议按这个顺序来先跑通 QEMU ARM64 的 u-boot理解启动流程和命令行读board_init_f和board_init_r的源码理解两阶段初始化的设计学设备树语法能看懂.dts文件知道compatible怎么匹配驱动写一个自定义命令理解U_BOOT_CMD宏和命令注册机制用 GDB 单步调试观察重定位前后的内存变化上真实开发板把 QEMU 上的经验迁移过去资源方面u-boot 官方文档doc/目录下的.rst文件是最权威的参考资料。另外u-boot.map和System.map是调试时的好帮手遇到不认识的函数直接在里面搜地址。最后分享一个我自己的习惯每次改 u-boot 代码之前先用git diff看一下当前修改确认没有误改其他文件。u-boot 的代码量很大一个不小心改错了地方编译能过但运行跑飞排查起来非常费时间。保持修改的最小化和可追溯性是玩 u-boot 的基本素养。
返回列表