qemu ARM 驱动学习
本文档记录从零开始搭建驱动学习项目的完整步骤,包含所有脚本的源码与说明。
目录
1. 安装前置工具
# 设备树编译器
apt-get install -y device-tree-compiler
# QEMU ARM 模拟器
apt-get install -y qemu-system-arm
# ARM 交叉编译器 (也可用 Linaro 等预编译工具链)
apt-get install -y gcc-arm-linux-gnueabihf
# 辅助工具
apt-get install -y parted dosfstools mtools cpio
验证:
dtc --version # Device Tree Compiler 1.6.x
qemu-system-arm --version # QEMU 6.x
arm-linux-gnueabihf-gcc --version # ARM gcc 11.x
2. 内核源码与编译
2.1 获取源码
# 方法 A:主线内核
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.226.tar.xz
tar -xf linux-5.10.226.tar.xz
mv linux-5.10.226 kernel
# 方法 B:厂商内核(RK、NXP 等,含 SoC 驱动补丁)
# 本项目使用 Rockchip 5.10 内核
# 目录: kernel/
2.2 配置内核
cd kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig
vexpress_defconfig 开启的关键选项:
| 配置项 | 说明 |
|---|---|
CONFIG_ARCH_VEXPRESS=y | vexpress 平台 |
CONFIG_SMP=y | 多核支持 |
CONFIG_ARM_GIC=y | 中断控制器 |
CONFIG_SERIAL_AMBA_PL011=y | 串口驱动 |
CONFIG_MMC_PL180=y | SD 卡 |
CONFIG_SMC_LAN9118=y | 网卡 |
涉及的文件:
| 文件 | 说明 |
|---|---|
arch/arm/configs/vexpress_defconfig | 默认配置 |
.config | 生成的最终配置 |
include/generated/autoconf.h | 配置的 C 头文件 |
include/config/ | 配置标记目录 |
2.3 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage dtbs modules_prepare
编译产物:
| 文件 | 说明 |
|---|---|
arch/arm/boot/zImage | 内核镜像 |
arch/arm/boot/dts/*.dtb | 设备树 |
vmlinux | 未压缩内核(带符号表) |
Module.symvers | 模块符号表 |
2.4 vermagic 匹配原理
vermagic 由 .config 决定,必须和运行内核一致:
| .config 选项 | vermagic 字段 |
|---|---|
CONFIG_SMP=y | SMP |
CONFIG_PREEMPT=y | preempt |
CONFIG_THUMB2_KERNEL=y | thumb2 |
# 查看内核 vermagic
strings arch/arm/boot/zImage | grep vermagic
2.5 kernel-build.sh
该脚本由 boot.sh 自动调用,也可单独执行。
位置:kernel-build.sh
#!/bin/bash
# kernel-build.sh - 编译 ARM 内核 (vexpress_defconfig)
# boot.sh 自动调用,也可单独执行
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
KERNEL_SRC="$DIR/kernel"
cd "$KERNEL_SRC"
if [ "$1" = "clean" ]; then
echo "清理内核..."
if [ -f arch/arm/boot/zImage ]; then
ts=$(date +%Y-%m-%d-%H:%M:%S)
cp arch/arm/boot/zImage "arch/arm/boot/zImage-vexpress-backup-${ts}"
echo "已备份: zImage-vexpress-backup-${ts}"
fi
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean 2>&1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean 2>&1
echo "=== 清理完成 ==="
exit 0
fi
echo "配置内核 (vexpress_defconfig)..."
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig 2>&1
echo "编译内核... make -j$(nproc)"
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage dtbs modules_prepare 2>&1
echo ""
echo "=== 内核编译完成 ==="
ls -lh arch/arm/boot/zImage
echo ""
echo "复制 vexpress 设备树到 dts/..."
cp arch/arm/boot/dts/vexpress-v2p-ca9.dts "$DIR/dts/" 2>/dev/null
cp arch/arm/boot/dts/vexpress-v2m.dtsi "$DIR/dts/" 2>/dev/null
cp arch/arm/boot/dts/vexpress-v2p-ca9.dtb "$DIR/dts/" 2>/dev/null
echo "完成"
用法:
./kernel-build.sh # 编译内核
./kernel-build.sh clean # 清理(自动备份 zImage)
3. 根文件系统 (initramfs)
3.1 设计思路
不使用完整 Linux 根文件系统(几百 MB),而是用最小 initramfs:
initramfs.cpio.gz (~1.4MB)
├── init (PID 1 进程, 编译自 init.c)
└── bin/
├── busybox (静态编译, 包含 sh/ls/insmod 等 100+ 命令)
├── sh → busybox
├── insmod → busybox
└── ...
3.2 busybox 编译
修改busybox 的.config (table补全功能)
CONFIG_ASH_TAB_COMPLETION = y
编译:
cd rootfs
tar -xf busybox.tar.bz2
cd busybox-1_36_1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
# → Settings: Build static binary (no shared libs) [=y]
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
# 产物: busybox (ARM 静态二进制)
3.3 init.c
PID 1 进程:挂载文件系统并启动 shell。
位置:rootfs/init.c
/* init: 挂载文件系统, 启动交互式 shell */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/reboot.h>
int main(void)
{
mkdir("/proc", 0755);
mount("proc", "/proc", "proc", 0, NULL);
mkdir("/sys", 0755);
mount("sysfs", "/sys", "sysfs", 0, NULL);
mkdir("/dev", 0755);
mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);
setenv("PS1", "\\w # ", 1);
printf("QEMU ARM 设备树学习环境 - poweroff 关机\n");
execl("/bin/sh", "sh", NULL);
sync();
reboot(0x4321fedc);
return 0;
}
3.4 build-initramfs.sh
位置:rootfs/build-initramfs.sh
#!/bin/bash
# build-initramfs.sh - 构建完整的 initramfs 根文件系统
# 1. 编译 init.c
# 2. 放入 busybox 并预创建所有命令的软链接
# 3. 打包成 cpio.gz
#
# 输出: initramfs.cpio.gz
set -e
cd "$(dirname "$0")"
arm-linux-gnueabihf-gcc -static -Os -o init init.c 2>&1 | grep -v warn_unused || true
# 清理并创建目录结构
rm -rf initramfs_root
mkdir -p initramfs_root/{bin,sbin,etc,dev,proc,sys,tmp}
cp init initramfs_root/
cp busybox-1_36_1/busybox initramfs_root/bin/
chmod +x initramfs_root/init initramfs_root/bin/busybox
# 复制 .ko 模块(如果有)
if [ -d ../modules ] && [ "$(ls -A ../modules/**/*.ko 2>/dev/null)" ]; then
echo "复制内核模块..."
mkdir -p initramfs_root/modules
cp ../modules/**/*.ko initramfs_root/modules/
fi
# 预创建 busybox 命令软链接
BB_LN="ln -s /bin/busybox initramfs_root"
for cmd in \
sh ls cp mv rm cat echo printf test sleep \
mkdir rmdir ln chmod chown chgrp \
clear reset stty tty \
ps top free uptime kill killall pkill \
insmod lsmod rmmod modprobe modinfo \
dmesg mount umount \
grep find head tail wc sort cut tr uniq \
seq basename dirname readlink realpath \
date cal hexdump xxd strings \
which env set export unset \
tar gzip gunzip zcat bzip2 bunzip2 \
vi diff patch cmp less more \
df du sync dd mknod \
xargs expr timeout yes nice \
pidof pgrep watch; do
$BB_LN/bin/$cmd 2>/dev/null || true
done
# poweroff 用 sysrq 触发关机
cat > initramfs_root/sbin/poweroff << 'EOF'
#!/bin/sh
sync
echo 1 > /proc/sys/kernel/sysrq 2>/dev/null
echo o > /proc/sysrq-trigger
EOF
chmod +x initramfs_root/sbin/poweroff
for cmd in \
reboot halt \
mdev ifconfig route ping ping6 arp \
udhcpc ip neigh \
swapon swapoff fdisk blkid blockdev \
hwclock sysctl trigger; do
$BB_LN/sbin/$cmd 2>/dev/null || true
done
cd initramfs_root
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
cd ..
echo "=== initramfs 已生成 ==="
ls -lh initramfs.cpio.gz
rm -rf initramfs_root init
3.5 手动构建
cd rootfs
bash build-initramfs.sh
4. 设备树编译
4.1 文件结构
dts/
├── vexpress-v2p-ca9.dts ← 板级设备树
└── vexpress-v2m.dtsi ← 母板描述(被 include)
vexpress-v2p-ca9.dts 通过 #include "vexpress-v2m.dtsi" 引用公共部分。
4.2 编译命令
# 因为使用了 #include,需要用 cpp 预处理后再用 dtc 编译
cpp -nostdinc -I dts/ -undef -x assembler-with-cpp \
dts/vexpress-v2p-ca9.dts | \
dtc -I dts -O dtb -o dts/vexpress-v2p-ca9.dtb -
# 反编译查看 DTB 内容
dtc -I dtb -O dts dts/vexpress-v2p-ca9.dtb
4.3 phandle 机制
DTS 中的标签(&gic、&uart0)在编译时被替换为数字 phandle:
DTS (源文件): interrupts = <&gic 0 37 4>;
DTB (二进制): interrupts = <0x0001 0x00 0x25 0x04>;
5. 内核模块开发
5.1 模块源码
位置:modules/002_of/002_of.c
#include "linux/module.h"
#include "linux/init.h"
#include "linux/of.h"
static int of_init(void)
{
printk("of_init\n");
return 0;
}
static void of_exit(void)
{
printk("of_exit\n");
}
module_init(of_init);
module_exit(of_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("of function test");
5.2 模块 Makefile
位置:modules/002_of/Makefile
# ARM 交叉编译内核模块
KERNELDIR ?= ../../kernel
PWD := $(shell pwd)
obj-m := 002_of.o
all:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
-C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
-C $(KERNELDIR) M=$(PWD) clean
5.3 编译流程
make -C kernel M=modules/002_of modules
├── 读取 kernel/Makefile
├── 读取 kernel/.config ← 生成 vermagic
├── 进入 scripts/mod/modpost ← 符号校验
├── 进入 modules/002_of/
│ ├── 002_of.c → .o
│ ├── modpost → .mod.c
│ └── 链接 → .ko
└── 生成 Module.symvers
5.4 常见问题
| 错误 | 原因 | 解决 |
|---|---|---|
invalid module format | vermagic 不匹配 | 用同一份 .config 重编 |
Unknown symbol | 内核符号未导出 | 检查 EXPORT_SYMBOL |
undefined reference | 函数不存在 | 确认内核 API 版本 |
6. QEMU 启动
6.1 boot.sh
主启动脚本。
位置:boot.sh
#!/bin/bash
# boot.sh - QEMU ARM 驱动/设备树学习环境 一键启动
DIR="$(cd "$(dirname "$0")" && pwd)"
KERNEL_DIR="$DIR/kernel/arch/arm/boot"
# 检查内核是否已编译
if [ ! -f "$KERNEL_DIR/zImage" ]; then
echo "================================================"
echo " 没有内核 zImage, 开始编译内核..."
echo "================================================"
bash "$DIR/kernel-build.sh"
echo ""
fi
echo "编译设备树..."
(cpp -nostdinc -I"$DIR/dts" -undef -x assembler-with-cpp \
"$DIR/dts/vexpress-v2p-ca9.dts" 2>/dev/null | \
dtc -I dts -O dtb -o "$DIR/dts/vexpress-v2p-ca9.dtb" - 2>&1) | grep -v Warning || true
echo "构建 initramfs..."
bash "$DIR/rootfs/build-initramfs.sh"
echo "=== QEMU vexpress-a9 启动 ==="
qemu-system-arm \
-M vexpress-a9 \
-kernel "$KERNEL_DIR/zImage" \
-dtb "$DIR/dts/vexpress-v2p-ca9.dtb" \
-initrd "$DIR/rootfs/initramfs.cpio.gz" \
-append "console=ttyAMA0,115200" \
-nographic -m 512M -no-reboot
6.2 启动流程
./boot.sh
│
├── ① 检查 zImage → 没有则 kernel-build.sh
│
├── ② 设备树: dts → dtb
│
├── ③ initramfs: init.c + busybox → cpio.gz
│
└── ④ QEMU
│
└── 虚拟机内部
├── 加载 zImage
├── 加载 initramfs
├── 传入 DTB
├── 内核启动 → /init
└── shell 就绪
6.3 QEMU 参数说明
| 参数 | 说明 |
|---|---|
-M vexpress-a9 | 模拟 vexpress-a9 开发板 |
-kernel | 内核镜像路径 |
-dtb | 设备树路径 |
-initrd | 根文件系统路径 |
-append "console=ttyAMA0" | 内核命令行 |
-nographic | 无图形界面 |
-m 512M | 512MB 内存 |
-no-reboot | 关机即退出 |
6.4 vexpress-a9 外设列表
| 地址 | 设备 | 内核驱动 |
|---|---|---|
0x10009000 | PL011 UART (控制台) | SERIAL_AMBA_PL011 |
0x1000a000 | PL011 UART #1 | 同上 |
0x10011000 | SP805 定时器 | ARM_TIMER_SP805 |
0x10017000 | PL031 RTC | RTC_DRV_PL031 |
0x10020000 | PL181 MMC (SD卡) | MMC_ARMMMCI |
0x100e0000 | SMSC LAN9118 网卡 | SMC_LAN9118 |
0x1e000000 | GIC 中断控制器 | ARM_GIC |
vexpress-a9 没有 I2C/SPI/GPIO 模拟。需要这些外设需用 -M virt。
7. 错误排查
7.1 QEMU 无输出
# 原因:控制台参数不对
# 排查:检查 DTS 中 UART 地址和内核命令行
# 解决:确认 console= 参数与 DTB 中的 UART 匹配
7.2 Kernel panic: VFS: Unable to mount root fs
# 原因:内核没找到根文件系统
# 排查:
# - -initrd 参数是否正确
# - initramfs 是否损坏
# 解决:
file rootfs/initramfs.cpio.gz # 应是 gzip compressed data
7.3 insmod: invalid module format
# 原因:vermagic 不匹配
# 排查:
strings kernel/arch/arm/boot/zImage | grep vermagic
strings modules/xxx/xxx.ko | grep vermagic
# 解决:确保 .config 一致,重新编译模块
7.4 make 报错 .git 不存在
# 原因:内核源码是 tar 解压的,没有 git 仓库
# 影响:无,只是警告信息
# 解决:忽略
附:全部脚本索引
| # | 文件名 | 用途 |
|---|---|---|
| 1 | boot.sh | 一键启动 QEMU |
| 2 | kernel-build.sh | 编译/清理内核 |
| 3 | rootfs/build-initramfs.sh | 构建 initramfs |
| 4 | rootfs/init.c | PID 1 进程源码 |
| 5 | modules/002_of/Makefile | 模块 Makefile |
| 6 | modules/002_of/002_of.c | 模块示例源码 |