跳转至

使用 QEMU 学习嵌入式系统开发

🐧 快速熟悉 U-Boot、Linux、HAL、驱动层的完整指南

创建时间:2026-03-13

目标:无需硬件,快速掌握嵌入式核心技能


📖 目录

  1. 为什么用 QEMU 学习嵌入式
  2. QEMU 安装与配置
  3. U-Boot 学习与调试
  4. Linux 内核学习与调试
  5. 硬件抽象层 (HAL) 学习
  6. 驱动程序学习
  7. 实战项目
  8. 调试技巧
  9. 学习路线建议

1. 为什么用 QEMU 学习嵌入式

1.1 优势对比

方面 真实硬件 QEMU 模拟
成本 开发板几百 - 几千元 免费
获取时间 购买 + 物流 3-7 天 即时下载
调试能力 有限(JTAG 昂贵) 强大(GDB、日志)
可恢复性 可能变砖 随时重置
并发学习 1 人 1 板 多人同时使用
快照功能 ✅ 保存状态,快速恢复
网络调试 需要配置 内置支持
外设支持 取决于开发板 可自定义

1.2 适用场景

✅ 适合用 QEMU: - 学习 U-Boot 启动流程 - 学习 Linux 内核机制 - 学习驱动程序框架 - 调试算法和逻辑 - 快速验证想法 - 团队协作学习

❌ 需要真实硬件: - 学习硬件电路设计 - 学习 NAND/DDR 时序调试 - 学习电源管理 - 学习高速信号完整性 - 性能测试和功耗测试

1.3 推荐开发板模型

模型 架构 特点 适合学习
versatilepb ARM9 经典、稳定 U-Boot 基础
vexpress-a9 Cortex-A9 功能丰富 U-Boot+Linux
vexpress-a15 Cortex-A15 高性能 Linux 内核
raspi2 Cortex-A7 树莓派 综合学习
smdkc210 Cortex-A8 三星 Android 底层
xilinx-zynq ARM+FPGA SoC 嵌入式 Linux

2. QEMU 安装与配置

2.1 Ubuntu 安装

# 安装 QEMU 和相关工具
sudo apt update
sudo apt install -y qemu-system-arm qemu-utils \
                    gcc-arm-linux-gnueabihf \
                    gcc-aarch64-linux-gnu \
                    gdb-multiarch \
                    device-tree-compiler

# 验证安装
qemu-system-arm --version
arm-linux-gnueabihf-gcc --version

2.2 目录结构

~/embedded-learning/
├── qemu/                    # QEMU 相关
│   ├── images/              # 镜像文件
│   ├── kernels/             # 内核镜像
│   ├── dtbs/                # 设备树
│   └── scripts/             # 启动脚本
├── uboot/                   # U-Boot 源码
│   └── u-boot/              # U-Boot 源码目录
├── linux/                   # Linux 内核源码
│   └── linux/               # Linux 源码目录
├── rootfs/                  # 根文件系统
│   └── buildroot/           # Buildroot 构建
└── projects/                # 学习项目
    ├── hello_driver/        # 第一个驱动
    ├── i2c_driver/          # I2C 驱动
    └── spi_driver/          # SPI 驱动

2.3 快速启动脚本

#!/bin/bash
# qemu-start.sh - 快速启动 QEMU ARM 虚拟机

KERNEL=$1
DTB=$2
ROOTFS=$3

# 默认值
KERNEL=${KERNEL:-~/embedded-learning/qemu/images/zImage}
DTB=${DTB:-~/embedded-learning/qemu/images/vexpress-v2p-ca9.dtb}
ROOTFS=${ROOTFS:-~/embedded-learning/qemu/images/rootfs.ext4}

# 启动 QEMU
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel $KERNEL \
    -dtb $DTB \
    -drive file=$ROOTFS,if=sd,format=raw \
    -append "root=/dev/mmcblk0 rw console=ttyAMA0,115200 init=/sbin/init" \
    -nographic \
    -netdev user,id=net0 \
    -device smsc91180,netdev=net0 \
    -serial mon:stdio \
    -d guest_errors,unimp \
    -D qemu.log

# 说明:
# -M vexpress-a9      : 机器型号
# -m 512M             : 内存大小
# -kernel             : 内核镜像
# -dtb                : 设备树
# -drive              : 根文件系统
# -append             : 内核启动参数
# -nographic          : 无图形界面(串口控制台)
# -netdev             : 网络配置
# -serial mon:stdio   : 串口重定向到终端
# -d                  : 调试日志
# -D                  : 日志文件

2.4 网络配置

# QEMU 网络模式

# 1. 用户模式(最简单,默认)
-netdev user,id=net0
# 特点:QEMU 内置 DHCP,虚拟机可访问外网
# 宿主机 IP: 10.0.2.2
# 虚拟机 IP: 10.0.2.15

# 2. TAP 模式(桥接,需要 root)
-netdev tap,id=net0,ifname=tap0,script=no
# 特点:虚拟机和宿主机在同一网段

# 3. 端口转发(从宿主机访问虚拟机)
-netdev user,id=net0,hostfwd=tcp::2222-:22
# 特点:宿主机 2222 端口 → 虚拟机 22 端口
# SSH 连接:ssh -p 2222 user@localhost

3. U-Boot 学习与调试

3.1 编译 U-Boot

# 1. 下载源码
git clone https://github.com/u-boot/u-boot.git
cd u-boot

# 2. 配置 vexpress-a9 开发板
make vexpress_ca9x4_defconfig

# 3. 编译
make CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

# 4. 生成镜像
# 输出文件:u-boot(ELF 格式,可用于 GDB 调试)
# 输出文件:u-boot.bin(二进制,用于 QEMU 启动)

3.2 启动 U-Boot

# 方法 1:直接启动 U-Boot
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel u-boot \
    -nographic

# 方法 2:使用 GDB 调试启动
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel u-boot \
    -nographic \
    -s -S
# -s : 在 1234 端口监听 GDB
# -S : 启动时暂停,等待 GDB 连接

# 另一个终端连接 GDB
arm-linux-gnueabihf-gdb u-boot
(gdb) target remote :1234
(gdb) continue

3.3 U-Boot 关键代码学习路径

u-boot/
├── arch/arm/                    # ARM 架构相关
│   ├── cpu/armv7/               # ARMv7 CPU 初始化
│   │   ├── cpu.c                # CPU 初始化代码
│   │   └── lowlevel_init.S      # 底层汇编初始化
│   ├── include/asm/             # ARM 头文件
│   └── lib/                     # ARM 库函数
├── board/armltd/vexpress/       # VExpress 开发板
│   ├── vexpress.c               # 开发板初始化
│   └── Kconfig                  # 配置选项
├── common/                      # 通用功能
│   ├── board_f.c                # 板级初始化(前)
│   ├── board_r.c                # 板级初始化(后)
│   ├── main.c                   # U-Boot 主入口
│   └── command.c                # 命令框架
├── drivers/                     # 驱动程序
│   ├── serial/                  # 串口驱动
│   ├── mmc/                     # MMC/SD 驱动
│   ├── net/                     # 网络驱动
│   ├── spi/                     # SPI 驱动
│   └── i2c/                     # I2C 驱动
├── include/                     # 头文件
│   ├── common.h                 # 通用头文件
│   └── configs/                 # 开发板配置
│       └── vexpress_ca9x4.h     # VExpress 配置
└── cmd/                         # 命令实现
    ├── bootm.c                  # 启动命令
    ├── mmc.c                    # MMC 命令
    └── net.c                    # 网络命令

3.4 U-Boot 启动流程分析

// 1. 汇编入口 (arch/arm/lib/vectors.S)
reset:
    b       reset           // 复位
    b       undefined_instruction
    b       software_interrupt
    b       prefetch_abort
    b       data_abort
    b       not_used
    b       irq
    b       fiq

// 2. C 语言入口 (common/main.c)
void main(void)
{
    // 板级初始化
    board_init_f();  // 前:初始化 DDR、串口等

    // 重定位到 DDR
    relocate_code();

    // 板级初始化
    board_init_r();  // 后:初始化网络、MMC 等

    // 主循环
    main_loop();
}

// 3. 板级初始化 (board/armltd/vexpress/vexpress.c)
int board_init(void)
{
    // 设置启动参数地址
    gd->bd->bi_boot_params = 0x60000100;

    // 初始化 GPIO
    vexpress_gpio_init();

    return 0;
}

// 4. 主循环 (common/main.c)
void main_loop(void)
{
    // 检查是否有启动命令
    if (run_command("bootcmd", 0) < 0) {
        // 没有则进入命令行
        cmd_loop();
    }
}

3.5 常用 U-Boot 命令

# 在 U-Boot 命令行中练习

# 1. 内存操作
md.b 0x60000000 100     # 显示内存(字节)
md.w 0x60000000 100     # 显示内存(字)
mw.b 0x60000000 0xff 100 # 写内存
cp.b 0x60000000 0x70000000 1000  # 复制内存
cmp.b 0x60000000 0x70000000 1000  # 比较内存

# 2. 环境变量
printenv                # 打印所有环境变量
setenv bootargs '...'   # 设置环境变量
saveenv                 # 保存到 MMC

# 3. 设备操作
mmc info                # MMC 信息
mmc read 0x70000000 0x100 0x100  # 从 MMC 读取
fatls mmc 0:1           # 列出 FAT 文件系统文件

# 4. 网络操作
ping 10.0.2.2           # ping 宿主机
tftp 0x70000000 zImage  # TFTP 下载
dhcp                    # DHCP 获取 IP

# 5. 启动
bootm 0x70000000        # 启动内核

3.6 U-Boot 实验项目

实验 1:添加自定义命令

// cmd/hello.c
#include <common.h>
#include <command.h>

static int do_hello(struct cmd_tbl *cmdtp, int flag,
                   int argc, char *const argv[])
{
    printf("Hello, Embedded World!\n");
    printf("Board: %s\n", CONFIG_SYS_BOARD_NAME);
    printf("U-Boot version: %s\n", U_BOOT_VERSION);

    return 0;
}

U_BOOT_CMD(
    hello,      // 命令名
    1,          // 最大参数数
    0,          // 不需要重复命令
    do_hello,   // 处理函数
    "Say hello",// 简短帮助
    "Print hello message\n"  // 完整帮助
);

实验 2:修改启动延迟

// common/main.c
void main_loop(void)
{
    int delay = 5;  // 改为 0,取消启动延迟

    if (delay > 0) {
        printf("Hit any key to stop autoboot: %d ", delay);
        // ...
    }
}

4. Linux 内核学习与调试

4.1 编译 Linux 内核

# 1. 下载源码
git clone https://github.com/torvalds/linux.git
cd linux

# 2. 配置 vexpress-a9
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig

# 3. 菜单配置(可选)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

# 4. 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

# 5. 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 6. 输出文件
# arch/arm/boot/zImage      : 内核镜像
# arch/arm/boot/dts/*.dtb   : 设备树文件

4.2 启动 Linux

# 启动脚本
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel ~/embedded-learning/linux/linux/arch/arm/boot/zImage \
    -dtb ~/embedded-learning/linux/linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
    -drive file=~/embedded-learning/qemu/images/rootfs.ext4,if=sd,format=raw \
    -append "root=/dev/mmcblk0 rw console=ttyAMA0,115200 init=/sbin/init" \
    -nographic \
    -netdev user,id=net0 \
    -device smsc91180,netdev=net0

# 内核启动参数说明
# root=/dev/mmcblk0    : 根文件系统设备
# rw                   : 读写挂载
# console=ttyAMA0      : 串口控制台
# init=/sbin/init      : 初始化进程

4.3 Linux 内核代码学习路径

linux/
├── arch/arm/                    # ARM 架构相关
│   ├── kernel/                  # 内核核心
│   │   ├── head.S               # 汇编入口
│   │   ├── setup.c              # 系统设置
│   │   ├── process.c            # 进程管理
│   │   ├── irq.c                # 中断处理
│   │   └── entry-armv.S         # 异常入口
│   ├── mm/                      # 内存管理
│   │   ├── init.c               # 内存初始化
│   │   └── fault.c              # 缺页处理
│   └── include/asm/             # ARM 头文件
├── drivers/                     # 驱动程序
│   ├── char/                    # 字符设备
│   ├── block/                   # 块设备
│   ├── mmc/                     # MMC/SD
│   ├── net/                     # 网络
│   ├── spi/                     # SPI
│   ├── i2c/                     # I2C
│   ├── gpio/                    # GPIO
│   ├── serial/                  # 串口
│   └── clk/                     # 时钟
├── include/                     # 头文件
│   ├── linux/                   # 内核头文件
│   │   ├── kernel.h             # 内核核心
│   │   ├── module.h             # 模块
│   │   ├── device.h             # 设备模型
│   │   └── driver.h             # 驱动
│   └── uapi/                    # 用户空间 API
├── kernel/                      # 内核核心
│   ├── sched/                   # 调度器
│   ├── signal.c                 # 信号
│   ├── sys.c                    # 系统调用
│   └── module.c                 # 模块加载
├── init/                        # 初始化
│   ├── main.c                   # 内核入口
│   └── do_mounts.c              # 挂载根文件系统
└── mm/                          # 内存管理
    ├── mmap.c                   # 内存映射
    ├── page_alloc.c             # 页分配
    └── vmalloc.c                # 虚拟内存

4.4 Linux 启动流程

// 1. 汇编入口 (arch/arm/kernel/head.S)
ENTRY(stext)
    mrc     p15, 0, r9, c0, c0      // 获取 CPU ID
    bl      __lookup_processor_type // 查找处理器类型
    bl      __create_page_tables    // 创建页表
    mov     r0, r9                  // 保存 CPU ID
    bl      __enable_mmu            // 使能 MMU
    b       __mmap_switched         // 切换到虚拟地址

    // 跳转到 C 语言入口
    b       start_kernel

// 2. C 语言入口 (init/main.c)
asmlinkage void __init start_kernel(void)
{
    // 初始化子系统
    setup_arch();           // 架构相关初始化
    setup_command_line();   // 命令行解析
    setup_cpu_irq();        // CPU 中断
    sched_init();           // 调度器初始化
    softirq_init();         // 软中断初始化
    console_init();         // 控制台初始化

    // 初始化驱动模型
    driver_init();

    // 初始化内存
    mem_init();

    // 初始化 VFS
    vfs_caches_init();

    // 启动内核线程
    kernel_thread(init, NULL, CLONE_FS);

    // 启动调度器
    cpu_startup_entry(CPUHP_ONLINE);
}

// 3. Init 进程 (init/main.c)
static int __ref kernel_init(void *unused)
{
    // 挂载根文件系统
    mount_root();

    // 执行用户空间 init
    run_init_process("/sbin/init");
}

4.5 Linux 实验项目

实验 1:添加系统调用

// 1. 添加系统调用号 (arch/arm/include/uapi/asm/unistd-common.h)
#define __NR_hello  390

// 2. 实现系统调用 (kernel/hello.c)
SYSCALL_DEFINE0(hello)
{
    printk(KERN_INFO "Hello from kernel!\n");
    return 0;
}

// 3. 添加到系统调用表 (arch/arm/kernel/calls.S)
    .long   sys_hello

// 4. 用户空间测试
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

int main()
{
    int ret = syscall(__NR_hello);
    printf("Return: %d\n", ret);
    return 0;
}

实验 2:修改进程调度

1
2
3
4
5
6
7
8
9
// kernel/sched/core.c
void __init sched_init(void)
{
    // 添加调试打印
    printk(KERN_INFO "Scheduler initialized\n");

    // 修改默认调度策略
    // ...
}

5. 硬件抽象层 (HAL) 学习

5.1 HAL 层概念

用户空间应用程序
C 库 (glibc/musl)
系统调用接口
Linux 内核
设备驱动层
硬件抽象层 (HAL) ← 本章节重点
硬件寄存器

5.2 QEMU 中的 HAL 学习

// QEMU 模拟的硬件抽象层示例
// drivers/gpio/gpio-pl061.c

// 1. 寄存器定义(HAL 层核心)
#define GPIO_DATA       0x000
#define GPIO_DIR        0x400
#define GPIO_IS         0x404
#define GPIO_IBE        0x408
#define GPIO_IEV        0x40C
#define GPIO_IE         0x410
#define GPIO_RIS        0x414
#define GPIO_MIS        0x418
#define GPIO_IC         0x41C

// 2. 寄存器访问宏
#define PL061_READ(reg)     readl(base + reg)
#define PL061_WRITE(reg, v) writel(v, base + reg)

// 3. HAL 层函数
static void pl061_gpio_direction_input(struct gpio_chip *chip,
                                       unsigned offset)
{
    void __iomem *base = gpiochip_get_data(chip);
    u32 gpio;

    // 读取方向寄存器
    gpio = PL061_READ(GPIO_DIR);

    // 清除对应位(设置为输入)
    gpio &= ~(1 << offset);

    // 写回方向寄存器
    PL061_WRITE(GPIO_DIR, gpio);
}

static void pl061_gpio_set(struct gpio_chip *chip,
                           unsigned offset, int value)
{
    void __iomem *base = gpiochip_get_data(chip);

    // 写入数据寄存器
    PL061_WRITE(GPIO_DATA, 1 << offset);
}

5.3 HAL 层实验:实现简单 GPIO 驱动

// drivers/gpio/gpio-myhal.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/io.h>

// 1. 定义寄存器基地址(QEMU 中模拟的地址)
#define MYHAL_GPIO_BASE     0x10000000
#define MYHAL_GPIO_SIZE     0x1000

// 2. 寄存器结构
struct myhal_gpio_regs {
    u32 data;      // 0x00: 数据寄存器
    u32 dir;       // 0x04: 方向寄存器
    u32 is;        // 0x08: 中断选择
    u32 ibe;       // 0x0C: 双边沿
    u32 iev;       // 0x10: 中断事件
    u32 ie;        // 0x14: 中断使能
    u32 ris;       // 0x18: 原始中断状态
    u32 mis;       // 0x1C: 屏蔽后中断状态
};

// 3. 全局变量
static struct myhal_gpio_regs __iomem *gpio_regs;

// 4. HAL 层 API
static void myhal_gpio_set_value(unsigned gpio, int value)
{
    u32 data = readl(&gpio_regs->data);

    if (value)
        data |= (1 << gpio);
    else
        data &= ~(1 << gpio);

    writel(data, &gpio_regs->data);
}

static int myhal_gpio_get_value(unsigned gpio)
{
    u32 data = readl(&gpio_regs->data);
    return (data >> gpio) & 1;
}

static void myhal_gpio_direction_output(unsigned gpio)
{
    u32 dir = readl(&gpio_regs->dir);
    dir |= (1 << gpio);  // 设置为输出
    writel(dir, &gpio_regs->dir);
}

static void myhal_gpio_direction_input(unsigned gpio)
{
    u32 dir = readl(&gpio_regs->dir);
    dir &= ~(1 << gpio);  // 设置为输入
    writel(dir, &gpio_regs->dir);
}

// 5. 驱动初始化
static int __init myhal_gpio_init(void)
{
    // 映射寄存器
    gpio_regs = ioremap(MYHAL_GPIO_BASE, MYHAL_GPIO_SIZE);
    if (!gpio_regs) {
        printk(KERN_ERR "Failed to map GPIO registers\n");
        return -ENOMEM;
    }

    printk(KERN_INFO "MyHAL GPIO initialized at 0x%p\n", gpio_regs);

    // 测试
    myhal_gpio_direction_output(0);
    myhal_gpio_set_value(0, 1);
    printk(KERN_INFO "GPIO0 set to HIGH\n");

    return 0;
}

static void __exit myhal_gpio_exit(void)
{
    iounmap(gpio_regs);
    printk(KERN_INFO "MyHAL GPIO removed\n");
}

module_init(myhal_gpio_init);
module_exit(myhal_gpio_exit);
MODULE_LICENSE("GPL");

6. 驱动程序学习

6.1 驱动学习路径

1
2
3
4
5
6
7
8
9
第 1 阶段:字符设备驱动
第 2 阶段:平台设备驱动
第 3 阶段:设备树驱动
第 4 阶段:总线驱动(I2C/SPI)
第 5 阶段:复杂驱动(网络/USB)

6.2 实验 1:Hello World 驱动

// drivers/char/hello_drv.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "hello"
#define BUFFER_SIZE 256

static int major_number;
static char message[BUFFER_SIZE];
static int message_size = 0;

// 文件操作结构
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
};

// 打开设备
static int hello_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello: Device opened\n");
    return 0;
}

// 关闭设备
static int hello_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Hello: Device closed\n");
    return 0;
}

// 读取设备
static ssize_t hello_read(struct file *file, char __user *buf,
                         size_t len, loff_t *offset)
{
    int bytes_to_read;

    if (*offset >= message_size)
        return 0;

    bytes_to_read = min((int)(message_size - *offset), (int)len);

    if (copy_to_user(buf, message + *offset, bytes_to_read))
        return -EFAULT;

    *offset += bytes_to_read;

    printk(KERN_INFO "Hello: Sent %d bytes\n", bytes_to_read);
    return bytes_to_read;
}

// 写入设备
static ssize_t hello_write(struct file *file, const char __user *buf,
                          size_t len, loff_t *offset)
{
    int bytes_to_write;

    bytes_to_write = min((int)len, BUFFER_SIZE - 1);

    if (copy_from_user(message, buf, bytes_to_write))
        return -EFAULT;

    message_size = bytes_to_write;
    message[message_size] = '\0';

    printk(KERN_INFO "Hello: Received %d bytes\n", bytes_to_write);
    return len;
}

// 模块初始化
static int __init hello_init(void)
{
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "Hello: Failed to register major number\n");
        return major_number;
    }

    printk(KERN_INFO "Hello: Registered with major number %d\n", major_number);
    return 0;
}

// 模块退出
static void __exit hello_exit(void)
{
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "Hello: Unregistered\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World driver");

测试命令:

# 加载驱动
insmod hello_drv.ko

# 查看主设备号
cat /proc/devices | grep hello

# 创建设备节点
mknod /dev/hello c 240 0

# 写入数据
echo "Hello from user" > /dev/hello

# 读取数据
cat /dev/hello

# 查看内核日志
dmesg | tail

6.3 实验 2:平台设备驱动

// drivers/platform/my_platform_drv.c

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>

// 平台数据
struct my_platform_data {
    int value;
    char name[32];
};

// 探针函数(设备匹配时调用)
static int my_platform_probe(struct platform_device *pdev)
{
    struct my_platform_data *pdata;
    struct device_node *np = pdev->dev.of_node;
    int ret;

    dev_info(&pdev->dev, "Probing platform device\n");

    // 从设备树读取数据
    if (np) {
        ret = of_property_read_u32(np, "my-value", &pdata->value);
        if (ret) {
            dev_err(&pdev->dev, "Failed to read my-value\n");
            return ret;
        }

        ret = of_property_read_string(np, "my-name", pdata->name);
        if (ret) {
            dev_err(&pdev->dev, "Failed to read my-name\n");
            return ret;
        }

        dev_info(&pdev->dev, "Value: %d, Name: %s\n", 
                pdata->value, pdata->name);
    }

    return 0;
}

// 移除函数(设备移除时调用)
static int my_platform_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "Removing platform device\n");
    return 0;
}

// 设备树匹配表
static const struct of_device_id my_platform_of_match[] = {
    { .compatible = "mycompany,my-platform" },
    { }
};
MODULE_DEVICE_TABLE(of, my_platform_of_match);

// 平台驱动结构
static struct platform_driver my_platform_driver = {
    .probe = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my-platform",
        .of_match_table = my_platform_of_match,
    },
};

module_platform_driver(my_platform_driver);
MODULE_LICENSE("GPL");

设备树配置:

// arch/arm/boot/dts/vexpress-v2p-ca9.dts

&amba {
    my_platform@10000000 {
        compatible = "mycompany,my-platform";
        my-value = <42>;
        my-name = "Test Device";
        reg = <0x10000000 0x1000>;
    };
};

6.4 实验 3:I2C 驱动

// drivers/i2c/chips/my_i2c_drv.c

#include <linux/module.h>
#include <linux/i2c.h>

// I2C 设备地址
#define MY_I2C_ADDR     0x50

// 探针函数
static int my_i2c_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    u8 data;
    int ret;

    dev_info(&client->dev, "Probing I2C device at 0x%x\n", client->addr);

    // 读取 I2C 设备
    ret = i2c_smbus_read_byte_data(client, 0x00);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to read from I2C device\n");
        return ret;
    }

    data = ret;
    dev_info(&client->dev, "Read data: 0x%x\n", data);

    // 写入 I2C 设备
    ret = i2c_smbus_write_byte_data(client, 0x00, 0xAA);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to write to I2C device\n");
        return ret;
    }

    dev_info(&client->dev, "Wrote data: 0xAA\n");

    return 0;
}

// 移除函数
static int my_i2c_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "Removing I2C device\n");
    return 0;
}

// I2C 设备 ID 表
static const struct i2c_device_id my_i2c_id[] = {
    { "my-i2c", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_id);

// I2C 驱动结构
static struct i2c_driver my_i2c_driver = {
    .driver = {
        .name = "my-i2c",
    },
    .probe = my_i2c_probe,
    .remove = my_i2c_remove,
    .id_table = my_i2c_id,
};

module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");

7. 实战项目

7.1 项目 1:完整的嵌入式系统

目标: 从零构建可启动的嵌入式系统

# 1. 编译 U-Boot
cd ~/embedded-learning/u-boot/u-boot
make vexpress_ca9x4_defconfig
make CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

# 2. 编译 Linux 内核
cd ~/embedded-learning/linux/linux
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 3. 构建根文件系统(使用 Buildroot)
cd ~/embedded-learning/rootfs/buildroot
make vexpress_defconfig
make menuconfig  # 配置需要的包
make

# 4. 启动系统
cd ~/embedded-learning
./scripts/qemu-start.sh \
    linux/linux/arch/arm/boot/zImage \
    linux/linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
    rootfs/buildroot/output/images/rootfs.ext4

7.2 项目 2:网络驱动调试

目标: 理解网络驱动工作原理

// 1. 添加网络驱动调试打印
// drivers/net/ethernet/smsc/smc91x.c

static netdev_tx_t smc91x_hard_start_xmit(struct sk_buff *pkt,
                                         struct net_device *dev)
{
    struct smc_local *lp = netdev_priv(dev);

    // 添加调试打印
    dev_info(dev, "TX: len=%d, data[0]=%02x\n", pkt->len, pkt->data[0]);

    // 原有代码
    // ...
}

static irqreturn_t smc91x_interrupt(int irq, void *dev_id)
{
    struct net_device *dev = dev_id;

    // 添加调试打印
    dev_info(dev, "IRQ: status=0x%x\n", status);

    // 原有代码
    // ...
}

// 2. 测试网络
# QEMU 启动后
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
ping 10.0.2.2  # ping 宿主机

# 宿主机测试
ping 10.0.2.15

7.3 项目 3:文件系统实验

目标: 理解文件系统挂载和 VFS

# 1. 创建不同文件系统镜像
# EXT4
dd if=/dev/zero of=ext4.img bs=1M count=64
mkfs.ext4 ext4.img

# FAT32
dd if=/dev/zero of=fat.img bs=1M count=64
mkfs.vfat fat.img

# 2. QEMU 启动时挂载
qemu-system-arm \
    ... \
    -drive file=ext4.img,if=sd,format=raw \
    -drive file=fat.img,if=sd,format=raw

# 3. Linux 中挂载测试
# 启动后
mount /dev/mmcblk0p1 /mnt/ext4
mount /dev/mmcblk0p2 /mnt/fat

# 4. 测试读写
echo "Hello EXT4" > /mnt/ext4/test.txt
echo "Hello FAT" > /mnt/fat/test.txt

8. 调试技巧

8.1 GDB 调试

# 1. 启动 QEMU 带 GDB 服务器
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel u-boot \
    -nographic \
    -s -S

# 2. 启动 GDB
arm-linux-gnueabihf-gdb u-boot

# 3. GDB 命令
(gdb) target remote :1234          # 连接 QEMU
(gdb) continue                     # 继续执行
(gdb) stepi                        # 单步执行(汇编)
(gdb) next                         # 单步执行(C 语言)
(gdb) break main                   # 设置断点
(gdb) break *0x60000000            # 地址断点
(gdb) info registers               # 查看寄存器
(gdb) x/10i $pc                    # 查看指令
(gdb) x/10wx 0x60000000            # 查看内存
(gdb) backtrace                    # 查看调用栈

8.2 QEMU 日志调试

# 启用详细日志
qemu-system-arm \
    ... \
    -d guest_errors,unimp,int,cpu_reset \
    -D qemu.log

# 日志级别
# guest_errors  : 客户机错误
# unimp         : 未实现功能
# int           : 中断
# cpu_reset     : CPU 复位
# exec          : 指令执行
# io            : I/O 操作

# 分析日志
grep -i "error" qemu.log
grep "interrupt" qemu.log

8.3 内核调试

# 1. 启用内核调试选项
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_PRINTK=y
CONFIG_EARLY_PRINTK=y

# 2. 内核启动参数
# 添加调试参数
console=ttyAMA0,115200 earlyprintk debug ignore_loglevel

# 3. 动态调试
# 启用特定模块调试
echo "file drivers/net/* +p" > /sys/kernel/debug/dynamic_debug/control

# 4. 查看内核信息
cat /proc/version
cat /proc/cpuinfo
cat /proc/meminfo
lsmod
dmesg

9. 学习路线建议

9.1 3 个月学习计划

周数 主题 目标 实验
第 1-2 周 QEMU 基础 安装配置,启动第一个系统 Hello World
第 3-4 周 U-Boot 基础 编译、启动、命令 自定义命令
第 5-6 周 U-Boot 驱动 串口、网络、MMC 驱动 添加驱动
第 7-8 周 Linux 基础 编译、启动、设备树 修改设备树
第 9-10 周 Linux 驱动 字符设备、平台设备 Hello 驱动
第 11-12 周 综合项目 完整系统构建 网络文件系统

9.2 每日学习流程

早上 (2 小时):
- 阅读相关文档/代码
- 理解概念和原理

下午 (2 小时):
- 动手实验
- 调试和测试

晚上 (1 小时):
- 总结笔记
- 记录问题和解决方案

9.3 推荐学习资源

资源 类型 链接
U-Boot 官方文档 文档 https://u-boot.readthedocs.io/
Linux 内核文档 文档 https://www.kernel.org/doc/
Bootlin 培训 视频/文档 https://bootlin.com/doc/
Elixir 代码浏览 代码 https://elixir.bootlin.com/
QEMU 官方文档 文档 https://www.qemu.org/docs/

📚 总结

关键要点

  1. QEMU 是学习嵌入式的利器 - 免费、强大、可调试
  2. 从简单开始 - Hello World 驱动 → 平台驱动 → 复杂驱动
  3. 理论与实践结合 - 阅读代码 + 动手实验
  4. 善用调试工具 - GDB、日志、打印
  5. 坚持和积累 - 每天进步,3 个月见成效

下一步行动

  • 安装 QEMU 和相关工具
  • 编译并启动 U-Boot
  • 编译并启动 Linux 内核
  • 编写第一个驱动程序
  • 完成综合项目

创建时间:2026-03-13

愿你在嵌入式学习的路上不断进步!🚀