Linux驱动开发学习笔记

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;

原理拆解

  1. 段属性 (section attribute)module_init 将函数指针放到一个特殊的 ELF 段(如 .initcall6.init)。

  2. 链接脚本 :内核链接脚本(vmlinux.lds)收集所有 initcall 段中的函数指针,构成一个函数指针数组

  3. 内核启动时do_initcalls() 依次调用数组中的每个函数,insmod 加载模块时同样会执行对应级别的 initcall。

  4. 模块卸载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");

内部原理

  1. class_create → 在 sysfs 中创建 /sys/class/hello_drv/

  2. device_create → 注册设备到设备模型,向用户空间发送 uevent 事件。

  3. 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].namepdev->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-updrive-push-pull 等)。
  • 可分组定义(pins1pins2),同一组内引脚配置相同。
② 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_auart0_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)  │ ← 驱动主动调用
                                            └─────────────────────┘

关键理解

  1. pinctrl 是自动的------内核在 probe 前根据设备树自动配置引脚复用和电气属性。
  2. GPIO 是手动的------驱动代码通过 gpiod API 主动控制电平。
  3. 设备树中的 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_irqdev_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.MXMX6UL_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 异步通知 + 应用层四种模式测试代码

十九、下一步学习建议

  1. 并发与竞态:多个 APP 同时 open 同一个设备怎么办?(mutex / spinlock / atomic)
  2. input 子系统:按键、触摸屏、鼠标等输入设备的统一驱动框架(input_dev / input_event)
  3. 定时器与时间管理:内核定时器(timer_list)、高精度定时器(hrtimer)、jiffies 与 HZ
  4. ioremap 与 MMIO:实际操作硬件寄存器的完整流程
  5. 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/中断子系统-按键驱动/命令速查附录)