目录与环境规划

为了确保全流程路径一致,强烈建议在宿主机中创建一个统一的工作空间。以下为完整的工程目录预览(此处仅为结构展示,无需手动创建):

/home/cp/Linux/
 ├── u-boot-2022.04/     # U-Boot 源码目录
 ├── linux-5.15.148/     # Linux 内核源码目录
 ├── busybox-1.36.1/     # BusyBox 源码目录
 ├── rootfs/             # 构建好的根文件系统目录, 用于nfs挂载
 ├── boot.cmd            # U-Boot 启动脚本源文件
 └── sdcard.img          # 最终生成的虚拟 SD 卡镜像

📌 系统架构与启动流向说明

本环境模拟真实的工业界开发板固化发布流程,系统启动链路如下:

  1. QEMU (硬件加电) -> 加载 u-boot (Bootloader)

  2. U-Boot -> 读取 SD 卡第一分区 (FAT32) -> 解析并执行 boot.scr

  3. boot.scr -> 将 zImage (内核) 和 .dtb (设备树) 载入物理内存 -> 交出控制权

  4. Linux Kernel -> 解析设备树 -> 挂载 SD 卡第二分区 (EXT4) -> 执行 init 进程


🛠️ 第一步:基础环境依赖安装

sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf qemu-system-arm \
    build-essential libncurses5-dev bison flex libssl-dev libc6-dev \
    u-boot-tools dosfstools parted

【核心依赖解析】

  • gcc-arm-linux-gnueabihf:针对 ARM 32位硬浮点架构的交叉编译器。

  • u-boot-tools:提供 mkimage 命令,用于将纯文本的启动脚本编译为 U-Boot 可执行的二进制包。

  • dosfstools:提供 mkfs.vfat 命令,这是格式化 SD 卡第一分区(Boot区)所必须的。

  • parted:提供大容量磁盘的非交互式分区能力,比 fdisk 更适合脚本化。


🛠️ 第二步:编译 U-Boot (Bootloader)

cd /home/cp/Linux
wget https://ftp.denx.de/pub/u-boot/u-boot-2022.04.tar.bz2
tar -xjvf u-boot-2022.04.tar.bz2
cd u-boot-2022.04

修改底层环境变量以劫持启动逻辑:

nano include/configs/vexpress_common.h

找到 #define CONFIG_EXTRA_ENV_SETTINGS \,覆盖为以下内容:

#define CONFIG_EXTRA_ENV_SETTINGS \
                "kernel_addr_r=0x60100000\0" \
                "fdt_addr_r=0x60000000\0" \
                "bootargs=console=tty0 console=ttyAMA0,38400n8\0" \
                BOOTENV \
		"console=ttyAMA0,38400n8\0" \
		"dram=1024M\0" \
		"root=/dev/sda1 rw\0" \
		"mtd=armflash:1M@0x800000(uboot),7M@0x1000000(kernel)," \
			"24M@0x2000000(initrd)\0" \
		"flashargs=setenv bootargs root=${root} console=${console} " \
			"mem=${dram} mtdparts=${mtd} mmci.fmax=190000 " \
			"devtmpfs.mount=0  vmalloc=256M\0" \
		"bootflash=run flashargs; " \
			"cp ${ramdisk_addr} ${ramdisk_addr_r} ${maxramdisk}; " \
			"bootm ${kernel_addr} ${ramdisk_addr_r}\0" \
		/* 新增内容 scriptaddr */  \
		"scriptaddr=0x62000000\0" \
                "bootcmd=echo Loading boot.scr...; load mmc 0:1 ${scriptaddr} boot.scr; source ${scriptaddr}\0" \
		"fdtfile=" CONFIG_DEFAULT_FDT_FILE "\0"

【宏定义解析】

  • scriptaddr=0x62000000:在 RAM 中划出一块绝对安全的物理地址,用于存放马上要从 SD 卡读取的 boot.scr 脚本。

  • load mmc 0:1:底层命令,指示 U-Boot 驱动第 0 号 MMC 设备(SD卡)的第 1 个分区,将其内容读入内存。

  • source:执行该内存地址中的脚本。

配置uboot链接地址为0x67800000:(不改也行)

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_ca9x4_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

在这里插入图片描述

编译执行:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

🛠️ 第三步:编译 Linux 内核与设备树

cd /home/cp/Linux
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.148.tar.xz
tar -xf linux-5.15.148.tar.xz
cd linux-5.15.148

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs modules -j$(nproc)

【命令解析】

  • zImage:生成自解压的内核压缩包,体积小,符合嵌入式存储受限的需求。

  • dtbs:编译 Device Tree Blob,将人类可读的 .dts 硬件描述文本编译为机器码。

  • modules:编译内核配置中被标记为 <M> 的可动态加载驱动模块。


🛠️ 第四步:构建根文件系统 (Rootfs)

cd /home/cp/Linux
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xf busybox-1.36.1.tar.bz2 && cd busybox-1.36.1

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

进入 Settings -> 勾选 Build static binary (no shared libs)
在这里插入图片描述

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install -j$(nproc)

mkdir -p ../rootfs
cp -r _install/* ../rootfs/
cd ../rootfs

mkdir -p dev etc/init.d lib proc sys tmp root var mnt

# 拷贝交叉编译器动态库
sudo cp -P /usr/arm-linux-gnueabihf/lib/* lib/

【命令解析】

  • cp -P:这是构建文件系统最容易踩坑的地方。-P 参数强制拷贝操作保留软链接 (Symlinks)。如果不加 -P,所有软链接会被解析为实体文件,不仅会导致文件系统体积暴增,还会破坏 C 语言运行库的内部依赖结构,导致编译的应用程序在开发板上报 Not found 错误。

配置系统初始化脚本:

cat << 'EOF' > etc/inittab
::sysinit:/etc/init.d/rcS
console::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
EOF

cat << 'EOF' > etc/init.d/rcS
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s
EOF

chmod 755 etc/init.d/rcS

【脚本解析】

  • devtmpfs:内核提供的一种机制,可以在 /dev 目录下自动动态生成当前系统识别到的硬件设备节点文件。

  • mdev -s:BusyBox 提供的 udev 缩减版,配合 hotplug 机制,用于在系统运行时响应硬件的热插拔事件。


🛠️ 第五步:制作 U-Boot 启动脚本 (boot.scr)

cd /home/cp/Linux
cat << 'EOF' > boot.cmd
setenv bootargs 'console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rw rootwait'
echo "Loading Kernel and DTB from FAT32 Boot Partition..."
fatload mmc 0:1 0x60008000 zImage
fatload mmc 0:1 0x61000000 vexpress-v2p-ca9.dtb
bootz 0x60008000 - 0x61000000
EOF

mkimage -A arm -O linux -T script -C none -a 0 -e 0 -n "Real Env Boot Script" -d boot.cmd boot.scr

【命令解析】

  • rootwait:关键参数。指示内核在启动时无限期挂起等待,直到 /dev/mmcblk0p2 (SD卡) 硬件驱动初始化完成并就绪。若缺失此参数,内核在启动初期可能因找不到磁盘而直接 Panic。

  • mkimage

    • -A arm -O linux:指定目标架构与操作系统。

    • -T script -C none:指定类型为脚本文件,不使用数据压缩。

    • -d boot.cmd boot.scr:将文本 boot.cmd 包装上 64 字节的 U-Boot 识别头,输出为 boot.scr


🛠️ 第六步:制作双分区虚拟 SD 卡镜像

dd if=/dev/zero of=sdcard.img bs=1M count=512

parted -s sdcard.img mklabel msdos
parted -s sdcard.img mkpart primary fat32 1MiB 100MiB
parted -s sdcard.img set 1 boot on
parted -s sdcard.img mkpart primary ext4 100MiB 100%

【命令解析】

  • dd:以字节级别直接操作系统磁盘,此处生成 512MB 的全零文件。

  • parted -s:以非交互模式 (script) 执行分区。

    • mklabel msdos:创建传统的 MBR (Master Boot Record) 分区表结构。

    • 1MiB:强制第一个分区从 1MB 地址开始,以保证磁道对齐,提升磁盘 I/O 性能。

# 挂载镜像为回环设备
sudo losetup -Pf sdcard.img

# 查看被分配的 loop 设备编号 (假设输出为 loop0)
lsblk | grep loop

# 格式化分区
sudo mkfs.vfat -F 32 -n "BOOT" /dev/loop0p1
sudo mkfs.ext4 -L "ROOTFS" /dev/loop0p2

# 拷贝数据
mkdir -p /tmp/mnt_boot /tmp/mnt_rootfs
sudo mount /dev/loop0p1 /tmp/mnt_boot
sudo mount /dev/loop0p2 /tmp/mnt_rootfs

sudo cp linux-5.15.148/arch/arm/boot/zImage /tmp/mnt_boot/
sudo cp linux-5.15.148/arch/arm/boot/dts/vexpress-v2p-ca9.dtb /tmp/mnt_boot/
sudo cp boot.scr /tmp/mnt_boot/
sudo cp -a rootfs/* /tmp/mnt_rootfs/

# 同步并清理
sync
sudo umount /tmp/mnt_boot /tmp/mnt_rootfs
sudo losetup -d /dev/loop0

【命令解析】

  • losetup -Pf-f 表示自动寻找第一个空闲的 loop 设备;-P 强制内核重新扫描该镜像的分区表,自动生成 /dev/loopXp1/dev/loopXp2 子设备节点。

  • cp -a:归档拷贝模式。等同于 -dpR,递归拷贝目录,并严格保留所有文件的读写执行权限、属主信息及软链接。(在构建根文件系统时至关重要)。


🛠️ 第七步:启动 QEMU 仿真环境

cat << 'EOF' > run_qemu.sh
#!/bin/bash

# 彻底关闭 QEMU 的音频输出,消除 ALSA 报错刷屏
export QEMU_AUDIO_DRV=none

sudo qemu-system-arm \
    -M vexpress-a9 \
    -m 1024M \
    -kernel u-boot-2022.04/u-boot \
    -nographic \
    -drive if=sd,format=raw,file=sdcard.img
EOF

chmod +x run_qemu.sh
./run_qemu.sh

【命令解析】

  • -drive if=sd,format=raw,file=sdcard.img-drive 是 QEMU 标准的磁盘挂载指令。

    • if=sd:指定接口类型为 SD 总线。

    • format=raw:显式声明镜像格式为原生裸数据块,可避免 QEMU 在启动时因格式推断失败而抛出警告信息。

如果需要退出qemu,只需要在终端执行ctrl+A,然后紧接着输入x即可。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐