Linux 驱动开发学习笔记
一、背景:从 MCU 到 MPU 的思维转变
1.1 MCU 开发模式(裸机 / RTOS)
┌─────────────────────────────┐
│ APP 层 │
│ 直接操作寄存器 │ ← 物理地址直接访问
│ *(volatile uint32_t*)0x40021000 = 0x01; │
├─────────────────────────────┤
│ 硬件外设 │
└─────────────────────────────┘
- 特点:所有代码运行在同一特权级别,APP 可直接读写物理地址(寄存器)。
- 优点:简单直接,实时性好。
- 缺点:无内存保护,一个野指针可能搞崩整个系统。
1.2 MPU + Linux 开发模式
┌──────────────────────────────────────────────┐
│ 用户空间 (User Space) │
│ APP 不能直接操作硬件 │
│ 所有硬件操作必须通过 系统调用 → 内核 │
├──────────────────────────────────────────────┤
│ 内核空间 (Kernel Space) │
│ 驱动代码运行在此,可直接操作硬件 │
│ 提供统一接口:open / read / write / ioctl │
├──────────────────────────────────────────────┤
│ 硬件层 (Hardware) │
└──────────────────────────────────────────────┘
- 核心差异 :用户态无权直接操作硬件,必须通过系统调用陷入内核态。
二、MMU 与内核态/用户态切换
2.1 MMU(Memory Management Unit)
| 特性 | MCU(无 MMU) | MPU(有 MMU) |
|---|---|---|
| 地址类型 | 物理地址 | 虚拟地址(MMU 映射到物理地址) |
| 内存保护 | 无 | 有(页表权限控制) |
| 进程隔离 | 弱(靠 MPU 简单分区) | 强(每个进程独立虚拟地址空间) |
| 典型 OS | FreeRTOS / RT-Thread | Linux / Android |
2.2 用户态 → 内核态切换流程
用户态 APP 调用 open("/dev/hello", O_RDWR)
│
▼
C 库封装 (glibc) → 触发 swi / svc 软中断
│
▼
CPU 进入异常模式(内核态)
根据软中断号查找系统调用表 (sys_call_table)
│
▼
sys_open() → 遍历 VFS → 找到驱动注册的 file_operations → 调用驱动的 .open()
│
▼
返回 fd 给用户态 APP
- 关键寄存器 :ARM 架构中
r7存放系统调用号,swi #0触发异常。 - MMU 的作用 :用户态和内核态使用不同的页表(或同一页表不同权限位),内核空间映射到高地址(如 0xC0000000 以上),用户空间在低地址。
重点理解 :同一虚拟地址,在不同进程的页表中映射到不同物理地址。这就是为什么同一段代码多次运行时,打印的虚拟地址相同但内容不同------MMU 给每个进程映射了独立的物理内存。
三、字符设备驱动核心框架
3.1 file_operations 结构体
file_operations 是连接用户态系统调用与驱动实现的抽象接口层:
c
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
// ... 更多回调
};
抽象接口的作用:
- Linux 一切皆文件,字符设备也被抽象成"文件"。
- 用户调用
read(fd, buf, len)→ VFS → 驱动fops->read()。 - 驱动开发者只需填充这个结构体,不用关心上层 VFS 的实现细节。
3.2 设备号注册:register_chrdev
c
int register_chrdev(unsigned int major, // 主设备号(0 表示自动分配)
const char *name, // 设备名称(/proc/devices 中可见)
const struct file_operations *fops); // 操作接口
两种注册方式对比:
| 方式 | API | 特点 |
|---|---|---|
| 老式 | register_chrdev |
一次注册 256 个次设备号,简单但浪费 |
| 新式 | alloc_chrdev_region + cdev_init + cdev_add |
按需申请,精确控制 |
四、module_init 的实现原理
c
// 当我们写下:
module_init(hello_init);
// 实际上宏展开后大致等价于:
#define module_init(x) \
__attribute__((section(".initcall6.init"))) \
initcall_t __initcall_##x = x;
原理拆解:
-
段属性 (section attribute) :
module_init将函数指针放到一个特殊的 ELF 段(如.initcall6.init)。 -
链接脚本 :内核链接脚本(
vmlinux.lds)收集所有initcall段中的函数指针,构成一个函数指针数组。 -
内核启动时 :
do_initcalls()依次调用数组中的每个函数,insmod加载模块时同样会执行对应级别的 initcall。 -
模块卸载 :
module_exit将清理函数指针放入.exitcall段,rmmod时调用。内核启动流程:
start_kernel()
└── rest_init()
└── kernel_init()
└── do_initcalls()
└── 遍历 initcall 数组
├── level 0: early init
├── ...
├── level 6: module_init 注册的函数 ← 我们的 hello_init
└── level 7: late init
insmod 时 :动态模块的
init函数在模块加载时由sys_init_module()系统调用直接执行,不经过 initcall 机制。
五、内核与用户空间的数据传输
5.1 为什么要 copy_to_user / copy_from_user?
用户空间 buf (虚拟地址 0x7fff...) ← APP 传下来的指针
│
│ ❌ 内核不能直接解引用用户空间指针!
│ 原因1:用户地址在内核页表中可能不可见
│ 原因2:用户可能传一个非法指针(安全风险)
│ 原因3:内核拒绝缺页异常
│
▼
copy_from_user(kbuf, ubuf, len) ← 内核先验证地址合法性,再拷贝
c
// 错误写法(内核态直接访问用户空间指针):
ssize_t bad_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
memcpy(kernel_buf, buf, len); // ❌ 可能 oops!
return len;
}
// 正确写法:
ssize_t good_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
if (copy_from_user(kernel_buf, buf, len))
return -EFAULT; // 拷贝失败,返回错误
return len;
}
copy_to_user:内核 → 用户空间copy_from_user:用户空间 → 内核- 这两个函数内部实现了地址校验 + 缺页处理 + 实际拷贝,返回非零表示失败。
六、完整伪代码示例:hello_drv_test.c
c
/*
* hello_drv_test.c ------ 一个简单的字符设备驱动
*
* 功能:在驱动中维护一个内核缓冲区,用户态通过 read/write 读写字符串。
*
* 完整流程:
* 1. 编译: make → 生成 hello_drv_test.ko
* 2. 加载: sudo insmod hello_drv_test.ko
* 3. 创建设备节点:
* 手动: sudo mknod /dev/hello c <主设备号> 0
* 自动: 驱动内 class_create + device_create(无需手动 mknod)
* 4. 测试: echo "hello" > /dev/hello && cat /dev/hello
* 5. 卸载: sudo rmmod hello_drv_test
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/cdev.h>
/* ========== 设备信息 ========== */
#define DEVICE_NAME "hello_drv"
#define BUF_SIZE 256
static int major; // 主设备号
static char kernel_buf[BUF_SIZE]; // 内核缓冲区(模拟"硬件")
static struct class *cls; // 设备类(用于自动创建设备节点)
static struct device *dev; // 设备实例
/* ========== file_operations 接口实现 ========== */
static int hello_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "[hello] open\n");
return 0;
}
static int hello_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "[hello] close\n");
return 0;
}
static ssize_t hello_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
int ret;
/* 防止用户读取超过缓冲区大小的数据 */
if (len > BUF_SIZE)
len = BUF_SIZE;
/* 将内核数据拷贝到用户空间 */
ret = copy_to_user(buf, kernel_buf, len);
if (ret) {
printk(KERN_ERR "[hello] copy_to_user failed\n");
return -EFAULT;
}
printk(KERN_INFO "[hello] read %zu bytes\n", len);
return len;
}
static ssize_t hello_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off)
{
int ret;
/* 防止用户写入超过缓冲区大小的数据 */
if (len > BUF_SIZE)
len = BUF_SIZE;
/* 将用户空间数据拷贝到内核 */
ret = copy_from_user(kernel_buf, buf, len);
if (ret) {
printk(KERN_ERR "[hello] copy_from_user failed\n");
return -EFAULT;
}
kernel_buf[len] = '\0'; // 确保字符串结尾
printk(KERN_INFO "[hello] write %zu bytes: %s\n", len, kernel_buf);
return len;
}
/* 操作接口绑定 */
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.read = hello_read,
.write = hello_write,
};
/* ========== 模块加载 & 卸载 ========== */
static int __init hello_init(void)
{
/* 1. 注册字符设备(老式接口,自动分配主设备号) */
major = register_chrdev(0, DEVICE_NAME, &hello_fops);
if (major < 0) {
printk(KERN_ERR "[hello] register_chrdev failed\n");
return major;
}
printk(KERN_INFO "[hello] registered, major = %d\n", major);
/* 2. 自动创建设备节点(替代手动 mknod) */
cls = class_create(THIS_MODULE, DEVICE_NAME); // 在 /sys/class/ 下创建类
if (IS_ERR(cls)) {
unregister_chrdev(major, DEVICE_NAME);
return PTR_ERR(cls);
}
dev = device_create(cls, NULL, MKDEV(major, 0), NULL,
DEVICE_NAME); // 自动在 /dev/ 下创建设备文件
if (IS_ERR(dev)) {
class_destroy(cls);
unregister_chrdev(major, DEVICE_NAME);
return PTR_ERR(dev);
}
printk(KERN_INFO "[hello] device node /dev/%s created\n", DEVICE_NAME);
return 0;
}
static void __exit hello_exit(void)
{
device_destroy(cls, MKDEV(major, 0)); // 删除 /dev/ 下的设备文件
class_destroy(cls); // 删除 /sys/class/ 下的类
unregister_chrdev(major, DEVICE_NAME); // 注销设备号
printk(KERN_INFO "[hello] unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Student");
MODULE_DESCRIPTION("A simple hello world char driver");
七、设备节点的两种创建方式
7.1 手动创建(mknod)
bash
# 先查主设备号
cat /proc/devices | grep hello_drv
# 手动创建节点
sudo mknod /dev/hello c 240 0
# │ │ │
# │ │ └── 次设备号
# │ └────── 主设备号
# └───────── c = 字符设备, b = 块设备
- 缺点:每次 insmod 后需要手动执行,设备号变化时需重新创建。
7.2 自动创建(class_create + device_create)★ 推荐
c
// 在驱动 init 中完成:
// Step 1: 在 /sys/class/ 下创建一个类目录
cls = class_create(THIS_MODULE, "hello_drv");
// Step 2: 让内核在 /dev/ 下自动创建对应的设备文件
dev = device_create(cls, NULL, MKDEV(major, 0), NULL, "hello_drv");
内部原理:
-
class_create→ 在 sysfs 中创建/sys/class/hello_drv/。 -
device_create→ 注册设备到设备模型,向用户空间发送 uevent 事件。 -
udev / mdev (用户空间守护进程)接收到 uevent,自动调用
mknod在/dev/下创建设备文件。insmod hello_drv.ko
└── hello_init()
├── register_chrdev() → 分配设备号,绑定 fops
├── class_create() → /sys/class/hello_drv/
└── device_create() → 发送 uevent
└── udev 收到 → mknod /dev/hello_drv
八、MMU 深入理解:虚拟地址与物理地址
8.1 为什么同一虚拟地址打印出来,内容不同?
进程 A 第一次运行: 进程 A 第二次运行(或进程 B 运行):
┌────────────────────┐ ┌────────────────────┐
│ 虚拟地址 0x7fff1234 │ │ 虚拟地址 0x7fff1234 │ ← 相同的虚拟地址!
│ ↓ │ │ ↓ │
│ 页表 A │ │ 页表 B │ ← 不同的页表!
│ ↓ │ │ ↓ │
│ 物理地址 0x8A000000│ │ 物理地址 0x9B000000│ ← 可能映射到不同物理页
│ 内容: "hello" │ │ 内容: "world" │ ← 所以内容不同!
└────────────────────┘ └────────────────────┘
- 每个进程有独立的页表(Page Table),MMU 通过当前进程的页表做地址翻译。
- 虚拟地址相同 ≠ 物理地址相同。
- 内核空间(通常 > 0xC0000000)在所有进程中映射到相同的物理地址(内核共享)。
8.2 驱动视角下的地址层次
用户态 APP
│ 虚拟地址 (user virtual address) ← copy_from_user 接收的地址
▼
MMU 翻译(用户页表)
│
▼
物理地址 (physical address)
│
▼
外设寄存器 (MMIO 区域)
│ 内核需要先 ioremap() 将物理地址映射到内核虚拟地址
▼
内核虚拟地址 (kernel virtual address) ← 驱动中用 ioread32 / iowrite32 操作
8.3 相关 API 速查
| 场景 | API |
|---|---|
| 物理地址 → 内核虚拟地址 | ioremap(phys_addr, size) |
| 内核虚拟地址 → 物理地址 | virt_to_phys(kva) |
| 用户空间拷贝到内核 | copy_from_user() |
| 内核拷贝到用户空间 | copy_to_user() |
| 访问 MMIO | ioread32() / iowrite32() |
九、完整工作流程总结
┌─────────────────────────────────────────────────────────┐
│ 1. 编写驱动代码 │
│ hello_drv_test.c │
│ └── 实现 file_operations 中的 open/read/write/release │
│ └── module_init / module_exit 注册入口 │
│ └── class_create / device_create 自动创建设备节点 │
├─────────────────────────────────────────────────────────┤
│ 2. 编译生成 .ko │
│ Makefile → make → hello_drv_test.ko │
├─────────────────────────────────────────────────────────┤
│ 3. 安装到内核 │
│ sudo insmod hello_drv_test.ko │
│ └── 执行 hello_init() │
│ ├── register_chrdev (注册字符设备) │
│ ├── class_create (创建 sysfs 类) │
│ └── device_create (触发 udev 创建设备文件) │
├─────────────────────────────────────────────────────────┤
│ 4. 查看结果 │
│ ls /dev/hello_drv ← 设备节点 │
│ cat /proc/devices | grep hello ← 设备号 │
│ ls /sys/class/hello_drv/ ← sysfs 信息 │
├─────────────────────────────────────────────────────────┤
│ 5. 用户态 APP 调用 │
│ fd = open("/dev/hello_drv", O_RDWR); │
│ └── swi 软中断 → 内核态 → VFS → hello_open() │
│ write(fd, "hello", 5); │
│ └── swi → 内核态 → hello_write() │
│ └── copy_from_user(kbuf, ubuf, 5) │
│ read(fd, buf, 5); │
│ └── swi → 内核态 → hello_read() │
│ └── copy_to_user(ubuf, kbuf, 5) │
│ close(fd); │
│ └── swi → hello_release() │
├─────────────────────────────────────────────────────────┤
│ 6. 卸载驱动 │
│ sudo rmmod hello_drv_test │
│ └── 执行 hello_exit() │
│ ├── device_destroy (删除 /dev 下的设备文件) │
│ ├── class_destroy (删除 sysfs 类) │
│ └── unregister_chrdev(注销设备号) │
└─────────────────────────────────────────────────────────┘
十、今日学习小结
| 知识点 | 核心理解 |
|---|---|
| MCU vs MPU | MCU 无 MMU,APP 可直接操作物理地址;MPU+Linux 通过 MMU 隔离用户态/内核态,APP 不可直接操作硬件 |
| 系统调用路径 | 用户态 open/read/write → glibc 封装 → SWI 软中断 → 内核态 → VFS → 驱动的 fops 回调 |
| file_operations | 驱动与 VFS 的抽象接口,驱动只需填充该结构体,上层系统调用会通过 VFS 转发到对应函数 |
| module_init | 利用 __attribute__((section)) 将函数指针放入特定段,内核启动时按 initcall 级别依次调用 |
| register_chrdev | 注册字符设备(老式 API),绑定主设备号和 file_operations |
| copy_to/from_user | 内核不能直接解引用用户空间指针,必须通过这两个函数做地址检验 + 安全拷贝 |
| mknod | 手动创建设备节点的命令,格式 mknod /dev/xxx c 主设备号 次设备号 |
| class_create + device_create | 自动创建设备节点的现代方式,依赖 udev/mdev 守护进程响应 uevent 事件 |
| MMU 页表隔离 | 每个进程有独立的页表,相同虚拟地址可映射到不同物理地址------解释了同名变量不同内容的现象 |
| 驱动的角色 | 用户态通过统一文件接口调用 → 驱动负责将数据拷贝到内核缓冲区 → 最终通过 ioremap 操作物理硬件 |
十一、Kconfig / .config / Makefile ------ 编译驱动入内核
之前的示例都是将驱动编译成独立的 .ko 模块,通过 insmod 动态加载。实际产品开发中,有些驱动需要直接编译进内核镜像(如串口驱动、中断控制器驱动),内核编译框架提供了 Kconfig + .config + Makefile 的机制:
11.1 三件套的职责
┌──────────────────────────────────────────────────────────────────┐
│ 内核编译配置流程 │
│ │
│ drivers/char/Kconfig → 定义配置选项(菜单项) │
│ │ │
│ ▼ make menuconfig 读取 Kconfig,生成图形界面 │
│ .config → 保存用户的配置选择 │
│ │ CONFIG_HELLO_DRV=y / =m / (未定义) │
│ ▼ Makefile 通过 .config 中宏决定编译行为 │
│ drivers/char/Makefile → 根据 CONFIG_xxx 决定编译方式 │
│ │
└──────────────────────────────────────────────────────────────────┘
11.2 Kconfig 文件
kconfig
# drivers/char/Kconfig 中新增以下内容:
menu "Hello Driver Test" # 菜单栏名称
config HELLO_DRV # 配置宏名(最终生成 CONFIG_HELLO_DRV)
tristate "Hello World Character Driver" # tristate = y(编入内核) / m(编译为模块) / n(不编译)
depends on ARM || X86 # 依赖的硬件架构
default m # 默认编译为模块
help # 帮助信息(在 menuconfig 中按 ? 查看)
This is a simple hello world character driver.
It demonstrates how to write a Linux char driver.
endmenu
关键关键字:
| 关键字 | 含义 |
|---|---|
tristate |
三态选项:y(编入内核)、m(编为模块)、n(不编译) |
bool |
布尔选项:y / n |
depends on |
依赖条件,条件不满足时选项隐藏 |
select |
当前选项被选中时自动选上另一个选项 |
default |
默认值 |
11.3 .config 文件
执行 make menuconfig 并勾选 hello 驱动后,.config 中会多出一行:
CONFIG_HELLO_DRV=y # 编译进内核
# 或
CONFIG_HELLO_DRV=m # 编译为模块
.config是顶层配置文件,内核顶层 Makefile 会include它。- 变量名自动加
CONFIG_前缀。
11.4 Makefile 中的编译规则
makefile
# drivers/char/Makefile 中新增:
obj-$(CONFIG_HELLO_DRV) += hello_drv.o
展开逻辑:
| .config 中的值 | $(CONFIG_HELLO_DRV) 展开为 |
等价于 |
|---|---|---|
CONFIG_HELLO_DRV=y |
y |
obj-y += hello_drv.o → 编入内核镜像 |
CONFIG_HELLO_DRV=m |
m |
obj-m += hello_drv.o → 编译为独立 .ko |
| 未定义 | (空) | 不参与编译 |
11.5 完整操作流程
bash
# 1. 将驱动源码放入内核源码树
cp hello_drv.c drivers/char/
# 2. 修改 Kconfig(添加配置项)
# 修改 Makefile(添加编译规则)
# 3. 配置内核
make ARCH=arm menuconfig
# → 进入 Device Drivers → Character devices → Hello World Character Driver
# → 按 Y(编入内核)或 M(编为模块)
# 4. 查看 .config 确认
grep CONFIG_HELLO_DRV .config
# CONFIG_HELLO_DRV=y
# 5. 编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules
# 6. 如果配置为 y,驱动已在 zImage 中,启动时自动执行 hello_init()
# 如果配置为 m,将 hello_drv.ko 拷贝到开发板,手动 insmod
十二、总线设备驱动模型(Bus-Device-Driver)
前面的字符设备驱动中,硬件信息(如寄存器基地址、中断号)硬编码在驱动里。一旦换一块开发板,驱动就得改代码重新编译------不可移植。
Linux 引入总线设备驱动模型来解决这个问题。
12.1 核心思想:分离(Separation)
┌─────────────────────────────────────────────────────┐
│ 传统写法(耦合) │
│ │
│ hello_drv.c │
│ ┌───────────────────────────────────────────────┐ │
│ │ #define LED_GPIO_BASE 0x50000000 ← 硬编码 │ │
│ │ #define LED_GPIO_PIN 17 │ │
│ │ │ │
│ │ int led_init() { │ │
│ │ 操作具体寄存器... │ │
│ │ } │ │
│ └───────────────────────────────────────────────┘ │
│ 换一块板子 → 改 GPIO → 重新编译 → 出错了! │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 总线模型(分离) │
│ │
│ led_dev.c (设备描述) led_drv.c (驱动实现) │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ .id = 1 │ │ .id_table 匹配 id=1 │ │
│ │ .irq = IRQ_GPIO │ │ .probe = led_probe() │ │
│ │ .mem_base = 0x... │ │ .remove = led_rm() │ │
│ └──────────────────┘ └──────────────────────┘ │
│ │ │ │
│ └────────┬────────────────┘ │
│ ▼ │
│ platform_bus 总线 │
│ ┌──────────────────────┐ │
│ │ 匹配成功后调用 probe │ │
│ └──────────────────────┘ │
│ 换一块板子 → 只改设备描述(dts)→ 驱动代码无需改动! │
└─────────────────────────────────────────────────────┘
dev(设备)的职责 :描述"我有什么硬件资源"------寄存器基地址、中断号、DMA 通道、时钟等。不包含任何操作逻辑。
drv(驱动)的职责 :实现"怎么操作硬件"------初始化流程、数据读写、中断处理等。不写死具体的硬件地址。
12.2 platform 总线三大核心结构体
c
// ========== 1. platform_device:描述硬件资源 ==========
struct platform_device {
const char *name; // 设备名称,用于与 driver 匹配
int id; // 设备 ID(同名设备用 id 区分)
struct device dev; // 通用设备模型
u32 num_resources;
struct resource *resource; // 硬件资源列表(IRQ、内存区域等)
};
// ========== 2. platform_driver:描述驱动逻辑 ==========
struct platform_driver {
int (*probe)(struct platform_device *); // 匹配成功后调用
int (*remove)(struct platform_device *); // 设备移除时调用
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; // 通用驱动模型
const struct platform_device_id *id_table; // 匹配表
};
// ========== 3. platform_bus_type:总线 ==========
struct bus_type platform_bus_type = {
.name = "platform",
.match = platform_match, // ← 核心:匹配函数
.uevent = platform_uevent,
// ...
};
12.3 probe 调用时机 ------ 为什么先后顺序无关?
这是总线模型最精妙的地方。platform_match() 函数会在两个方向上都执行匹配检查:
c
// platform_bus_type.match 的核心逻辑
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
// 匹配策略(按优先级排列):
// 1. OF 匹配(设备树 compatible 属性)
if (of_driver_match_device(dev, drv))
return 1;
// 2. ACPI 匹配
// 3. id_table 匹配
if (platform_match_id(pdrv->id_table, pdev) != NULL)
return 1;
// 4. name 字符串直接比较
if (strcmp(pdev->name, drv->name) == 0)
return 1;
return 0;
}
关键流程:
场景 A:先注册 device,后注册 driver
───────────────────────────────────────
platform_device_register(led_dev)
→ device_add()
→ 将 dev 挂到 platform_bus 上
→ 遍历总线上已有的 drv 链表,调用 match()...
(此时总线上还没有 driver,匹配为空,无事发生)
// ... 一段时间后 ...
platform_driver_register(led_drv)
→ driver_register()
→ 将 drv 挂到 platform_bus 上
→ 遍历总线上已有的 dev 链表,调用 match()...
★ 发现之前挂上的 led_dev 匹配成功!
→ 调用 led_drv.probe(led_dev) ← 触发 probe
场景 B:先注册 driver,后注册 device
───────────────────────────────────────
platform_driver_register(led_drv)
→ driver_register()
→ 将 drv 挂到 platform_bus 上
→ 遍历总线上已有的 dev 链表,调用 match()...
(此时总线上还没有 device,匹配为空,无事发生)
// ... 一段时间后 ...
platform_device_register(led_dev)
→ device_add()
→ 将 dev 挂到 platform_bus 上
→ 遍历总线上已有的 drv 链表,调用 match()...
★ 发现之前挂上的 led_drv 匹配成功!
→ 调用 led_drv.probe(led_dev) ← 触发 probe
结论 :无论谁先注册,总线都会在后注册的那一方到来时遍历已有的对方链表,一旦匹配成功就触发 probe。这就是"松耦合"设计的核心------device 和 driver 各自独立注册,总线负责撮合。
12.4 代码示例对比
传统写法(耦合):
c
// 换板子就要改这几行
#define LED_REG_BASE 0x01C20800 // ← 硬编码
#define LED_PIN 5 // ← 硬编码
static int led_init(void) {
// 直接用物理地址操作寄存器
writel(0x01, ioremap(LED_REG_BASE, 4096) + 0x10);
return 0;
}
总线模型写法(分离):
c
// ====== led_dev.c:设备描述 ======
#include <linux/platform_device.h>
static struct resource led_res[] = {
[0] = DEFINE_RES_MEM(0x01C20800, 4096), // 寄存器基地址
[1] = DEFINE_RES_IRQ(52), // 中断号
};
static struct platform_device led_pdev = {
.name = "my_led",
.id = 0,
.num_resources = ARRAY_SIZE(led_res),
.resource = led_res,
};
static int __init led_dev_init(void) {
return platform_device_register(&led_pdev);
}
module_init(led_dev_init);
// ====== led_drv.c:驱动实现 ======
#include <linux/platform_device.h>
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
void __iomem *base;
// ★ 从 platform_device 中获取资源,不再硬编码!
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
// 获取 IRQ
int irq = platform_get_irq(pdev, 0);
// 操作硬件...
writel(0x01, base + 0x10);
printk(KERN_INFO "[led] probed, base=%p, irq=%d\n", base, irq);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
// 清理资源
return 0;
}
static const struct platform_device_id led_id_table[] = {
{ "my_led", 0 },
{ }
};
static struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = { .name = "my_led" },
.id_table = led_id_table,
};
module_platform_driver(led_drv); // 等价于 module_init + module_exit 的封装
12.5 总线模型的核心优势
┌───────────────────────────────────────────────────────────────┐
│ 总线设备驱动模型架构优势 │
├───────────────────────────────────────────────────────────────┤
│ │
│ 1. 可移植性 │
│ 换硬件平台只需修改 dev(设备描述),drv 代码零改动。 │
│ 设备树(dts)更是将 dev 也从 C 代码中分离出来。 │
│ │
│ 2. 可配置性 │
│ 同一份 drv 可以服务多个 dev 实例(如多路串口)。 │
│ dev 通过 id / name 匹配不同的 drv。 │
│ │
│ 3. 松耦合 │
│ dev 和 drv 各自独立开发、编译、加载,互不依赖。 │
│ 注册顺序无关,总线自动撮合。 │
│ │
│ 4. 层次化抽象 │
│ 通用代码集中在 bus 层(match、uevent、电源管理), │
│ drv 和 dev 只需关注自身逻辑。 │
│ │
│ 5. 资源管理统一 │
│ 硬件资源(IRQ、MEM、DMA)通过 resource 结构统一描述, │
│ 驱动通过标准 API 获取,避免硬编码寄存器地址。 │
│ │
│ 6. 生命周期管理 │
│ probe/remove 提供明确的生命周期回调, │
│ 配合 devm_* 系列 API 实现自动资源回收。 │
│ │
└───────────────────────────────────────────────────────────────┘
十三、设备树 (Device Tree) ------ 彻底分离硬件描述
13.1 为什么需要设备树?
经过上一章的学习,我们已经用 platform_device 代替了硬编码------但 led_dev.c 仍然是 C 代码 。每换一种板子,仍然要写一个 .c 文件、编译、更新内核镜像。
# 问题场景: 同一个 I2C 触摸屏驱动,适配 3 种开发板
# 老方案 (platform_device.c):
boards/board_A.c → 描述 A 板的 I2C 地址 = 0x5D, IRQ = 88
boards/board_B.c → 描述 B 板的 I2C 地址 = 0x14, IRQ = 52
boards/board_C.c → 描述 C 板的 I2C 地址 = 0x38, IRQ = 100
# 每个板子一个 C 文件,内核源码不断膨胀!
# 新方案 (Device Tree):
board_A.dts → 描述 A 板的硬件拓扑 (文本文件)
board_B.dts → 描述 B 板的硬件拓扑 (文本文件)
board_C.dts → 描述 C 板的硬件拓扑 (文本文件)
# 驱动不变,只换 .dtb 文件!
设备树的本质 :用一种与 C 代码无关的**数据结构(FDT,Flattened Device Tree)**来描述硬件,内核启动时解析它,动态创建 platform_device。
┌──────────────────────────────────────────────────────────────┐
│ 设备树的角色 │
│ │
│ 老方案: platform_device.c (C 代码) → 编译进内核 │
│ 新方案: board.dts (文本) → dtc 编译 → board.dtb (二进制) │
│ U-Boot 将 .dtb 传给内核 │
│ 内核解析 .dtb → 自动创建 platform_device │
│ │
│ 驱动代码 = 完全不变的同一份! │
└──────────────────────────────────────────────────────────────┘
13.2 设备树源文件结构 (.dts / .dtsi)
板级 dts SoC 级 dtsi 厂商级 dtsi
───────── ──────────── ────────────
my_board.dts ←include sun8i-v3s.dtsi ←include sunxi-h3-h5.dtsi
│ │ │
│ / { │ / { │ / {
│ model = "MyBoard"; │ soc { │ cpus { ... };
│ chosen { ... }; │ i2c0: i2c@...; │ timer { ... };
│ leds { ... }; │ uart0: serial@...; │ interrupt-controller { ... };
│ &uart0 { ... }; │ }; │ clocks { ... };
│ &i2c0 { ... }; │ }; │ };
│ }; │ │
| 文件类型 | 扩展名 | 说明 |
|---|---|---|
| 设备树源文件 | .dts |
描述一个具体板子的硬件(板级) |
| 设备树包含文件 | .dtsi |
描述 SoC 公共部分(SoC 级),可被多个 .dts 引用 |
| 设备树二进制 | .dtb |
.dts 经 DTC 编译后的二进制,由 U-Boot 传给内核 |
13.3 设备树基本语法
dts
/dts-v1/;
/ {
model = "My Test Board";
compatible = "myboard,test", "vendor,test-soc";
#address-cells = <1>;
#size-cells = <1>;
/* 内存节点 */
memory@40000000 {
device_type = "memory";
reg = <0x40000000 0x10000000>; /* 256MB 起始地址 0x40000000 */
};
/* 一个 LED 设备 (platform_device) */
leds {
compatible = "gpio-leds";
led0 {
label = "led0";
gpios = <&gpio 3 17 0>; /* GPIO 控制器句柄, pin=17, flags=0 */
};
};
/* SoC 片上外设总线 */
soc {
compatible = "simple-bus";
/* UART 控制器 */
uart0: serial@01C28000 {
compatible = "snps,dw-apb-uart";
reg = <0x01C28000 0x400>; /* 寄存器基址, 大小 */
interrupts = <0 1 4>; /* 中断号 */
clocks = <&clk 0>; /* 时钟 */
status = "okay";
};
/* I2C 控制器 */
i2c0: i2c@01C2AC00 {
compatible = "vendor,i2c-v1";
reg = <0x01C2AC00 0x400>;
interrupts = <0 6 4>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* ★ I2C 从设备挂载在 I2C 控制器下 */
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>; /* I2C 从设备地址 */
};
};
/* SPI 控制器 */
spi0: spi@01C68000 {
compatible = "vendor,spi-v1";
reg = <0x01C68000 0x1000>;
interrupts = <0 10 4>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* ★ SPI 从设备挂载在 SPI 控制器下 */
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>; /* SPI 片选号 */
spi-max-frequency = <50000000>;
};
};
/* GPIO 控制器 */
gpio: gpio@01C20800 {
compatible = "vendor,gpio-v1";
reg = <0x01C20800 0x400>;
interrupts = <0 11 4>;
gpio-controller;
#gpio-cells = <3>;
};
};
/* 板级配置:覆盖 SoC 级节点 */
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
};
};
/* ★ 引用 SoC 级节点,板级补充配置 */
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pins>;
status = "okay";
};
&i2c0 {
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins>;
clock-frequency = <400000>;
};
关键语法速查:
| 语法 | 含义 | 示例 |
|---|---|---|
/dts-v1/; |
DTS 版本声明(必须) | /dts-v1/; |
/ { }; |
根节点 | / { ... }; |
compatible |
核心匹配属性 ,格式 "厂商,型号" |
compatible = "atmel,24c02"; |
reg |
寄存器/地址信息,长度由 #address-cells / #size-cells 定义 |
reg = <0x01C28000 0x400>; |
&label |
引用已定义的节点(追加/修改属性) | &uart0 { status = "okay"; }; |
label: name@addr |
定义带标签的节点 | i2c0: i2c@01C2AC00 |
status |
设备启用状态:"okay" / "disabled" |
status = "okay"; |
#address-cells |
reg 属性中地址占几个 32 位单元 |
#address-cells = <1>; |
#size-cells |
reg 属性中长度占几个 32 位单元 |
#size-cells = <1>; |
interrupts |
中断号(格式由中断控制器决定) | interrupts = <0 1 4>; |
phandle / &label |
节点间引用(句柄),替代硬编码的设备 ID | gpios = <&gpio 3 17 0>; |
13.4 五种匹配模式详解
在 platform_match() 中,驱动与设备的匹配按优先级从高到低分为五种:
platform_match(dev, drv)
│
├── ① OF 匹配 (of_match_table) 优先级最高
│ 驱动 of_match_table 中的 .compatible 与 设备树节点 compatible 比较
│ ★ 设备树时代的首选匹配方式
│
├── ② ACPI 匹配 优先级次之
│ x86 PC 平台使用,ARM 嵌入式一般不用
│
├── ③ id_table 匹配 优先级第三
│ 驱动 id_table[] 中 .name 与 平台设备 pdev->name 比较
│
├── ④ name 字符串匹配 优先级第四
│ 驱动 drv->name 与 平台设备 pdev->name 直接 strcmp()
│
└── ⑤ override (sysfs driver_override) 优先级最低但可强制
通过 sysfs 手动指定匹配关系,覆盖前面所有规则
模式① --- OF 匹配(Device Tree)★ 最常用:
c
// 驱动侧:定义 of_match_table
static const struct of_device_id mydrv_of_match[] = {
{ .compatible = "vendor,mydrv-v1" },
{ .compatible = "vendor,mydrv-v2" }, // 可匹配多个版本
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, mydrv_of_match);
static struct platform_driver mydrv = {
.driver = {
.name = "mydrv",
.of_match_table = mydrv_of_match, // ← 绑定 OF 匹配表
},
.probe = mydrv_probe,
.remove = mydrv_remove,
};
dts
// 设备树侧:设备节点定义 compatible
/ {
mydevice {
compatible = "vendor,mydrv-v1"; // ← 与驱动的 of_match_table 匹配
reg = <0x01C00000 0x1000>;
interrupts = <0 5 4>;
};
};
匹配逻辑 :of_match_device() 遍历 mydrv_of_match[],找 .compatible 与设备树节点 compatible 属性相同的条目。
模式③ --- id_table 匹配:
c
static const struct platform_device_id mydrv_id_table[] = {
{ "mydrv,a", 0 },
{ "mydrv,b", 1 },
{ }
};
static struct platform_driver mydrv = {
.id_table = mydrv_id_table, // ← 绑定 id_table
.driver = { .name = "mydrv" },
.probe = mydrv_probe,
};
- 匹配时比较
id_table[i].name与pdev->name。
模式④ --- name 匹配:
c
// 驱动: drv->name = "mydrv"
// 设备: pdev->name = "mydrv"
// → strcmp("mydrv", "mydrv") == 0 → 匹配成功
模式⑤ --- driver_override(sysfs 强制匹配):
bash
# 将某个已注册的 platform_device 强制绑定到指定 driver
echo "mydrv" > /sys/devices/platform/mydevice/driver_override
echo "mydevice" > /sys/bus/platform/drivers/mydrv/bind
13.5 设备树的编译与反编译
设备树编译器:DTC (Device Tree Compiler)
.dts (文本) .dtb (二进制)
┌──────────────┐ dtc ┌──────────────┐
│ / { │ ─────────→ │ 0xD00DFEED │
│ compatible │ dtc -I dts │ (魔数) │
│ = "xx,xx"; │ -O dtb │ 结构化 │
│ ... │ │ 二进制数据 │
│ }; │ └──────┬───────┘
└──────────────┘ │
│ dtc -I dtb -O dts (反编译)
▼
┌──────────────┐
│ 还原为 .dts │
└──────────────┘
常用命令:
bash
# 编译 .dts → .dtb
dtc -I dts -O dtb -o output.dtb input.dts
# 反编译 .dtb → .dts (查看/修改厂家提供的 dtb)
dtc -I dtb -O dts -o output.dts input.dtb
# 内核源码中编译设备树 (通过 Kbuild)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs
# 所有 .dts 会被批量编译为 .dtb
# 开发板上的调试
cat /proc/device-tree/model # 查看根节点 model 属性
ls /sys/firmware/devicetree/base/ # 查看整个设备树
dtc -I fs -O dts /sys/firmware/devicetree/base # 从运行中的内核导出设备树
13.6 设备树如何实现 drv 层彻底分离
以 I2C 设备驱动 为例,说明设备树如何让 drv 完全不依赖板级硬件细节:
┌─────────────────────────────────────────────────────────────────┐
│ I2C 触摸屏驱动 (drv 层) │
│ │
│ touchscreen_drv.c │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ static const struct of_device_id ts_of_match[] = { ││
│ │ { .compatible = "goodix,gt911" }, ││
│ │ { } ││
│ │ }; ││
│ │ ││
│ │ static int ts_probe(struct i2c_client *client, ...) { ││
│ │ // ★ 不硬编码 I2C 地址、IRQ、GPIO! ││
│ │ int irq = client->irq; // 来自设备树 ││
│ │ int addr = client->addr; // 来自设备树 reg ││
│ │ struct gpio_desc *rst = ││
│ │ devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW);// 来自设备树 │
│ │ ... ││
│ │ } ││
│ │ ││
│ │ static struct i2c_driver ts_drv = { ││
│ │ .driver = { ││
│ │ .name = "gt911", ││
│ │ .of_match_table = ts_of_match, ││
│ │ }, ││
│ │ .probe = ts_probe, ││
│ │ }; ││
│ │ module_i2c_driver(ts_drv); ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ 这份驱动在 A 板、B 板、C 板上 = 完全相同! │
└─────────────────────────────────────────────────────────────────┘
▲
│ compatible 匹配
│
┌─────────────────────────────────────────────────────────────────┐
│ 设备树 (dev 层描述) │
│ │
│ board_A.dts: board_B.dts: │
│ &i2c1 { &i2c0 { │
│ touchscreen@5d { touchscreen@14 { │
│ compatible = "..."; compatible = "..."; │
│ reg = <0x5D>; ← 不同地址 │
│ interrupt-parent = <&gpio>; │
│ interrupts = <7 2>; ← 不同中断 │
│ reset-gpios = <&gpio 3 17 0>; │
│ }; }; │
│ }; }; │
│ │
│ 换板子 = 换 .dtb,驱动 .ko 无需改动! │
└─────────────────────────────────────────────────────────────────┘
13.7 SPI 控制器驱动的分离示例
c
// ====== spi_master_drv.c (不需要知道具体挂载了哪些从设备) ======
static int spi_master_probe(struct platform_device *pdev)
{
struct spi_master *master;
void __iomem *base;
// 从设备树 (reg 属性) 获取物理地址 → ioremap
base = devm_platform_ioremap_resource(pdev, 0);
int irq = platform_get_irq(pdev, 0);
// 分配 SPI 控制器
master = devm_spi_alloc_master(&pdev->dev, sizeof(...));
// 设置控制器操作函数 (硬件无关的抽象)
master->transfer_one = my_spi_transfer_one; // ← 实现 SPI 字节级的收发
master->bits_per_word_mask = SPI_BPW_MASK(8);
// 注册 SPI 控制器,内核会自动扫描该总线下的从设备
return devm_spi_register_master(&pdev->dev, master);
}
dts
// 设备树中描述 SPI 从设备:
&spi0 {
status = "okay";
flash@0 {
compatible = "jedec,spi-nor"; // 匹配 spi-nor 驱动
reg = <0>;
spi-max-frequency = <104000000>;
};
display@1 {
compatible = "ilitek,ili9341"; // 匹配 ili9341 驱动
reg = <1>;
spi-max-frequency = <32000000>;
dc-gpios = <&gpio 4 24 0>;
};
};
关键思想:
- SPI 控制器驱动只管 SPI 时序和协议(clock、CS、数据移位),不知道上面挂的是什么从设备。
- 每个从设备有自己独立的驱动(spi-nor.c、ili9341.c),它们都调用通用的
spi_write()/spi_read()接口。 - 设备和驱动的绑定关系完全由设备树
compatible决定。
13.8 设备树总结对比
| 对比维度 | 旧方案 (platform_device.c) | 新方案 (Device Tree) |
|---|---|---|
| 硬件描述方式 | C 代码结构体 | DTS 文本文件 |
| 修改硬件信息 | 改 .c → 编译 → 更新 zImage | 改 .dts → dtc → 更新 .dtb |
| 内核镜像 | 每个板子不同 | 同一镜像 + 不同 .dtb |
| 可读性 | C 语法,嵌在代码中 | 树形文本,清晰直观 |
| 驱动可移植性 | dev 是 C 代码,未必同级抽象 | 驱动零依赖板级细节 |
| 多人协作 | 改同一份 C 代码易冲突 | 每个人改自己的 .dts 文件 |
十四、pinctrl 与 GPIO 子系统
14.1 为什么 SoC 引脚需要"配置"?
现代 SoC 的一个物理引脚(ball/pad)往往可以复用作多种功能:
┌──────────────────────────────────┐
│ SoC 一个物理引脚 (如 PA3) │
│ │
│ 功能0: GPIO (通用输入输出) │
│ 功能1: UART2_TX │
│ 功能2: I2C1_SCL │
│ 功能3: SPI0_MOSI │
│ 功能4: PWM1_OUT │
│ 功能5: TIM2_CH1 │
│ │
│ ★ 由引脚复用寄存器决定当前功能 │
└──────────────────────────────────┘
除了功能选择(MUX),引脚还需要配置电气属性:
| 配置项 | 含义 | 典型值 |
|---|---|---|
| pull-up / pull-down | 上下拉电阻 | 上拉 47KΩ、下拉 100KΩ |
| drive-strength | 驱动能力(输出电流) | 2mA / 4mA / 8mA / 12mA |
| slew-rate | 信号翻转速率(快/慢) | fast / slow |
| schmitt-trigger | 施密特触发器(输入去抖) | enable / disable |
| open-drain | 开漏输出 | enable / disable |
pinctrl 子系统的职责:将设备树中描述的引脚配置翻译为对引脚复用寄存器(IOMUXC / PIO / SYSCFG)的实际写入操作。
14.2 设备树中的 pinctrl 典型结构
dts
// ====== SoC 级 .dtsi 中定义 pinctrl 控制器节点 ======
pinctrl: pinctrl@01C20800 {
compatible = "...";
reg = <0x01C20800 0x400>;
#address-cells = <1>;
#size-cells = <1>;
/* ★ 每一组引脚配置定义为一个子节点 */
uart0_pins: uart0-pins {
pins = "PA4", "PA5"; // 使用哪些物理引脚
function = "uart0"; // 复用功能名
bias-pull-up; // 电气属性:上拉
drive-strength = <8>; // 驱动能力:8mA
};
i2c0_pins: i2c0-pins {
pins = "PB6", "PB7";
function = "i2c0";
bias-pull-up;
drive-strength = <6>;
};
spi0_pins: spi0-pins {
pins = "PC0", "PC1", "PC2", "PC3";
function = "spi0";
bias-pull-down;
drive-strength = <10>;
};
/* 同一引脚可定义多组配置,用于不同运行状态 */
mmc0_pins_default: mmc0-pins-default {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
drive-strength = <12>;
bias-pull-up;
};
mmc0_pins_sleep: mmc0-pins-sleep {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "gpio_in"; // 休眠时切回 GPIO 输入,省电
bias-pull-down;
};
};
// ====== 板级 .dts 中引用 pinctrl 配置 ======
&uart0 {
pinctrl-names = "default"; // 命名状态:default
pinctrl-0 = <&uart0_pins>; // 绑定 default 状态的引脚配置
status = "okay";
};
&mmc0 {
pinctrl-names = "default", "sleep"; // 定义两种状态
pinctrl-0 = <&mmc0_pins_default>; // 工作时使用 default 配置
pinctrl-1 = <&mmc0_pins_sleep>; // 休眠时使用 sleep 配置
status = "okay";
};
14.3 default 状态与 sleep 状态
设备工作时 设备休眠时
┌─────────────────┐ ┌─────────────────┐
│ pinctrl-0 │ │ pinctrl-1 │
│ (default) │ 切换 │ (sleep) │
│ │ ────────→ │ │
│ 功能: MMC0 │ ←──────── │ 功能: GPIO 输入 │
│ 驱动: 12mA │ 唤醒 │ 驱动: 2mA │
│ 上下拉: 上拉 │ │ 上下拉: 下拉 │
└─────────────────┘ └─────────────────┘
为什么需要 sleep 状态?
- 外设休眠时高速引脚不再需要,切回普通 GPIO 输入可省电。
- 某些引脚需要在休眠时保持固定电平(下拉/上拉),避免悬空引脚产生漏电流。
- 驱动强度降低可进一步减少功耗。
内核自动切换时机:
default → sleep:驱动调用pm_runtime_put()或系统进入 suspend。sleep → default:驱动调用pm_runtime_get()或系统 resume。
c
// 驱动侧通过 pinctrl API 手动控制
#include <linux/pinctrl/consumer.h>
struct pinctrl *p;
struct pinctrl_state *default_state, *sleep_state;
p = devm_pinctrl_get(dev);
default_state = pinctrl_lookup_state(p, "default");
sleep_state = pinctrl_lookup_state(p, "sleep");
// 进入休眠
pinctrl_select_state(p, sleep_state);
// 唤醒
pinctrl_select_state(p, default_state);
14.4 厂商 pinctrl 格式对比
① ST (意法半导体 STM32MP1) 格式
dts
// STM32MP1 的 pinctrl 使用 "引脚编号 + 复用功能 + 电气属性" 三元组
// 引脚编号: PZ_bank(ABCD...).pin, 如 PA0 = 0, PZ0 = 25*16+0 = 400
&pinctrl {
uart4_pins_a: uart4-0 {
pins1 {
pinmux = <STM32_PINMUX('B', 2, AF8)>; // PB2 → UART4_RX (AF8)
bias-pull-down;
};
pins2 {
pinmux = <STM32_PINMUX('G', 11, AF6)>; // PG11 → UART4_TX (AF6)
bias-disable; // 无上下拉
drive-push-pull; // 推挽输出
slew-rate = <0>; // 慢速翻转
};
};
i2c1_pins_a: i2c1-0 {
pins {
pinmux = <STM32_PINMUX('D', 12, AF5>, // SCL
<STM32_PINMUX('F', 15, AF5>>; // SDA
bias-pull-up;
drive-open-drain; // I2C 必须开漏
slew-rate = <0>;
};
};
};
// STM32_PINMUX(bank, pin, af) 宏解析:
// bank: GPIO 端口, 'A'~'Z'
// pin: 引脚编号 0~15
// af: 复用功能编号 (AF0~AF15)
ST 格式特点:
- 使用宏
STM32_PINMUX()打包引脚信息,编译时计算寄存器偏移。 - 每个 pinmux 条目 =
(bank << 8) | (pin << 4) | af。 - 电气属性用标准 DTS 属性(
bias-pull-up、drive-push-pull等)。 - 可分组定义(
pins1、pins2),同一组内引脚配置相同。
② NXP (恩智浦 i.MX 系列) 格式
dts
// i.MX6/6ULL/8M 系列使用 IOMUXC 控制器
// 格式: 寄存器偏移 + 电气配置值
&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1B0B1
MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x0B0A5
>;
};
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_CSI_DATA00__I2C1_SCL 0x4001B8B0
MX6UL_PAD_CSI_DATA01__I2C1_SDA 0x4001B8B0
>;
};
pinctrl_usdhc1: usdhc1grp {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x17059
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x10071
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x17059
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x17059
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x17059
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x17059
>;
};
/* sleep 态配置:降低驱动能力 + 关闭上下拉 */
pinctrl_usdhc1_100mhz: usdhc1grp100mhz {
fsl,pins = <
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x170B9
MX6UL_PAD_SD1_CLK__USDHC1_CLK 0x100B9
MX6UL_PAD_SD1_DATA0__USDHC1_DATA0 0x170B9
MX6UL_PAD_SD1_DATA1__USDHC1_DATA1 0x170B9
MX6UL_PAD_SD1_DATA2__USDHC1_DATA2 0x170B9
MX6UL_PAD_SD1_DATA3__USDHC1_DATA3 0x170B9
>;
};
};
// 在板级 dts 中引用
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
i.MX 宏字段解析:
MX6UL_PAD_SD1_CMD__USDHC1_CMD 0x1 7 0 5 9
│ │ │ │ │ │ │ └── SPEED (速度等级)
│ │ │ │ │ │ └──── DSE (驱动强度)
│ │ │ │ │ └─────── PUE (上拉/保持使能)
│ │ │ │ └────────── PUS (上下拉选择)
│ │ │ └────────────── HYS (施密特触发)
│ │ └──────────────────────── 复用功能目标名
│ └──────────────────────────────── 引脚名 (宏前半部分)
└────────────────────────────────────────────── SoC 型号前缀
i.MX 格式特点:
- 每个条目 =
宏(引脚_复用功能) + 32位配置值。 - 宏名称直接表明"哪个引脚 复用为 什么功能",非常直观。
- 电气配置打包成一个 32 位十六进制数,需要查数据手册解析各字段。
- 配置值直接写入 IOMUXC 的
SW_PAD_CTL寄存器。
③ Allwinner (全志 sunxi) 格式
dts
// 全志 sunxi 系列使用 PIO 控制器
// 格式: 引脚组 + 复用功能 + 电气属性
&pio {
uart0_pins_a: uart0-pins {
pins = "PB4", "PB5"; // 引脚名
function = "uart0"; // 功能名
bias-pull-up;
};
uart0_pins_b: uart0-pins-1 { // 同一功能可有多组可选引脚
pins = "PF0", "PF1";
function = "uart0";
bias-pull-up;
};
i2c0_pins: i2c0-pins {
pins = "PH2", "PH3";
function = "i2c0";
bias-pull-up;
drive-strength = <20>; // sunxi 驱动强度单位 mA
};
mmc0_pins_a: mmc0-pins {
pins = "PF0", "PF1", "PF2", "PF3", "PF4", "PF5";
function = "mmc0";
drive-strength = <40>;
bias-pull-up;
};
spi0_pins_a: spi0-pins {
pins = "PC0", "PC1", "PC2", "PC3";
function = "spi0";
drive-strength = <20>;
bias-pull-down;
};
};
Allwinner 格式特点:
- 直接用字符串引脚名
"PA0"~"PZ15",无需宏。 function用字符串名如"uart0"、"i2c0",与数据手册一致。drive-strength直接写 mA 值(20、30、40)。- 同一功能可有多组备选引脚(如
uart0_pins_a、uart0_pins_b),板级 dts 自由选择。 - 结构最接近"标准" pinctrl 绑定,可读性最好。
④ Rockchip (瑞芯微 RK3399 / RK3568) 格式
dts
// Rockchip 使用 pinctrl 驱动解析 "引脚 + 功能 + 电气属性"
// 内核驱动: drivers/pinctrl/pinctrl-rockchip.c
// 引脚编号: 每组 32 个引脚, GPIO0_A0 = 0, GPIO0_B0 = 8, GPIO1_A0 = 32...
&pinctrl {
/* ===== UART 配置 ===== */
uart2a_xfer: uart2a-xfer {
rockchip,pins =
<0 RK_PB0 1 &pcfg_pull_up>, // GPIO0_B0 → UART2_TX, func=1, 上拉
<0 RK_PB1 1 &pcfg_pull_up>; // GPIO0_B1 → UART2_RX, func=1, 上拉
};
/* ===== I2C 配置 (I2C 必须共享同一电气配置节点) ===== */
i2c1_xfer: i2c1-xfer {
rockchip,pins =
<2 RK_PC0 1 &pcfg_pull_none_smt>, // GPIO2_C0 → I2C1_SCL, 无上下拉, 施密特
<2 RK_PC1 1 &pcfg_pull_none_smt>; // GPIO2_C1 → I2C1_SDA
};
/* ===== SPI 配置 ===== */
spi1_clk: spi1-clk {
rockchip,pins =
<1 RK_PA7 1 &pcfg_pull_up>; // GPIO1_A7 → SPI1_CLK
};
spi1_cs0: spi1-cs0 {
rockchip,pins =
<1 RK_PB0 1 &pcfg_pull_up>; // GPIO1_B0 → SPI1_CS0
};
spi1_tx: spi1-tx {
rockchip,pins =
<1 RK_PA5 1 &pcfg_pull_up_8ma>;// GPIO1_A5 → SPI1_TX, 8mA 驱动
};
spi1_rx: spi1-rx {
rockchip,pins =
<1 RK_PA6 1 &pcfg_pull_up>; // GPIO1_A6 → SPI1_RX
};
// 将分散的 SPI 引脚合并为一组,便于外设节点引用
spi1_pins: spi1-pins {
rockchip,pins =
<1 RK_PA7 1 &pcfg_pull_up>,
<1 RK_PA5 1 &pcfg_pull_up_8ma>,
<1 RK_PA6 1 &pcfg_pull_up>,
<1 RK_PB0 1 &pcfg_pull_up>;
};
/* ===== SDMMC (eMMC / SD 卡) 配置 ===== */
sdmmc_bus4: sdmmc-bus4 {
rockchip,pins =
<4 RK_PC0 1 &pcfg_pull_up_8ma>, // D0
<4 RK_PC1 1 &pcfg_pull_up_8ma>, // D1
<4 RK_PC2 1 &pcfg_pull_up_8ma>, // D2
<4 RK_PC3 1 &pcfg_pull_up_8ma>; // D3
};
sdmmc_clk: sdmmc-clk {
rockchip,pins =
<4 RK_PC4 1 &pcfg_pull_none_12ma>; // CLK, 12mA 驱动
};
sdmmc_cmd: sdmmc-cmd {
rockchip,pins =
<4 RK_PC5 1 &pcfg_pull_up_8ma>; // CMD
};
/* ===== GMAC (千兆以太网) 配置 ===== */
gmac_rgmii_pins: gmac-rgmii-pins {
rockchip,pins =
/* RGMII 接口, func=1, 12mA 驱动 */
<3 RK_PA4 1 &pcfg_pull_none_12ma>, // TXD0
<3 RK_PA5 1 &pcfg_pull_none_12ma>, // TXD1
<3 RK_PA6 1 &pcfg_pull_none_12ma>, // TXD2
<3 RK_PA7 1 &pcfg_pull_none_12ma>, // TXD3
<3 RK_PB0 1 &pcfg_pull_none_12ma>, // TXC
<3 RK_PB1 1 &pcfg_pull_none_12ma>, // TX_CTL
<3 RK_PB2 1 &pcfg_pull_none_12ma>, // RXD0
<3 RK_PB3 1 &pcfg_pull_none_12ma>, // RXD1
<3 RK_PB4 1 &pcfg_pull_none_12ma>, // RXD2
<3 RK_PB5 1 &pcfg_pull_none_12ma>, // RXD3
<3 RK_PB6 1 &pcfg_pull_none_12ma>, // RXC
<3 RK_PB7 1 &pcfg_pull_none_12ma>; // RX_CTL
};
};
// ====== 板级 dts 中引用 ======
&uart2 {
pinctrl-names = "default";
pinctrl-0 = <&uart2a_xfer>;
status = "okay";
};
&sdmmc {
pinctrl-names = "default";
pinctrl-0 = <&sdmmc_clk>, <&sdmmc_cmd>, <&sdmmc_bus4>; // ★ 组合多个 pin 节点
status = "okay";
};
RK3399 pinctrl 宏字段解析:
rockchip,pins = <bank pin func &pcfg_xxx>;
│ │ │ │
│ │ │ └── 电气属性配置节点 (&pcfg_pull_up, &pcfg_pull_none_12ma...)
│ │ └───────── 复用功能编号 (0=GPIO, 1=外设功能1, 2=外设功能2...)
│ └───────────── 引脚在 GPIO bank 内的编号 (RK_PA0=0, RK_PA1=1...RK_PD7=31)
└────────────────── GPIO bank 编号 (0=GPIO0, 1=GPIO1, 2=GPIO2, 3=GPIO3, 4=GPIO4)
RK3399 电气属性预定义节点 (SoC 级 .dtsi 中定义):
dts
// 内核源码: include/dt-bindings/pinctrl/rockchip.h
// SoC 级 .dtsi 中预定义的标准电气配置节点
pcfg_pull_up: pcfg-pull-up {
bias-pull-up; // 上拉
};
pcfg_pull_down: pcfg-pull-down {
bias-pull-down; // 下拉
};
pcfg_pull_none: pcfg-pull-none {
bias-disable; // 无上下拉
};
pcfg_pull_none_12ma: pcfg-pull-none-12ma {
bias-disable;
drive-strength = <12>; // 12mA 驱动
};
pcfg_pull_none_8ma: pcfg-pull-none-8ma {
bias-disable;
drive-strength = <8>;
};
pcfg_pull_up_8ma: pcfg-pull-up-8ma {
bias-pull-up;
drive-strength = <8>;
};
pcfg_pull_none_smt: pcfg-pull-none-smt {
bias-disable;
input-schmitt-enable; // ★ 施密特触发器 (I2C 推荐)
};
Rockchip 格式特点:
- 每条
rockchip,pins是一个四元组:<bank, pin, function, electrical_config>。 bank是 GPIO 控制器编号(0~4),pin是组内偏移,function是复用功能号。- 电气属性通过 phandle 引用 预定义配置节点(
&pcfg_xxx),高度复用,避免重复写电气参数。 - 可以对同一外设的不同引脚分别指定不同驱动强度(如 SDMMC 的 CLK 用 12mA,数据线用 8mA)。
- 版级
.dts引用时可以将多个 pin 节点组合(pinctrl-0 = <&sdmmc_clk>, <&sdmmc_cmd>, <&sdmmc_bus4>)。
⑤ 四家厂商对比总结
┌──────────┬──────────────────┬──────────────────┬──────────────────────────┐
│ 厂商 │ 引脚标识方式 │ 复用功能表示 │ 电气属性表示 │
├──────────┼──────────────────┼──────────────────┼──────────────────────────┤
│ ST │ 宏: STM32_PINMUX │ AF编号 (AF0-AF15) │ DTS标准属性 │
│(STM32MP1)│ ('B', 2, AF8) │ │ bias-pull-up等 │
├──────────┼──────────────────┼──────────────────┼──────────────────────────┤
│ NXP │ 宏: MX6UL_PAD_ │ 宏后半部分功能名 │ 32位hex配置值 │
│(i.MX) │ NAME__FUNC │ (__USDHC1_CMD) │ 0x17059 │
├──────────┼──────────────────┼──────────────────┼──────────────────────────┤
│ Allwinner│ 字符串: "PB7" │ 字符串: "uart0" │ DTS标准属性 │
│(sunxi) │ │ "i2c0" "spi0" │ drive-strength=<20> │
├──────────┼──────────────────┼──────────────────┼──────────────────────────┤
│ Rockchip │ 四元组: <bank, │ 数字编号 (0/1/2..)│ phandle 引用预定义节点 │
│(RK3399等)│ pin, func, cfg> │ 0=GPIO,1=func1.. │ &pcfg_pull_up_8ma │
└──────────┴──────────────────┴──────────────────┴──────────────────────────┘
14.5 GPIO 子系统 ------ 驱动中操作引脚
pinctrl 只负责"配置引脚模式",要在驱动代码中拉高/拉低/读取 GPIO,需要 GPIO 子系统。
旧 API(不推荐):
c
#include <linux/gpio.h>
// 整型编号,已废弃
int gpio = 17;
gpio_request(gpio, "my_gpio");
gpio_direction_output(gpio, 1);
gpio_set_value(gpio, 0);
gpio_free(gpio);
新 API(gpiod,推荐):
c
#include <linux/gpio/consumer.h>
struct gpio_desc *reset_gpio, *power_gpio, *irq_gpio;
// 从设备树获取 GPIO(名称需匹配 "-gpios" 属性)
reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); // 初始输出低
power_gpio = devm_gpiod_get_optional(dev, "power", GPIOD_OUT_HIGH); // 可选
irq_gpio = devm_gpiod_get(dev, "irq", GPIOD_IN); // 输入
if (IS_ERR(reset_gpio))
return PTR_ERR(reset_gpio);
// 操作 GPIO
gpiod_set_value(reset_gpio, 1); // 拉高
gpiod_set_value(reset_gpio, 0); // 拉低
int val = gpiod_get_value(irq_gpio); // 读取电平
// devm_ 前缀表示设备移除时自动释放,无需手动 free
对应的设备树:
dts
my_device {
compatible = "vendor,mydev";
reg = <0x01C00000 0x1000>;
reset-gpios = <&pio 2 17 GPIO_ACTIVE_LOW>; // PE17,低电平有效
power-gpios = <&pio 3 5 GPIO_ACTIVE_HIGH>; // PF5,高电平有效
irq-gpios = <&pio 1 10 GPIO_ACTIVE_HIGH>; // PB10
};
GPIO 标志位:
| 标志 | 含义 |
|---|---|
GPIO_ACTIVE_HIGH |
高电平有效(逻辑 1 = 物理高电平) |
GPIO_ACTIVE_LOW |
低电平有效(逻辑 1 = 物理低电平,gpiod API 自动翻转) |
GPIO_OPEN_DRAIN |
开漏输出 |
GPIO_OPEN_SOURCE |
开源输出 |
GPIO_PULL_UP |
内部上拉 |
GPIO_PULL_DOWN |
内部下拉 |
14.6 完整示例:一个需要 pinctrl + GPIO 的 SPI 设备驱动
c
// ====== spi_dev_drv.c ======
#include <linux/spi/spi.h>
#include <linux/gpio/consumer.h>
struct my_chip {
struct spi_device *spi;
struct gpio_desc *rst_gpio;
struct gpio_desc *dc_gpio; // Data/Command 选择线
};
static int my_spi_probe(struct spi_device *spi)
{
struct my_chip *chip;
// ★ pinctrl 已由内核在 probe 前自动设置为 "default" 状态
// 无需手动操作 pinctrl
// 获取 GPIO 资源
chip->rst_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(chip->rst_gpio))
return PTR_ERR(chip->rst_gpio);
chip->dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW);
if (IS_ERR(chip->dc_gpio))
return PTR_ERR(chip->dc_gpio);
// 硬件复位时序
gpiod_set_value(chip->rst_gpio, 0); // 拉低复位
msleep(10);
gpiod_set_value(chip->rst_gpio, 1); // 释放复位
msleep(50);
// SPI 数据传输(使用标准 SPI API)
spi_write(spi, init_seq, sizeof(init_seq));
return 0;
}
static int my_spi_remove(struct spi_device *spi)
{
// devm_ 资源自动释放,无需手动清理
return 0;
}
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "vendor,my-spi-device" },
{ }
};
static struct spi_driver my_spi_drv = {
.probe = my_spi_probe,
.remove = my_spi_remove,
.driver = {
.name = "my_spi_dev",
.of_match_table = my_spi_of_match,
},
};
module_spi_driver(my_spi_drv);
对应的设备树:
dts
&spi0 {
pinctrl-names = "default"; // ★ probe 前内核自动配置引脚
pinctrl-0 = <&spi0_pins>; // SPI0: CLK/MOSI/MISO/CS
status = "okay";
mydev@0 {
compatible = "vendor,my-spi-device";
reg = <0>;
spi-max-frequency = <32000000>;
reset-gpios = <&pio 4 17 GPIO_ACTIVE_LOW>; // PE17
dc-gpios = <&pio 4 18 GPIO_ACTIVE_HIGH>; // PE18
};
};
14.7 pinctrl + GPIO 全流程总结
设备树 (.dts) 内核行为 驱动代码 (.c)
─────────── ──────── ────────────
pinctrl@... { ┌─────────────────────┐
uart0_pins { ... }; │ pinctrl 子系统 │
}; │ 解析设备树 pin 配置 │
│ 写入 IOMUXC/PIO 寄存器│
&uart0 { └─────────┬───────────┘
pinctrl-0 = <&uart0_pins>; │
status = "okay"; │ 在 driver probe 之前
}; │ 自动设置引脚为 UART 功能
▼
&gpio { 引脚就绪,UART 功能可用
reset-gpios = <&pio 2 5 GPIO_ACTIVE_LOW>;
}; │
▼
┌─────────────────────┐
│ GPIO 子系统 │
│ gpiod_get("reset") │ ← devm_gpiod_get()
│ gpiod_set_value(0) │ ← 驱动主动调用
└─────────────────────┘
关键理解:
- pinctrl 是自动的------内核在 probe 前根据设备树自动配置引脚复用和电气属性。
- GPIO 是手动的------驱动代码通过 gpiod API 主动控制电平。
- 设备树中的
reset-gpios是在描述"需要被驱动代码控制的 GPIO",与 pinctrl 描述的"该外设用到的所有引脚如何复用"是不同层面的配置。
十五、中断子系统与按键驱动实战
15.1 驱动只提供能力,不提供策略
这是 Linux 驱动开发的一条核心设计原则:
┌──────────────────────────────────────────────────────────────────┐
│ │
│ 驱动 (Driver) 应用 (Application) │
│ ──────────── ──────────────── │
│ │
│ ✓ 检测按键是否按下 ✓ 决定按下了做什么 │
│ ✓ 读取 ADC 原始值 ✓ 决定阈值是多少 │
│ ✓ 提供 SPI 收发接口 ✓ 决定发什么数据、何时发 │
│ ✓ 通知数据就绪 ✓ 决定读不读、何时读 │
│ │
│ ✗ 不判断"按下几次算双击" ←──── 这是策略 │
│ ✗ 不决定 ADC > 3000 算亮灯 ←──── 这是策略 │
│ ✗ 不定时发送心跳包 ←──── 这是策略 │
│ │
└──────────────────────────────────────────────────────────────────┘
驱动的职责 :暴露硬件能力(寄存器读写、中断通知、DMA 传输)。
应用的职责:决定如何使用这些能力(业务逻辑、时序策略、状态机)。
以按键为例,驱动需要提供的是"当按键状态变化时,让应用知道"的能力。至于"应用知道了之后是开灯还是关灯",那是应用的事。
15.2 四种交互模式总览
应用读取按键状态,可以有四种方式:
┌────────────────┬──────────────────┬─────────────────────────────┐
│ 交互方式 │ 行为 │ 适用场景 │
├────────────────┼──────────────────┼─────────────────────────────┤
│ ① 非阻塞轮询 │ 立即返回,无数据 │ APP 有主循环,不能卡住 │
│ (O_NONBLOCK) │ 返回 -EAGAIN │ │
├────────────────┼──────────────────┼─────────────────────────────┤
│ ② 阻塞等待 │ 无数据时休眠 │ APP 只关心按键,可以卡住 │
│ (默认) │ 有数据时唤醒返回 │ 简单 shell 命令测试常用 │
├────────────────┼──────────────────┼─────────────────────────────┤
│ ③ poll/select │ 同时监听多个设备 │ GUI 事件循环 / 网络服务器 │
│ (多路复用) │ 哪个就绪读哪个 │ │
├────────────────┼──────────────────┼─────────────────────────────┤
│ ④ 异步通知 │ 驱动主动发 SIGIO │ 数据到达时机不确定, │
│ (fasync) │ 应用被动接收 │ 应用不想反复 poll │
└────────────────┴──────────────────┴─────────────────────────────┘
15.3 ARM GIC 中断控制器架构
在深入到代码之前,先理解 ARM 平台上中断是如何从外设到达 CPU 的:
┌──────────────────────────────────────────────────────────────────────┐
│ ARM GIC 中断架构 (GIC-400 / GIC-500) │
│ │
│ 外设产生中断信号 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ GIC (Generic Interrupt Controller) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Distributor │ ───→ │ CPU Interface │ ──→ │ Virtual CPU ││ │
│ │ │ (分发器) │ │ (CPU接口) │ │ Interface ││ │
│ │ │ │ │ │ │ (虚拟化) ││ │
│ │ │ 中断优先级管理 │ │ 发送到具体CPU │ │ ││ │
│ │ │ 中断使能/禁止 │ │ 抢占/ACK处理 │ │ ││ │
│ │ │ 路由到目标CPU │ │ 优先级屏蔽 │ │ ││ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ IRQ / FIQ │
│ ┌───────────┐ │
│ │ CPU │ │
│ │ Core 0~N │ │
│ └───────────┘ │
└──────────────────────────────────────────────────────────────────────┘
三种中断源类型:
| 类型 | 全称 | 中断号范围 | 说明 |
|---|---|---|---|
| SGI | Software Generated Interrupt | ID0 ~ ID15 | 软件触发,用于核间通信(IPI) |
| PPI | Private Peripheral Interrupt | ID16 ~ ID31 | 每个 CPU 核私有的外设中断(如定时器) |
| SPI | Shared Peripheral Interrupt | ID32 ~ ID1019 | 共享外设中断,可路由到任意 CPU 核(GPIO、UART、I2C...) |
按键使用的 GPIO 中断属于 SPI 类型(ID ≥ 32),由 GIC 的 Distributor 路由到目标 CPU。
设备树中中断的级联描述:
dts
// GIC 控制器节点 (SoC 级 .dtsi)
gic: interrupt-controller@fff00000 {
compatible = "arm,gic-400";
reg = <0xfff00000 0x1000>, // Distributor 寄存器
<0xfff01000 0x2000>; // CPU Interface 寄存器
#interrupt-cells = <3>; // ★ 中断属性占 3 个 cell
interrupt-controller; // 标记为中断控制器
};
// GPIO 控制器兼作中断控制器 (级联到 GIC)
gpio0: gpio@ff720000 {
compatible = "rockchip,gpio-bank";
reg = <0xff720000 0x100>;
interrupts = <GIC_SPI 14 IRQ_TYPE_LEVEL_HIGH>; // ★ GPIO0 的中断线接到 GIC 的 SPI 14
gpio-controller;
#gpio-cells = <2>;
interrupt-controller; // ★ GPIO 控制器也是中断控制器
#interrupt-cells = <2>; // 中断属性占 2 个 cell
};
// 外设引用 GPIO 中断
button {
compatible = "gpio-keys";
interrupt-parent = <&gpio0>; // ★ 中断父级是 GPIO 控制器
interrupts = <5 IRQ_TYPE_EDGE_BOTH>; // GPIO0_PC5, 双边沿触发
};
中断号与触发类型标志:
c
// include/dt-bindings/interrupt-controller/irq.h
#define IRQ_TYPE_NONE 0
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH 3 // 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发
// include/dt-bindings/interrupt-controller/arm-gic.h
#define GIC_SPI 0 // Shared Peripheral Interrupt
#define GIC_PPI 1 // Private Peripheral Interrupt
15.3.1 GIC Distributor 控制方式详解
GIC 的 Distributor 是整个中断系统的中央路由器,它通过一组 MMIO 寄存器实现对每个中断的精细控制:
┌──────────────────────────────────────────────────────────────────┐
│ GIC Distributor 关键寄存器 │
├──────────────┬───────────────────────────────────────────────────┤
│ 寄存器 │ 作用 │
├──────────────┼───────────────────────────────────────────────────┤
│ GICD_CTLR │ 全局使能 Distributor (bit0=1 使能) │
│ GICD_TYPER │ 只读,ITLinesNumber 字段表示支持多少组 32 个中断 │
│ GICD_ISENABLER│ 中断使能 Set-Enable (写 1 使能某个中断线) │
│ GICD_ICENABLER│ 中断使能 Clear-Enable (写 1 禁止某个中断线) │
│ GICD_IPRIORITYR│ 中断优先级 (每个中断 8bit, 值越小优先级越高) │
│ GICD_ITARGETSR│ 中断目标 CPU (每个中断 8bit, 每 bit 对应一个 CPU) │
│ GICD_ICFGR │ 中断触发类型: 0=电平敏感, 1=边沿触发 (每中断 2bit) │
│ GICD_SGIR │ 写此寄存器产生 SGI (软件中断,用于核间通信) │
│ GICD_ISPENDR │ 软件触发一个边沿中断 (用于测试/调试) │
└──────────────┴───────────────────────────────────────────────────┘
中断分发决策流程 (硬件自动执行):
外设中断请求到达 Distributor
│
▼
┌─ GICD_CTLR.Enable == 1? ──→ 否 ──→ 丢弃
│ 是
▼
┌─ GICD_ISENABLER[n] 该中断被使能? ──→ 否 ──→ 挂起等待使能
│ 是
▼
┌─ 该中断优先级 > 当前已发给该 CPU 的最高优先级? ──→ 否 ──→ 等待
│ 是 (数值小 = 优先级高)
▼
┌─ 查找 GICD_ITARGETSR[n] 获取目标 CPU 列表 ─────────────┐
│ │
│ 如果设置了多个 CPU bit (多播): │
│ → 选择当前负载最低的 CPU (硬件仲裁) │
│ 如果只设置了一个 CPU bit: │
│ → 固定路由到该 CPU │
│ │
▼ │
将中断转发到目标 CPU 的 GIC CPU Interface │
│ │
▼ │
CPU Interface 拉高 IRQ 信号 → CPU 响应 │
优先级抢占示例:
时间轴 ──────────────────────────────────────────────────────→
CPU 正在处理: 中断 A (优先级 100)
│
│ 中断 B 到达 (优先级 50, 数值更小)
▼
┌──────────────────────┐
│ GIC 判定: 50 < 100 │
│ → 抢占! CPU Interface │
│ 发送新 IRQ │
└──────────┬───────────┘
▼
CPU 保存中断 A 的现场
开始处理中断 B (优先级 50)
│
│ 中断 B 处理完毕, 写 EOIR
▼
CPU 恢复中断 A 的现场
继续处理中断 A (优先级 100)
关键 :高优先级中断可以抢占低优先级中断的处理(硬件自动完成,不需要软件参与)。
15.3.2 为什么需要 #interrupt-cells = <3> ------ 三个 cell 的含义
设备树中 GIC 的 #interrupt-cells = <3> 表示描述一个中断需要 3 个 32 位整数,每个 cell 各司其职:
interrupts = <CELL1 CELL2 CELL3>
───── ───── ─────
中断类型 中断号 触发类型
示例: interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
│ │ │
│ │ └── Cell3: 触发类型 (flags)
│ └──────────── Cell2: 中断号 (SPI 89)
└──────────────────── Cell1: 中断类型 (0=SPI, 1=PPI)
Cell1 --- 中断类型:
| 值 | 宏定义 | 含义 | 范围 |
|---|---|---|---|
| 0 | GIC_SPI |
Shared Peripheral Interrupt | 共享外设中断,可路由到任意 CPU |
| 1 | GIC_PPI |
Private Peripheral Interrupt | 每个 CPU 核私有的中断 |
注意:SGI (0~15) 软件中断不通过设备树描述,仅由内核代码调用
gic_raise_softirq()触发。
Cell2 --- 中断号:
| 类型 | 设备树中填的中断号 | 实际 GIC 中断 ID |
|---|---|---|
| SPI | 89 |
32 + 89 = 121(内核自动加偏移) |
| PPI | 14 |
16 + 14 = 30(内核自动加偏移) |
设备树中的中断号是相对偏移,不是绝对 ID。内核解析时会自动加上基址偏移。
Cell3 --- 触发类型 (IRQ flags):
| 值 | 宏定义 | 硬件行为 |
|---|---|---|
| 0 | IRQ_TYPE_NONE |
无特殊配置(使用默认) |
| 1 | IRQ_TYPE_EDGE_RISING |
上升沿触发(0→1 瞬间) |
| 2 | IRQ_TYPE_EDGE_FALLING |
下降沿触发(1→0 瞬间) |
| 3 | IRQ_TYPE_EDGE_BOTH |
双边沿触发(0→1 和 1→0 都触发) |
| 4 | IRQ_TYPE_LEVEL_HIGH |
高电平触发(持续高电平时反复触发) |
| 8 | IRQ_TYPE_LEVEL_LOW |
低电平触发(持续低电平时反复触发) |
边沿触发 vs 电平触发:
边沿触发 (Edge) 电平触发 (Level)
────────────── ────────────────
┌─┐ ┌─────────
│ │ ← 检测到跳变沿, 触发一次中断 │ ← 只要电平为高, 就持续触发
──────┘ └────── ──────┘
不关心电平持续多久 必须由中断处理程序清除中断源
(如读状态寄存器或操作外设), 否则会反复触发
(中断风暴!)
实践建议:按键用边沿触发(双边沿),I2C/SPI 等共享中断线用电平触发。
为什么 GPIO 控制器的 #interrupt-cells = <2>?
dts
gpio0: gpio@ff720000 {
interrupt-controller;
#interrupt-cells = <2>; // ← 只需 2 个 cell
};
button {
interrupts = <5 IRQ_TYPE_EDGE_BOTH>;
│ │
│ └── Cell2: 触发类型 (不需要 Cell1, GPIO 只有 SPI 类型)
└───── Cell1: GPIO 组内引脚偏移 (GPIO0_PC5 = 第 5 脚)
};
GPIO 控制器的中断都固定为 SPI 类型,不需要额外指定类型 cell,因此只需 2 个 cell。
15.4 中断处理流程 (硬件视角)
当按键按下,GPIO 引脚电平变化触发中断的完整硬件流程:
用户按下按键
│ GPIO 引脚电平变化 (边缘检测电路检测到)
▼
GPIO 控制器的中断状态寄存器置位
│ GPIO 中断线 (irq line) 拉高
▼
GIC Distributor 接收中断
├── 检查该中断是否被使能
├── 检查该中断优先级是否高于当前运行的中断
├── 选择目标 CPU (根据 affinity routing)
└── 转发到 GIC CPU Interface
│
▼
GIC CPU Interface 向 CPU 发送 IRQ 信号
│
▼ CPU 硬件自动执行
┌── CPU 硬件中断响应序列 ──────────────────┐
│ 1. 保存 CPSR → SPSR_irq │
│ 2. 切换到 IRQ 模式 (CPSR[4:0]=10010) │
│ 3. 屏蔽 IRQ (CPSR[7]=1) │
│ 4. 保存返回地址 LR_irq = PC + 4 │
│ 5. 跳转到异常向量表 IRQ 入口 (0xFFFF0018) │
└───────────────────────────────────────────┘
│
▼
异常向量表 → irq_handler (汇编入口)
│ 保存现场 (所有通用寄存器压栈)
│ 读取 GIC CPU Interface 的 IAR 寄存器 → 获取中断号
▼
gic_handle_irq()
│ 根据中断号查找 irq_desc[] 数组
│ 调用 irq_desc[irq].handle_irq()
▼
handle_edge_irq() / handle_level_irq() ← 根据触发类型
│ 遍历该中断线上的所有 irqaction
│ 调用每个 irqaction->handler() ← 我们注册的中断回调!
▼
key_irq_handler() ← ★ 驱动注册的中断处理函数
│ 读取 GPIO 电平 → 确定按键状态
│ 唤醒等待队列 → 通知阻塞的 read()
│ 发送 SIGIO → 通知 fasync 应用
│ return IRQ_HANDLED;
▼
返回 gic_handle_irq()
│ 写 GIC CPU Interface EOIR 寄存器 → 结束中断
▼
恢复现场 (寄存器出栈)
│ subs pc, lr, #4 → 返回被中断的代码
│ CPSR ← SPSR_irq → 恢复之前的 CPU 模式
15.5 中断顶半部与底半部
15.5.1 为什么需要拆分?
中断处理有一个根本矛盾:
┌─────────────────────────────────────────────────────────────┐
│ 中断的矛盾 │
│ │
│ 要求 1: 中断处理要快 │
│ 中断期间 CPU 屏蔽了同级中断,处理太久会导致: │
│ ✗ 其他中断丢失 (中断延迟增大) │
│ ✗ 系统实时性下降 │
│ ✗ 调度延迟 (关中断期间无法调度) │
│ │
│ 要求 2: 中断处理要做很多事 │
│ 按键: 消抖 (20ms 延迟) + 读电平 + 唤醒进程 │
│ 网卡: 从 DMA 环取包 + 校验 + 重组 SKB + 送入协议栈 │
│ 硬盘: 处理 DMA 完成 + 更新缓存 + 唤醒 IO 等待 │
│ │
│ 矛盾: 又要快,又要做多事 → 拆成两半! │
└─────────────────────────────────────────────────────────────┘
15.5.2 顶半部与底半部的分工
┌──────────────────────┬──────────────────────────────────────────┐
│ 顶半部 (Top Half) │ 底半部 (Bottom Half) │
│ 硬中断上下文 │ 内核线程 / 软中断上下文 │
├──────────────────────┼──────────────────────────────────────────┤
│ ✓ 读/写硬件寄存器 │ ✓ 数据处理 (拷贝、转换、校验) │
│ ✓ 清除中断标志 │ ✓ 耗时计算 │
│ ✓ 应答 GIC (ACK) │ ✓ 睡眠操作 (msleep, mutex_lock) │
│ ✓ 启动底半部 │ ✓ copy_to/from_user │
│ ✓ 记录关键状态 │ ✓ 发送网络包/协议栈处理 │
│ │ ✓ 唤醒等待进程 │
├──────────────────────┼──────────────────────────────────────────┤
│ 不能做的事: │ 不能做的事: │
│ ✗ 睡眠 (msleep) │ ✗ 直接访问硬件寄存器 (顶半部已处理) │
│ ✗ 持 mutex (会死锁) │ (但通过顶半部保存的数据间接访问) │
│ ✗ copy_to_user │ │
│ ✗ 做耗时循环 │ │
└──────────────────────┴──────────────────────────────────────────┘
执行时间线:
中断到来
│
▼ ──── 顶半部开始 (IRQ 上下文, 关中断) ────
│ 1. 读 GPIO 中断状态寄存器 → 确认是哪个引脚
│ 2. 写寄存器清除中断标志 (ACK)
│ 3. 保存 GPIO 电平到结构体
│ 4. 启动底半部 (启动定时器 / 调度 tasklet / 唤醒 workqueue)
│ 5. return IRQ_HANDLED
▼ ──── 顶半部结束 (开中断) ────
│
│ ... CPU 继续做其他事 ...
│
▼ ──── 底半部开始 (内核线程上下文, 开中断) ────
│ 1. 消抖等待 20ms (可以睡眠!)
│ 2. 再次读取 GPIO 电平确认
│ 3. 更新按键值
│ 4. wake_up 等待队列 → 唤醒阻塞的 read()
│ 5. kill_fasync → 发送 SIGIO 给应用
▼ ──── 底半部结束 ────
15.5.3 四种底半部实现方案
┌─────────────┬────────────┬──────────┬──────────────────────────┐
│ 方案 │ 运行上下文 │ 并发性 │ 适用场景 │
├─────────────┼────────────┼──────────┼──────────────────────────┤
│ softirq │ 软中断上下文 │ 可多核并发 │ 网络子系统 (NET_RX/TX) │
│ │ (不可睡眠) │ 需自旋锁 │ 块设备 (BLOCK) │
│ │ │ │ ★ 静态分配, 不能动态注册 │
├─────────────┼────────────┼──────────┼──────────────────────────┤
│ tasklet │ 软中断上下文 │ 同一tasklet│ 简单外设中断下半部 │
│ │ (不可睡眠) │ 不会并发 │ (串口、I2C、SPI DMA 完成) │
│ │ │ 不需锁 │ ★ 基于 softirq (TASKLET) │
├─────────────┼────────────┼──────────┼──────────────────────────┤
│ workqueue │ 内核线程上下文│ 可多核并发 │ 需要睡眠的操作 │
│ │ (可以睡眠!) │ 可用mutex│ (文件系统、USB、MMC) │
│ │ │ │ ★ 可以调用 msleep/copy_xxx │
├─────────────┼────────────┼──────────┼──────────────────────────┤
│ threaded_irq│ 独立内核线程 │ 每中断一线程│ 对延迟不敏感的外设 │
│ │ (可以睡眠!) │ 自动串行 │ (GPIO按键、传感器) │
│ │ │ │ ★ 最简单, 一个函数搞定全部 │
└─────────────┴────────────┴──────────┴──────────────────────────┘
15.5.4 各方案代码示例
① softirq(内核静态定义,驱动一般不直接使用):
c
// 只有内核核心子系统才注册 softirq (编译时静态分配)
// 驱动开发中几乎用不到, 了解即可
enum {
HI_SOFTIRQ = 0, // 高优先级 tasklet
TIMER_SOFTIRQ = 1, // 定时器
NET_TX_SOFTIRQ = 2, // 网络发送
NET_RX_SOFTIRQ = 3, // 网络接收 ★ 最常用
BLOCK_SOFTIRQ = 4, // 块设备
TASKLET_SOFTIRQ = 5, // 普通 tasklet
// ...
};
// 触发 softirq:
raise_softirq(NET_RX_SOFTIRQ); // 标记, 在适当时机执行
② tasklet(轻量级,常用于简单外设):
c
#include <linux/interrupt.h>
/* 声明 tasklet */
static void my_tasklet_func(struct tasklet_struct *t);
static DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)&my_data);
// 或动态创建:
// DECLARE_TASKLET_DISABLED (初始禁用)
/* 顶半部 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
// 紧急操作: 清中断标志, 读寄存器
tasklet_schedule(&my_tasklet); // ★ 调度底半部
return IRQ_HANDLED;
}
/* 底半部 (软中断上下文, 不能睡眠!) */
static void my_tasklet_func(struct tasklet_struct *t)
{
struct my_data *data = (struct my_data *)t->data;
// 数据处理... (不能调用 msleep!)
}
// 销毁时:
tasklet_kill(&my_tasklet);
③ workqueue(可睡眠,最常用):
c
#include <linux/workqueue.h>
#include <linux/timer.h>
/* 声明 work */
static void my_work_func(struct work_struct *work);
static DECLARE_WORK(my_work, my_work_func);
// 或延迟执行:
// DECLARE_DELAYED_WORK(my_dwork, my_work_func);
/* 顶半部 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
schedule_work(&my_work); // ★ 调度到系统默认 workqueue
// 或指定队列:
// queue_work(my_wq, &my_work);
return IRQ_HANDLED;
}
/* 底半部 (内核线程上下文, 可以睡眠!) */
static void my_work_func(struct work_struct *work)
{
msleep(20); // ✓ 可以睡眠!
mutex_lock(&my_mutex); // ✓ 可以用 mutex!
// 数据处理...
copy_to_user(...); // ✓ 可以!
mutex_unlock(&my_mutex);
}
// 销毁时:
cancel_work_sync(&my_work);
④ threaded_irq(最简单,一行代码分离顶底半部):
c
#include <linux/interrupt.h>
/* 底半部 (独立内核线程, 可以睡眠) */
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
struct key_dev *kdev = dev_id;
msleep(20); // ✓ 消抖
int val = gpio_get_value(kdev->gpio);
kdev->key_val = val ? 0 : 1;
kdev->changed = 1;
wake_up_interruptible(&kdev->wq);
if (kdev->fasync)
kill_fasync(&kdev->fasync, SIGIO, POLL_IN);
return IRQ_HANDLED;
}
/* 顶半部 (硬中断上下文) --- 可省略(设为 NULL) */
static irqreturn_t my_hard_handler(int irq, void *dev_id)
{
// 最简单的顶半部: 只清中断标志
return IRQ_WAKE_THREAD; // ★ 告诉内核: 去调度底半部线程
}
/* 注册: 一次调用搞定 */
ret = request_threaded_irq(
irq,
my_hard_handler, // 顶半部 (可为 NULL, 内核自动应答)
my_thread_fn, // ★ 底半部 (独立线程)
IRQF_TRIGGER_FALLING,
"my_dev",
dev_id);
推荐 :按键这种对延迟不敏感的外设,直接用
request_threaded_irq,顶半部传NULL,底半部集中写所有逻辑,最简单清晰。
15.5.5 方案选择决策树
需要睡眠 (msleep/mutex/copy_to_user)?
├── ✗ 不需要 ──→ 处理逻辑很轻量?
│ ├── 是 ──→ tasklet (禁止并发用非定时器tasklet)
│ └── 否 ──→ workqueue
│
└── ✓ 需要
├── 需要实时性保证? ──→ 专用 workqueue (alloc_workqueue, WQ_HIGHPRI)
├── 代码最简洁? ──→ request_threaded_irq ★ 推荐
└── 一般场景 ──→ schedule_work (系统默认 workqueue)
15.6 按键驱动 ------ 完整实现
以下是一个完整的按键驱动,同时支持 ① 非阻塞读 ② 阻塞等待 ③ poll 多路复用 ④ 异步通知:
c
/*
* key_drv.c ------ 按键中断驱动 (支持四种交互方式)
*
* 硬件: GPIO 按键,按下为低电平,松开为高电平
*
* 编译: make → key_drv.ko
* 加载: insmod key_drv.ko
* 测试:
* 非阻塞: cat /dev/key0 (卡住等待)
* cat /dev/key0 & (后台,用 fasync 通知)
* poll: 编写 poll 应用同时监听多个 fd
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
/* ========== 设备信息 ========== */
#define DEV_NAME "key"
#define KEY_NUM 1 // 按键个数
#define DEBOUNCE_MS 20 // 去抖时间 (ms)
struct key_dev {
int major;
struct cdev cdev;
struct class *cls;
struct device *dev;
int gpio; // GPIO 编号
int irq; // 中断号
int key_val; // 当前按键值: 0=松开, 1=按下
int changed; // 是否有新数据: 0=无, 1=有
wait_queue_head_t wq; // ★ 等待队列 (用于阻塞 read)
struct fasync_struct *fasync; // ★ 异步通知队列
spinlock_t lock; // 自旋锁保护共享数据
};
static struct key_dev key_devs[KEY_NUM];
/* ========== 中断处理 (下半部: 工作队列 + 定时器去抖) ========== */
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct key_dev *kdev = dev_id;
/*
* 中断上下文 (IRQ 模式):
* ✗ 不能睡眠 (不能调用 msleep、copy_to_user、mutex_lock)
* ✗ 不能做耗时操作
* ✓ 应该尽快返回
*
* 这里只做最简单的事: 禁用中断 + 启动定时器去抖
*/
disable_irq_nosync(kdev->irq); // 禁止该中断线 (防抖动)
/* 启动下半部定时器: 20ms 后读取稳定电平 */
mod_timer(&kdev->timer, jiffies + msecs_to_jiffies(DEBOUNCE_MS));
return IRQ_HANDLED;
}
/* 定时器回调: 去抖后读取电平 (此时已离开中断上下文, 可睡眠) */
static void key_timer_handler(struct timer_list *t)
{
struct key_dev *kdev = from_timer(kdev, t, timer);
int val;
val = gpio_get_value(kdev->gpio); // 读取 GPIO 电平
kdev->key_val = val ? 0 : 1; // 低电平 = 按下 = 1
kdev->changed = 1; // 标记有新数据
/* ★ 1. 唤醒阻塞在 read 的进程 (阻塞等待模式) */
wake_up_interruptible(&kdev->wq);
/* ★ 2. 发送异步通知 SIGIO (fasync 模式) */
if (kdev->fasync)
kill_fasync(&kdev->fasync, SIGIO, POLL_IN);
/* ★ 3. 重新使能中断 */
enable_irq(kdev->irq);
}
/* ========== file_operations 实现 ========== */
static int key_open(struct inode *inode, struct file *filp)
{
int minor = iminor(inode);
if (minor >= KEY_NUM)
return -ENODEV;
filp->private_data = &key_devs[minor];
return 0;
}
static int key_release(struct inode *inode, struct file *filp)
{
struct key_dev *kdev = filp->private_data;
/* 关闭时清理 fasync 队列 */
key_fasync(-1, filp, 0);
return 0;
}
/* ---------- read: 同时支持 阻塞 和 非阻塞 ---------- */
static ssize_t key_read(struct file *filp, char __user *buf,
size_t len, loff_t *off)
{
struct key_dev *kdev = filp->private_data;
int ret;
char val;
/* ★ 非阻塞模式: O_NONBLOCK 设置了就立即返回 */
if (filp->f_flags & O_NONBLOCK) {
if (!kdev->changed)
return -EAGAIN; // 无数据, 立即返回
} else {
/* ★ 阻塞模式: 无数据就休眠, 直到被 wake_up 唤醒 */
ret = wait_event_interruptible(kdev->wq, kdev->changed);
if (ret)
return ret; // 被信号中断
}
/* 数据就绪, 拷贝给用户 */
val = kdev->key_val ? '1' : '0';
ret = copy_to_user(buf, &val, 1);
if (ret)
return -EFAULT;
kdev->changed = 0; // 消费数据, 清除标志
return 1;
}
/* ---------- poll: 多路复用支持 ---------- */
static __poll_t key_poll(struct file *filp,
struct poll_table_struct *wait)
{
struct key_dev *kdev = filp->private_data;
__poll_t mask = 0;
/*
* poll_wait: 将当前进程的等待队列项挂入 kdev->wq
* 不是在此处阻塞, 而是在调用者 (do_poll) 中阻塞
*/
poll_wait(filp, &kdev->wq, wait);
if (kdev->changed)
mask |= POLLIN | POLLRDNORM; // 数据可读
return mask;
}
/* ---------- fasync: 异步通知 ---------- */
static int key_fasync(int fd, struct file *filp, int on)
{
struct key_dev *kdev = filp->private_data;
return fasync_helper(fd, filp, on, &kdev->fasync);
}
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.release = key_release,
.read = key_read,
.poll = key_poll, // ★ 提供 poll 接口
.fasync = key_fasync, // ★ 提供 fasync 接口
};
/* ========== 模块加载 & 卸载 ========== */
static int __init key_init(void)
{
int i, ret;
/* 1. 注册字符设备 */
ret = alloc_chrdev_region(&key_devs[0].major, 0, KEY_NUM, DEV_NAME);
if (ret) return ret;
cdev_init(&key_devs[0].cdev, &key_fops);
ret = cdev_add(&key_devs[0].cdev, key_devs[0].major, KEY_NUM);
if (ret) goto err_region;
/* 2. 创建类 & 设备节点 */
key_devs[0].cls = class_create(THIS_MODULE, DEV_NAME);
for (i = 0; i < KEY_NUM; i++) {
key_devs[i].dev = device_create(key_devs[0].cls, NULL,
MKDEV(key_devs[0].major, i),
NULL, "key%d", i);
}
/* 3. 从设备树获取 GPIO 和 IRQ (假定设备树中已定义节点) */
{
struct device_node *np;
np = of_find_node_by_name(NULL, "key0");
if (!np) {
pr_err("[key] device node 'key0' not found\n");
ret = -ENODEV;
goto err_dev;
}
key_devs[0].gpio = of_get_named_gpio(np, "key-gpios", 0);
if (!gpio_is_valid(key_devs[0].gpio)) {
ret = -EINVAL;
goto err_dev;
}
key_devs[0].irq = gpio_to_irq(key_devs[0].gpio);
if (key_devs[0].irq < 0) {
ret = key_devs[0].irq;
goto err_dev;
}
}
/* 4. 初始化等待队列、自旋锁、定时器 */
init_waitqueue_head(&key_devs[0].wq);
spin_lock_init(&key_devs[0].lock);
timer_setup(&key_devs[0].timer, key_timer_handler, 0);
/* 5. ★ 注册中断处理程序 */
ret = request_irq(key_devs[0].irq,
key_irq_handler, // 中断回调
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, // 双边沿触发
"key0", // 名称 (/proc/interrupts)
&key_devs[0]); // dev_id (传给回调)
if (ret) {
pr_err("[key] request_irq failed: %d\n", ret);
goto err_dev;
}
pr_info("[key] loaded: gpio=%d, irq=%d, major=%d\n",
key_devs[0].gpio, key_devs[0].irq, key_devs[0].major);
return 0;
err_dev:
for (i = 0; i < KEY_NUM; i++)
device_destroy(key_devs[0].cls, MKDEV(key_devs[0].major, i));
class_destroy(key_devs[0].cls);
cdev_del(&key_devs[0].cdev);
err_region:
unregister_chrdev_region(key_devs[0].major, KEY_NUM);
return ret;
}
static void __exit key_exit(void)
{
int i;
free_irq(key_devs[0].irq, &key_devs[0]); // ★ 释放中断
del_timer_sync(&key_devs[0].timer); // 删除定时器
for (i = 0; i < KEY_NUM; i++)
device_destroy(key_devs[0].cls, MKDEV(key_devs[0].major, i));
class_destroy(key_devs[0].cls);
cdev_del(&key_devs[0].cdev);
unregister_chrdev_region(key_devs[0].major, KEY_NUM);
pr_info("[key] unloaded\n");
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Key interrupt driver with blocking/poll/fasync");
15.7 按键设备树配置
dts
// ====== 板级 .dts ======
/ {
key0: key0 {
compatible = "gpio-keys";
pinctrl-names = "default";
pinctrl-0 = <&key0_pins>;
key-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>; // GPIO0_PC5, 低电平有效
interrupt-parent = <&gpio0>;
interrupts = <5 IRQ_TYPE_EDGE_BOTH>; // 双边沿触发, 按下和松开都通知
label = "User Key";
};
};
// ====== pinctrl 配置 ======
&pinctrl {
key0_pins: key0-pins {
rockchip,pins = <0 RK_PC5 0 &pcfg_pull_up>; // GPIO0_PC5, 上拉
// 上拉确保未按下时读到高电平
};
};
15.8 应用层测试代码
c
/*
* key_test.c ------ 按键驱动测试程序
*
* 编译: arm-linux-gnueabihf-gcc -o key_test key_test.c
* 运行:
* ./key_test 阻塞模式
* ./key_test -n 非阻塞轮询
* ./key_test -p poll 多路复用
* ./key_test -a fasync 异步通知
*/
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include <string.h> // memset
#include <unistd.h> // read, close
#include <fcntl.h> // open, fcntl, O_NONBLOCK
#include <poll.h> // poll
#include <signal.h> // signal, SIGIO
#include <errno.h>
static int fd;
static int run = 1;
static int async_mode = 0;
/* ====== fasync 模式: SIGIO 信号处理函数 ====== */
void sigio_handler(int signo)
{
char val;
if (read(fd, &val, 1) > 0)
printf("[ASYNC] key = %c\n", val);
}
/* ====== 非阻塞轮询模式 ====== */
void test_nonblock(void)
{
char val;
int flags;
fd = open("/dev/key0", O_RDONLY);
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); // ★ 设置非阻塞
printf("Non-blocking mode, press key...\n");
while (run) {
int ret = read(fd, &val, 1);
if (ret > 0) {
printf("key = %c\n", val);
} else if (ret < 0 && errno != EAGAIN) {
perror("read");
break;
}
usleep(100000); // 100ms 轮询间隔
}
close(fd);
}
/* ====== poll 多路复用模式 ====== */
void test_poll(void)
{
struct pollfd fds[2];
int ret;
fd = open("/dev/key0", O_RDONLY);
fds[0].fd = fd;
fds[0].events = POLLIN; // ★ 监听可读事件
fds[1].fd = STDIN_FILENO; // 同时监听标准输入
fds[1].events = POLLIN;
printf("Poll mode, press key or type 'q' to quit...\n");
while (run) {
ret = poll(fds, 2, -1); // ★ 阻塞, 直到有事件
if (ret < 0) break;
if (fds[0].revents & POLLIN) { // 按键事件
char val;
read(fd, &val, 1);
printf("[POLL] key = %c\n", val);
}
if (fds[1].revents & POLLIN) { // 标准输入
char buf[8];
read(STDIN_FILENO, buf, sizeof(buf));
if (buf[0] == 'q') run = 0;
}
}
close(fd);
}
/* ====== fasync 异步通知模式 ====== */
void test_fasync(void)
{
int flags;
fd = open("/dev/key0", O_RDONLY);
/* 1. 注册 SIGIO 信号处理函数 */
signal(SIGIO, sigio_handler);
/* 2. 设置接收 SIGIO 的进程 */
fcntl(fd, F_SETOWN, getpid());
/* 3. 使能异步通知 */
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); // ★ 触发驱动的 .fasync 回调
printf("Async mode, press key... (Ctrl+C to quit)\n");
while (run)
pause(); // 什么都不做, 等待信号
close(fd);
}
int main(int argc, char *argv[])
{
if (argc < 2) {
/* 默认: 阻塞模式 */
char val;
fd = open("/dev/key0", O_RDONLY);
printf("Blocking mode, press key...\n");
while (run) {
if (read(fd, &val, 1) > 0) // ★ 无数据时卡住
printf("key = %c\n", val);
}
close(fd);
} else if (!strcmp(argv[1], "-n")) {
test_nonblock();
} else if (!strcmp(argv[1], "-p")) {
test_poll();
} else if (!strcmp(argv[1], "-a")) {
test_fasync();
}
return 0;
}
15.9 四种模式内核实现对比
① 非阻塞 (O_NONBLOCK)
=====================
APP: read(fd, buf, 1) → 内核: key_read()
fcntl O_NONBLOCK │
├── changed=0? → return -EAGAIN (立即返回)
└── changed=1? → copy_to_user → return 1
② 阻塞等待 (默认)
=====================
APP: read(fd, buf, 1) → 内核: key_read()
│
├── changed=0? → wait_event_interruptible(wq, changed)
│ │
│ └── 进程休眠 (SCHEDULING)
│
│ ... 中断到来 ...
│ key_irq_handler() → wake_up_interruptible(&wq)
│ │
│ └── 唤醒进程
│
└── changed=1? → copy_to_user → return 1
③ poll 多路复用
=====================
APP: poll(fds, n, timeout) → 内核: do_poll()
│
├── 对每个 fd 调用 key_poll()
│ ├── poll_wait(filp, &wq, wait) // 注册等待
│ └── return mask (如果 changed=0 返回0)
│
├── 所有 fd 都无数据? → 休眠
│ ... 中断到来 ...
│ key_irq_handler() → wake_up_interruptible(&wq)
│ → 唤醒 poll
│
└── 有 fd 就绪? → 返回就绪的 fd 集合
APP 遍历 → read 已就绪的 fd
④ fasync 异步通知
=====================
APP: fcntl(fd, F_SETOWN, getpid()) // 告诉内核 SIGIO 发给谁
fcntl(fd, FASYNC) // 触发 key_fasync() → fasync_helper()
signal(SIGIO, sigio_handler) // 注册信号处理函数
... APP 继续干别的事 ...
中断到来:
key_irq_handler()
└── kill_fasync(&fasync, SIGIO, POLL_IN)
└── 内核向 APP 进程发送 SIGIO 信号
└── APP 的 sigio_handler() 被调用
└── handler 中 read(fd) → 立即读到数据
15.10 中断处理要点总结
| 要点 | 说明 |
|---|---|
| 中断上下文限制 | 不能睡眠、不能用 mutex、不能做耗时操作(用 workqueue / timer 下半部) |
| request_irq vs request_threaded_irq | request_irq 在硬中断上下文执行;request_threaded_irq 可分离出内核线程处理 |
| 中断共享 | request_irq 的 dev_id 用于区分共享同一中断线的不同设备 |
| 上半部 / 下半部 | 上半部(硬中断)做紧急的寄存器操作;下半部(tasklet/workqueue/timer)做耗时逻辑 |
| 去抖动 | 机械按键需要硬件 RC 滤波或软件定时器去抖(20~50ms) |
| free_irq 的坑 | 必须先 free_irq 再释放 dev_id 指向的内存,否则崩溃 |
| 中断嵌套 | ARM Linux 默认不允许同类型中断嵌套(IRQ 中屏蔽 IRQ),但可被 FIQ 打断 |
| /proc/interrupts | 查看系统所有中断统计:`cat /proc/interrupts |
十六、Linux 芯片拓扑与虚拟地址/物理地址映射
16.1 芯片内部拓扑结构
┌───────────────────────────────────────────────────────────────────────┐
│ SoC (System on Chip) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ CPU Core 0 │ │ CPU Core 1 │ │ CPU Core N │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ │
│ │ │ L1 I-cache │ │ │ │ L1 I-cache │ │ │ │ │
│ │ │ L1 D-cache │ │ │ │ L1 D-cache │ │ │ │ │
│ │ └──────┬───────┘ │ │ └──────┬───────┘ │ │ │ │
│ └─────────┼──────────┘ └─────────┼──────────┘ └───────┬───────┘ │
│ │ │ │ │
│ └─────────────────────────┼───────────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ L2 Cache (共享) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ MMU (每核独立) │ ← 虚拟地址 → 物理地址 │
│ │ TLB + 页表遍历单元 │ │
│ └───────────┬───────────┘ │
│ │ 物理地址 │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ AXI / AHB 总线互连 │ │
│ └───┬───────┬───────┬───────┬─────┬───┘ │
│ │ │ │ │ │ │
│ ┌────────▼──┐ ┌──▼───┐ ┌─▼───┐ ┌─▼───┐ ┌─▼──────────┐ │
│ │ DDR 控制器 │ │ROM │ │SRAM │ │NAND │ │ 外设总线 │ │
│ └─────┬──────┘ └──────┘ └─────┘ └─────┘ │ (APB Bus) │ │
│ │ └─┬──┬──┬──┬─┘ │
│ ▼ │ │ │ │ │
│ ┌──────────┐ 物理地址空间 ▼ ▼ ▼ ▼ │
│ │ DDR RAM │ ┌──────────────────────────────────────┐ │
│ │ (2GB/4GB) │ │ 0x02000000 UART │ │
│ └──────────┘ │ 0x02100000 SPI │ │
│ │ 0x02200000 I2C │ │
│ │ 0x02300000 GPIO │ │
│ │ 0x1C200000 Timer │ │
│ └──────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
16.2 物理地址空间的典型布局(以 32 位 ARM 为例)
0xFFFFFFFF ┌─────────────────────────┐
│ 预留 / 系统 │
├─────────────────────────┤
│ 外设寄存器 (MMIO) │ ← GPIO、UART、SPI、I2C、Timer...
0x01C00000 ├─────────────────────────┤
│ │
│ DRAM 内存 │ ← 操作系统可用的物理内存
│ │
0x40000000 ├─────────────────────────┤
│ 内部 SRAM / ROM │
0x00000000 └─────────────────────────┘
16.3 MMU 映射关系
MMU 地址翻译全景图
CPU 发出虚拟地址 物理地址输出
│ ▲
▼ │
┌───────────┐ ┌──────────┐
│ 虚拟地址 │ │ 物理地址 │
│ 0xC0008000│ │0x80008000│
└─────┬─────┘ └────┬─────┘
│ │
▼ │
┌──────────────────────────────────────────┐
│ MMU 翻译流程 │
├──────────────────────────────────────────┤
│ │
│ 虚拟地址 = 页表基址 + 一级偏移 + 二级偏移 + 页内偏移 │
│ ┌────────┬──────────┬──────────┬────────┐ │
│ │ 31..20 │ 19..12 │ 11..0 │ │ │
│ │ 一级表 │ 二级表 │ 页内偏移 │ │ │
│ │ 索引 │ 索引 │(offset) │ │ │
│ └───┬────┴────┬─────┴────┬─────┘ │ │
│ │ │ │ │ │
│ ▼ ▼ │ │ │
│ ┌───────┐ ┌──────────┐ │ │ │
│ │Page │→│Page │→──┘ │ │
│ │Table │ │Table │ 物理页框号+偏移 │ │
│ │(PGD) │ │(PTE) │ = 物理地址 │ │
│ └───────┘ └──────────┘ │ │
│ │ │
│ ★ TLB (Translation Lookaside Buffer) │ │
│ 缓存最近翻译结果,命中时跳过页表遍历 │ │
└──────────────────────────────────────────┘
16.4 内核驱动中的地址转换全景
用户空间 内核空间 物理世界
──────── ──────── ────────
APP 调用 write()
buf = malloc(256) copy_from_user()
│ │
▼ ▼
虚拟地址 0x7FFF1234 ──── 用户页表 ────→ 物理地址 0x87654321
(用户虚拟地址) (某个物理页框)
│
│ copy_from_user 内部:
│ 1. 查用户页表,找到物理页
│ 2. 将物理页内容拷贝到内核缓冲区
│
▼
┌──────────────────┐
ioremap(0x01C20000, 4096) │ GPIO 外设寄存器 │
│ │ 物理地址: │
▼ │ 0x01C20800 │
内核虚拟地址 0xF0C20000 ←── 内核页表 ────→│ │
│ └──────────────────┘
▼
writel(value, 0xF0C20000 + 0x10)
│
▼
写数据到 GPIO 输出寄存器 → LED 亮/灭
三种地址的关系:
| 地址类型 | 说明 | 示例值 | 谁使用 |
|---|---|---|---|
| 用户虚拟地址 (UVA) | 用户进程使用的地址,每个进程独立 | 0x7FFF1234 |
APP 代码 |
| 内核虚拟地址 (KVA) | 内核访问内存的地址,直接映射物理地址 | 0xC0008000 |
内核代码 |
| 物理地址 (PA) | 总线上的真实地址 | 0x80008000 |
MMU 输出 |
| MMIO 虚拟地址 | ioremap 后映射外设寄存器的内核虚拟地址 | 0xF0C20000 |
驱动代码 |
十七、CPU 模式与异常切换内核态原理
17.1 ARM CPU 的 8 种工作模式
ARM 架构(ARMv7)定义了 8 种处理器模式,每种模式有不同的特权级别和寄存器组:
┌─────────────────────────────────────────────────────────────────┐
│ ARM CPU 工作模式一览 │
├──────────┬──────┬──────────┬────────────────────────────────────┤
│ 模式名称 │ 缩写 │ CPSR[4:0]│ 触发条件 │
├──────────┼──────┼──────────┼────────────────────────────────────┤
│ User │ USR │ 10000 │ 普通 APP 运行模式(唯一非特权模式) │
│ FIQ │ FIQ │ 10001 │ 快速中断(NMI 的一种实现) │
│ IRQ │ IRQ │ 10010 │ 普通硬件中断 │
│ Supervisor│ SVC │ 10011 │ 复位 / SWI 软中断(系统调用) │
│ Abort │ ABT │ 10111 │ 数据访问异常 / 预取指异常 │
│ Undefined │ UND │ 11011 │ 未定义指令异常 │
│ System │ SYS │ 11111 │ 特权模式,与 User 共享寄存器组 │
│ Monitor │ MON │ 10110 │ TrustZone 安全监控模式 │
└──────────┴──────┴──────────┴────────────────────────────────────┘
除 User 模式外,其余 7 种统称为"内核态"(特权模式),它们可以直接访问所有系统资源。
17.2 CPSR 寄存器 ------ 模式切换的核心
CPSR (Current Program Status Register) --- 32 位
┌─────┬───┬───┬───┬───┬───┬───────┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 31 │30 │29 │28 │27 │26 │25..8 │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │
├─────┼───┼───┼───┼───┼───┼───────┼───┼───┼───┼───┼───┼───┼───┼───┤
│ N │ Z │ C │ V │ Q │ ..│ 保留 │ I │ F │ T │ M[4:0] (模式位) │
└─────┴───┴───┴───┴───┴───┴───────┴───┴───┴───┴───┴───┴───┴───┴───┘
条件码标志 中断屏蔽位 │ │
I=1 屏蔽 IRQ │ M[4:0] │
F=1 屏蔽 FIQ │ 10000=User │
│ 10011=SVC │
└── 决定当前模式 ─┘
关键 :CPSR 的低 5 位(M4:0)决定了 CPU 当前处于哪种模式。模式切换的本质就是修改 CPSR 的低 5 位。
17.3 异常触发内核态的硬件执行流程
当异常发生时,CPU 硬件自动完成以下步骤(不需要任何软件参与):
异常触发(如 IRQ 信号到达)
│
▼
┌─── CPU 硬件自动执行 ─────────────────────────────┐
│ │
│ Step 1: 保存 CPSR │
│ SPSR_<exception_mode> = CPSR │
│ (把当前的 CPSR 备份到对应异常模式的 SPSR 中) │
│ │
│ Step 2: 切换模式位 │
│ CPSR[4:0] = <exception_mode_number> │
│ (修改 CPSR 低 5 位,进入对应的异常模式) │
│ ★ 此时已经进入内核态! │
│ │
│ Step 3: 设置中断屏蔽 │
│ CPSR[7] = 1 (屏蔽 IRQ,防止嵌套) │
│ (FIQ 异常:还会 CPSR[6] = 1 屏蔽 FIQ) │
│ │
│ Step 4: 保存返回地址 │
│ LR_<exception_mode> = PC + offset │
│ (保存发生异常时的 PC 值 + 偏移到 LR) │
│ │
│ Step 5: 跳转到异常向量表 │
│ PC = exception_vector_base + vector_offset │
│ (强制跳转到异常向量表中对应入口) │
│ │
└───────────────────────────────────────────────────┘
│
▼
进入异常向量表中的处理函数
(此时已是特权模式,可执行内核代码)
17.4 异常向量表 ------ 硬件跳转目标
异常向量表 (Exception Vector Table)
基地址:0x00000000 (低端向量) 或 0xFFFF0000 (高端向量,由 SCTLR.V 控制)
偏移 异常类型 模式 硬件动作
──── ──────── ──── ────────
0x00 Reset SVC PC = 0x00(上电/复位)
0x04 Undefined Inst UND PC = 0x04(遇到未定义指令,如浮点指令但无 FPU)
0x08 Supervisor Call SVC PC = 0x08(SWI/SVC 指令,系统调用)
0x0C Prefetch Abort ABT PC = 0x0C(取指令时 MMU 报错)
0x10 Data Abort ABT PC = 0x10(访存时 MMU 报错,如野指针)
0x18 (保留) - -
0x1C IRQ IRQ PC = 0x1C(硬件中断信号)
0x20 FIQ FIQ PC = 0x20(快速中断,最高优先级)
17.5 SWI 软中断实现系统调用的完整流程
以用户态调用 open("/dev/hello") 为例:
用户态 APP:
fd = open("/dev/hello", O_RDWR);
│
▼
glibc 实现:
mov r7, #5 ; r7 = __NR_open (系统调用号)
mov r0, #filename ; r0 = 文件名指针
mov r1, #O_RDWR ; r1 = 打开标志
swi #0 ; ★ 触发软中断,切入内核态
│ ┌── CPU 硬件自动执行 ──┐
│ │ SPSR_svc = CPSR │ 备份用户态 CPSR
│ │ CPSR[4:0] = 10011 │ 切换到 SVC 模式(内核态!)
│ │ CPSR[7] = 1 │ 屏蔽 IRQ
│ │ LR_svc = PC + 4 │ 保存返回地址
│ │ PC = 0xFFFF0008 │ 跳转到异常向量表 SWI 入口
│ └─────────────────────┘
▼
异常向量表 SWI 入口 (0xFFFF0008):
b vector_swi ; 跳转到 SWI 处理函数
▼
vector_swi (汇编): ; ↑ 此时已在内核态,使用 SVC 模式的 SP/LR
保存用户态寄存器到内核栈
从 LR_svc 取出 SWI 指令编码,提取系统调用号
(新版内核直接从 r7 获取)
...
ldr pc, [sys_call_table + r7 * 4] ; 跳转到 sys_open()
▼
sys_open():
遍历 VFS → 找到 /dev/hello → 调用驱动 hello_open()
...
返回 fd
│ 返回用户态:
▼
恢复用户态寄存器 (包括 CPSR ← SPSR_svc)
movs pc, lr ; ★ 同时把 SPSR_svc 恢复到 CPSR
; CPSR[4:0] 恢复为 10000 → 回到 User 模式
17.6 各种异常是否都能切换到内核态?
| 异常类型 | 目标模式 | 能切入内核态? | 说明 |
|---|---|---|---|
| Reset | SVC | ✅ | 复位后直接进入 SVC 模式 |
| SWI / SVC | SVC | ✅ | 软件主动触发,系统调用 |
| IRQ | IRQ | ✅ | 所有硬件中断都进入 IRQ 模式 |
| FIQ | FIQ | ✅ | 快速中断,也是一种内核态 |
| Data Abort | ABT | ✅ | MMU 缺页 / 野指针触发 |
| Prefetch Abort | ABT | ✅ | 取指令时的 MMU 错误 |
| Undefined Instruction | UND | ✅ | 非法指令触发 |
全部 7 种异常都能切入内核态。 原理都是硬件自动修改 CPSR 的低 5 位模式域,不需要软件预先做任何准备。
17.7 补充:User 模式无法主动进入内核态
User 模式无法直接:
✗ 修改 CPSR (非法,会触发 Undefined 异常或直接被忽略)
✗ 访问内核地址空间 (MMU 会触发 Data Abort)
✗ 执行特权指令 (如写协处理器 CP15,触发 Undefined 异常)
User 模式只能通过触发异常进入内核态:
✓ SWI / SVC 软中断 (主动,系统调用)
✓ 被硬件中断打断 (被动,IRQ/FIQ)
✓ 访问非法地址触发 Data Abort (被动,由 MMU 检测)
✓ 执行未定义指令 (被动,由指令译码单元检测)
十八、今日学习小结
| 知识点 | 核心理解 |
|---|---|
| Kconfig / .config / Makefile | 三件套实现内核模块的配置化编译:Kconfig 定义选项 → menuconfig 生成 .config → Makefile 根据配置决定 obj-y 或 obj-m |
| obj-y vs obj-m | obj-y 编译进内核镜像(zImage),obj-m 编译为独立 .ko 模块 |
| dev / drv 分离 | dev 描述"有什么硬件资源"(地址、IRQ),drv 实现"怎么操作硬件"(probe/remove),两者完全解耦 |
| platform_bus 总线 | 匹配 dev 和 drv 的"红娘",核心是 match() 函数(按 name/id_table/compatible 匹配) |
| 注册顺序无关 | 后注册的一方会遍历总线上已有的另一方链表,匹配成功即刻调用 probe |
| 五种匹配模式 | ① OF 匹配(of_match_table + compatible)② ACPI 匹配 ③ id_table 匹配 ④ name 字符串匹配 ⑤ driver_override 强制绑定 |
| 设备树 (.dts/.dtb) | 用数据结构替代 C 代码描述硬件,内核解析后自动创建 platform_device,实现"同一份驱动 + 不同 .dtb = 适配不同板子" |
| .dts / .dtsi 层级 | .dtsi 描述 SoC 公共部分(可被多板引用),.dts 描述具体板级差异,通过 &label 引用和覆盖 |
| DTC 编译反编译 | dtc -I dts -O dtb 编译,dtc -I dtb -O dts 反编译,make dtbs 批量编译 |
| compatible 属性 | 设备树与驱动匹配的核心纽带,格式 "厂商,型号",通过驱动的 of_match_table 进行 OF 匹配 |
| drv 层彻底分离 | I2C/SPI 控制器驱动只管协议时序不管从设备,设备树描述从设备并匹配独立从设备驱动,各层职责清晰 |
| 芯片拓扑 | CPU Core → L1/L2 Cache → MMU → AXI/AHB 总线 → DDR 控制器/外设总线 → 内存/外设寄存器 |
| MMU 页表翻译 | 虚拟地址分解为 PGD 索引 + PTE 索引 + 页内偏移,MMU 硬件自动执行页表遍历,TLB 缓存加速 |
| CPU 模式 | ARM 有 8 种模式,除 User 外均为内核态,CPSR4:0 决定当前模式 |
| 异常切入内核态原理 | 硬件自动:保存 CPSR→SPSR → 修改 CPSR 模式位 → 屏蔽中断 → 保存返回地址 → 跳转异常向量表,全程无需软件干预 |
| pinctrl 子系统 | 管理引脚复用(MUX)和电气属性(上下拉/驱动强度/翻转速率),内核在 probe 前根据设备树自动配置,无需驱动代码手动操作 |
| pinctrl default vs sleep | 设备工作时用 default 状态(高速/高驱动),休眠时切到 sleep 状态(GPIO 输入/低驱动/下拉),省电防漏电流 |
| GPIO 子系统 (gpiod) | 新一代 GPIO API,通过 devm_gpiod_get() 获取描述符,gpiod_set_value() 控制电平,devm_ 前缀实现自动资源回收 |
| 四厂商 pinctrl 对比 | ST 用 STM32_PINMUX() 宏+AF编号;NXP i.MX 用 MX6UL_PAD__FUNC 宏+32位hex;Allwinner sunxi 用字符串引脚名+字符串功能名;Rockchip 用四元组 <bank, pin, func, &pcfg> phandle 引用预定义电气配置 |
| 驱动只提供能力不提供策略 | 驱动暴露硬件操作接口和通知机制,APP 决定何时读、如何处理数据;驱动不编码按键消抖策略、不决定 ADC 阈值 |
| 四种交互模式 | ① 非阻塞 O_NONBLOCK(无数据立即返回 -EAGAIN)② 阻塞默认(wait_event 休眠)③ poll 多路复用(同时监听多个 fd)④ fasync 异步通知(驱动主动发 SIGIO) |
| GIC 中断架构 | SGI(软件触发/核间通信 ID0-15) → PPI(私有外设 ID16-31) → SPI(共享外设 ID32+) → GIC Distributor(使能/优先级/路由) → CPU Interface(抢占/ACK) → IRQ→CPU |
| GIC 控制方式 | Distributor 通过 MMIO 寄存器控制每个中断的使能(GICD_ISENABLER)、优先级(GICD_IPRIORITYR)、目标 CPU(GICD_ITARGETSR)、触发类型(GICD_ICFGR);高优先级可抢占低优先级(硬件自动) |
| interrupt-cells | GIC #interrupt-cells=<3>: Cell1=中断类型(SPI/PPI), Cell2=相对中断号(内核自动加基址偏移), Cell3=触发类型(边沿/电平);GPIO 只需 <2> 因为它只有 SPI 类型 |
| 中断硬件处理流程 | 外设信号→GIC 仲裁路由→CPU: 保存 CPSR→切换 IRQ 模式→屏蔽 IRQ→保存 LR→跳转向量表 0xFFFF0018→gic_handle_irq→驱动 handler→写 EOIR→恢复现场 |
| 中断顶半部/底半部 | 顶半部(硬中断): 快速清标志+ACK, 不能睡眠;底半部: 做耗时逻辑。四种方案: softirq(网络/块设备,静态) < tasklet(简单外设,不可睡眠) < workqueue(可睡眠,最常用) < threaded_irq(一行分离,★推荐按键用) |
| 按键驱动实战 | 完整实现:gpio_to_irq + request_irq + 定时器去抖 + wait_queue 阻塞读 + poll 支持 + kill_fasync 异步通知 + 应用层四种模式测试代码 |
十九、下一步学习建议
- 并发与竞态:多个 APP 同时 open 同一个设备怎么办?(mutex / spinlock / atomic)
- input 子系统:按键、触摸屏、鼠标等输入设备的统一驱动框架(input_dev / input_event)
- 定时器与时间管理:内核定时器(timer_list)、高精度定时器(hrtimer)、jiffies 与 HZ
- ioremap 与 MMIO:实际操作硬件寄存器的完整流程
- IIO 子系统 / 硬件监控 (hwmon):传感器、ADC 等工业 I/O 的驱动框架
附录:常用命令速查
A. 模块管理
| 命令 | 说明 | 示例 |
|---|---|---|
insmod |
加载 .ko 模块到内核,触发 module_init |
sudo insmod hello_drv.ko |
rmmod |
卸载已加载的模块,触发 module_exit |
sudo rmmod hello_drv |
lsmod |
列出当前已加载的所有内核模块 | `lsmod |
modprobe |
智能加载模块(自动处理依赖) | sudo modprobe hello_drv |
modprobe -r |
智能卸载模块(同时卸载不再使用的依赖) | sudo modprobe -r hello_drv |
modinfo |
查看模块的详细信息(作者、描述、参数等) | modinfo hello_drv.ko |
B. 设备节点管理
| 命令 | 说明 | 示例 |
|---|---|---|
mknod |
手动创建设备文件节点 | sudo mknod /dev/hello c 240 0 |
ls -l /dev/ |
查看 /dev 下的设备文件(含主次设备号) |
ls -l /dev/hello_drv |
rm |
删除设备节点文件 | sudo rm /dev/hello_drv |
C. 内核编译与设备树
| 命令 | 说明 | 示例 |
|---|---|---|
make menuconfig |
图形化配置内核选项(Kconfig) | make ARCH=arm menuconfig |
make |
编译内核镜像和模块 | make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules |
make dtbs |
批量编译所有 .dts → .dtb |
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs |
dtc -I dts -O dtb |
编译单个 .dts → .dtb |
dtc -I dts -O dtb -o board.dtb board.dts |
dtc -I dtb -O dts |
反编译 .dtb → .dts(查看二进制内容) |
dtc -I dtb -O dts -o board.dts board.dtb |
D. 系统信息查看
| 命令 | 说明 | 示例 |
|---|---|---|
cat /proc/devices |
查看系统已注册的字符/块设备及主设备号 | `cat /proc/devices |
ls /sys/class/ |
查看 sysfs 中已注册的设备类 | ls /sys/class/hello_drv/ |
cat /proc/device-tree/model |
查看当前设备树的板级型号 | cat /proc/device-tree/model |
ls /sys/firmware/devicetree/base/ |
浏览内核中运行的完整设备树 | ls /sys/firmware/devicetree/base/ |
dtc -I fs -O dts /sys/firmware/devicetree/base |
从运行中的内核导出设备树为 .dts |
如上 |
cat /proc/interrupts |
查看系统中断统计 | cat /proc/interrupts |
cat /proc/iomem |
查看 IO 内存地址分配 | cat /proc/iomem |
cat /proc/kallsyms |
查看内核符号表(需 root) | `sudo cat /proc/kallsyms |
E. 日志与调试
| 命令 | 说明 | 示例 |
|---|---|---|
dmesg |
查看内核日志(printk 输出) | `dmesg |
dmesg -w |
实时跟踪内核日志 | dmesg -w |
dmesg -C |
清除内核日志缓冲区 | sudo dmesg -C |
cat /var/log/kern.log |
查看持久化的内核日志(部分发行版) | `cat /var/log/kern.log |
F. 驱动功能测试
| 命令 | 说明 | 示例 |
|---|---|---|
echo ... > /dev/xxx |
向设备写入数据(触发驱动的 .write) |
echo "hello" > /dev/hello_drv |
cat /dev/xxx |
从设备读取数据(触发驱动的 .read) |
cat /dev/hello_drv |
echo ... > ... && cat ... |
写入后立刻读取验证(回环测试) | echo "test" > /dev/hello_drv && cat /dev/hello_drv |
G. Sysfs 操作(高级)
| 命令 | 说明 | 示例 |
|---|---|---|
echo ... > /sys/.../driver_override |
强制指定 platform_device 匹配的驱动 | echo "mydrv" > /sys/devices/platform/mydev/driver_override |
echo ... > /sys/.../bind |
手动触发设备-驱动绑定 | echo "mydev" > /sys/bus/platform/drivers/mydrv/bind |
echo ... > /sys/.../unbind |
手动解除设备-驱动绑定 | echo "mydev" > /sys/bus/platform/drivers/mydrv/unbind |
H. 常用命令组合
bash
# 一键:编译模块 → 加载 → 查看日志 → 测试 → 卸载
make
sudo insmod hello_drv.ko
dmesg | tail -5
echo "Linux Driver" > /dev/hello_drv && cat /dev/hello_drv
sudo rmmod hello_drv
# 查看模块依赖关系
modinfo hello_drv.ko | grep depends
lsmod | grep hello_drv
# 查看设备树中某个节点的属性
ls /proc/device-tree/soc/i2c@01C2AC00/
cat /proc/device-tree/soc/i2c@01C2AC00/compatible
# 查看设备号的分配情况
cat /proc/devices | grep -E "hello|gpio|i2c|spi"
# 卸载模块前确认无进程在使用设备
sudo lsof /dev/hello_drv
笔记日期:2026-06-20
更新日期:2026-06-21(补充设备树/五种匹配模式/I2C-SPI分层分离/pinctrl-GPIO/中断子系统-按键驱动/命令速查附录)