一、驱动的本质

驱动 = 按内核框架的规矩,把寄存器操作包装成标准接口,注册给内核,由内核代为调度。

用户程序
   ↓
系统调用 (read/write/ioctl)
   ↓
内核 VFS/子系统
   ↓
驱动程序  ← 把通用接口翻译成具体硬件操作
   ↓
硬件寄存器/总线

驱动 vs 普通代码

普通应用代码 驱动代码
用户态运行 内核态运行
崩溃只影响自己 崩溃整个系统挂
用 libc/malloc 用 kmalloc/kzalloc
可以随便循环 不能长时间阻塞

二、驱动骨架(通用模式)

所有驱动都遵循这个套路:

┌────────────────────────────────────────────────────────┐
│                     驱动骨架                            │
├────────────────────────────────────────────────────────┤
│ 1. of_match_table    →  声明我能驱动什么硬件            │
│ 2. probe()           →  匹配后初始化(ioremap/注册)     │
│ 3. 操作函数           →  readl/writel 操作寄存器        │
│ 4. 注册到子系统       →  gpiochip_add / i2c_add_adapter │
│ 5. 中断处理(可选)     →  request_irq + handler          │
└────────────────────────────────────────────────────────┘

三、设备树与驱动匹配

3.1 设备树是什么

设备树 = 硬件的"简历",告诉内核有什么硬件、在哪里。

// 文件: rtl8730e-pinctrl.dtsi:272
gpioa: gpio@4200D000 {
    compatible = "realtek,ameba-gpio";   // 身份标识(最关键)
    reg = <0x4200D000 0x400>;            // 寄存器地址
    rtk,gpio-bank = <0>;                 // 自定义属性
    interrupts = <GIC_SPI 9 ...>;        // 中断号
    clocks = <&rcc RTK_CKE_GPIO>;        // 时钟源
};

3.2 内核匹配机制

     设备树 (硬件简历)                    驱动 (找工作的人)
┌──────────────────────────┐    ┌──────────────────────────┐
│ compatible = "realtek,   │    │ .of_match_table = {      │
│              ameba-gpio" │◄──►│   "realtek,ameba-gpio"   │
│                          │    │ }                        │
└──────────────────────────┘    └──────────────────────────┘
                  │                          │
                  └────────────┬─────────────┘
                               ▼
                    ┌──────────────────┐
                    │   内核:匹配成功! │
                    │   调用 probe()   │
                    └──────────────────┘

本质就是字符串匹配:

// 内核匹配逻辑(简化)
if (strcmp(设备树.compatible, 驱动.of_match_table.compatible) == 0) {
    驱动.probe(device);
}

3.3 各组件角色

组件 作用 比喻
设备树 描述"有什么硬件、在哪里" 房产证
compatible 匹配用的"身份证号" 身份证
驱动 实现"怎么操作硬件" 装修队
内核 根据 compatible 匹配 中介
probe 匹配成功后的初始化 入职手续

四、GPIO 驱动详解

源码位置: kernel/linux-5.4/drivers/rtkdrivers/gpio/realtek-gpio.c

4.1 驱动身份证

// realtek-gpio.c:524-544
static const struct of_device_id realtek_ameba_gpio_of_match[] = {
    { .compatible = "realtek,ameba-gpio", },
    { },
};

static struct platform_driver realtek_ameba_gpio_driver = {
    .probe = realtek_gpio_probe,
    .driver = {
        .name = "realtek-ameba-gpio",
        .of_match_table = realtek_ameba_gpio_of_match,
    },
};

arch_initcall(realtek_ameba_gpio_register);  // 开机时注册

4.2 probe 函数(入职)

// realtek-gpio.c:415-493
int realtek_gpio_probe(struct platform_device *pdev)
{
    // 1. 从设备树读配置
    of_property_read_u32(np, "rtk,gpio-bank", &id);

    // 2. 映射硬件寄存器地址
    bank->reg_base = devm_ioremap_resource(&pdev->dev, res);

    // 3. 向 GPIO 子系统注册
    gpiochip_add_data(&bank->gpio_chip, bank);

    // 4. 设置中断
    irq_set_chained_handler_and_data(...);
}

4.3 核心操作函数

// 读 GPIO 引脚电平
static int realtek_gpio_get(struct gpio_chip *chip, unsigned offset)
{
    ret = !!(readl(bank->reg_base + GPIO_EXT_PORT) & BIT(offset));
    return ret;
}

// 写 GPIO 引脚电平
static void realtek_gpio_set(struct gpio_chip *chip, unsigned offset, int value)
{
    reg_value = readl(bank->reg_base + GPIO_DR);
    if (value)
        reg_value |= BIT(offset);
    else
        reg_value &= ~BIT(offset);
    writel(reg_value, bank->reg_base + GPIO_DR);
}

// 设置方向为输入
static int realtek_gpio_direction_input(...)
{
    reg_value = readl(bank->reg_base + GPIO_DDR);
    reg_value &= ~BIT(offset);  // DDR寄存器:0=输入
    writel(reg_value, bank->reg_base + GPIO_DDR);
}

// 设置方向为输出
static int realtek_gpio_direction_output(...)
{
    reg_value = readl(bank->reg_base + GPIO_DDR);
    reg_value |= BIT(offset);   // DDR寄存器:1=输出
    writel(reg_value, bank->reg_base + GPIO_DDR);
}

4.4 注册到框架

// realtek-gpio.c:380-407
.gpio_chip = {
    .label = "GPIO0",
    .get = realtek_gpio_get,
    .set = realtek_gpio_set,
    .direction_input = realtek_gpio_direction_input,
    .direction_output = realtek_gpio_direction_output,
    .ngpio = 32,
},

4.5 完整调用流程

用户: echo 1 > /sys/class/gpio/gpio5/value
         ↓
内核 GPIO 子系统: 找到 gpio5 属于哪个 chip
         ↓
调用 realtek_gpio_set(chip, 5, 1)
         ↓
驱动内部:
  reg_value = readl(0x4200D000 + GPIO_DR);
  reg_value |= BIT(5);
  writel(reg_value, 0x4200D000 + GPIO_DR);
         ↓
硬件: GPIO 引脚 5 输出高电平

五、I2C 驱动详解

源码位置: kernel/linux-5.4/drivers/rtkdrivers/i2c/rtk-i2c-core.c

5.1 GPIO vs I2C 对比

部分 GPIO 驱动 I2C 驱动
身份证 realtek,ameba-gpio realtek,ameba-i2c
注册子系统 gpiochip_add() i2c_add_numbered_adapter()
核心操作 gpio_get/set i2c_xfer
复杂度 简单(高低电平) 复杂(协议+时序+错误处理)

5.2 驱动身份证

// rtk-i2c-core.c:856-872
static const struct of_device_id rtk_i2c_match[] = {
    { .compatible = "realtek,ameba-i2c"},
    {},
};

static struct platform_driver rtk_i2c_driver = {
    .probe = rtk_i2c_probe,
    .driver = {
        .name = "realtek-ameba-i2c",
        .of_match_table = rtk_i2c_match,
    },
};

module_platform_driver(rtk_i2c_driver);

5.3 probe 函数

// rtk-i2c-core.c:691-784
static int rtk_i2c_probe(struct platform_device *pdev)
{
    // 1. 分配内存
    i2c_dev = devm_kzalloc(...);

    // 2. 映射寄存器
    i2c_dev->base = devm_ioremap_resource(&pdev->dev, res);

    // 3. 获取时钟并使能
    i2c_dev->clock = devm_clk_get(&pdev->dev, NULL);
    clk_prepare_enable(i2c_dev->clock);

    // 4. 注册中断处理函数
    devm_request_irq(&pdev->dev, i2c_dev->irq, rtk_i2c_isr_event, ...);

    // 5. 从设备树读配置
    rtk_i2c_struct_init(i2c_dev, np, &i2c_dev->i2c_param);

    // 6. 注册到 I2C 子系统
    i2c_add_numbered_adapter(adap);
}

5.4 寄存器操作封装

// rtk-i2c-core.c:24-42
void rtk_i2c_writel(void __iomem *ptr, u32 reg, u32 value)
{
    writel(value, ptr + reg);
}

u32 rtk_i2c_readl(void __iomem *ptr, u32 reg)
{
    return readl(ptr + reg);
}

void rtk_i2c_reg_update(void __iomem *ptr, u32 reg, u32 mask, u32 value)
{
    temp = rtk_i2c_readl(ptr, reg);
    temp |= mask;
    temp &= (~mask | value);
    rtk_i2c_writel(ptr, reg, temp);
}

5.5 I2C 比 GPIO 复杂的原因

GPIO:单向、简单
┌─────────┐     ┌─────────┐
│  CPU    │────►│ GPIO引脚 │  只是高低电平
└─────────┘     └─────────┘

I2C:双向、协议复杂
┌─────────┐     ┌─────────┐     ┌─────────┐
│  CPU    │◄───►│ I2C控制器│◄───►│ 外设芯片 │
└─────────┘     └─────────┘     └─────────┘
                     │
            需要处理:地址、时序、
            ACK/NACK、中断、错误

六、关键 API 总结

6.1 寄存器操作

readl(addr)              // 读 32 位寄存器
writel(value, addr)      // 写 32 位寄存器
ioremap(phys, size)      // 物理地址映射到虚拟地址
devm_ioremap_resource()  // 自动管理的 ioremap

6.2 设备树操作

of_property_read_u32(np, "name", &value)  // 读取整数属性
platform_get_resource(pdev, type, index)  // 获取资源(内存/中断)
platform_get_irq(pdev, index)             // 获取中断号

6.3 子系统注册

// GPIO
gpiochip_add_data(&chip, data)

// I2C
i2c_add_numbered_adapter(&adapter)

// 字符设备
register_chrdev(major, name, &fops)

6.4 中断

devm_request_irq(dev, irq, handler, flags, name, data)
irq_set_chip_and_handler(irq, chip, handler)

七、文件位置索引

文件 路径
GPIO 驱动 kernel/linux-5.4/drivers/rtkdrivers/gpio/realtek-gpio.c
I2C 驱动 kernel/linux-5.4/drivers/rtkdrivers/i2c/rtk-i2c-core.c
GPIO 设备树 kernel/linux-5.4/arch/arm/boot/dts/rtl8730e-pinctrl.dtsi:272
I2C 设备树 kernel/linux-5.4/arch/arm/boot/dts/rtl8730e-ocp.dtsi:117

八、一句话总结

驱动 = 身份证(of_match) + 入职(probe) + 干活(readl/writel) + 填表(注册子系统)

掌握这个模式,所有驱动都是这个套路的变体。


九、内核启动流程

9.1 整体流程图

┌─────────────────────────────────────────────────────────────────────┐
│                         上电                                         │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  1. BootROM (芯片固化)                                               │
│     - 芯片内部固化代码,不可修改                                       │
│     - 初始化最基本硬件(时钟、内存控制器)                              │
│     - 从 Flash/SD/USB 加载下一阶段                                   │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  2. U-Boot (Bootloader)                                             │
│     - 初始化 DDR、串口、网络、存储                                    │
│     - 加载内核镜像 (zImage/Image) 到内存                              │
│     - 加载设备树 (.dtb) 到内存                                        │
│     - 跳转到内核入口: bootz <内核地址> - <设备树地址>                  │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  3. Linux 内核启动                                                   │
│     ┌─────────────────────────────────────────────────────────────┐ │
│     │ 3.1 汇编阶段 (arch/arm/boot/compressed/head.S)              │ │
│     │     - 解压内核 (如果是 zImage)                               │ │
│     │     - 设置临时页表                                           │ │
│     │     - 跳转到 start_kernel                                   │ │
│     └─────────────────────────────────────────────────────────────┘ │
│                                │                                     │
│                                ▼                                     │
│     ┌─────────────────────────────────────────────────────────────┐ │
│     │ 3.2 start_kernel() (init/main.c)                           │ │
│     │     - setup_arch(): 解析设备树、初始化内存                    │ │
│     │     - mm_init(): 内存管理初始化                              │ │
│     │     - sched_init(): 调度器初始化                             │ │
│     │     - init_IRQ(): 中断初始化                                 │ │
│     │     - time_init(): 时钟初始化                                │ │
│     │     - rest_init(): 创建 init 进程                           │ │
│     └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  4. init 进程 (PID=1)                                                │
│     - 挂载根文件系统                                                  │
│     - 执行 /sbin/init (systemd 或 busybox init)                     │
│     - 启动用户空间服务                                                │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  5. 用户空间                                                         │
│     - 登录 shell                                                     │
│     - 应用程序运行                                                    │
└─────────────────────────────────────────────────────────────────────┘

9.2 start_kernel 关键步骤

// init/main.c (简化)
asmlinkage void __init start_kernel(void)
{
    // 1. 锁、调试基础设施
    lockdep_init();

    // 2. 架构相关初始化(解析设备树在这里)
    setup_arch(&command_line);

    // 3. 内存管理
    mm_init();

    // 4. 调度器
    sched_init();

    // 5. 中断
    init_IRQ();

    // 6. 时钟
    time_init();

    // 7. 控制台
    console_init();

    // 8. 各种子系统 initcall
    //    这里会调用所有 arch_initcall / module_init 注册的驱动!
    do_initcalls();

    // 9. 创建 init 进程
    rest_init();  // → kernel_init() → 执行 /sbin/init
}

9.3 驱动在哪里被加载?

// do_initcalls() 会按优先级调用:
early_initcall       // 最早
core_initcall
postcore_initcall
arch_initcall        // ← GPIO 驱动在这里 (arch_initcall)
subsys_initcall
fs_initcall
device_initcall      // ← 大多数驱动在这里 (module_init)
late_initcall        // 最晚

GPIO 驱动里的 arch_initcall(realtek_ameba_gpio_register) 就是在这个阶段被调用。


十、中断机制

10.1 中断是什么?

没有中断:
CPU: 不停轮询 → "有数据吗?有数据吗?有数据吗?" → 浪费 CPU

有中断:
CPU: 干自己的事
硬件: "叮!有数据了!" → 中断信号
CPU: 暂停当前工作 → 处理中断 → 返回继续

10.2 ARM 中断架构

┌─────────────────────────────────────────────────────────────┐
│                        外设                                  │
│  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐                        │
│  │GPIO │  │I2C  │  │UART │  │Timer│                        │
│  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘                        │
│     │        │        │        │                            │
│     └────────┴────────┴────────┘                            │
│                    │                                         │
│                    ▼                                         │
│     ┌──────────────────────────────┐                        │
│     │      GIC (中断控制器)         │                        │
│     │  Generic Interrupt Controller │                        │
│     │  - 收集所有中断               │                        │
│     │  - 优先级仲裁                 │                        │
│     │  - 分发到 CPU                 │                        │
│     └──────────────┬───────────────┘                        │
│                    │                                         │
│                    ▼                                         │
│     ┌──────────────────────────────┐                        │
│     │           CPU                 │                        │
│     │  - IRQ/FIQ 中断线             │                        │
│     │  - 保存上下文                 │                        │
│     │  - 跳转到中断向量表           │                        │
│     └──────────────────────────────┘                        │
└─────────────────────────────────────────────────────────────┘

10.3 GIC —— 中断的"交通警察"

GIC = Generic Interrupt Controller(通用中断控制器)

为什么需要 GIC?

没有 GIC 的世界(单片机时代):
┌─────┐ ┌─────┐ ┌─────┐
│GPIO │ │UART │ │Timer│
└──┬──┘ └──┬──┘ └──┬──┘
   │       │       │
   └───────┼───────┘
           ▼
        ┌─────┐
        │ CPU │  ← 中断线有限,怎么接这么多设备?
        └─────┘     多个中断同时来,先处理谁?

有 GIC:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ... (几百个中断源)
│GPIO │ │UART │ │Timer│ │ I2C │
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
   │       │       │       │
   └───────┴───────┴───────┘
                │
                ▼
        ┌─────────────┐
        │     GIC     │  ← 中断管理中心
        │  - 收集中断  │
        │  - 优先级排序│
        │  - 分发给CPU │
        └──────┬──────┘
               │ (只需要几根线)
               ▼
        ┌─────────────┐
        │    CPU      │
        └─────────────┘

GIC 的核心功能:

功能 说明
汇聚 把几百个中断源汇聚到一起
编号 每个中断分配唯一 ID(如 GPIO_A = 9)
优先级 高优先级中断先处理
屏蔽 可以临时关闭某些中断
分发 多核 CPU 时,决定哪个核处理

GIC 中断类型:

┌─────────────────────────────────────────────────────────────┐
│  SGI (Software Generated Interrupt)  0-15                   │
│  软件中断,CPU 之间通信用(多核同步)                          │
├─────────────────────────────────────────────────────────────┤
│  PPI (Private Peripheral Interrupt)  16-31                  │
│  私有中断,每个 CPU 核独有(如本地定时器)                     │
├─────────────────────────────────────────────────────────────┤
│  SPI (Shared Peripheral Interrupt)   32-1019                │
│  共享中断,外设用的(GPIO、I2C、UART 都是这种)               │
└─────────────────────────────────────────────────────────────┘

设备树里的 GPIO 中断号 9,实际是 SPI 9 = 硬件中断号 32 + 9 = 41

10.4 GIC 是硬件,但需要软件配置

┌─────────────────────────────────────────────────────────────┐
│                      GIC 的本质                              │
│                                                             │
│   GIC 是一块 硬件电路,集成在 SoC 芯片里                      │
│   和 CPU、GPIO 控制器一样,是实实在在的硬件模块               │
│                                                             │
│   但它有一组 寄存器,软件通过读写寄存器来配置它               │
└─────────────────────────────────────────────────────────────┘

硬件做的事 vs 软件做的事:

硬件自动完成 软件需要配置
检测中断信号 使能/禁用某个中断
锁存中断状态 设置优先级
优先级仲裁 设置触发方式(边沿/电平)
通知 CPU 指定发给哪个 CPU 核
清除中断标志

GIC 寄存器(简化):

┌────────────────────────────────────────────────────────────┐
│ 地址           │ 名称              │ 作用                   │
├────────────────┼───────────────────┼────────────────────────┤
│ 0x1000         │ GICD_CTLR         │ 分发器总开关           │
│ 0x1100         │ GICD_ISENABLER    │ 中断使能(写1使能)     │
│ 0x1180         │ GICD_ICENABLER    │ 中断禁用(写1禁用)     │
│ 0x1400         │ GICD_IPRIORITYR   │ 优先级设置             │
│ 0x1800         │ GICD_ITARGETSR    │ 目标 CPU 设置          │
│ 0x1C00         │ GICD_ICFGR        │ 触发方式(边沿/电平)   │
├────────────────┼───────────────────┼────────────────────────┤
│ 0x2000         │ GICC_CTLR         │ CPU 接口总开关         │
│ 0x200C         │ GICC_IAR          │ 读取当前中断号         │
│ 0x2010         │ GICC_EOIR         │ 中断处理完成通知       │
└────────────────┴───────────────────┴────────────────────────┘

10.5 中断处理完整流程

1. GPIO 引脚电平变化
        │
        ▼ (硬件信号)
2. GIC 硬件检测到中断,锁存状态
        │
        ▼ (硬件自动)
3. GIC 根据优先级仲裁,通知 CPU
        │
        ▼ (硬件触发 IRQ 线)
4. CPU 跳转到中断向量表
        │
        ▼ (软件)
5. 内核读 GICC_IAR 寄存器,获取中断号
   irqnr = readl(gic_cpu_base + GICC_IAR);
        │
        ▼ (软件)
6. 根据中断号找到对应 handler 执行
   handle_irq(irqnr);
        │
        ▼ (软件)
7. 写 GICC_EOIR 告诉 GIC "我处理完了"
   writel(irqnr, gic_cpu_base + GICC_EOIR);
        │
        ▼ (硬件)
8. GIC 清除该中断状态,准备接收下一个

10.6 上半部 vs 下半部

┌─────────────────────────────────────────────────────────────┐
│  上半部 (Top Half) - 中断上下文                              │
│  - 必须快!不能睡眠                                          │
│  - 只做最紧急的事(清中断、读数据)                           │
│  - 调度下半部处理复杂逻辑                                    │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  下半部 (Bottom Half) - 进程上下文                           │
│  - 可以睡眠、执行耗时操作                                    │
│  - tasklet / workqueue / threaded irq                       │
└─────────────────────────────────────────────────────────────┘

十一、内存管理

11.1 物理地址 vs 虚拟地址

┌─────────────────────────────────────────────────────────────┐
│  用户程序 / 内核代码                                         │
│  看到的地址: 0xC0008000 (虚拟地址)                           │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼  MMU (内存管理单元) 翻译
┌─────────────────────────────────────────────────────────────┐
│  实际硬件                                                    │
│  真实地址: 0x4200D000 (物理地址)                             │
└─────────────────────────────────────────────────────────────┘

11.2 为什么要虚拟地址?

好处 说明
隔离 每个进程有独立地址空间,互不干扰
安全 用户态不能访问内核内存
灵活 物理内存可以不连续,虚拟地址连续
共享 多进程可以映射同一物理页(共享库)

11.3 ioremap —— 驱动访问硬件的关键

// 问题:硬件寄存器在物理地址 0x4200D000
// CPU 只能用虚拟地址访问

// 解决:ioremap 建立映射
void __iomem *base = ioremap(0x4200D000, 0x400);

// 现在可以用 base 访问硬件了
value = readl(base + OFFSET);
writel(value, base + OFFSET);

11.4 内存布局 (ARM 32位)

虚拟地址空间 (4GB)
┌─────────────────────┐ 0xFFFFFFFF
│                     │
│   内核空间 (1GB)     │ ← 内核代码、驱动、ioremap 区域
│                     │
├─────────────────────┤ 0xC0000000
│                     │
│                     │
│   用户空间 (3GB)     │ ← 应用程序
│                     │
│                     │
└─────────────────────┘ 0x00000000

11.5 DMA —— 硬件直接访问内存

普通方式:
CPU ←→ 内存 ←→ 外设     (CPU 搬运数据,慢)

DMA 方式:
CPU: "把这块内存的数据发到外设"
DMA控制器: 自己搬运
CPU: 干别的事去了
DMA控制器: "搬完了!" (中断通知)
// DMA 需要物理地址,不能用虚拟地址
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// buf = 虚拟地址 (CPU 用)
// dma_handle = 物理地址 (DMA 控制器用)

十二、总结对比

概念 核心要点
驱动 寄存器操作 + 内核框架注册
设备树 描述硬件,compatible 做匹配
启动流程 BootROM → U-Boot → 内核 → init → 用户空间
中断 硬件信号 → GIC → CPU → 内核 → 驱动 handler
GIC 中断的交通警察,硬件电路但需软件配置
内存 虚拟地址 → MMU → 物理地址;ioremap 访问硬件

核心思想:硬件干活,软件配置。

Linux 驱动学习笔记

基于 Realtek Ameba RTL8730E 平台,Linux 5.4 内核


一、驱动的本质

驱动 = 按内核框架的规矩,把寄存器操作包装成标准接口,注册给内核,由内核代为调度。

用户程序
   ↓
系统调用 (read/write/ioctl)
   ↓
内核 VFS/子系统
   ↓
驱动程序  ← 把通用接口翻译成具体硬件操作
   ↓
硬件寄存器/总线

驱动 vs 普通代码

普通应用代码 驱动代码
用户态运行 内核态运行
崩溃只影响自己 崩溃整个系统挂
用 libc/malloc 用 kmalloc/kzalloc
可以随便循环 不能长时间阻塞

二、驱动骨架(通用模式)

所有驱动都遵循这个套路:

┌────────────────────────────────────────────────────────┐
│                     驱动骨架                            │
├────────────────────────────────────────────────────────┤
│ 1. of_match_table    →  声明我能驱动什么硬件            │
│ 2. probe()           →  匹配后初始化(ioremap/注册)     │
│ 3. 操作函数           →  readl/writel 操作寄存器        │
│ 4. 注册到子系统       →  gpiochip_add / i2c_add_adapter │
│ 5. 中断处理(可选)     →  request_irq + handler          │
└────────────────────────────────────────────────────────┘

三、设备树与驱动匹配

3.1 设备树是什么

设备树 = 硬件的"简历",告诉内核有什么硬件、在哪里。

// 文件: rtl8730e-pinctrl.dtsi:272
gpioa: gpio@4200D000 {
    compatible = "realtek,ameba-gpio";   // 身份标识(最关键)
    reg = <0x4200D000 0x400>;            // 寄存器地址
    rtk,gpio-bank = <0>;                 // 自定义属性
    interrupts = <GIC_SPI 9 ...>;        // 中断号
    clocks = <&rcc RTK_CKE_GPIO>;        // 时钟源
};

3.2 内核匹配机制

     设备树 (硬件简历)                    驱动 (找工作的人)
┌──────────────────────────┐    ┌──────────────────────────┐
│ compatible = "realtek,   │    │ .of_match_table = {      │
│              ameba-gpio" │◄──►│   "realtek,ameba-gpio"   │
│                          │    │ }                        │
└──────────────────────────┘    └──────────────────────────┘
                  │                          │
                  └────────────┬─────────────┘
                               ▼
                    ┌──────────────────┐
                    │   内核:匹配成功! │
                    │   调用 probe()   │
                    └──────────────────┘

本质就是字符串匹配:

// 内核匹配逻辑(简化)
if (strcmp(设备树.compatible, 驱动.of_match_table.compatible) == 0) {
    驱动.probe(device);
}

3.3 各组件角色

组件 作用 比喻
设备树 描述"有什么硬件、在哪里" 房产证
compatible 匹配用的"身份证号" 身份证
驱动 实现"怎么操作硬件" 装修队
内核 根据 compatible 匹配 中介
probe 匹配成功后的初始化 入职手续

四、GPIO 驱动详解

源码位置: kernel/linux-5.4/drivers/rtkdrivers/gpio/realtek-gpio.c

4.1 驱动身份证

// realtek-gpio.c:524-544
static const struct of_device_id realtek_ameba_gpio_of_match[] = {
    { .compatible = "realtek,ameba-gpio", },
    { },
};

static struct platform_driver realtek_ameba_gpio_driver = {
    .probe = realtek_gpio_probe,
    .driver = {
        .name = "realtek-ameba-gpio",
        .of_match_table = realtek_ameba_gpio_of_match,
    },
};

arch_initcall(realtek_ameba_gpio_register);  // 开机时注册

4.2 probe 函数(入职)

// realtek-gpio.c:415-493
int realtek_gpio_probe(struct platform_device *pdev)
{
    // 1. 从设备树读配置
    of_property_read_u32(np, "rtk,gpio-bank", &id);

    // 2. 映射硬件寄存器地址
    bank->reg_base = devm_ioremap_resource(&pdev->dev, res);

    // 3. 向 GPIO 子系统注册
    gpiochip_add_data(&bank->gpio_chip, bank);

    // 4. 设置中断
    irq_set_chained_handler_and_data(...);
}

4.3 核心操作函数

// 读 GPIO 引脚电平
static int realtek_gpio_get(struct gpio_chip *chip, unsigned offset)
{
    ret = !!(readl(bank->reg_base + GPIO_EXT_PORT) & BIT(offset));
    return ret;
}

// 写 GPIO 引脚电平
static void realtek_gpio_set(struct gpio_chip *chip, unsigned offset, int value)
{
    reg_value = readl(bank->reg_base + GPIO_DR);
    if (value)
        reg_value |= BIT(offset);
    else
        reg_value &= ~BIT(offset);
    writel(reg_value, bank->reg_base + GPIO_DR);
}

// 设置方向为输入
static int realtek_gpio_direction_input(...)
{
    reg_value = readl(bank->reg_base + GPIO_DDR);
    reg_value &= ~BIT(offset);  // DDR寄存器:0=输入
    writel(reg_value, bank->reg_base + GPIO_DDR);
}

// 设置方向为输出
static int realtek_gpio_direction_output(...)
{
    reg_value = readl(bank->reg_base + GPIO_DDR);
    reg_value |= BIT(offset);   // DDR寄存器:1=输出
    writel(reg_value, bank->reg_base + GPIO_DDR);
}

4.4 注册到框架

// realtek-gpio.c:380-407
.gpio_chip = {
    .label = "GPIO0",
    .get = realtek_gpio_get,
    .set = realtek_gpio_set,
    .direction_input = realtek_gpio_direction_input,
    .direction_output = realtek_gpio_direction_output,
    .ngpio = 32,
},

4.5 完整调用流程

用户: echo 1 > /sys/class/gpio/gpio5/value
         ↓
内核 GPIO 子系统: 找到 gpio5 属于哪个 chip
         ↓
调用 realtek_gpio_set(chip, 5, 1)
         ↓
驱动内部:
  reg_value = readl(0x4200D000 + GPIO_DR);
  reg_value |= BIT(5);
  writel(reg_value, 0x4200D000 + GPIO_DR);
         ↓
硬件: GPIO 引脚 5 输出高电平

五、I2C 驱动详解

源码位置: kernel/linux-5.4/drivers/rtkdrivers/i2c/rtk-i2c-core.c

5.1 GPIO vs I2C 对比

部分 GPIO 驱动 I2C 驱动
身份证 realtek,ameba-gpio realtek,ameba-i2c
注册子系统 gpiochip_add() i2c_add_numbered_adapter()
核心操作 gpio_get/set i2c_xfer
复杂度 简单(高低电平) 复杂(协议+时序+错误处理)

5.2 驱动身份证

// rtk-i2c-core.c:856-872
static const struct of_device_id rtk_i2c_match[] = {
    { .compatible = "realtek,ameba-i2c"},
    {},
};

static struct platform_driver rtk_i2c_driver = {
    .probe = rtk_i2c_probe,
    .driver = {
        .name = "realtek-ameba-i2c",
        .of_match_table = rtk_i2c_match,
    },
};

module_platform_driver(rtk_i2c_driver);

5.3 probe 函数

// rtk-i2c-core.c:691-784
static int rtk_i2c_probe(struct platform_device *pdev)
{
    // 1. 分配内存
    i2c_dev = devm_kzalloc(...);

    // 2. 映射寄存器
    i2c_dev->base = devm_ioremap_resource(&pdev->dev, res);

    // 3. 获取时钟并使能
    i2c_dev->clock = devm_clk_get(&pdev->dev, NULL);
    clk_prepare_enable(i2c_dev->clock);

    // 4. 注册中断处理函数
    devm_request_irq(&pdev->dev, i2c_dev->irq, rtk_i2c_isr_event, ...);

    // 5. 从设备树读配置
    rtk_i2c_struct_init(i2c_dev, np, &i2c_dev->i2c_param);

    // 6. 注册到 I2C 子系统
    i2c_add_numbered_adapter(adap);
}

5.4 寄存器操作封装

// rtk-i2c-core.c:24-42
void rtk_i2c_writel(void __iomem *ptr, u32 reg, u32 value)
{
    writel(value, ptr + reg);
}

u32 rtk_i2c_readl(void __iomem *ptr, u32 reg)
{
    return readl(ptr + reg);
}

void rtk_i2c_reg_update(void __iomem *ptr, u32 reg, u32 mask, u32 value)
{
    temp = rtk_i2c_readl(ptr, reg);
    temp |= mask;
    temp &= (~mask | value);
    rtk_i2c_writel(ptr, reg, temp);
}

5.5 I2C 比 GPIO 复杂的原因

GPIO:单向、简单
┌─────────┐     ┌─────────┐
│  CPU    │────►│ GPIO引脚 │  只是高低电平
└─────────┘     └─────────┘

I2C:双向、协议复杂
┌─────────┐     ┌─────────┐     ┌─────────┐
│  CPU    │◄───►│ I2C控制器│◄───►│ 外设芯片 │
└─────────┘     └─────────┘     └─────────┘
                     │
            需要处理:地址、时序、
            ACK/NACK、中断、错误

六、关键 API 总结

6.1 寄存器操作

readl(addr)              // 读 32 位寄存器
writel(value, addr)      // 写 32 位寄存器
ioremap(phys, size)      // 物理地址映射到虚拟地址
devm_ioremap_resource()  // 自动管理的 ioremap

6.2 设备树操作

of_property_read_u32(np, "name", &value)  // 读取整数属性
platform_get_resource(pdev, type, index)  // 获取资源(内存/中断)
platform_get_irq(pdev, index)             // 获取中断号

6.3 子系统注册

// GPIO
gpiochip_add_data(&chip, data)

// I2C
i2c_add_numbered_adapter(&adapter)

// 字符设备
register_chrdev(major, name, &fops)

6.4 中断

devm_request_irq(dev, irq, handler, flags, name, data)
irq_set_chip_and_handler(irq, chip, handler)

七、文件位置索引

文件 路径
GPIO 驱动 kernel/linux-5.4/drivers/rtkdrivers/gpio/realtek-gpio.c
I2C 驱动 kernel/linux-5.4/drivers/rtkdrivers/i2c/rtk-i2c-core.c
GPIO 设备树 kernel/linux-5.4/arch/arm/boot/dts/rtl8730e-pinctrl.dtsi:272
I2C 设备树 kernel/linux-5.4/arch/arm/boot/dts/rtl8730e-ocp.dtsi:117

八、一句话总结

驱动 = 身份证(of_match) + 入职(probe) + 干活(readl/writel) + 填表(注册子系统)

掌握这个模式,所有驱动都是这个套路的变体。


九、内核启动流程

9.1 整体流程图

┌─────────────────────────────────────────────────────────────────────┐
│                         上电                                         │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  1. BootROM (芯片固化)                                               │
│     - 芯片内部固化代码,不可修改                                       │
│     - 初始化最基本硬件(时钟、内存控制器)                              │
│     - 从 Flash/SD/USB 加载下一阶段                                   │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  2. U-Boot (Bootloader)                                             │
│     - 初始化 DDR、串口、网络、存储                                    │
│     - 加载内核镜像 (zImage/Image) 到内存                              │
│     - 加载设备树 (.dtb) 到内存                                        │
│     - 跳转到内核入口: bootz <内核地址> - <设备树地址>                  │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  3. Linux 内核启动                                                   │
│     ┌─────────────────────────────────────────────────────────────┐ │
│     │ 3.1 汇编阶段 (arch/arm/boot/compressed/head.S)              │ │
│     │     - 解压内核 (如果是 zImage)                               │ │
│     │     - 设置临时页表                                           │ │
│     │     - 跳转到 start_kernel                                   │ │
│     └─────────────────────────────────────────────────────────────┘ │
│                                │                                     │
│                                ▼                                     │
│     ┌─────────────────────────────────────────────────────────────┐ │
│     │ 3.2 start_kernel() (init/main.c)                           │ │
│     │     - setup_arch(): 解析设备树、初始化内存                    │ │
│     │     - mm_init(): 内存管理初始化                              │ │
│     │     - sched_init(): 调度器初始化                             │ │
│     │     - init_IRQ(): 中断初始化                                 │ │
│     │     - time_init(): 时钟初始化                                │ │
│     │     - rest_init(): 创建 init 进程                           │ │
│     └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  4. init 进程 (PID=1)                                                │
│     - 挂载根文件系统                                                  │
│     - 执行 /sbin/init (systemd 或 busybox init)                     │
│     - 启动用户空间服务                                                │
└─────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────┐
│  5. 用户空间                                                         │
│     - 登录 shell                                                     │
│     - 应用程序运行                                                    │
└─────────────────────────────────────────────────────────────────────┘

9.2 start_kernel 关键步骤

// init/main.c (简化)
asmlinkage void __init start_kernel(void)
{
    // 1. 锁、调试基础设施
    lockdep_init();

    // 2. 架构相关初始化(解析设备树在这里)
    setup_arch(&command_line);

    // 3. 内存管理
    mm_init();

    // 4. 调度器
    sched_init();

    // 5. 中断
    init_IRQ();

    // 6. 时钟
    time_init();

    // 7. 控制台
    console_init();

    // 8. 各种子系统 initcall
    //    这里会调用所有 arch_initcall / module_init 注册的驱动!
    do_initcalls();

    // 9. 创建 init 进程
    rest_init();  // → kernel_init() → 执行 /sbin/init
}

9.3 驱动在哪里被加载?

// do_initcalls() 会按优先级调用:
early_initcall       // 最早
core_initcall
postcore_initcall
arch_initcall        // ← GPIO 驱动在这里 (arch_initcall)
subsys_initcall
fs_initcall
device_initcall      // ← 大多数驱动在这里 (module_init)
late_initcall        // 最晚

GPIO 驱动里的 arch_initcall(realtek_ameba_gpio_register) 就是在这个阶段被调用。


十、中断机制

10.1 中断是什么?

没有中断:
CPU: 不停轮询 → "有数据吗?有数据吗?有数据吗?" → 浪费 CPU

有中断:
CPU: 干自己的事
硬件: "叮!有数据了!" → 中断信号
CPU: 暂停当前工作 → 处理中断 → 返回继续

10.2 ARM 中断架构

┌─────────────────────────────────────────────────────────────┐
│                        外设                                  │
│  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐                        │
│  │GPIO │  │I2C  │  │UART │  │Timer│                        │
│  └──┬──┘  └──┬──┘  └──┬──┘  └──┬──┘                        │
│     │        │        │        │                            │
│     └────────┴────────┴────────┘                            │
│                    │                                         │
│                    ▼                                         │
│     ┌──────────────────────────────┐                        │
│     │      GIC (中断控制器)         │                        │
│     │  Generic Interrupt Controller │                        │
│     │  - 收集所有中断               │                        │
│     │  - 优先级仲裁                 │                        │
│     │  - 分发到 CPU                 │                        │
│     └──────────────┬───────────────┘                        │
│                    │                                         │
│                    ▼                                         │
│     ┌──────────────────────────────┐                        │
│     │           CPU                 │                        │
│     │  - IRQ/FIQ 中断线             │                        │
│     │  - 保存上下文                 │                        │
│     │  - 跳转到中断向量表           │                        │
│     └──────────────────────────────┘                        │
└─────────────────────────────────────────────────────────────┘

10.3 GIC —— 中断的"交通警察"

GIC = Generic Interrupt Controller(通用中断控制器)

为什么需要 GIC?

没有 GIC 的世界(单片机时代):
┌─────┐ ┌─────┐ ┌─────┐
│GPIO │ │UART │ │Timer│
└──┬──┘ └──┬──┘ └──┬──┘
   │       │       │
   └───────┼───────┘
           ▼
        ┌─────┐
        │ CPU │  ← 中断线有限,怎么接这么多设备?
        └─────┘     多个中断同时来,先处理谁?

有 GIC:
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ... (几百个中断源)
│GPIO │ │UART │ │Timer│ │ I2C │
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
   │       │       │       │
   └───────┴───────┴───────┘
                │
                ▼
        ┌─────────────┐
        │     GIC     │  ← 中断管理中心
        │  - 收集中断  │
        │  - 优先级排序│
        │  - 分发给CPU │
        └──────┬──────┘
               │ (只需要几根线)
               ▼
        ┌─────────────┐
        │    CPU      │
        └─────────────┘

GIC 的核心功能:

功能 说明
汇聚 把几百个中断源汇聚到一起
编号 每个中断分配唯一 ID(如 GPIO_A = 9)
优先级 高优先级中断先处理
屏蔽 可以临时关闭某些中断
分发 多核 CPU 时,决定哪个核处理

GIC 中断类型:

┌─────────────────────────────────────────────────────────────┐
│  SGI (Software Generated Interrupt)  0-15                   │
│  软件中断,CPU 之间通信用(多核同步)                          │
├─────────────────────────────────────────────────────────────┤
│  PPI (Private Peripheral Interrupt)  16-31                  │
│  私有中断,每个 CPU 核独有(如本地定时器)                     │
├─────────────────────────────────────────────────────────────┤
│  SPI (Shared Peripheral Interrupt)   32-1019                │
│  共享中断,外设用的(GPIO、I2C、UART 都是这种)               │
└─────────────────────────────────────────────────────────────┘

设备树里的 GPIO 中断号 9,实际是 SPI 9 = 硬件中断号 32 + 9 = 41

10.4 GIC 是硬件,但需要软件配置

┌─────────────────────────────────────────────────────────────┐
│                      GIC 的本质                              │
│                                                             │
│   GIC 是一块 硬件电路,集成在 SoC 芯片里                      │
│   和 CPU、GPIO 控制器一样,是实实在在的硬件模块               │
│                                                             │
│   但它有一组 寄存器,软件通过读写寄存器来配置它               │
└─────────────────────────────────────────────────────────────┘

硬件做的事 vs 软件做的事:

硬件自动完成 软件需要配置
检测中断信号 使能/禁用某个中断
锁存中断状态 设置优先级
优先级仲裁 设置触发方式(边沿/电平)
通知 CPU 指定发给哪个 CPU 核
清除中断标志

GIC 寄存器(简化):

┌────────────────────────────────────────────────────────────┐
│ 地址           │ 名称              │ 作用                   │
├────────────────┼───────────────────┼────────────────────────┤
│ 0x1000         │ GICD_CTLR         │ 分发器总开关           │
│ 0x1100         │ GICD_ISENABLER    │ 中断使能(写1使能)     │
│ 0x1180         │ GICD_ICENABLER    │ 中断禁用(写1禁用)     │
│ 0x1400         │ GICD_IPRIORITYR   │ 优先级设置             │
│ 0x1800         │ GICD_ITARGETSR    │ 目标 CPU 设置          │
│ 0x1C00         │ GICD_ICFGR        │ 触发方式(边沿/电平)   │
├────────────────┼───────────────────┼────────────────────────┤
│ 0x2000         │ GICC_CTLR         │ CPU 接口总开关         │
│ 0x200C         │ GICC_IAR          │ 读取当前中断号         │
│ 0x2010         │ GICC_EOIR         │ 中断处理完成通知       │
└────────────────┴───────────────────┴────────────────────────┘

10.5 中断处理完整流程

1. GPIO 引脚电平变化
        │
        ▼ (硬件信号)
2. GIC 硬件检测到中断,锁存状态
        │
        ▼ (硬件自动)
3. GIC 根据优先级仲裁,通知 CPU
        │
        ▼ (硬件触发 IRQ 线)
4. CPU 跳转到中断向量表
        │
        ▼ (软件)
5. 内核读 GICC_IAR 寄存器,获取中断号
   irqnr = readl(gic_cpu_base + GICC_IAR);
        │
        ▼ (软件)
6. 根据中断号找到对应 handler 执行
   handle_irq(irqnr);
        │
        ▼ (软件)
7. 写 GICC_EOIR 告诉 GIC "我处理完了"
   writel(irqnr, gic_cpu_base + GICC_EOIR);
        │
        ▼ (硬件)
8. GIC 清除该中断状态,准备接收下一个

10.6 上半部 vs 下半部

┌─────────────────────────────────────────────────────────────┐
│  上半部 (Top Half) - 中断上下文                              │
│  - 必须快!不能睡眠                                          │
│  - 只做最紧急的事(清中断、读数据)                           │
│  - 调度下半部处理复杂逻辑                                    │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  下半部 (Bottom Half) - 进程上下文                           │
│  - 可以睡眠、执行耗时操作                                    │
│  - tasklet / workqueue / threaded irq                       │
└─────────────────────────────────────────────────────────────┘

十一、内存管理

11.1 物理地址 vs 虚拟地址

┌─────────────────────────────────────────────────────────────┐
│  用户程序 / 内核代码                                         │
│  看到的地址: 0xC0008000 (虚拟地址)                           │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼  MMU (内存管理单元) 翻译
┌─────────────────────────────────────────────────────────────┐
│  实际硬件                                                    │
│  真实地址: 0x4200D000 (物理地址)                             │
└─────────────────────────────────────────────────────────────┘

11.2 为什么要虚拟地址?

好处 说明
隔离 每个进程有独立地址空间,互不干扰
安全 用户态不能访问内核内存
灵活 物理内存可以不连续,虚拟地址连续
共享 多进程可以映射同一物理页(共享库)

11.3 ioremap —— 驱动访问硬件的关键

// 问题:硬件寄存器在物理地址 0x4200D000
// CPU 只能用虚拟地址访问

// 解决:ioremap 建立映射
void __iomem *base = ioremap(0x4200D000, 0x400);

// 现在可以用 base 访问硬件了
value = readl(base + OFFSET);
writel(value, base + OFFSET);

11.4 内存布局 (ARM 32位)

虚拟地址空间 (4GB)
┌─────────────────────┐ 0xFFFFFFFF
│                     │
│   内核空间 (1GB)     │ ← 内核代码、驱动、ioremap 区域
│                     │
├─────────────────────┤ 0xC0000000
│                     │
│                     │
│   用户空间 (3GB)     │ ← 应用程序
│                     │
│                     │
└─────────────────────┘ 0x00000000

11.5 DMA —— 硬件直接访问内存

普通方式:
CPU ←→ 内存 ←→ 外设     (CPU 搬运数据,慢)

DMA 方式:
CPU: "把这块内存的数据发到外设"
DMA控制器: 自己搬运
CPU: 干别的事去了
DMA控制器: "搬完了!" (中断通知)
// DMA 需要物理地址,不能用虚拟地址
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// buf = 虚拟地址 (CPU 用)
// dma_handle = 物理地址 (DMA 控制器用)

十二、总结对比

概念 核心要点
驱动 寄存器操作 + 内核框架注册
设备树 描述硬件,compatible 做匹配
启动流程 BootROM → U-Boot → 内核 → init → 用户空间
中断 硬件信号 → GIC → CPU → 内核 → 驱动 handler
GIC 中断的交通警察,硬件电路但需软件配置
内存 虚拟地址 → MMU → 物理地址;ioremap 访问硬件

核心思想:硬件干活,软件配置。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐