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=yvexpress 平台
CONFIG_SMP=y多核支持
CONFIG_ARM_GIC=y中断控制器
CONFIG_SERIAL_AMBA_PL011=y串口驱动
CONFIG_MMC_PL180=ySD 卡
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=ySMP
CONFIG_PREEMPT=ypreempt
CONFIG_THUMB2_KERNEL=ythumb2
# 查看内核 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 formatvermagic 不匹配用同一份 .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 512M512MB 内存
-no-reboot关机即退出

6.4 vexpress-a9 外设列表

地址设备内核驱动
0x10009000PL011 UART (控制台)SERIAL_AMBA_PL011
0x1000a000PL011 UART #1同上
0x10011000SP805 定时器ARM_TIMER_SP805
0x10017000PL031 RTCRTC_DRV_PL031
0x10020000PL181 MMC (SD卡)MMC_ARMMMCI
0x100e0000SMSC LAN9118 网卡SMC_LAN9118
0x1e000000GIC 中断控制器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 仓库
# 影响:无,只是警告信息
# 解决:忽略

附:全部脚本索引

#文件名用途
1boot.sh一键启动 QEMU
2kernel-build.sh编译/清理内核
3rootfs/build-initramfs.sh构建 initramfs
4rootfs/init.cPID 1 进程源码
5modules/002_of/Makefile模块 Makefile
6modules/002_of/002_of.c模块示例源码