ARTICLE DETAIL

资讯详情

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

从单片机到u-boot:用QEMU模拟ARM64跑通嵌入式Linux启动全流程

从单片机到u-boot:用QEMU模拟ARM64跑通嵌入式Linux启动全流程 1. 从单片机到u-boot为什么我劝你尽早跨过这道分水岭如果你现在还在用51单片机点灯、调数码管、写跑马灯或者用STM32标准库折腾各种外设那说明你已经站在了嵌入式世界的门口。但门口和客厅是两码事。单片机玩得再溜你面对的始终是一个没有操作系统、没有内存管理、没有进程调度、所有代码裸奔在硬件上的世界。而u-boot就是带你从“裸奔”走进“有组织有纪律”的嵌入式Linux世界的第一道大门。我见过太多人卡在这个分水岭上。他们能熟练配置STM32的GPIO、能写中断服务函数、能用I2C驱动OLED屏幕但一听到“u-boot”“设备树”“内核启动参数”就头皮发麻。问题不在于他们不够聪明而在于没有人告诉他们u-boot本质上就是一个“超级单片机程序”。它做的事情和你写的裸机代码没有本质区别——初始化时钟、配置内存、操作外设、加载数据。唯一的区别是它做完这些之后会把控制权交给一个更复杂的系统Linux内核。所以这篇文章的核心目标很明确帮你把单片机思维平滑地迁移到u-boot的世界里。我会用QEMU模拟ARM64环境让你不需要买任何开发板就能跑通整个流程。你会看到u-boot怎么启动、怎么加载内核、怎么传递参数以及在这个过程中你之前学的那些单片机知识到底哪些还有用、哪些需要彻底重构。适合谁看如果你已经能用C语言写单片机程序理解寄存器操作和中断的基本概念但还没碰过u-boot或者只停留在“听说过”的阶段那这篇就是为你准备的。如果你连单片机都没碰过建议先去把GPIO和串口通信搞明白再回来。提示本文所有操作都在QEMU模拟环境中完成不需要任何真实硬件。你只需要一台能跑Linux的电脑物理机或虚拟机都行装好必要的工具链即可。2. 环境搭建用QEMU模拟ARM64跑u-boot的完整流程2.1 为什么选QEMU而不是买开发板很多人学u-boot的第一反应是买一块树莓派或者正点原子的开发板。我不推荐这么做原因有三个。第一开发板从下单到到手至少两天这段时间你的学习热情可能已经凉了一半。第二开发板一旦变砖恢复过程对新手来说极其痛苦很容易产生挫败感。第三QEMU可以让你在几分钟内反复重来今天把u-boot刷坏了删掉镜像重新来一遍就行零成本试错。QEMU模拟ARM64的“virt”机器类型是专门为虚拟化设计的它提供了一套标准的虚拟硬件CPU、内存、串口、中断控制器、定时器。u-boot对这套虚拟硬件有完整的支持你不需要写任何板级代码就能跑起来。这就像学编程时先用Python而不是C语言——先跑通流程再深入细节。2.2 工具链安装与版本选择在Ubuntu 22.04上你需要安装以下工具sudo apt update sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ qemu-system-arm device-tree-compiler bison flex \ libssl-dev swig python3-pip这里有几个关键点。gcc-aarch64-linux-gnu是ARM64的交叉编译器用来编译u-boot源码。qemu-system-arm是QEMU的ARM模拟器注意不是qemu-system-aarch64虽然名字里有arm但它同时支持32位和64位ARM。device-tree-compiler用来编译设备树文件u-boot在启动时会读取设备树来了解硬件布局。版本方面我实测下来QEMU 6.2以上、u-boot 2022.01以上、GCC 11以上这个组合最稳定。太老的版本可能缺少某些ARM64 virt机器的特性太新的版本有时候会有编译警告。你可以用qemu-system-aarch64 --version和aarch64-linux-gnu-gcc --version确认版本。2.3 获取u-boot源码与配置编译从官方仓库拉取源码git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2023.10选v2023.10这个版本是因为它对QEMU ARM64 virt的支持已经非常成熟而且编译过程中不会遇到太多依赖问题。进入源码目录后先做一次清理make distclean然后配置QEMU ARM64的目标make qemu_arm64_defconfig这个defconfig是u-boot官方为QEMU ARM64 virt机器准备的默认配置。它已经开启了串口、virtio、设备树等必要功能。如果你想看看具体开了哪些选项可以执行make menuconfig在图形界面里逐项查看。不过对于第一次跑通流程来说默认配置完全够用。编译make CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)编译完成后你会在源码根目录看到u-boot.bin和u-boot两个文件。u-boot.bin是纯二进制镜像u-boot是ELF格式的带符号文件调试时用后者。2.4 启动QEMU并观察u-boot输出直接用一条命令启动qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic \ -bios u-boot.bin -m 1G参数解释-machine virt指定虚拟机器类型-cpu cortex-a57模拟一颗ARM Cortex-A57处理器-nographic把串口重定向到当前终端-bios u-boot.bin把u-boot作为固件加载-m 1G给1GB内存。按下回车后你应该能看到类似这样的输出U-Boot 2023.10 (Oct 15 2023 - 10:23:45 0800) DRAM: 1 GiB Core: 51 devices, 19 uclasses, devicetree: board Flash: 64 MiB MMC: mmca0000: 0 Loading Environment from nowhere... OK In: serial Out: serial Err: serial Net: No ethernet found. Hit any key to stop autoboot: 3看到这个界面恭喜你u-boot已经在QEMU里跑起来了。你现在面对的是一个命令行环境可以输入各种u-boot命令。输入help能看到所有支持的命令列表输入bdinfo能看到板级信息输入printenv能看到环境变量。注意如果你看到的是“Hit any key to stop autoboot”倒计时按任意键就能进入u-boot命令行。如果不按它会尝试自动启动但因为没配置启动脚本最终会停在命令行。3. u-boot核心机制拆解从单片机思维到Bootloader思维3.1 u-boot的启动阶段划分u-boot的启动过程分为两个阶段SPL和主u-boot。SPL全称Secondary Program Loader它的任务是初始化最基础的硬件——DDR内存、时钟、串口——然后把主u-boot从存储介质加载到内存里运行。在QEMU环境下因为内存已经由QEMU初始化好了SPL的作用被弱化但理解这个概念对以后移植到真实硬件至关重要。主u-boot阶段做的事情就多了初始化外设、探测存储设备、加载环境变量、执行启动脚本、加载内核镜像和设备树、跳转到内核入口。你可以把SPL想象成单片机的启动文件startup.s它只做最紧急的事情主u-boot则相当于你的完整应用程序功能齐全但需要先被加载到内存里。在QEMU里-bios u-boot.bin直接把主u-boot加载到内存起始位置跳过了SPL阶段。如果你想模拟完整的启动流程可以编译SPL并让QEMU加载SPL然后由SPL去加载主u-boot。不过对于学习u-boot命令和启动流程来说直接跑主u-boot已经足够了。3.2 环境变量的存储与读取机制u-boot的环境变量是一组键值对存储在非易失性存储介质里SPI Flash、MMC、NAND等。在QEMU里默认配置是“Loading Environment from nowhere”意思是环境变量只存在于内存中掉电即失。你可以用env save命令把当前环境变量保存到某个存储设备但QEMU的virt机器默认没有持久化存储所以每次重启都会恢复默认值。环境变量的核心用途是控制启动行为。最重要的几个变量是bootcmd、bootargs、bootdelay。bootcmd是自动启动时执行的命令序列bootargs是传递给Linux内核的启动参数bootdelay是自动启动前的倒计时秒数。你可以用setenv修改它们用saveenv保存如果有持久化存储的话。这里有个单片机思维的迁移点在单片机里你通常把配置参数写在代码里的宏定义或者EEPROM里。u-boot的环境变量就相当于一个更灵活、更结构化的EEPROM。你可以随时通过串口修改不需要重新编译代码。3.3 设备树在u-boot中的角色设备树是ARM Linux世界里描述硬件布局的标准方式。在单片机时代硬件信息通常硬编码在C代码里——比如#define UART_BASE 0x40000000。设备树把这个信息从代码里抽离出来变成一个独立的二进制文件.dtb由bootloader在启动时传递给内核。u-boot自己也会读取设备树用来了解有哪些外设可用。在QEMU ARM64 virt机器上设备树是QEMU动态生成的u-boot通过-machine virt参数获取。你可以用fdt print命令在u-boot命令行里查看当前设备树的内容。比如fdt print /soc/serial9000000能看到串口控制器的寄存器地址和中断号。设备树的存在意味着u-boot的移植工作大大简化了。以前移植u-boot到新板子需要改大量C代码现在只需要改设备树文件。这是嵌入式Linux开发模式对单片机开发模式的一次重大升级。3.4 u-boot命令体系与常用操作u-boot的命令体系分为几大类内存操作md、mm、mw、存储操作mmc、sf、nand、网络操作tftp、dhcp、ping、启动操作bootm、booti、bootz、环境变量操作setenv、printenv、saveenv、设备树操作fdt。你不需要记住所有命令但有几个必须熟练掌握。mdmemory display用来查看内存内容mwmemory write用来修改内存mmmemory modify用来交互式修改内存。这三个命令在调试时极其有用。比如你想看看u-boot把自己加载到了哪个地址可以用md 0x40000000查看内存起始位置的内容。booti是启动ARM64 Linux内核的命令格式是booti kernel_addr - fdt_addr。中间的横线表示initrd地址为空。bootm用于启动uImage格式的内核bootz用于启动ARM32的zImage内核。ARM64统一用booti。fdt命令用来操作设备树fdt addr设置设备树在内存中的地址fdt print打印设备树内容fdt set修改设备树属性。在调试内核启动问题时经常需要用fdt set临时修改设备树参数。4. 实操用u-boot加载Linux内核并启动4.1 准备内核镜像与根文件系统要启动Linux你需要三样东西内核镜像Image、设备树二进制.dtb、根文件系统rootfs。在QEMU环境下最方便的方式是用Buildroot生成一个最小的根文件系统或者直接下载一个预编译的initramfs。这里我用一个更简单的方法用QEMU自带的-kernel和-initrd参数先验证内核能启动然后再回到u-boot里手动加载。不过既然我们的目标是学u-boot那就从头手动来。先下载一个ARM64内核镜像。你可以从Ubuntu的官方仓库获取也可以用Buildroot自己编译。为了节省时间我建议用Buildroot生成一个最小系统git clone https://git.buildroot.net/buildroot cd buildroot make qemu_aarch64_virt_defconfig make -j$(nproc)编译完成后在output/images/目录下你会找到Image内核镜像、rootfs.ext4根文件系统、qemu_aarch64_virt.dtb设备树。这三个文件就是u-boot需要加载的东西。4.2 在u-boot中加载内核与设备树把这三个文件放到QEMU能访问的目录里然后启动QEMU时加上-drive参数挂载一个虚拟SD卡qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic \ -bios u-boot.bin -m 1G \ -drive filerootfs.ext4,formatraw,ifnone,idhd0 \ -device virtio-blk-device,drivehd0进入u-boot命令行后先看看存储设备 virtio scan ls virtio 0你应该能看到根文件系统里的目录结构。接下来把内核和设备树从存储设备加载到内存 load virtio 0:0 0x40000000 /boot/Image load virtio 0:0 0x50000000 /boot/qemu_aarch64_virt.dtb这里0x40000000是内核加载地址0x50000000是设备树加载地址。这两个地址不能重叠也不能覆盖u-boot自身所在的内存区域。在QEMU ARM64 virt机器上内存起始于0x40000000u-boot通常被加载到0x40000000附近所以你需要把内核放到更高的地址比如0x48000000。具体地址取决于你的u-boot配置可以用bdinfo查看。4.3 设置bootargs并启动内核bootargs是传递给内核的启动参数告诉内核根文件系统在哪里、串口控制台是哪个、内存大小是多少。对于QEMU ARM64 virt机器典型的bootargs是 setenv bootargs consolettyAMA0 root/dev/vda rwconsolettyAMA0指定串口控制台root/dev/vda指定根文件系统在第一个virtio块设备上rw表示以读写方式挂载。然后启动内核 booti 0x48000000 - 0x50000000如果一切顺利你会看到内核启动日志滚动最后停在登录提示符或者shell提示符。这意味着u-boot成功完成了它的使命把控制权交给了Linux内核。注意如果你看到“Kernel panic - not syncing: VFS: Unable to mount root fs”说明根文件系统路径不对。检查root参数是否指向了正确的设备。在QEMU里virtio块设备通常是/dev/vda但如果你用了其他存储控制器可能是/dev/sda或/dev/mmcblk0。4.4 用bootcmd实现自动启动每次手动输入命令太麻烦你可以把启动命令写入bootcmd环境变量 setenv bootcmd virtio scan; load virtio 0:0 0x48000000 /boot/Image; load virtio 0:0 0x50000000 /boot/qemu_aarch64_virt.dtb; booti 0x48000000 - 0x50000000 setenv bootargs consolettyAMA0 root/dev/vda rw saveenv下次启动时u-boot会在倒计时结束后自动执行bootcmd你就能直接看到Linux启动。这就是嵌入式产品出厂时的标准启动流程u-boot自动加载内核用户只需要按电源键。5. 常见问题与排查技巧实录5.1 u-boot启动后没有任何输出这是最常见的问题。首先检查QEMU命令是否正确特别是-nographic参数是否加了。如果没有这个参数串口输出不会重定向到终端。其次检查-bios参数指向的文件是否存在且非空。如果u-boot.bin大小为0说明编译失败需要重新编译并查看错误信息。还有一个可能的原因是CPU型号不匹配。QEMU的virt机器支持多种CPU但u-boot的默认配置可能只针对某几种。试试-cpu cortex-a53或-cpu max。max会让QEMU模拟所有支持的CPU特性兼容性最好。5.2 内核启动到一半卡死内核卡死通常有几个原因。第一设备树地址不对内核找不到硬件信息。检查booti命令的第三个参数是否指向了正确的设备树地址。第二bootargs里的console参数不对内核把日志输出到了不存在的串口。试试改成consolettyAMA0,115200。第三内存地址冲突内核覆盖了u-boot自身或者设备树。用bdinfo查看u-boot的内存布局确保内核加载地址在空闲区域。5.3 virtio设备识别不到在u-boot里执行virtio scan后如果没有任何输出说明virtio驱动没有编译进u-boot。检查qemu_arm64_defconfig里是否开启了CONFIG_VIRTIO和CONFIG_VIRTIO_BLK。你可以用grep VIRTIO .config查看。如果没有开启执行make menuconfig在Device Drivers - Virtio drivers里手动开启然后重新编译。5.4 环境变量保存失败在QEMU里执行saveenv通常会报错“No storage device found”因为QEMU的virt机器默认没有持久化存储。这不是bug是预期行为。如果你确实需要保存环境变量可以给QEMU挂一个虚拟Flash设备或者把环境变量写到virtio块设备的一个保留分区里。不过对于学习来说每次重启恢复默认环境变量反而更方便不用担心把配置搞乱。5.5 常见问题速查表问题现象可能原因排查方法u-boot无输出缺少-nographic参数检查QEMU命令行u-boot无输出u-boot.bin为空重新编译查看错误内核panic根文件系统路径错误检查bootargs的root参数内核panic设备树地址错误检查booti第三个参数virtio扫描无结果驱动未编译检查.config里的VIRTIO选项saveenv失败无持久化存储正常现象忽略即可启动卡在Starting kernel内核地址或设备树地址错误用bdinfo确认内存布局提示遇到问题时先在u-boot命令行里用bdinfo、printenv、fdt print这三个命令收集信息。大部分启动问题都能从这三个命令的输出里找到线索。6. 从u-boot继续深入下一步该学什么跑通u-boot加载内核只是第一步。接下来你可以往几个方向深入。第一个方向是SPL移植把u-boot的启动流程拆成两阶段理解真实硬件上bootROM如何加载SPL、SPL如何加载主u-boot。第二个方向是设备树移植尝试把u-boot移植到一块真实的开发板上修改设备树文件适配实际硬件。第三个方向是内核启动流程分析用earlycon和earlyprintk参数让内核输出更详细的启动日志理解内核如何初始化内存、中断、时钟、驱动。我个人建议先把QEMU环境下的整个流程反复跑几遍直到你能不看笔记就完成从编译u-boot到启动Linux的全过程。然后尝试修改bootargs比如加上init/bin/sh让内核直接进入shell而不启动init进程或者加上loglevel8让内核输出所有调试信息。这些操作能帮你建立对启动流程的直觉。最后分享一个小技巧在u-boot里用mw命令直接修改内存中的设备树可以临时改变内核看到的硬件参数。比如你想让内核认为内存只有512MB可以用fdt set /memory reg 0x0 0x40000000 0x0 0x20000000修改设备树然后启动内核。内核会按照修改后的设备树来初始化内存。这个技巧在调试内存相关问题时非常有用比重新编译设备树快得多。
返回列表