1.1 设备驱动的作用
计算机系统可以抽象为三层:硬件 、内核(含驱动) 、应用程序。硬件不懂"读写文件",应用程序也不懂"寄存器位域",中间必须有人翻译。设备驱动就是这段翻译层:
- 对上:++向内核其他子系统或用户空间提供统一的抽象接口++(字符设备文件、块设备、网络接口、sysfs 属性、input 事件......)。
- 对下:++把抽象操作翻译成具体硬件动作++(读写寄存器、搬运 DMA 缓冲、收发总线报文、处理中断)。
驱动不是"给某个芯片写一段代码"这么简单。现代 Linux 驱动真正的职责是:
- 发现设备:设备树 / ACPI / 总线枚举 / 软件节点告诉内核"这里有个设备"。
- 申请资源:内存映射寄存器、中断号、时钟、复位线、GPIO、regulator、DMA 通道。
- 注册到子系统:字符设备、块设备、网络设备、input、RTC、IIO、DRM......
- 处理事件:中断、工作队列、定时器、NAPI 轮询。
- 生命周期管理:probe/remove、系统睡眠/唤醒、runtime PM、热插拔、错误恢复。
驱动开发的核心矛盾:硬件千差万别,内核接口必须统一;驱动就是把这个矛盾收敛到一处。
1.2 无操作系统时的设备驱动
在裸机(或裸机循环 + 中断)环境下,驱动通常长这样:
cpp
/* 裸机 LED 闪烁:无任何抽象,直接操作寄存器 */
#define GPIO_BASE 0x0209C000UL
#define GPIO_DIR (*(volatile unsigned long *)(GPIO_BASE + 0x04))
#define GPIO_DATA (*(volatile unsigned long *)(GPIO_BASE + 0x00))
static void led_on(void) { GPIO_DATA |= (1u << 3); }
static void led_off(void) { GPIO_DATA &= ~(1u << 3); }
int main(void)
{
GPIO_DIR |= (1u << 3); /* 配置为输出 */
for (;;) {
led_on();
delay_ms(500);
led_off();
delay_ms(500);
}
}
裸机驱动的特点,也是它的局限:
- 无并发模型:只有一个执行流(主循环 + 中断),加锁问题靠"关中断"解决。
- 无内存保护:一个野指针可以覆盖任何地址,包括驱动自己。
- 无统一接口:每换一个外设,应用程序就要改代码。
- 资源独占:一个外设只能被一段代码使用。
- 无设备模型:设备是否存在、是否就绪,全靠人写死。
理解裸机驱动的价值在于:++它让你看清"驱动最终要做什么"++ 。后面所有 Linux 驱动框架,本质都是"在裸机操作外面套上内核提供的++并发、资源、生命周期和抽象层++"。
1.3 有操作系统时的设备驱动
引入操作系统后,驱动的位置发生了变化:

操作系统为驱动提供的东西:
| 能力 | 提供的机制 | 对应源码位置 |
|---|---|---|
| 并发控制 | 自旋锁、mutex、RCU、原子操作、完成量 | kernel/locking/、include/linux/spinlock.h、include/linux/rcupdate.h |
| 内存管理 | kmalloc/vmalloc/页分配器/ioremap |
mm/、include/linux/slab.h |
| 中断管理 | request_threaded_irq、IRQ domain、软中断 |
kernel/irq/、include/linux/interrupt.h |
| 时间管理 | 定时器、hrtimer、延时、jiffies | kernel/time/、include/linux/timer.h |
| 设备模型 | struct device/driver/bus/class |
drivers/base/ |
| 资源管理 | devm_* 托管分配 |
drivers/base/devres.c |
| 电源管理 | dev_pm_ops、runtime PM、genpd |
drivers/base/power/ |
| 调试 | printk、ftrace、debugfs、KASAN | kernel/trace/、mm/kasan/ |
同时,操作系统也给驱动加了约束:不能睡的地方不能睡 、用户指针必须校验、错误路径必须回滚 、中断上下文不能睡眠。
1.4 Linux 设备驱动
1.4.1 设备的分类及特点
Linux 把设备分成三大类,这个分类从 2.6 一直延续到 7.2.5:
| 类别 | 抽象接口 | 访问单位 | 典型例子 | 本手册章节 |
|---|---|---|---|---|
| 字符设备 | /dev/xxx,file_operations |
字节流 | 串口、LED、按键、EEPROM、RTC | 第 6 章 |
| 块设备 | /dev/sdX、/dev/ram0,block_device_operations |
扇区(512B 或 4K) | 磁盘、SSD、U 盘、ramdisk | 第 13 章 |
| 网络设备 | eth0/wlan0,net_device_ops |
数据包 | 网卡、Wi-Fi、虚拟网卡 | 第 14 章 |
补充说明(7.2.5 内核):
- 很多"设备"其实同时属于多个子系统。例如 USB 无线网卡:USB 设备驱动 + 网络设备驱动;一块带 MMC 控制器的 SoC:platform 驱动 + MMC 子系统 + 块设备层。
- 还有一批"非字符/非块/非网络"的子系统,它们最终也大多落到字符设备或 sysfs 上:input(
/dev/input/eventX)、IIO(/dev/iio:deviceX)、DRM(/dev/dri/cardX)、V4L2(/dev/videoX)、ALSA(/dev/snd/*)。 - ++现代 Linux 更强调用现成的子系统而不是自己造字符设备++ 。++造一个私有 ioctl 字符设备是最容易出错的方案;能用 input/IIO/RTC/GPIO/leds/hwmon 就优先用它们++。
1.4.2 驱动与整个软硬件系统的关系

关键认识:++驱动永远站在"子系统接口"和"硬件接口"之间++。写驱动之前先问两个问题:
- 这个设备应该挂到哪个子系统?(决定你要实现哪套 ops)
- 这个设备的硬件资源从哪来?(决定你要用设备树、ACPI、PCI/USB 枚举还是软件节点)
1.4.3 驱动开发的重点与难点
原书列出的重点难点,在 7.2.5 上依然成立,而且被强化了:
| 难点 | 4.0 时代的表现 | 7.2.5 的表现 |
|---|---|---|
| 并发与竞态 | 自旋锁/mutex/RCU | 增加了 KCSAN、lockdep 更严格的检查,原子上下文规则更明确 |
| 中断处理 | 上半部/下半部、tasklet | 主推 threaded IRQ;tasklet 只用于维护旧代码 |
| 内存与 DMA | dma_alloc_coherent/cache 一致性 |
加上 IOMMU、DMA API debug、dma_map_sgtable |
| 资源生命周期 | goto 错误处理 |
devm_* 托管 + cleanup.h 的 __free() 自动清理 |
| 硬件描述 | ARM 板级文件 + 部分设备树 | 设备树/ACPI 全面取代板级文件; overlay 动态加载 |
| 电源管理 | 系统睡眠为主 | runtime PM + genpd + OPP + interconnect 组合拳 |
| 调试 | printk + GDB | + ftrace/perf/eBPF/KASAN/KCSAN/lockdep/pstore |
| 上游协作 | 邮件列表补丁 | 邮件列表补丁 + b4 + checkpatch + 更严格的 API 约定 |
1.5 开发环境构建
1.5.1 PC 上的 Linux 环境
推荐 Ubuntu/Debian x86_64,安装工具链与实验依赖:
bash
sudo apt update
sudo apt install -y \
build-essential bc bison flex libssl-dev libelf-dev libncurses-dev \
dwarves cpio kmod \
gcc-aarch64-linux-gnu \
qemu-system-x86 qemu-system-arm qemu-utils \
device-tree-compiler \
busybox-static e2fsprogs dosfstools \
i2c-tools spi-tools gpiod libgpiod-dev \
python3 perl tar xz-utils
逐项说明:
bison flex:内核 Kconfig/词法分析器生成需要(7.2.5 构建时缺了会直接报错)。libelf-dev dwarves(pahole):CONFIG_DEBUG_INFO_BTF需要;没有 pahole 时 BTF 会编译失败。libssl-dev:模块签名、sign-file需要。qemu-system-x86:x86_64 实验主力;qemu-system-arm提供qemu-system-aarch64。device-tree-compiler:提供dtc/fdtget/fdtdump,第 18 章必备。busybox-static:制作 initramfs 的 shell 与基本命令。i2c-tools/gpiod:用户空间验证 I²C 与 GPIO 设备。
验证安装:
bash
gcc --version
make --version
qemu-system-x86_64 --version
qemu-system-aarch64 --version
dtc --version
busybox | head -1
1.5.2 QEMU 实验平台
本手册的全部实验都设计为不需要真实开发板。QEMU 提供三类能力:
- 虚拟平台 :
qemu-system-x86_64(PC 风格,含 PCI/virtio)与qemu-system-aarch64 -machine virt(嵌入式风格,含设备树)。 - 虚拟外设:virtio-blk、virtio-net、virtio-console、virtio-input、virtio-i2c、virtio-spi、virtio-gpio、USB xHCI/存储/键盘等。
- 软件模拟外设 :内核自带的
gpio-sim、i2c-stub、spi-loopback-test、dummy_hcd、brd、null_blk、gpio-virtuser,完全不依赖 QEMU 是否支持该设备型号。
查看当前 QEMU 支持哪些设备(这是最可靠的判断方式,不要凭记忆猜设备名):
bash
qemu-system-x86_64 -device help | grep -Ei 'virtio|i2c|spi|gpio|usb|xhci'
qemu-system-aarch64 -machine virt -device help | grep -Ei 'virtio|i2c|spi|gpio'
完整的环境搭建、initramfs 制作、9p 共享、GDB 调试流程见 《22-QEMU实验总览》。本章只需要确认一件事:你能编译内核并进入 QEMU 的 shell。
7.2.5 的重要变化(阅读源码前必读) :本版本的设备模型头文件已经拆分,不能再按旧书去
include/linux/device.h里找全部定义:
内容 7.2.5 路径 struct class、class_create()、class_register()include/linux/device/class.hstruct bus_typeinclude/linux/device/bus.hstruct device_driver、module_driver()include/linux/device/driver.hstruct of_device_id、platform_device_id、i2c_device_idinclude/linux/device-id/*.hstruct device、device_create()、devm_*()include/linux/device.h也就是说
include/linux/device-id/of.h里才是struct of_device_id的定义,include/linux/device/driver.h里才有of_match_table字段。旧书(乃至很多网上博客)里写的
include/linux/mod_devicetable.h现在是"汇总转发头 ",只做#include "device-id/*.h"。
编辑代码的硬性要求:内核代码必须用 Tab 缩进(8 列),必须能通过:
scripts/checkpatch.pl --no-tree --strict -f your_driver.c
1.6 设备驱动 Hello World:LED 驱动
1.6.1 无操作系统时的 LED 驱动
见 1.2 节的裸机代码。要点:直接访问物理地址 + 忙等延时,没有任何资源申请与释放的概念。
1.6.2 Linux 下的 LED 驱动
在 Linux 里"点亮一个 LED"有三条路,从"最贴近硬件"到"最贴近子系统":
路径 A:裸字符设备 + 寄存器操作(教学用,理解 ioctl 与并发)
不申请 GPIO 子系统,直接把一个物理地址 ioremap 进来,通过 ioctl 控制。第 6 章会完整实现。它适合理解字符设备框架,但不适合产品代码。
路径 B:GPIO 子系统 + 自写 platform 驱动(推荐理解驱动模型)
cpp
/* hello-led.c ------ 基于 GPIO 子系统的最小 LED 平台驱动(Linux 7.2.5) */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/mod_devicetable.h>
struct hello_led {
struct gpio_desc *led;
};
static int hello_led_probe(struct platform_device *pdev)
{
struct hello_led *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 从设备树 / 软件节点 / gpio-sim 提供的 "led-gpios" 属性取 GPIO */
priv->led = devm_gpiod_get(&pdev->dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(priv->led))
return dev_err_probe(&pdev->dev, PTR_ERR(priv->led),
"failed to get led gpio\n");
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "hello led probed, gpio = %d\n",
desc_to_gpio(priv->led));
return 0;
}
static void hello_led_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "hello led removed\n");
}
static const struct of_device_id hello_led_of_match[] = {
{ .compatible = "hello,led" },
{ }
};
MODULE_DEVICE_TABLE(of, hello_led_of_match);
static struct platform_driver hello_led_driver = {
.probe = hello_led_probe,
.remove = hello_led_remove,
.driver = {
.name = "hello-led",
.of_match_table = hello_led_of_match,
},
};
module_platform_driver(hello_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin");
MODULE_DESCRIPTION("Minimal LED platform driver for QEMU experiments");
注意 7.2.5 的签名细节(旧书里不是这样):
struct platform_driver的.remove返回 void ,不是int。.probe参数是struct platform_device *。- 设备匹配优先用
.of_match_table(设备树)或.id_table(platform_device_id),而不是靠名字字符串。 - 使用
devm_gpiod_get()+GPIOD_OUT_LOW,不要 再用旧书的gpio_request()/gpio_direction_output()/gpio_set_value()(这些是已废弃的整数 GPIO 接口,新代码禁止使用)。 dev_err_probe()是现代 probe 函数报告错误的推荐方式:它会把-EPROBE_DEFER记录为 debug 级别而不刷屏。
bash
# Makefile
obj-m += hello-led.o
KDIR ?= ~/study/build/linux-7.2.5_out
PWD := $(CURDIR)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
路径 C:直接用内核现成的 LED 子系统
内核已有 drivers/leds/ 子系统:设备树写一个 leds { led { gpios = <...>; } } 节点,leds-gpio 驱动就会创建 /sys/class/leds/<name>/brightness。产品代码几乎都走这条路。
实验 1-A:用 gpio-sim + hello-led 点亮"虚拟 LED"(x86_64 QEMU)
gpio-sim 是内核自带的 configfs GPIO 模拟器(Documentation/admin-guide/gpio/gpio-sim.rst),在 QEMU 里创建虚拟 GPIO 芯片,然后把我们的驱动绑上去。
内核配置(make menuconfig 或直接改 .config):
CONFIG_CONFIGFS_FS=y
CONFIG_GPIO_SIM=m # 注意:gpio-sim 只能是模块(依赖 CONFIGFS_FS 与 IRQ_SIM)
CONFIG_GPIOLIB=y
CONFIG_OF=y
CONFIG_OF_OVERLAY=y
在 guest 中:
mount -t configfs none /sys/kernel/config
cd /sys/kernel/config/gpio-sim
mkdir -p gpio-device0/bank0
echo 8 > gpio-device0/bank0/num_lines
echo 1 > gpio-device0/live # 实例化虚拟芯片
ls /sys/class/gpio/ # 会看到 gpiochipN
ls /dev/ # 会看到 gpiochipN 字符设备
此时你已经有了"虚拟 LED 引脚"。把上面的 hello-led.c 编译成模块,用 gpio-sim 提供的 line 名字或设备树 overlay 绑定。最简做法是用 gpio-virtuser 或直接写一个 platform_device 的软件节点(见第 12 章)。
实验 1-B:用 QEMU 真实 virtio 设备做 Hello World
如果只想先感受"设备出现在内核里",用 virtio-console 最快:
qemu-system-x86_64 -m 2G -smp 2 \
-kernel "$KBUILD/arch/x86/boot/bzImage" \
-initrd "$INITRD" \
-append 'console=ttyS0' -nographic \
-device virtio-serial-pci
guest 中:
dmesg | grep -i virtio
ls /sys/bus/virtio/devices/
ls /sys/bus/virtio/drivers/
你会看到 virtio-pci、virtio_console 驱动与设备在 sysfs 中的绑定关系------这就是 7.2.5 的设备模型在说话。
观察点
dmesg中能看到hello led probed,说明 probe 被调用、GPIO 申请成功。ls /sys/bus/platform/drivers/hello-led/下出现指向设备的符号链接,说明驱动与设备已绑定。rmmod hello_led后再insmod,probe/remove 会再次成对出现。- 故意把
.compatible写成不匹配的值,观察驱动不 probe(说明匹配失败)。
常见错误
| 现象 | 原因 | 处理 |
|---|---|---|
insmod: ERROR: could not insert module: Invalid module format |
模块与内核版本/配置不一致 | 模块必须用同一个 O= 输出目录编译,见第 4 章 |
probe 不执行,dmesg 无任何输出 |
设备树 compatible 与 .of_match_table 不匹配 |
用 cat /sys/firmware/devicetree/base/.../compatible 核对 |
-EPROBE_DEFER 反复出现 |
GPIO/时钟/regulator provider 尚未就绪 | 正常现象;用 dev_err_probe() 抑制噪音 |
| 卸载模块时 oops | remove 里释放了 devm_ 已托管的资源 |
托管资源不要手工释放 |
QEMU 中看不到 gpiochip |
CONFIG_GPIO_SIM 未开或没 mkdir+live=1 |
回到实验 1-A 步骤 |
小结
- 驱动的本质是"抽象接口 ↔ 硬件动作"的翻译层,操作系统为其提供并发、内存、中断、时间、设备模型和资源管理。
- 设备分为字符/块/网络三大类,但现代驱动应优先复用成熟子系统。
- 7.2.5 的源码结构与 4.0 已有明显差异(设备模型头文件拆分、device-id 目录),必须按当前源码阅读。
- 无开发板也能学驱动:QEMU 虚拟设备 + 内核自带软件模拟外设,覆盖了本手册绝大多数实验。