关于嵌入式驱动的知识
一、驱动的本质
驱动 = 按内核框架的规矩,把寄存器操作包装成标准接口,注册给内核,由内核代为调度。
用户程序
↓
系统调用 (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 访问硬件 |
核心思想:硬件干活,软件配置。
更多推荐



所有评论(0)