ARTICLE DETAIL

资讯详情

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

从单片机到u-boot:嵌入式Linux分水岭与QEMU ARM64实战

从单片机到u-boot:嵌入式Linux分水岭与QEMU ARM64实战 1. 从单片机到u-boot为什么我劝你尽早跨过这道分水岭如果你现在还在用51单片机点灯、调数码管、写1602液晶的驱动或者刚用STM32跑通了一个TFT触摸屏程序觉得自己已经“入门嵌入式”了那我接下来要说的话可能会让你不太舒服你离真正的嵌入式开发还有相当长的一段路。单片机那套东西——裸机跑while(1)大循环、手动管理每一个寄存器、中断里塞满业务逻辑——放在今天的嵌入式Linux和ARM64平台上连“前菜”都算不上。真正让嵌入式工程师拉开差距的是u-boot这个看似枯燥、实则决定整个系统能不能跑起来的关键组件。我见过太多人卡在“单片机玩得很溜但一碰嵌入式Linux就懵”的状态。他们能背出C51的每一个SFR地址能手动配置STM32的时钟树但面对u-boot的启动流程、设备树传递、DDR初始化、重定位这些概念时完全不知道从哪下手。问题出在哪出在思维模型没有切换。单片机是“你直接控制硬件”而u-boot是“你为操作系统搭建一个能运行的环境”。前者是手工作坊后者是城市规划。你不需要知道每一块砖怎么烧但你必须清楚水电管网怎么铺、交通怎么组织、应急通道在哪。这篇文章就是写给那些已经厌倦了在单片机圈子里打转、想真正踏入嵌入式Linux和ARM64世界的朋友。我会用QEMU模拟ARM64环境带你从零把u-boot跑起来拆解它的启动流程解释每一个关键步骤背后的“为什么”并分享我在实际移植和调试中踩过的坑。你不需要有一块真实的ARM64开发板一台能跑Linux的电脑就够了。学完这一套你再回头看单片机的那些东西会有一种“降维打击”的清晰感。注意本文所有操作均在QEMU模拟环境中进行不涉及任何真实硬件烧录也不需要任何特殊网络配置。你只需要一个标准的Linux开发环境Ubuntu 22.04或类似发行版即可。2. 核心思路拆解为什么u-boot是嵌入式的分水岭2.1 单片机思维与嵌入式Linux思维的本质差异先把这个事情说透。单片机开发的核心逻辑是上电→初始化时钟→配置外设→进入主循环。你写的每一行代码都直接对应硬件的一个动作。C51单片机里你写P1 0xFEP1口的8个引脚立刻输出对应的电平。STM32里你配置GPIO的MODER寄存器引脚方向就变了。这种“所见即所得”的控制感很爽但它有一个致命局限你写的所有代码都是为这一个特定芯片服务的。换一颗芯片哪怕只是同系列不同型号你的代码可能就要大改。嵌入式Linux的思路完全不同。它把系统分成三层引导加载程序bootloader→ 操作系统内核kernel→ 用户空间应用rootfs。u-boot就是最下面那一层。它的任务不是“控制硬件”而是“把硬件准备好然后把控制权交给内核”。这个“准备好”包括初始化DDR内存控制器、配置时钟树、加载内核镜像到内存、准备设备树、设置内核启动参数。做完这些u-boot就功成身退把CPU控制权交给Linux内核。为什么说这是分水岭因为从这一刻起你的代码不再直接控制硬件了。你写的是一个“环境搭建者”而不是“硬件操作者”。你需要理解内存映射、地址空间、启动介质、镜像格式这些抽象概念。这些东西在单片机世界里几乎不存在——51单片机哪有DDR控制器哪有设备树哪有内核镜像加载2.2 为什么选择u-boot而不是其他bootloader市面上bootloader不止u-boot一家。RedBoot、Barebox、Little KernelLK都有人用。但u-boot是事实上的行业标准原因有几个支持架构最全ARM、ARM64、MIPS、PowerPC、RISC-V、x86几乎你能想到的架构它都支持。这意味着你学一套东西换平台时迁移成本极低。驱动模型成熟u-boot有自己的驱动模型DM支持设备树和Linux内核的设计理念一脉相承。你学u-boot的过程也是在为理解Linux内核打基础。社区活跃几乎每一款主流芯片原厂都会第一时间往u-boot主线提交支持代码。你遇到问题搜到的答案大概率是有效的。工具链完善mkimage、dumpimage、fw_printenv这些工具让你可以方便地制作镜像、查看镜像信息、管理环境变量。相比之下LK更轻量适合快速启动场景比如Android设备但生态和文档远不如u-boot。Barebox设计更现代但用户基数小。对于想系统学习嵌入式Linux的人来说u-boot是性价比最高的选择。2.3 QEMU模拟ARM64没有开发板也能学很多人卡在“我没有ARM64开发板”这一步。树莓派算ARM64但它是博通定制的SoCu-boot支持并不主线化。NXP、瑞芯微、全志的开发板动辄几百上千块对初学者不友好。QEMU解决了这个问题。QEMU可以模拟一台完整的ARM64虚拟机包括CPU、内存、串口、存储控制器。你可以在里面跑u-boot、加载内核、挂载rootfs整个流程和真实硬件几乎一致。唯一的区别是真实硬件上你需要用烧录器把u-boot写到SPI Flash或SD卡QEMU里你只需要用-bios参数指定u-boot二进制文件。提示QEMU模拟的ARM64机器类型推荐用virt这是QEMU专门为虚拟化设计的通用平台设备树由QEMU动态生成不需要你手动写DTS。对于学习u-boot启动流程来说这是最省事的方案。3. 环境搭建与工具链准备从零把架子搭起来3.1 安装交叉编译工具链ARM64的u-boot需要在x86主机上编译所以你需要一套AArch64的交叉编译工具链。Ubuntu下最方便的是直接装包sudo apt update sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu装完之后验证一下aarch64-linux-gnu-gcc --version你应该能看到类似gcc version 11.4.0的输出。这个工具链既能编译u-boot也能编译Linux内核一举两得。如果你想要更新的版本可以去ARM官方下载GNU Toolchain但说实话Ubuntu自带的版本对于学习u-boot来说完全够用。我试过用ARM官方12.x版本编译和用Ubuntu自带的11.x版本编译生成的u-boot二进制在QEMU里跑起来没有任何区别。3.2 获取u-boot源码u-boot的官方仓库在GitHub上但国内直接clone可能比较慢。我一般用以下方式git clone https://source.denx.de/u-boot/u-boot.git cd u-boot git checkout v2024.01这里我特意checkout到一个稳定tag而不是用master分支。原因很简单master分支每天都在变可能今天能编译通过明天就因为某个驱动改动而挂掉。对于学习来说稳定性比新特性重要得多。v2024.01是我实测下来比较稳的一个版本对QEMU ARM64 virt平台的支持很完善。如果你clone速度慢也可以直接下载tarballwget https://ftp.denx.de/pub/u-boot/u-boot-2024.01.tar.bz2 tar xf u-boot-2024.01.tar.bz2 cd u-boot-2024.013.3 安装QEMUQEMU的安装更简单sudo apt install qemu-system-arm注意包名是qemu-system-arm它同时包含ARM32和ARM64的模拟支持。装完之后验证qemu-system-aarch64 --version你应该能看到QEMU的版本信息。我推荐用6.0以上的版本对ARM64 virt平台的支持更完善。3.4 配置和编译u-boot进入u-boot源码目录执行make ARCHarm CROSS_COMPILEaarch64-linux-gnu- qemu_arm64_defconfig make ARCHarm CROSS_COMPILEaarch64-linux-gnu- -j$(nproc)第一行是加载QEMU ARM64的默认配置。这个defconfig已经帮你选好了所有必要的选项串口用PL011、存储用virtio、内存布局适配QEMU virt平台。第二行是编译-j$(nproc)表示用你CPU的所有核心并行编译能快不少。编译完成后你会在根目录下看到u-boot.bin文件。这就是我们要的u-boot二进制。另外还有一个u-boot文件ELF格式包含符号信息调试时用得上。注意如果你编译时报错说找不到dtc设备树编译器需要额外安装sudo apt install device-tree-compiler。这个坑我踩过因为u-boot编译过程中需要把DTS编译成DTB。4. 启动流程深度解析u-boot到底在干什么4.1 从上电到串口输出第一阶段发生了什么当你用QEMU启动u-boot时CPU从固定的复位向量地址开始执行。对于ARM64来说这个地址是0x00000000或者由硬件配置决定的高地址。QEMU virt平台把u-boot加载到0x400000001GB地址处然后跳转过去执行。u-boot的第一阶段代码SPL或TPL如果启用的话做的是最底层的事情设置栈指针、初始化CPU关键寄存器、配置串口如果启用了early debug。在QEMU环境下DDR已经由QEMU模拟好了不需要我们初始化内存控制器这省掉了最麻烦的一步。但真实硬件上DDR初始化是u-boot第一阶段最核心的任务。你需要根据DDR芯片的型号、频率、时序参数配置内存控制器的几十个寄存器。这一步如果错了整个系统连串口都出不来。我当年第一次移植u-boot到一块全志H3的板子上就是因为DDR时序参数差了一个周期导致系统随机死机查了整整一周才找到问题。4.2 重定位u-boot为什么要“自己搬自己”u-boot编译出来的链接地址通常是0x40000000对于QEMU ARM64但实际运行时它可能被加载到其他地址。为了让代码能正确运行u-boot会把自己重定位到内存的高端地址通常是RAM的顶部附近。这个过程叫relocation。为什么要多此一举因为u-boot需要为内核和rootfs留出连续的低端内存空间。如果u-boot一直占着0x40000000不放内核就没法加载到预期地址。重定位之后u-boot把自己搬到高端地址低端内存就空出来了。重定位的实现涉及位置无关代码PIC的概念。u-boot通过gd-relocaddr记录重定位后的地址所有绝对地址引用都需要加上偏移。这个过程在board_init_r函数中完成你可以通过串口输出看到类似这样的信息Relocating to 0x7ff00000, new gd at 0x7feff0004.3 设备树传递内核怎么知道硬件长什么样设备树Device Tree是嵌入式Linux最核心的抽象之一。它用文本格式DTS描述硬件信息CPU有几个核、内存多大、串口在哪个地址、中断怎么连。u-boot的任务之一就是把设备树二进制DTB加载到内存并把地址告诉内核。在QEMU virt平台上设备树是由QEMU动态生成的u-boot通过-dtb参数或者内置的DTB来获取。启动内核时u-boot会把DTB的地址写入x0寄存器ARM64的约定内核启动后从x0读取设备树。提示你可以用dtc -I dtb -O dts -o output.dts input.dtb命令把DTB反编译成DTS看看QEMU到底生成了什么样的设备树。这对于理解设备树的结构非常有帮助。4.4 内核启动参数bootargs怎么传u-boot通过bootargs环境变量把启动参数传给内核。这些参数包括控制台设备consolettyAMA0、根文件系统位置root/dev/vda、内存大小mem1G等。内核启动时会解析这些参数决定从哪里挂载rootfs、用哪个串口输出日志。在QEMU环境下一个典型的bootargs是这样的consolettyAMA0 root/dev/vda rwttyAMA0是PL011串口的设备名/dev/vda是virtio块设备的第一个分区。这些名字不是随便起的它们对应设备树里的节点名称。如果你改了设备树这些名字也要跟着改。5. 实操全流程在QEMU里把u-boot跑起来5.1 启动u-boot并进入命令行编译完成后用以下命令启动QEMUqemu-system-aarch64 \ -M virt \ -cpu cortex-a57 \ -nographic \ -bios u-boot.bin \ -m 1G参数解释-M virt使用QEMU的通用虚拟平台-cpu cortex-a57模拟Cortex-A57 CPU这是ARM64的经典核心-nographic不使用图形界面串口输出直接打到终端-bios u-boot.bin把u-boot二进制作为BIOS加载-m 1G给虚拟机分配1GB内存执行后你应该能看到u-boot的启动日志最后停在提示符。这就是u-boot的命令行界面。5.2 常用命令实操在u-boot命令行里你可以执行很多有用的命令 bdinfo这个命令打印板级信息内存起始地址、内存大小、环境变量保存位置、波特率等。我一般用这个命令确认u-boot是否正确识别了内存布局。 printenv打印所有环境变量。你会看到bootargs、bootcmd、baudrate等。bootcmd是u-boot启动后自动执行的命令序列默认可能是空的或者指向网络启动。 mmc list列出所有MMC设备。在QEMU virt平台上如果你没有挂载虚拟SD卡这个命令会返回空。但你可以用virtio list查看virtio设备。 virtio list列出virtio设备。如果你启动QEMU时加了-drive filerootfs.img,ifvirtio,formatraw这里就能看到virtio块设备。5.3 手动加载内核并启动假设你已经编译好了一个ARM64 Linux内核Image文件和一个设备树virt.dtb你可以用以下命令手动启动 load virtio 0:0 0x41000000 Image load virtio 0:0 0x42000000 virt.dtb setenv bootargs consolettyAMA0 root/dev/vda rw booti 0x41000000 - 0x42000000load命令从virtio设备的第一个分区加载文件到指定内存地址。booti是ARM64专用的启动命令参数依次是内核镜像地址、initrd地址-表示没有、设备树地址。执行booti后CPU控制权交给内核你会看到Linux内核的启动日志。如果一切正常最后会进入登录提示或者shell。5.4 制作可启动的SD卡镜像在真实硬件上你需要把u-boot写到SD卡的特定偏移处。QEMU里也可以模拟这个过程dd if/dev/zero ofsdcard.img bs1M count64 dd ifu-boot.bin ofsdcard.img convnotrunc bs512 seek1第一行创建一个64MB的空镜像第二行把u-boot写到偏移512字节处跳过MBR分区表。然后启动QEMU时用-drive filesdcard.img,ifsd,formatraw挂载这个镜像。注意真实硬件上u-boot的写入偏移取决于SoC的启动ROM要求。比如全志的芯片通常要求写在8KB偏移处瑞芯微的芯片要求写在32KB偏移处。这个偏移量在芯片手册的“Boot ROM”章节里有详细说明写错了系统根本起不来。6. 常见问题与排查技巧实录6.1 u-boot启动后没有任何输出这是最常见的问题。排查思路如下可能原因排查方法解决方案串口配置错误检查defconfig里的CONFIG_BAUDRATE改成115200和QEMU默认一致CPU型号不匹配检查QEMU的-cpu参数换成cortex-a57或cortex-a53加载地址错误检查QEMU的-bios参数确认u-boot.bin路径正确编译架构错误检查make时的ARCH和CROSS_COMPILE确保是arm和aarch64-linux-gnu-我遇到过一次编译出来的u-boot在QEMU里完全没输出。后来发现是CONFIG_DEBUG_UART没有使能early debug串口没初始化。在defconfig里加上CONFIG_DEBUG_UARTy和CONFIG_DEBUG_UART_BASE0x9000000QEMU virt平台的PL011串口地址就好了。6.2 内核启动后卡在“Starting kernel...”这个问题的原因通常是设备树地址传错了或者内核镜像格式不对。ARM64内核必须是Image格式未压缩的原始二进制不能是zImage或bzImage。你可以用file命令检查file arch/arm64/boot/Image输出应该是Linux kernel ARM64 boot executable Image。如果是gzip compressed data说明你拿的是压缩过的内核需要用booti而不是bootz。另一个常见原因是bootargs里的console参数写错了。QEMU virt平台的串口是ttyAMA0不是ttyS0。写错了内核日志就出不来了。6.3 环境变量保存失败u-boot默认把环境变量保存在CONFIG_ENV_OFFSET指定的存储偏移处。在QEMU里如果没有配置可写的存储设备saveenv命令会失败。解决方法是在QEMU启动参数里加一个可写的virtio块设备-drive fileenv.img,ifvirtio,formatraw然后在u-boot里配置CONFIG_ENV_IS_IN_VIRTIOy和CONFIG_ENV_VIRTIO_DEV0。这样saveenv就能把环境变量写到virtio设备里了。6.4 QEMU退出后重新启动环境变量丢失这是因为QEMU默认不保存virtio设备的状态。你需要确保env.img文件在QEMU退出后没有被删除并且下次启动时用同样的-drive参数挂载。我一般会创建一个固定大小的文件dd if/dev/zero ofenv.img bs1M count1然后每次启动QEMU都挂载这个文件。这样环境变量就能持久化了。7. 从u-boot延伸出去下一步该学什么7.1 深入设备树从消费者变成生产者u-boot帮你把设备树加载到内存但设备树本身是怎么来的QEMU virt平台的设备树是QEMU动态生成的但真实硬件上设备树是芯片原厂或板级厂商写的。你需要学会阅读DTS文件理解compatible属性、reg属性、interrupts属性的含义。我建议你从QEMU生成的设备树开始用dtc反编译成DTS然后对照Linux内核源码里的Documentation/devicetree/bindings/目录逐个理解每个节点的含义。等你能够自己写一个简单的DTS描述一个虚拟的I2C设备你就真正掌握了设备树。7.2 移植u-boot到真实硬件QEMU是学习工具但最终你还是要面对真实硬件。移植u-boot到一块新板子的流程大致是找到芯片原厂的u-boot源码通常叫BSP在board/目录下创建板级目录在configs/目录下创建defconfig在arch/arm/dts/目录下添加设备树配置DDR初始化参数这是最难的一步配置串口、存储、网络等外设编译、烧录、调试每一步都有坑但有了QEMU里的经验你至少知道u-boot的启动流程是什么样的调试时能快速定位问题出在哪个阶段。7.3 理解SPL和TPLu-boot的自我进化现代SoC的启动流程通常是Boot ROM → SPL → u-boot proper → Linux内核。SPLSecondary Program Loader是一个精简版的u-boot只做最核心的DDR初始化和加载u-boot proper。TPLTertiary Program Loader更精简用于DDR还没初始化时的SRAM运行。理解SPL和TPL的分工对于调试启动问题至关重要。如果你的板子上电后串口没有任何输出问题可能出在SPL阶段而不是u-boot proper阶段。你需要用示波器或者JTAG来确认SPL是否执行了。7.4 从u-boot看Linux内核的启动u-boot的最后一个动作是跳转到内核入口。ARM64内核的入口地址在arch/arm64/kernel/head.S。内核启动后会重新初始化MMU、设置页表、解析设备树、初始化各个子系统。如果你理解了u-boot怎么把参数传给内核再看内核的启动代码就会顺畅很多。我个人的经验是把u-boot的booti命令和内核的head.S对照着看你会对“引导加载程序”和“操作系统”之间的边界有非常清晰的认识。这个边界就是嵌入式Linux开发的核心分界线。最后分享一个小技巧在u-boot里用md命令查看内存内容用mw命令修改内存用cmp命令比较内存。这些命令在调试内核加载问题时非常有用。比如你可以用md 0x41000000 0x10查看内核镜像的前16个字确认它是否被正确加载到了内存中。
返回列表