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 |
一个最简单的字符设备驱动框架通常包含以下步骤:
- 注册设备号:使用
alloc_chrdev_region()或register_chrdev_region()分配主/次设备号。 - 初始化 cdev:调用
cdev_init()将 file_operations 绑定到 cdev 结构体。 - 添加设备:通过
cdev_add()将设备注册到内核。 - 创建设备节点:使用
device_create()在/dev/下生成设备文件。 - 实现操作函数:填充
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 中的那些回调函数。
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 驱动开发的“通用语言”。
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 是骨架,中断是灵魂。
1.4 本章小结:三者的关系与课程路线图
字符设备驱动、Platform 驱动、中断处理三者并非孤立的知识点,而是层层递进、相互依赖的有机整体:
- 字符设备驱动 是对外接口——它决定了用户空间如何与硬件交互(open/read/write/ioctl)。
- Platform 驱动 是框架骨架——它规范了设备信息与驱动代码的分离方式,让驱动具备可移植性和可维护性。
- 中断处理 是性能引擎——它让驱动能够及时响应硬件事件,避免 CPU 空转轮询。
课程路线图:本课程将按照“先基础、后框架、再深入”的顺序展开。前 5 章聚焦字符设备驱动核心概念与实现;第 6-12 章深入 Platform 驱动模型与设备树;第 13-20 章系统讲解中断处理、底半部机制与并发控制;最后 10 章通过综合项目(如按键中断驱动、UART 驱动、I2C 传感器驱动)将三者融会贯通。