Linux 内核驱动开发详解 | 杨超
← 返回博客列表

一、驱动开发导论

驱动程序(Device Driver)是操作系统内核中直接与硬件交互的代码模块。它向上为应用层提供统一的软件接口(如 open/read/write/ioctl),向下通过寄存器、总线协议与硬件通信。驱动开发是嵌入式 Linux 的核心技能。

操作系统分层架构:驱动的位置 应用层(Applications) ls / open / cat / python ... C 库 / 系统调用接口(glibc / syscall) open() read() write() ioctl() mmap() Linux 内核(Kernel) 进程管理 内存管理 文件系统 网络协议 → 设备驱动 ← 硬件抽象层(HAL)/ 总线控制器 GPIO / I2C / SPI UART / USB / PCI DRAM / NAND DMA Controller 硬件(Hardware) CPU(ARM/RISC-V/x86) 外设芯片(EEPROM/ADC/DAC) 板级电路(LED/按键/传感器)
图 1-1:操作系统分层架构——驱动位于内核与硬件之间

1.1 为什么需要驱动

无驱动 有驱动
每个应用都要直接操作硬件寄存器 驱动封装硬件细节,应用只需调用 open/read/write
硬件变化时所有应用都要改 只改驱动,应用不变
无并发保护,多进程同时操作硬件会崩溃 内核提供互斥锁、信号量保护
无法统一接口(每个硬件 API 不同) 遵循 Linux VFS 统一接口

1.2 驱动的三大类型

Linux 内核将驱动分为三大类,每类有不同的数据结构、操作接口和注册 API:

Linux 驱动三大类型对比 字符设备(Character) 核心结构体 struct cdev file_operations 数据特征 • 字节流 / 顺序读写 • 无缓存层(直接 I/O) • 支持 seek 随机访问 注册 API alloc_chrdev_region() cdev_add() 典型设备 UART / I2C / SPI / GPIO 块设备(Block) 核心结构体 struct gendisk request_queue / bio 数据特征 • 固定块(512B / 4KB) • 有 Page Cache 缓存层 • I/O 调度(CFQ/Deadline/NONE) 注册 API alloc_disk() add_disk() 典型设备 硬盘 / SSD / SD卡 / eMMC 网络设备(Network) 核心结构体 struct net_device sk_buff(socket buffer) 数据特征 • 面向数据包(无 /dev 节点) • 通过 socket 接口访问 • 异步收发 + NAPI 轮询 注册 API alloc_netdev() register_netdev() 典型设备 网卡 / WiFi / 4G-5G / loopback
图 1-2:Linux 驱动三大类型对比(字符 / 块 / 网络)

二、Linux 内核驱动模型

Linux 内核通过 设备模型(Device Model)统一管理所有硬件设备。核心概念是 kobject(内核对象)——每个设备、驱动、总线、类都是一个 kobject,形成一棵层次化的设备树。这棵树映射到用户空间就是 /sys 目录。

2.1 四大核心概念

概念内核结构体/sys 路径含义
Bus(总线)bus_type/sys/bus设备连接的总线类型(platform/i2c/spi/pcie)
Device(设备)device/sys/devices具体的硬件实例(GPIO 控制器、传感器芯片)
Driver(驱动)device_driver/sys/drivers操作设备的代码(probe/remove)
Class(类)class/sys/class按功能分类(input/net/block),把设备聚合

2.2 总线-设备-驱动绑定过程

// ① 设备注册(内核启动或设备树解析时自动触发)
platform_device_register(&my_pdev);
// → 挂到 /sys/bus/platform/devices/my_device/

// ② 驱动注册(modprobe 时调用)
platform_driver_register(&my_pdriver);
// → 挂到 /sys/bus/platform/drivers/my_driver/

// ③ Bus 自动匹配(platform_match)
// 匹配规则:of_match_table (设备树 compatible) → acpi_match → id_table

// ④ 匹配成功 → 调用 driver.probe()
//   probe 中申请资源、注册字符设备、导出 sysfs 属性…
// ⑤ 匹配失败 → 设备留在总线上等待,驱动被卸载

# 用户空间可以看到绑定结果:
# ls /sys/bus/platform/devices/ | grep my_device
# ls /sys/bus/platform/devices/my_device/driver  → 若有输出说明已绑定
# 手动解绑:echo my_device > /sys/bus/platform/drivers/my_driver/unbind
# 手动绑定:echo my_device > /sys/bus/platform/drivers/my_driver/bind

2.3 常用设备类型与对应子系统

子系统总线类型设备例子注册函数
PlatformplatformGPIO 控制器、I2C 控制器、片上外设platform_driver_register()
I2Ci2c温度传感器、EEPROM、ADCi2c_driver_register()
SPIspiOLED 屏幕、Flash、高速 ADCspi_driver_register()
USBusbU盘、鼠标、USB 转串口usb_driver_register()
PCIepci网卡、显卡、FPGA 加速卡pci_register_driver()
platform_driver vs 其他 driver
platform 是最基础的总线——SoC 上所有不插在 PCI/USB 总线上的外设(GPIO、I2C/SPI 控制器、UART、Timer)都走 platform。
几乎所有 SoC 外设驱动都是 platform_driver。
Linux 设备模型树(/sys 目录结构) /sys /sys/bus i2c/spi/platform /sys/devices 硬件拓扑 /sys/drivers 已注册驱动 /sys/class dev/chr/input /sys/module 已加载模块 总线-设备-驱动绑定流程 ① 设备注册 → bus 的 device list ② 驱动注册 → bus 的 driver list ③ bus 匹配 → 调用 driver.probe() ④ 匹配成功 → 设备与驱动绑定 三大核心 API device_register() 创建设备节点 driver_register() 注册驱动 bus_match() 总线匹配 class_create() 创建设备类
图 2-1:Linux 设备模型树与总线-设备-驱动绑定流程

三、内核模块基础

Linux 内核模块(Loadable Kernel Module, LKM)是可以动态加载/卸载的驱动代码。相比直接编译进内核,模块有以下优势:

内核模块加载流程图 modprobe 高级加载工具 查找依赖 modinfo / depmod insmod 基础加载工具 内核 vmalloc 分配模块内存 module_init() 执行初始化 模块管理命令速查 加载 modprobe xxx / insmod xxx.ko 卸载 rmmod xxx / modprobe -r xxx 查看 lsmod / cat /proc/modules 信息 modinfo xxx.ko 关键:insmod 不解析依赖,modprobe 会自动加载所有依赖模块 → 开发期推荐用 modprobe 卸载前先 close 设备文件,否则会报 "Module xxx is in use"
图 3-1:内核模块加载流程与管理命令速查
// hello.c —— 最简单的内核模块
#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, Linux Driver! module loaded\n");
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Hello module unloaded\n");
}

module_init(hello_init);    // insmod 时调用
module_exit(hello_exit);    // rmmod 时调用

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello module");

# 编译命令(Makefile)
obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all: make -C $(KDIR) M=$(PWD) modules

# 加载/卸载
sudo insmod hello.ko
dmesg | tail -5          # 查看 printk 输出
sudo rmmod hello

3.1 模块参数与符号导出

// 带参数的模块
#include <linux/moduleparam.h>

static int rate = 9600;
module_param(rate, int, 0644);
MODULE_PARM_DESC(rate, "Baud rate (default 9600)");

// insmod hello.ko rate=115200

// 导出符号供其他模块使用
void my_driver_func(int val) { ... }
EXPORT_SYMBOL(my_driver_func);     // 允许 GPL 模块使用
EXPORT_SYMBOL_GPL(my_driver_func);  仅允许 GPL 模块使用

四、字符设备驱动

字符设备驱动是最常见的驱动类型,遵循 Linux VFS(虚拟文件系统)接口。应用空间通过 open/read/write/ioctl 操作设备文件(/dev/xxx),内核通过 file_operations 结构体将调用分发到驱动实现。

字符设备驱动架构 应用程序 open("/dev/led") VFS 文件系统分发 cdev 层 字符设备注册 file_operations .open .release .read .write .ioctl .llseek .mmap .poll .unlocked_ioctl 驱动实现 static int led_open(...) 硬件 GPIO / I2C / SPI 主设备号(major)标识驱动类型,次设备号(minor)标识具体实例 mknod /dev/led c 200 0 → 字符设备文件 register_chrdev_region() 注册主设备号
图 4-1:字符设备驱动架构(应用→VFS→cdev→驱动→硬件)
// led_chardev.c —— 一个最简字符设备驱动
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "led_chrdev"
#define BUF_SIZE 1024

static int major;
static struct cdev led_cdev;
static struct device *led_device;
static struct class *led_class;
static char kernel_buf[BUF_SIZE];
static int open_count;

static int led_open(struct inode *inode, struct file *filp) {
    open_count++;
    printk(KERN_INFO "LED device opened (count=%d)\n", open_count);
    return 0;
}

static int led_release(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "LED device closed\n");
    return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) {
    int bytes_to_copy = min(count, (size_t)(BUF_SIZE - *offset));
    if (bytes_to_copy == 0) return 0;
    copy_to_user(buf, kernel_buf + *offset, bytes_to_copy);
    *offset += bytes_to_copy;
    return bytes_to_copy;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset) {
    int bytes_to_copy = min(count, (size_t)(BUF_SIZE - *offset));
    if (bytes_to_copy == 0) return -EFAULT;
    copy_from_user(kernel_buf + *offset, buf, bytes_to_copy);
    *offset += bytes_to_copy;
    return bytes_to_copy;
}

static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
    case 0x1001:
        printk(KERN_INFO "LED ON, value=%lu\n", arg);
        break;
    case 0x1002:
        printk(KERN_INFO "LED OFF, value=%lu\n", arg);
        break;
    default: return -ENOTTY;
    }
    return 0;
}

static const struct file_operations led_fops = {
    .owner   = THIS_MODULE,
    .open    = led_open,
    .release = led_release,
    .read    = led_read,
    .write   = led_write,
    .unlocked_ioctl = led_ioctl,
};

static int __init led_init(void) {
    dev_t dev_num = MKDEV(0, 0);
    major = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);

    cdev_init(&led_cdev, &led_fops);
    led_cdev.owner = THIS_MODULE;
    cdev_add(&led_cdev, dev_num, 1);

    led_class = class_create(DEVICE_NAME);
    led_device = device_create(led_class, NULL, dev_num, NULL, DEVICE_NAME);

    printk(KERN_INFO "LED char device registered, major=%d\n", major);
    return 0;
}

static void __exit led_exit(void) {
    dev_t dev_num = MKDEV(major, 0);
    device_destroy(led_class, dev_num);
    class_destroy(led_class);
    cdev_del(&led_cdev);
    unregister_chrdev_region(dev_num, 1);
    printk(KERN_INFO "LED char device unregistered\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
用户空间测试
• echo "hello" > /dev/led_chrdev 写入
• cat /dev/led_chrdev 读取
• dmesg | tail 查看 printk
• ls -la /dev/led_chrdev 查看设备文件

五、平台驱动

平台驱动(Platform Driver)是 Linux 为 SoC 片上外设设计的统一驱动框架。SoC 上的 GPIO、I2C、SPI 控制器、UART 等都是"平台设备"——它们不插在 PCI/USB 总线上,而是直接通过地址映射连接 CPU。

平台驱动 probe/remove 生命周期 platform_driver_register 驱动注册到总线 platform_match compatible 匹配 platform_get_resource 获取 IO 内存/IRQ probe() 成功 硬件初始化完成 platform_driver 结构体 .name = "my_driver" .probe = my_probe .remove = my_remove .shutdown = my_shutdown .of_match_table = my_of_match .platform_driver.driver.owner = THIS_MODULE remove 生命周期(模块卸载时触发) ① release_mem_region() 释放 IO 内存 ② iounmap() 解除映射 ③ free_irq() 释放中断 ④ platform_driver_unregister() 注销驱动 remove 中必须释放 probe 中申请的所有资源,否则内核泄漏!
图 5-1:平台驱动 probe/remove 生命周期
static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static int my_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;

    // 1. 获取 IO 内存资源
    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) return -ENODEV;

    // 2. 请求并映射 IO 内存
    my_base = devm_ioremap_resource(dev, res);
    if (IS_ERR(my_base)) return PTR_ERR(my_base);

    // 3. 获取 IRQ
    my_irq = platform_get_irq(pdev, 0);
    if (my_irq < 0) return my_irq;

    // 4. 注册中断处理
    ret = devm_request_irq(dev, my_irq, my_irq_handler, 0, "my-irq", my_dev);

    // 5. 创建设备类与节点
    my_class = class_create(DEVICE_NAME);
    my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);

    return 0;
}

static int my_remove(struct platform_device *pdev) {
    device_destroy(my_class, MKDEV(major, 0));
    class_destroy(my_class);
    // devm 资源自动释放,无需手动 iounmap/free_irq
    return 0;
}

static struct platform_driver my_driver = {
    .probe    = my_probe,
    .remove   = my_remove,
    .driver   = {
        .name = "my_driver",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);  // 自动生成 init/exit

六、设备树绑定

设备树(Device Tree, DT)是 Linux 内核描述硬件的标准方式。驱动不需要硬编码硬件地址/中断号,而是通过设备树获取。设备树用 DTS(文本)编写,经 DTC 编译为 DTB(二进制),内核启动时解析。

设备树编译与驱动匹配流程 xxx.dts 设备树源文件 人类可读 DTS 格式 dtc 编译器 dtc -I dts -O dtb -o xxx.dtb xxx.dts xxx.dtb 设备树 blob 二进制格式 内核解析 early_init_dt_scan 构建 device tree 匹配 compatible → probe() DTS 示例:一个 I2C 温度传感器 &amp;i2c@7e800000 { temperature-sensor@48 { compatible = "ti,tmp102"; // 关键!驱动匹配字符串 reg = &lt;0x48&gt;; // I2C 设备地址 interrupt-parent = &amp;&lt;&amp;gpio&gt;; // 中断控制器 interrupts = &lt;23 GPIO_ACTIVE_LOW&gt;; }; };
图 6-1:设备树编译与驱动匹配流程
// 驱动中通过 compatible 匹配设备树节点
static const struct i2c_device_id tmp102_id[] = {
    { "tmp102", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);

static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "ti,tmp102" },
    { }
};

static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    printk(KERN_INFO "TMP102 probed at addr 0x%02x\n", client->addr);
    // 从设备树读取属性
    if (client->dev.of_node) {
        int irq = of_irq_get(client->dev.of_node, 0);
        dev_info(&client->dev, "IRQ = %d\n", irq);
    }
    return 0;
}

static struct i2c_driver tmp102_driver = {
    .driver = {
        .name = "tmp102",
        .of_match_table = tmp102_of_match,
    },
    .probe    = tmp102_probe,
    .remove   = tmp102_remove,
    .id_table = tmp102_id,
};
module_i2c_driver(tmp102_driver);

七、GPIO 驱动实战

GPIO 是最基础的外设,几乎每个 SoC 都有。Linux 内核提供了两套 GPIO 操作 API:

GPIO 子系统调用链 应用空间 /sys/class/gpio/ /dev/gpiochip0 gpiod API devm_gpiod_get() / gpiod_set_value() gpiolib gpio_request() / gpiod_direction_output() GPIO Controller Driver 硬件寄存器操作 LED GPIO 驱动完整代码示例 设备树 led-gpio { gpios = &lt;&amp;gpio 23 0&gt;; }; probe 中获取 GPIO led_gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); ioctl 控制 gpiod_set_value(led_gpio, arg); 用户空间 echo 1 &gt; /dev/led_gpio → LED 亮 devm_ 前缀:驱动卸载时自动释放 GPIO,无需在 remove 中手动 free
图 7-1:GPIO 子系统调用链与 LED GPIO 驱动
// led_gpio.c —— LED GPIO 平台驱动
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static struct gpio_desc *led_gpio;
static int major;
static struct cdev led_cdev;

static long led_ioctl(struct file *f, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
    case 0x1001: gpiod_set_value(led_gpio, 1); break;  // LED ON
    case 0x1002: gpiod_set_value(led_gpio, 0); break;  // LED OFF
    default: return -ENOTTY;
    }
    return 0;
}

static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .unlocked_ioctl = led_ioctl,
};

static int led_probe(struct platform_device *pdev) {
    struct device *dev = &pdev->dev;
    dev_t dev_num;

    // 获取 GPIO(设备树中 led-gpios 属性)
    led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
    if (IS_ERR(led_gpio)) return PTR_ERR(led_gpio);

    // 注册字符设备
    alloc_chrdev_region(&dev_num, 0, 1, "led_gpio");
    major = MAJOR(dev_num);
    cdev_init(&led_cdev, &led_fops);
    cdev_add(&led_cdev, dev_num, 1);

    dev_info(dev, "LED GPIO driver probed, major=%d\n", major);
    return 0;
}

static int led_remove(struct platform_device *pdev) {
    dev_t dev_num = MKDEV(major, 0);
    cdev_del(&led_cdev);
    unregister_chrdev_region(dev_num, 1);
    gpiod_set_value(led_gpio, 0);
    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "vendor,led-gpio" },
    { }
};

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "led_gpio",
        .of_match_table = led_of_match,
    },
};
module_platform_driver(led_driver);
MODULE_LICENSE("GPL");

八、I2C 驱动实战

I2C 是嵌入式中最常用的串行总线,连接温度传感器、EEPROM、ADC/DAC 等外设。Linux I2C 子系统分三层:Adapter(控制器驱动)、Client(设备实例)、Driver(设备驱动)。

Linux I2C 驱动架构 I2C Core 总线管理层 • i2c_transfer() • i2c_msg 数组 • i2c_master_send() • i2c_master_recv() 角色: 协调 Adapter 和 Driver I2C Adapter Driver 控制器驱动(SoC 内置) • dw-i2c / designware • bcm2835-i2c 实现 master_xfer() 硬件寄存器操作 角色: 主设备,发起总线通信 I2C Device Driver 外设驱动(传感器等) • tmp102 (温度传感器) • at24 (EEPROM) • adxl345 (加速度计) probe/remove 角色: 从设备,响应读写请求 Hardware SDA / SCL GPIO 引脚 上拉电阻 外设芯片 TMP102 关键 API i2c_master_send(client, buf, len) i2c_master_recv(client, buf, len) smbus_read_word_data(client, reg) 快捷 SMBus API:i2c_smbus_read_byte / i2c_smbus_write_byte_data / i2c_smbus_read_word_data
图 8-1:Linux I2C 驱动架构(Adapter-Core-Driver 三层)
// tmp102.c —— TI TMP102 温度传感器驱动
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/hwmon-sysfs.h>

struct tmp102_data {
    struct device *hwmon_dev;
};

static int tmp102_read_temp(struct i2c_client *client) {
    s32 temp_raw = i2c_smbus_read_word_data(client, 0x00);
    if (temp_raw < 0) return temp_raw;
    // 高字节整数,低字节小数(0.0625°C/LSB)
    return (temp_raw >> 4) * 625;  // 单位 milli-°C
}

static ssize_t show_temp_input(struct device *dev, struct device_attribute *attr, char *buf) {
    struct tmp102_data *data = dev_get_drvdata(dev);
    struct i2c_client *client = to_i2c_client(dev->parent);
    int temp = tmp102_read_temp(client);
    return sprintf(buf, %d\n, temp);
}
static DEVICE_ATTR(temp1_input, 0444, show_temp_input, NULL);

static int tmp102_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    struct tmp102_data *data;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data) return -ENOMEM;

    data->hwmon_dev = devm_hwmon_device_register_with_groups(
        &client->dev, "tmp102", data, NULL);

    sysfs_create_file(&client->dev.kobj, &dev_attr_temp1_input.attr);
    i2c_set_clientdata(client, data);

    dev_info(&client->dev, "TMP102 probed at 0x%02x\n", client->addr);
    return 0;
}

static int tmp102_remove(struct i2c_client *client) {
    sysfs_remove_file(&client->dev.kobj, &dev_attr_temp1_input.attr);
    return 0;
}

static const struct i2c_device_id tmp102_id[] = {
    { "tmp102", 0 }, { }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);

static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "ti,tmp102" }, { }
};

static struct i2c_driver tmp102_driver = {
    .driver = {
        .name = "tmp102",
        .of_match_table = tmp102_of_match,
    },
    .probe    = tmp102_probe,
    .remove   = tmp102_remove,
    .id_table = tmp102_id,
};
module_i2c_driver(tmp102_driver);
MODULE_LICENSE("GPL");

# 用户空间读取温度
cat /sys/class/hwmon/hwmon0/temp1_input   # 输出 23125 → 23.125°C

九、SPI 驱动实战

SPI 比 I2C 速度更快但需要更多引脚(CS/SCK/MOSI/MISO)。Linux SPI 子系统架构与 I2C 类似:Controller Driver 管理硬件,Device Driver 实现外设协议。SPI 支持 Mode 0-3(CPOL/CPHA 组合)和不同位宽。

9.1 SPI Device Driver 代码示例

// spi_oled.c —— SPI 驱动 SSD1306 OLED 屏幕
#include <linux/spi/spi.h>

#define OLED_CMD  0x00   // DC 低:命令
#define OLED_DATA 0x40   // DC 高:数据

static struct gpio_desc *dc_gpio;       // DC 引脚
static struct gpio_desc *rst_gpio;     // RST 引脚

// 发送一条命令
static int oled_write_cmd(struct spi_device *spi, u8 cmd) {
    gpiod_set_value(dc_gpio, 0);  // DC=低
    return spi_write(spi, &cmd, 1);
}

// 发送数据
static int oled_write_data(struct spi_device *spi, const u8 *buf, int len) {
    gpiod_set_value(dc_gpio, 1);  // DC=高
    return spi_write(spi, buf, len);
}

// probe:从设备树获取 GPIO,初始化 OLED
static int oled_probe(struct spi_device *spi) {
    // ① 获取 DC 和 RST GPIO(devm_ 自动释放)
    dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH);
    rst_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH);

    // ② 硬件复位
    gpiod_set_value(rst_gpio, 0);
    msleep(10);
    gpiod_set_value(rst_gpio, 1);
    msleep(10);

    // ③ 初始化 OLED(发送一系列命令)
    oled_write_cmd(spi, 0xAE);  // 关闭显示
    oled_write_cmd(spi, 0xD5);  // 设置时钟分频
    oled_write_cmd(spi, 0x80);
    // ... 更多初始化命令
    oled_write_cmd(spi, 0xAF);  // 开启显示

    return 0;
}

static DEVICE_ATTR(compatible, 0444, ...);

// SPI 设备匹配表
static const struct of_device_id oled_of_match[] = {
    { .compatible = "vendor,ssd1306-oled" },
    { }
};
MODULE_DEVICE_TABLE(of, oled_of_match);

static struct spi_driver oled_driver = {
    .probe  = oled_probe,
    .remove = oled_remove,
    .driver = {
        .name = "ssd1306_oled",
        .of_match_table = oled_of_match,
    },
};

module_spi_driver(oled_driver);  // 自动生成 init/exit
SPI vs I2C 选型
• SPI 速度快(几十 MHz vs I2C 400KHz)、支持全双工,但需要更多 GPIO
• I2C 只需 2 根线(SCL/SDA)、支持多主多从、有 ACK 机制,但速度慢
• OLED、LCD、ADC/DAC、Flash(SPI Nor)→ 选 SPI;EEPROM、小传感器 → 选 I2C
SPI 总线拓扑与驱动架构 SPI Master • CS (片选) • SCK (时钟) • MOSI (主出从入) • MISO (主入从出) CPU / SoC SPI Controller CS SPI Device 0 SCK SPI Device 1 MOSI/MISO SPI Device N SPI 驱动关键 API spi_sync(spi, &msg) spi_write_then_read() devm_spi_get_device() SPI mode 0~3 SPI 模式:CPOL(时钟极性) + CPHA(时钟相位) 组合决定采样沿
图 9-1:SPI 总线拓扑与驱动关键 API

十、中断与下半部

中断驱动硬件通知 CPU 有事件发生。Linux 中断处理分两阶段:上半部(hardirq,原子上下文)和 下半部(softirq/tasklet/workqueue,可睡眠)。上半部只做标记,将耗时工作推给下半部。

10.1 原子上下文 vs 睡眠上下文

上下文位置能否睡眠可用 API
HardIRQ(硬中断)irqreturn_t handler❌ 绝对不能spinlock、atomic_t、kmalloc(GFP_ATOMIC)
softirq / tasklet下半部(原子型)❌ 不能spinlock、atomic_t
workqueue下半部(可睡眠型)✅ 可以mutex、kmalloc(GFP_KERNEL)、msleep
进程上下文file_operations✅ 可以所有 API

10.2 按键中断驱动完整示例

// key_irq.c —— 按键中断 + workqueue 处理
#include <linux/interrupt.h>
#include <linux/workqueue.h>

static int key_irq;            // 中断号
static struct gpio_desc *key_gpio;  按键 GPIO

// 工作队列:在进程上下文处理耗时工作
static struct work_struct key_work;

// 下半部处理(可睡眠)
static void key_work_func(struct work_struct *work) {
    // 读取按键状态(需要 debounce)
    msleep(20);  // 消抖,这里可以睡眠!

    int val = gpiod_get_value(key_gpio);
    if (val == 0) {
        // 按键按下,执行业务逻辑
        dev_info(dev, "Key pressed!\n");
    }
}

// 上半部 HardIRQ(原子上下文,绝对不能睡眠!)
static irqreturn_t key_irq_handler(int irq, void *dev_id) {
    // ① 读取 GPIO 电平,确定是按下还是释放
    // ② 只是标记一下,然后把活推给 workqueue
    schedule_work(&key_work);  // 投递到系统工作队列

    return IRQ_HANDLED;  // 中断已处理
}

// probe 中注册中断
static int my_probe(struct platform_device *pdev) {
    key_gpio = devm_gpiod_get(&pdev->dev, "key", GPIOD_IN);
    key_irq = gpiod_to_irq(key_gpio);  // GPIO → 中断号

    // 初始化工作队列
    INIT_WORK(&key_work, key_work_func);

    // 注册中断(devm_ 自动释放)
    devm_request_irq(&pdev->dev, key_irq, key_irq_handler,
                     IRQF_TRIGGER_FALLING, "key_irq", NULL);

    return 0;
}

// remove 中取消 workqueue 并等待完成
static int my_remove(struct platform_device *pdev) {
    cancel_work_sync(&key_work);  // 等 work 真正跑完
    return 0;
}
下半部选型指南
① workqueue——最常用,可睡眠,有自己的线程池。绝大多数场景都用它。
② tasklet——不能睡眠,比 workqueue 更早执行。需要很高优先级、且绝对不会睡眠时用。
③ softirq——内核内部使用,驱动一般不直接操作。
④ 不要在 HardIRQ 中做任何可能耗时的事情——会拖慢系统响应、影响实时性。
Linux 中断处理流程 硬件中断 GPIO/IRQ 引脚电平 中断控制器 GIC / IRQ Controller 上半部 HardIRQ irq_handler / request_irq 顶半部标记 tasklet_schedule() / queue_work() 下半部 Workqueue / SoftIRQ 完成 唤醒等待进程 上半部 vs 下半部对比 上半部(HardIRQ) 原子上下文 / 不可睡眠 / 尽快返回 / request_irq() / threaded irq 例外 下半部(SoftIRQ/Workqueue) 可睡眠 / 可调度 / 处理耗时逻辑 / workqueue_struct / tasklet_struct
图 10-1:Linux 中断处理流程(上半部 + 下半部)

十一、DMA 与内存管理

DMA(Direct Memory Access)让外设直接访问内存,无需 CPU 介入,大幅提升吞吐量。DMA 缓冲区需要特殊内存属性(连续、cache-safe),用 dma_alloc_coherent() 分配。

11.1 DMA vs CPU 拷贝对比

CPU memcpyDMA
CPU 占用100%(阻塞 CPU)0%(CPU 可并行执行)
延迟低(缓存命中快)较高(需要 DMA 握手)
吞吐量受限 CPU 频率受限于总线带宽(通常更高)
适用场景小块数据、低频率大块连续数据、高频率(音频/视频/高速采集)

11.2 DMA 缓冲区分配示例

// dma_example.c —— SPI + DMA 发送
#include <linux/dma-mapping.h>
#include <linux/spi/spi.h>

#define DMA_BUF_SIZE 4096

static struct dma_chan *rx_chan;
static struct dma_chan *tx_chan;
static void *dma_buf;
static dma_addr_t dma_handle;

// probe 中分配 DMA 缓冲区
static int my_probe(struct platform_device *pdev) {
    // ① 分配 coherent 内存(CPU 和 DMA 都能访问,cache-safe)
    dma_buf = dma_alloc_coherent(dev, DMA_BUF_SIZE, &dma_handle, GFP_KERNEL);
    if (!dma_buf)
        return -ENOMEM;

    // ② 获取 DMA 通道
    tx_chan = dma_request_slave_channel(dev, "tx");
    rx_chan = dma_request_slave_channel(dev, "rx");

    return 0;
}

// remove 中释放(不用 devm_ 就手动释放)
static int my_remove(struct platform_device *pdev) {
    dma_free_coherent(dev, DMA_BUF_SIZE, dma_buf, dma_handle);
    dma_release_channel(tx_chan);
    dma_release_channel(rx_chan);
    return 0;
}

// DMA 传输数据
static void dma_transfer(struct device *dev) {
    struct dma_async_tx_descriptor *tx_desc;

    // 准备 DMA 描述符
    tx_desc = dmaengine_prep_slave_single(tx_chan, dma_handle,
                                          DMA_BUF_SIZE, DMA_MEM_TO_DEV, 0);

    // 提交并启动 DMA
    dma_submit(tx_desc);
    dma_async_issue_pending(tx_chan);

    // 等 DMA 完成(也可以用回调异步通知)
    dma_sync_wait(tx_chan, tx_desc->cookie);
}
DMA 内存关键约束
① dma_alloc_coherent() ≠ kmalloc()——DMA buffer 必须物理连续且 cache-coherent
② 如果需要多次小传输,可以用 dma_map_single() / dma_unmap_single() 动态映射 kmalloc 内存
③ 64 位地址需要在设备树中声明 dma-coherent 属性,并在驱动中设置 dma_set_mask(dev, DMA_BIT_MASK(64))
DMA 数据传输路径 CPU • 配置 DMA 通道 • 设置源/目的地址 • 启动 DMA 传输 • 等待 DMA 中断 DMA Controller • 传输引擎 • 地址寄存器 • 计数器 • 通道管理 DRAM • DMA Coherent 内存 • CPU 可访问 • DMA 可访问 • 不经过 Cache 外设(SPI/I2S/USB) • FIFO 数据 • 传输请求 • 完成中断 → CPU 继续执行 关键 API:dma_alloc_coherent() / dma_free_coherent() / dma_map_single() / dma_unmap_single() DMA buffer 不能用 kmalloc() 分配——必须连续且 cache-safe,否则 CPU 读到脏数据
图 11-1:DMA 数据传输路径(CPU → DMA Controller → DRAM → 外设)

十二、内核-用户空间接口

驱动需要向用户空间暴露操作接口,Linux 提供四种方式:ioctl、sysfs、procfs、debugfs。它们的定位和使用场景各有不同,下图展示了四者的对比:

12.1 四种接口对比

接口文件位置特点适用场景
ioctl/dev/xxx灵活、可传结构体、需要定义 cmd设备控制(开/关/重置/配置)
sysfs/sys/class/xxx/单值属性、可脚本化、有 show/store导出状态/配置参数(只读或单值读写)
procfs/proc/driver/xxx历史悠久、可输出多行文本导出运行时信息、全局状态
debugfs/sys/kernel/debug/xxx调试专用、内核可裁掉临时调试、不应在生产环境依赖

12.2 sysfs 属性导出示例

// sysfs_ex.c —— 用 sysfs 导出 LED 亮度属性
#include <linux/sysfs.h>

static int led_brightness = 50;  // 当前亮度 0-100

// show:驱动 → 用户空间(读取属性)
static ssize_t brightness_show(struct device *dev,
                                  struct device_attribute *attr, char *buf) {
    return sprintf(buf, "%d\n", led_brightness);
}

// store:用户空间 → 驱动(写入属性)
static ssize_t brightness_store(struct device *dev,
                                   struct device_attribute *attr,
                                   const char *buf, size_t count) {
    kstrtoint(buf, 0, &led_brightness);
    if (led_brightness < 0) led_brightness = 0;
    if (led_brightness > 100) led_brightness = 100;
    return count;
}

// DEVICE_ATTR 宏自动生成 show/store
static DEVICE_ATTR(brightness, 0644, brightness_show, brightness_store);

// probe 中创建 sysfs 属性
device_create_file(dev, &dev_attr_brightness);
// remove 中删除
device_remove_file(dev, &dev_attr_brightness);

// 用户空间访问:
// cat /sys/class/mydev/mydev0/brightness    ← 读取
// echo 80 > /sys/class/mydev/mydev0/brightness  ← 写入

12.3 ioctl 示例

// ioctl_ex.c —— ioctl 实现 LED 控制
#include <linux/ioctl.h>

// 定义 ioctl 命令(_IO / _IOR / _IOW / _IOWR 宏)
#define LED_MAGIC  'L'
#define LED_ON    _IO(LED_MAGIC, 1)
#define LED_OFF   _IO(LED_MAGIC, 2)
#define LED_SET  _IOW(LED_MAGIC, 3, int)

static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    switch (cmd) {
    case LED_ON:   gpiod_set_value(led_gpio, 1); break;
    case LED_OFF:  gpiod_set_value(led_gpio, 0); break;
    case LED_SET:
        led_brightness = (int)arg;
        break;
    default:
        return -ENOTTY;  // 不支持的命令
    }
    return 0;
}

static const struct file_operations led_fops = {
    .owner          = THIS_MODULE,
    .unlocked_ioctl = led_ioctl,
    // ... open/release/read/write
};
接口选型原则
① 有状态/配置类属性 → sysfs(脚本友好)
② 复杂控制/传结构体 → ioctl(灵活但需定义命令)
③ 运行时调试信息 → procfs 或 debugfs(debugfs 可裁掉)
④ 不要用 procfs 做 sysfs 的事——内核社区已明确:新代码优先 sysfs
内核-用户空间交互方式对比 ioctl 场景: 设备特有控制命令 优点: 灵活、可传指针 无需定义 sysfs 属性 缺点: 不可脚本化 命令号需手动定义 示例: ioctl(fd, LED_ON) sysfs 场景: 设备属性读写 优点: 文件系统风格 可脚本化(cat/echo) 缺点: 只能传字符串 大量属性时管理不便 示例: echo 1 > /sys/class/led/brightness procfs 场景: 调试信息/状态 优点: 类 sysfs 可输出格式化数据 缺点: procfs 已不推荐新增 倾向用 debugfs 替代 示例: cat /proc/driver/mydev debugfs 场景: 调试专用接口 优点: 简单、无需权限检查 仅调试期使用 缺点: 无 ABI 保证 内核版本间可能变化 示例: cat /sys/kernel/debug/mydev
图 12-1:内核-用户空间交互方式对比

十三、驱动编译与构建

Linux 内核驱动使用 Kbuild 构建系统,类似 Kconfig 配置。编译分为 模块编译(.ko 可动态加载)和 内建编译(直接编进内核 Image)。下面是最常见的外部模块编译流程:

13.1 Makefile 模板(外部模块)

# Makefile —— 驱动编译入口

# 指定驱动名,obj-m 表示编译为模块(.ko)
# obj-y 表示编进内核(built-in)
obj-m += my_led_driver.o

# 多文件驱动(两个 .c 文件编译进同一个 .ko)
# my_led_driver-objs = led_main.o led_gpio.o

# 内核源码目录(交叉编译时要指定)
# KDIR := /lib/modules/$(shell uname -r)/build   # 本机编译
KDIR := $(HOME)/linux-source/5.15.0          # 或你的内核源码树

# 交叉编译工具链(可选,本机编译可省略)
# CROSS_COMPILE := arm-linux-gnueabihf-

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

# 编译后得到:my_led_driver.ko
# scp my_led_driver.ko root@target:/lib/modules/$(uname -r)/
# modprobe my_led_driver

13.2 Kconfig 选项(如果驱动要进内核源码树)

# drivers/my_drivers/Kconfig
config MY_LED_DRIVER
    tristate "My LED GPIO Driver"
    depends on GPIOLIB
    help
      Say Y here to enable the GPIO LED driver.
      If unsure, say M (module).

# drivers/my_drivers/Makefile
obj-$(CONFIG_MY_LED_DRIVER) += led_driver.o

# 编译步骤:
# 1. make menuconfig → Device Drivers → My Drivers → 选 M 或 Y
# 2. make modules   (如果选 M)
# 3. 或 make Image  (如果选 Y,编进内核)

13.3 交叉编译实战(树莓派 4B 为例)

# Step 1: 获取内核源码(版本必须和目标机 uname -r 一致)
git clone --depth=1 -b rpi-5.15.y https://github.com/raspberrypi/linux

# Step 2: 安装工具链
sudo apt install gcc-aarch64-linux-gnu

# Step 3: 使用目标机 defconfig
KERNEL=kernel8
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2711_defconfig

# Step 4: 编译模块(-j 并行加速)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) modules_prepare

# Step 5: 编译你的驱动(在驱动目录下 make)
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C /path/to/linux M=$(pwd) modules

# Step 6: 验证驱动内核版本
modinfo my_driver.ko | grep vermagic

# Step 7: 部署到目标机
scp my_driver.ko pi@raspberrypi:/tmp/
ssh pi@raspberrypi "sudo insmod /tmp/my_driver.ko"
编译踩坑
① Kernel version mismatch——.ko 的 vermagic 必须和目标机内核精确一致(包括 CONFIG 选项),否则 modprobe 拒绝加载
② extern "C"——驱动代码用 C 写,C++ 调用内核 API 需要 extern "C"
③ 不能用 C 标准库——驱动不能 include 标准 C 头文件(stdlib/stdio/malloc),用内核替代(kmalloc/printk)
④ -Werror——内核开启了警告即错误,编译警告要及时修
驱动编译构建流程 Kconfig CONFIG_MY_DRIVER defconfig CONFIG_MY_DRIVER=m Makefile obj-m += mydrv.o 交叉编译 make ARCH=arm64 产物 mydrv.ko / mydrv.o Kconfig 三态选择 m → 模块编译(.ko,insmod 加载) y → 内建编译(编进内核 Image) n → 不编译 独立模块编译(不在内核树中) make -C /lib/modules/$(uname -r)/build M=$PWD modules # 针对当前内核编译
图 13-1:驱动编译构建流程(Kconfig → defconfig → Makefile → 交叉编译 → .ko)

十四、驱动调试

内核驱动调试比应用更困难——没有 gdb 直接调试内核代码,需要依赖 printk、ftrace、kprobe、kgdb 等工具。调试的基本原则是:先 printk 看日志 → 再 ftrace 追踪调用 → 必要时上 KGDB。

14.1 printk 使用技巧

// 基本 printk(优先用 pr_info/pr_err 封装)
printk(KERN_INFO "led: probe ok, base=%#x\n", base_addr);
pr_info("led: probe ok\n");       // 自动加函数名前缀
pr_err("led: probe failed: %d\n", ret);

// 动态调试(dynamic debug)——生产环境保留但不输出
// 代码中用 dev_dbg / dev_vdbg
dev_dbg(dev, "probe called\n");  // 默认不输出

// 运行时开启:
# echo 'file my_driver.c +p' > /sys/kernel/debug/dynamic_debug/control
# echo 'func my_probe +p' > /sys/kernel/debug/dynamic_debug/control

// 查看内核日志:
dmesg | tail -50                    # 最近 50 行
dmesg -w                           # 实时追踪(tail -f)
dmesg -T                           # 带人类可读时间戳
cat /var/log/kern.log              # 持久化日志(syslog)

// 日志级别控制:
# cat /proc/sys/kernel/printk  → 查看当前级别
# echo '8' > /proc/sys/kernel/printk  → 显示所有级别的 printk

14.2 ftrace 追踪函数调用

# ftrace 无需改源码,运行时追踪函数调用链

# ① 查看可用追踪器
cat /sys/kernel/tracing/available_tracers

# ② 函数图追踪(看调用关系和耗时)
cd /sys/kernel/tracing
echo function_graph > current_tracer
echo 1 > tracing_on
# 执行你的操作…
echo 0 > tracing_on
cat trace

# ③ 只追踪你关心的函数
echo my_probe > set_ftrace_filter
echo my_remove > set_ftrace_filter
echo function > current_tracer
# 加载/卸载模块,然后 cat trace 看结果

# ④ trace-cmd 工具(更强大)
sudo trace-cmd record -p function_graph -g my_probe -g my_remove
sudo trace-cmd report trace.dat     # 生成可读报告

# ⑤ perf(另一个选择)
sudo perf record -g -- sleep 5       # 5秒调用栈采样
sudo perf report                     # 交互式分析

14.3 Oops 分析实战

# Oops 是内核的"段错误",dmesg 里会有完整调用栈

# Step 1: 找 dmesg 里的关键行
dmesg | grep -A 30 -i "oops\|panic\|BUG:"

# Step 2: 看 Call trace(调用栈回溯)
# 找到你的驱动函数名,它是崩溃的触发点

# Step 3: addr2line 解析地址 → 源码行号
# Call trace 里类似:
# [] my_probe+0x50/0x100
# 0x50 是函数内偏移,0x100 是函数总大小

# 编译时用 -g 保留调试信息
addr2line -e my_driver.ko 0x50 -f
# 输出:my_probe
#       /home/dev/my_driver.c:42

# Step 4: objdump 反汇编看那行代码
objdump -d my_driver.ko | grep -A5 -B5 "call trace 里的地址"
调试效率提升技巧
① 先用 printk 定位大方向(哪个函数、哪个条件),再上 ftrace
② printk 不要太频繁——会改变时序、掩盖竞态条件
③ dmesg 缓冲有限(默认环形 64KB),加 log_buf_len=2M 内核启动参数扩大缓冲
④ KGDB 需要串口或 JTAG 连接,适合开发阶段,但生产环境一般不用
驱动调试工具链 printk 最常用的调试手段 printk(KERN_INFO "val=%d", x); 动态调试:dynamic_debug 限制:影响性能、重启丢失 ftrace 函数调用追踪 trace-cmd record -p function_graph 无需修改源码 限制:需要 CONFIG_FUNCTION_TRACER kprobe 内核函数探针 kprobe_events / tracefs 无需编译、可动态开关 限制:不能 probe 自身代码 KGDB / JTAG 真正的内核级调试 断点、单步、查看寄存器 需要内核开启 CONFIG_KGDB 限制:需要串口或 JTAG Oops / Panic 分析 ① dmesg 找 "Unable to handle kernel NULL pointer dereference" 或 "BUG:" 行 ② 看 Call trace 堆栈回溯,定位是哪个函数调用导致 ③ addr2line -e mydrv.o 0xffffff8000xxxx 解析指令地址 → 源码行号 ④ 常见 Oops 原因:空指针解引用、use-after-free、越界访问、未对齐访问
图 14-1:驱动调试工具链(printk → ftrace → kprobe → KGDB 逐步深入)

十五、常见问题与故障排查

驱动开发中遇到的问题千奇百怪,但掌握正确的排查方法论可以事半功倍。核心思路是:先看现象 → 缩小范围 → 验证假设 → 定位根因。以下是常见故障分类和排查要点:

15.1 模块加载/卸载类问题

现象可能原因排查方法
insmod 报 "Unknown symbol"依赖模块未加载 / 符号未 EXPORTmodprobe 替代 insmod;检查依赖模块 lsmod
modprobe 报 "Module version mismatch"驱动编译内核版本 ≠ 运行内核版本检查 uname -r 和驱动的 vermagic
rmmod 报 "Module is in use"有进程打开了设备文件fuser /dev/xxx 或 lsof /dev/xxx
rmmod 成功但设备节点还在驱动中 class_create 但没 class_destroy补 remove 中的清理代码,或检查 devm_ 是否正确使用

15.2 probe 失败类问题

现象可能原因排查方法
dmesg 显示 "probe failed"compatible 字符串不匹配检查 DTS 的 compatible 和驱动 of_match_table 是否一致
"Failed to request GPIO"GPIO 已被其他驱动占用 / 设备树 gpios 属性写错cat /sys/kernel/debug/gpio 查看占用
"Failed to request IRQ"中断号冲突 / 设备树 interrupts 属性错误cat /proc/interrupts 查看中断表
"Failed to ioremap"物理地址范围不对 / 地址被其他驱动映射检查 DTS reg 属性的起始地址和长度

15.3 运行时崩溃类问题(Oops / Panic)

典型日志关键词最可能原因修复方向
"Unable to handle kernel NULL pointer dereference"空指针解引用probe 返回值未检查;指针在 remove 后被引用
"BUG: unable to handle kernel paging request"use-after-free / 越界用 kfree 释放后又访问;缓冲区边界计算错误
"kernel stack overflow"内核栈使用超过 8KB大数组改用 kmalloc;递归太深;spinlock 嵌套
"INFO: task xxx blocked for more than 120 seconds"死锁 / mutex 在原子上下文HardIRQ/tasklet 中不能用 mutex;锁顺序要一致
黄金法则
① devm_ 前缀的 API 自动管理资源——probe 中用了 devm_ioremap/devm_gpio_request,remove 中不用手动清理
② 原子上下文(HardIRQ / spinlock / tasklet)绝对不能睡眠——不能用 mutex、不能调用可能阻塞的函数
③ DMA 缓冲区用 dma_alloc_coherent(),不能用 kmalloc——需要物理连续且 cache-safe
驱动常见故障速查 模块加载失败 insmod / modprobe 常见原因:符号未导出 probe 失败 设备树 / GPIO / IRQ 常见原因:compatible 不匹配 Oops / BUG 内核崩溃 常见原因:use-after-free 死锁 hang 住 lockdep 检测 排查方法论 看 dmesg dmesg | grep -i err 看设备树 dtc -I dtb -O dts /boot/*.dtb 看 /sys ls /sys/bus/platform/devices/ lockdep 内核开启 CONFIG_LOCKDEP 黄金法则 ① 原子上下文不能睡眠(HardIRQ / spinlock / rwlock) ② 睡眠上下文可以睡眠(tasklet 也不能!workqueue 可以) ③ probe 中申请的资源,remove 中必须释放 ④ 使用 devm_ 自动管理资源,避免忘记释放 ⑤ 不能睡眠的上下文不能 copy_from_user/copy_to_user ⑥ 并发访问数据结构必须加锁(mutex / spinlock)
图 15-1:驱动常见故障速查与排查方法论

十六、完整实战:从设备树到用户测试

把前面所有知识点串起来,完成一个完整的 LED + TMP102 温度传感器驱动 项目。

完整驱动开发流程图 1. 设备树 led-gpio & tmp102 节点 2. 写驱动 led_gpio.c + tmp102.c 3. 编译 make → .ko 4. 加载 modprobe xxx.ko 5. 测试 用户空间 Step 1:设备树 &amp;gpio { led-gpio { compatible = "vendor,led-gpio"; led-gpios = &lt;&amp;gpio 23 0&gt;; }; }; &amp;i2c@7e800000 { temperature@48 { compatible = "ti,tmp102"; reg = &lt;0x48&gt;; }; }; dtc -I dts -O dtb -o my.dtb my.dts Step 5:用户空间测试 # LED 控制 echo 1 &gt; /dev/led_gpio # ON echo 0 &gt; /dev/led_gpio # OFF # 温度读取 cat /sys/class/hwmon/hwmon0/temp1_input # 卸载驱动 modprobe -r led_gpio tmp102
图 16-1:完整驱动开发流程(设备树 → 代码 → 编译 → 加载 → 测试)

驱动开发核心要点

  1. 驱动分层:应用→VFS→驱动→硬件,file_operations 是关键接口
  2. 模块加载:modprobe 自动解析依赖,insmod 不解析
  3. 平台驱动:platform_driver + probe/remove,devm_ 自动管理资源
  4. 设备树:compatible 是驱动与设备匹配的桥梁,reg/interrupts/gpios 传递硬件信息
  5. 字符设备:cdev_init + cdev_add + alloc_chrdev_region + class_create
  6. I2C/SPI:Adapter Driver 管硬件,Device Driver 管协议,Core 协调两者
  7. 中断:HardIRQ 原子上下文不能睡眠 → 标记;Workqueue 可睡眠 → 处理耗时逻辑
  8. DMA:dma_alloc_coherent() 分配连续 cache-safe 内存,不能用 kmalloc
  9. 用户接口:ioctl 灵活、sysfs 可脚本化、debugfs 调试专用
  10. 调试:printk→ftrace→kprobe→KGDB,addr2line 解析 Oops 地址
  11. 原子 vs 睡眠:HardIRQ/spinlock/rwlock/tasklet 不能睡眠;mutex/workqueue 可以睡眠
  12. 资源管理:devm_ 前缀的 API 自动释放,remove 中不用手动清理
内核模块字符设备file_operations平台驱动probe/remove设备树GPIOI2CSPI中断DMAioctlsysfsdevm_printkftraceOopsLinux Kernel