01-设备驱动概述与开发环境构建

1.1 设备驱动的作用

计算机系统可以抽象为三层:硬件 、内核(含驱动) 、应用程序。硬件不懂"读写文件",应用程序也不懂"寄存器位域",中间必须有人翻译。设备驱动就是这段翻译层:

  • 对上:++向内核其他子系统或用户空间提供统一的抽象接口++(字符设备文件、块设备、网络接口、sysfs 属性、input 事件......)。
  • 对下:++把抽象操作翻译成具体硬件动作++(读写寄存器、搬运 DMA 缓冲、收发总线报文、处理中断)。

驱动不是"给某个芯片写一段代码"这么简单。现代 Linux 驱动真正的职责是:

  1. 发现设备:设备树 / ACPI / 总线枚举 / 软件节点告诉内核"这里有个设备"。
  2. 申请资源:内存映射寄存器、中断号、时钟、复位线、GPIO、regulator、DMA 通道。
  3. 注册到子系统:字符设备、块设备、网络设备、input、RTC、IIO、DRM......
  4. 处理事件:中断、工作队列、定时器、NAPI 轮询。
  5. 生命周期管理: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 驱动与整个软硬件系统的关系

关键认识:++驱动永远站在"子系统接口"和"硬件接口"之间++。写驱动之前先问两个问题:

  1. 这个设备应该挂到哪个子系统?(决定你要实现哪套 ops)
  2. 这个设备的硬件资源从哪来?(决定你要用设备树、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 提供三类能力:

  1. 虚拟平台 :qemu-system-x86_64(PC 风格,含 PCI/virtio)与 qemu-system-aarch64 -machine virt(嵌入式风格,含设备树)。
  2. 虚拟外设:virtio-blk、virtio-net、virtio-console、virtio-input、virtio-i2c、virtio-spi、virtio-gpio、USB xHCI/存储/键盘等。
  3. 软件模拟外设 :内核自带的 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.h
struct bus_type include/linux/device/bus.h
struct device_driver、module_driver() include/linux/device/driver.h
struct of_device_id、platform_device_id、i2c_device_id include/linux/device-id/*.h
struct 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 虚拟设备 + 内核自带软件模拟外设,覆盖了本手册绝大多数实验。
相关推荐
Snasph12 小时前
05-Linux 文件系统与设备文件
linux 驱动学习
Snasph13 小时前
04-Linux 内核模块
linux 驱动学习