1. 课程导论

1.1 字符设备驱动:Linux 驱动世界的“Hello World”

在 Linux 系统中,设备被抽象为三种基本类型:字符设备、块设备和网络设备。字符设备驱动是其中最基础、最常用的一类,也是大多数嵌入式开发者入门驱动的第一站。

字符设备(Character Device) 是指那些以字节流形式进行数据传输的设备。典型的例子包括:串口(UART)、按键、LED、触摸屏、I2C/SPI 传感器等。用户空间程序通过 open()、read()、write()、ioctl() 等标准文件操作接口与字符设备交互——这正是“一切皆文件”哲学在驱动层的体现。

核心概念:字符设备驱动的本质是实现 file_operations 结构体。该结构体定义了驱动与 VFS(虚拟文件系统)之间的回调函数映射表。驱动开发者只需填充这些函数指针,内核便能在用户发起系统调用时自动调用对应的驱动函数。

下表对比了字符设备与其他设备类型的核心差异:

设备类型 数据访问方式 典型设备 驱动核心接口
字符设备 字节流,顺序访问 串口、LED、按键 file_operations
块设备 块(512B/4KB),随机访问 eMMC、SD卡、NVMe block_device_operations + request_queue
网络设备 数据包(sk_buff) 以太网、WiFi、CAN net_device_ops

一个最简单的字符设备驱动框架通常包含以下步骤:

  1. 注册设备号:使用 alloc_chrdev_region() 或 register_chrdev_region() 分配主/次设备号。
  2. 初始化 cdev:调用 cdev_init() 将 file_operations 绑定到 cdev 结构体。
  3. 添加设备:通过 cdev_add() 将设备注册到内核。
  4. 创建设备节点:使用 device_create() 在 /dev/ 下生成设备文件。
  5. 实现操作函数:填充 open/read/write/release 等回调。
// 字符设备驱动核心骨架(伪代码)
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .release = my_release,
};

static int __init my_init(void) {
    alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);
    device_create(my_class, NULL, dev_num, NULL, "my_device");
    return 0;
}
module_init(my_init);
关键认知:字符设备驱动是 Linux 驱动开发的基石。即使后续学习 Platform 驱动、中断处理、DMA 等高级主题,最终落地的仍然是 file_operations 中的那些回调函数。
字符设备驱动在 Linux 内核中的位置与数据流 用户空间 (User Space) 应用程序 (App) — open/read/write/ioctl 系统调用层 (System Call) — VFS 虚拟文件系统 内核空间 (Kernel Space) 字符设备驱动 — file_operations 回调实现 硬件抽象层 (HAL) — 寄存器读写 / GPIO 控制 / 中断处理 硬件设备 (Hardware) UART / GPIO / I2C / SPI / 传感器

1.2 为什么需要 Platform 驱动?—— 解决“设备与驱动如何绑定”的问题

在传统的嵌入式系统中,设备信息往往被硬编码在板级文件(如 arch/arm/mach-xxx/board-xxx.c)中。这种方式在设备数量少、硬件变化不频繁时勉强可用,但随着 SoC 越来越复杂、外设越来越多,硬编码方式暴露出严重问题:

  • 代码冗余:每块新板子都要复制粘贴大量板级文件。
  • 难以维护:设备资源(中断号、内存地址、DMA 通道)散落在各处。
  • 不支持设备树:无法与现代 ARM 生态(Device Tree)接轨。

Platform 驱动(平台驱动) 正是为解决上述问题而生的。它是 Linux 内核提供的一种虚拟总线模型,用于管理那些不依附于 PCI、USB、I2C 等物理总线的设备(通常称为“平台设备”)。

Platform 驱动的核心思想:将“设备信息”与“驱动代码”分离。设备信息通过 platform_device 描述(资源、名称、ID),驱动代码通过 platform_driver 描述(probe/remove 回调、匹配表)。内核的 Platform 总线负责将两者匹配起来,匹配成功后调用驱动的 probe() 函数完成初始化。

下表对比了传统硬编码方式与 Platform 驱动模型的关键差异:

对比维度 传统硬编码方式 Platform 驱动模型
设备信息存放 板级 C 文件(board-xxx.c) 设备树(.dts)或 platform_device 结构体
驱动与设备耦合 强耦合,驱动直接引用板级宏 松耦合,通过 name/compatible 匹配
资源获取方式 直接使用宏定义的地址/中断号 通过 platform_get_resource() 动态获取
可移植性 差,换板子需重写驱动 好,只需修改设备树或 platform_device
内核版本趋势 已废弃(ARM 32 逐步淘汰) 主流,ARM64/RISC-V 标配

一个典型的 Platform 驱动框架包含两部分:

// 1. 设备端(设备树或 platform_device)
// 设备树示例(.dts):
// my_device: my-device@1c00000 {
//     compatible = "vendor,my-device";
//     reg = <0x01c00000 0x1000>;
//     interrupts = <0 42 4>;
// };

// 2. 驱动端(platform_driver)
static struct platform_driver my_pdrv = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,  // 设备树匹配表
    },
};

static int __init my_init(void) {
    return platform_driver_register(&my_pdrv);
}
module_init(my_init);
学习重点:Platform 驱动是嵌入式 Linux 驱动开发的“标准姿势”。无论你是写 LED 驱动、按键驱动、还是 SPI/I2C 控制器驱动,几乎都会用到 Platform 框架。掌握它,就等于掌握了 Linux 驱动开发的“通用语言”。
Platform 总线驱动模型架构 platform_device • name / id • resource[] (mem/irq/dma) • dev.platform_data Platform 总线 匹配规则: name 匹配 / of_match_table 匹配 ACPI 匹配 / ID 表匹配 platform_driver • probe() • remove() • driver.of_match_table 匹配成功后内核自动调用 probe() 在 probe() 中:获取资源 → 注册字符设备 → 初始化硬件 → 申请中断 在 remove() 中:释放资源 → 注销字符设备 → 释放中断 → 反初始化硬件

1.3 中断处理在驱动中的作用——从“轮询”到“事件驱动”

在驱动开发中,CPU 与硬件交互有两种基本方式:轮询(Polling) 和 中断(Interrupt)。

  • 轮询:CPU 不断读取硬件状态寄存器,检查是否有事件发生。优点是实现简单,缺点是浪费 CPU 资源,不适合低功耗场景。
  • 中断:硬件在需要 CPU 处理时主动发送中断信号,CPU 暂停当前任务,执行中断服务程序(ISR)。优点是高效、实时、省电。
重要原则:中断服务程序(ISR)必须短小精悍。在中断上下文中,不能调用可能引起睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock()、copy_to_user())。如果处理逻辑复杂,应使用底半部机制(Tasklet / Workqueue / threaded IRQ)将耗时操作推迟到更宽松的上下文中执行。

中断处理在驱动中的典型作用包括:

应用场景 中断触发条件 ISR 典型处理
按键输入 GPIO 电平变化(上升/下降沿) 读取按键状态 → 上报输入事件
UART 接收 接收 FIFO 达到阈值 读取数据 → 放入环形缓冲区 → 唤醒读取进程
DMA 传输完成 DMA 控制器传输结束 清理 DMA 描述符 → 通知上层数据就绪
定时器溢出 硬件定时器计数值归零 更新系统 jiffies / 触发软件定时器
I2C/SPI 传输完成 控制器传输结束或出错 检查状态 → 唤醒等待的传输线程

在 Platform 驱动中申请中断的标准流程如下:

static irqreturn_t my_irq_handler(int irq, void *dev_id) {
    // 中断处理:读取硬件状态,清除中断标志
    // 注意:不能调用可能导致睡眠的函数
    return IRQ_HANDLED;  // 或 IRQ_WAKE_THREAD 启用 threaded IRQ
}

static int my_probe(struct platform_device *pdev) {
    struct resource *res;
    int irq, ret;

    // 1. 从 platform_device 获取中断号
    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    irq = res->start;

    // 2. 申请中断
    ret = request_irq(irq, my_irq_handler,
                      IRQF_TRIGGER_RISING,   // 上升沿触发
                      "my_device", dev);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed\n");
        return ret;
    }
    return 0;
}

static int my_remove(struct platform_device *pdev) {
    int irq = platform_get_irq(pdev, 0);
    free_irq(irq, dev);  // 释放中断
    return 0;
}
本课程的核心脉络:我们将从最简单的字符设备驱动开始,逐步引入 Platform 框架来解耦设备与驱动,再深入中断处理来提升系统实时性。这三者构成了嵌入式 Linux 驱动开发的“铁三角”——字符设备是接口,Platform 是骨架,中断是灵魂。
中断处理在驱动中的完整流程 硬件设备 产生中断信号 中断控制器 (GIC) 分发到 CPU 核 CPU 内核 保存上下文 → 执行 ISR ISR (顶半部) 快速处理 底半部机制(推迟执行) • Tasklet:软中断上下文,不可睡眠 • Workqueue:进程上下文,可睡眠 • Threaded IRQ:内核线程上下文,可睡眠

1.4 本章小结:三者的关系与课程路线图

字符设备驱动、Platform 驱动、中断处理三者并非孤立的知识点,而是层层递进、相互依赖的有机整体:

  • 字符设备驱动 是对外接口——它决定了用户空间如何与硬件交互(open/read/write/ioctl)。
  • Platform 驱动 是框架骨架——它规范了设备信息与驱动代码的分离方式,让驱动具备可移植性和可维护性。
  • 中断处理 是性能引擎——它让驱动能够及时响应硬件事件,避免 CPU 空转轮询。
课程路线图:本课程将按照“先基础、后框架、再深入”的顺序展开。前 5 章聚焦字符设备驱动核心概念与实现;第 6-12 章深入 Platform 驱动模型与设备树;第 13-20 章系统讲解中断处理、底半部机制与并发控制;最后 10 章通过综合项目(如按键中断驱动、UART 驱动、I2C 传感器驱动)将三者融会贯通。