一、 核心思想整理:从"写死"到"通用"
1. 什么是"分离"?
- 分层 (Layering) :是纵向 切割。把
file_operations(给内核看的)和led_operations(给硬件看的)分开。 - 分离 (Separation) :是横向 切割。在硬件操作层内部,把 "资源(Resource)" 和 "逻辑(Logic/Driver)" 彻底分开。
2. 为什么要分离?
假设你有 10 款开发板,用的都是同一款 CPU(比如 i.MX6ULL),但 LED 接的引脚不同:
- 不分离 :你需要写 10 个
board_xxx.c,每个文件里都要写一遍"读寄存器、改方向、写寄存器"的代码。代码冗余极大。 - 分离 :
- 你只需要写 1 个 通用的
chip_gpio.c(负责算地址、读写)。 - 然后写 10 个 很小的
board_xxx.c(只记录引脚编号)。 - 效率极高,不易出错。
- 你只需要写 1 个 通用的
二、 代码层面的深度分析
为了让你看懂"分离"是如何在 C 语言中实现的,我们要构建三个部分:资源定义 、资源数据 、通用逻辑。
1. 定义标准(头文件)
首先,我们需要一个"协议",规定如何描述一个 LED 硬件资源。
c
// led_resource.h
// 定义描述硬件资源的结构体
struct led_resource {
int group; // GPIO组号,例如 1 代表 GPIO1
int pin; // 引脚号,例如 3 代表 GPIO1_3
};
// 获取资源的函数声明
struct led_resource *get_led_resource(void);
2. 资源层(board_A_led.c)
这个文件只关心数据。它不包含任何寄存器操作,只负责告诉驱动:"我的灯接在哪里"。
c
// board_A_led.c (针对单板A)
#include "led_resource.h"
// 定义具体的硬件资源:我的灯接在 GPIO1_3
static struct led_resource board_A_led = {
.group = 1,
.pin = 3,
};
// 提供给外部获取资源的接口
struct led_resource *get_led_resource(void) {
return &board_A_led;
}
思考 :如果你换了板子,灯接在
GPIO5_4,你只需要修改上面这一小段代码,其他所有文件都不用动!
3. 驱动逻辑层(chipY_gpio.c)
这个文件只关心逻辑。它是通用的,不知道也不关心具体的引脚,它只负责"计算和操作"。
c
// chipY_gpio.c (针对芯片Y的通用驱动)
#include "led_resource.h"
#include "led_opr.h" // 包含 led_operations 定义
// 假设的寄存器基地址 (i.MX6ULL)
// 实际代码中需要通过 ioremap 映射,这里仅作逻辑演示
#define CCM_CCGR1_BASE 0x20C406C0
#define GPIO1_BASE 0x209C0000
#define GPIO5_BASE 0x20AC0000
static struct led_resource *p_res;
// 初始化函数:通用的,不管你是哪组 GPIO
static int chipY_gpio_init(int which) {
// 1. 获取资源(关键步骤!)
// 驱动去问资源层:"嘿,我们要操作哪个引脚?"
p_res = get_led_resource();
// 2. 根据资源计算寄存器地址 (逻辑部分)
// 无论 p_res->group 是 1 还是 5,这套逻辑都适用
unsigned int base_addr;
if (p_res->group == 1) {
base_addr = GPIO1_BASE;
// 使能 GPIO1 时钟 (通用逻辑)
// ...
} else if (p_res->group == 5) {
base_addr = GPIO5_BASE;
// 使能 GPIO5 时钟
// ...
}
// 3. 设置 GPIO 方向为输出 (通用逻辑)
// 这里的逻辑是:读取 DIR 寄存器,把对应 pin 位置 1
// *GPIO_DIR(base_addr) |= (1 << p_res->pin);
return 0;
}
static int chipY_gpio_ctl(int which, char status) {
// 控制函数同理,根据 p_res->group 和 p_res->pin 计算地址和位移
// ...
return 0;
}
// 定义 operations 结构体,供上层 led_drv.c 调用
struct led_operations chipY_gpio_opr = {
.init = chipY_gpio_init,
.ctl = chipY_gpio_ctl,
};
三、 总结:分离的精髓
通过上面的代码分析,我们可以清晰地看到"分离"的效果:
- 左边是
board_A_led.c(变化的部分) :- 这里全是变量 和数字。
- 负责回答 "Who?" (是哪个引脚?)
- 将来这部分会演变成 Linux 内核中的 Device Tree (.dts 文件)。
- 右边是
chipY_gpio.c(不变的部分) :- 这里全是公式 和算法。
- 负责回答 "How?" (怎么操作寄存器?)。
- 将来这部分会演变成 Linux 内核中的 Platform Driver (.c 文件)。
下一步
上述的"分离"思想正是 "总线-设备-驱动"模型 的雏形,是在模拟 Linux 内核中最伟大的发明之一------**Platform Bus(平台总线)**模型。
- 现在 :你还在用 C 语言的
struct来手动传递资源(通过get_led_resource)。 - 未来 :你会学习如何用文本文件(
.dts)来描述group=1, pin=3,然后内核会自动解析这个文本,把它变成结构体传给你的驱动。
一、 概念解析
1. 总线-设备-驱动 (Bus-Device-Driver) 模型
在 Linux 内核中,这是一种管理机制。
- 设备 (Device):包含硬件资源(如:GPIO 引脚号、中断号、寄存器基地址)。
- 驱动 (Driver):包含操作逻辑(如:如何初始化、如何读写寄存器)。
- 总线 (Bus) :它是中间人,负责匹配(Match)。当一个新的"设备"被注册时,总线会去寻找能处理它的"驱动";反之亦然。
2. 设备树 (Device Tree)
在没有设备树之前,所有的"设备"信息都是写在 .c 文件里的。
- 设备树 :是一种特殊的文本文件 (.dts),它用树状结构描述硬件资源。
- 作用 :它把原本写在 C 代码里的硬件参数(如
pin=3)剥离出来,写到一个独立的配置文件里。
二、 进化之路:从"手动分离"到"自动化管理"
把你现在学习的代码与内核成熟模型进行对比,改进点如下:
1. 从"手动调用"到"自动匹配" (总线模型的改进)
- 你现在的做法 :你在
chipY_gpio.c中必须手动调用get_led_resource()。这意味着驱动程序必须"知道"资源函数的存在。 - 总线模型的改进 :
- 优势 :解耦更彻底 。驱动程序只需要声明"我支持名为
my_led的设备"。 - 机制 :内核启动时,总线会对比"设备名字"和"驱动名字"。如果对上了,内核就自动调用驱动的
probe函数,并把资源丢给驱动。驱动完全不需要知道资源定义在哪个文件里。
- 优势 :解耦更彻底 。驱动程序只需要声明"我支持名为
2. 从"硬编码"到"配置化" (设备树的改进)
- 你现在的做法 :你修改引脚后,需要重新编译
board_A_led.c,然后重新生成.ko驱动文件。 - 设备树的改进 :
- 优势 :一套驱动,到处运行。驱动程序编译一次后,可以发往所有使用该芯片的客户。
- 机制 :不同厂商的板子只需要提供自己的
.dts(设备树文件)。内核在启动时解析这个文本文件。你换引脚只需改一行文本,甚至不需要重新编译内核,只需编译这个文本即可。
三、 核心优势对比表
| 特性 | 现在的"分离"思想 | 总线-设备-驱动模型 | 设备树 (Device Tree) |
|---|---|---|---|
| 存放位置 | 两个 .c 文件 |
两个 .c 文件 |
driver.c + .dts 文本 |
| 匹配方式 | 函数硬调用 | 总线根据名字自动匹配 | 总线根据兼容性字符串匹配 |
| 可维护性 | 中等(需重编驱动) | 高(逻辑与数据解耦) | 极高(硬件变化不改驱动代码) |
| 通用性 | 仅限本项目 | 符合内核规范,易于移植 | 工业标准,跨平台能力最强 |
四、 代码层面的视觉进化
1. 你的现状(C 结构体):
c
static struct led_resource board_A_led = { .pin = 3 }; // 写在 C 里
2. 设备树时代(DTS 文本):
json
/* 写在 .dts 文件里,类似 JSON/XML */
led@1 {
compatible = "chipY,my-led";
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
- 驱动程序(Driver):
驱动不再去问"引脚是谁",而是从内核给的 device 结构体里"领礼物":
c
int led_probe(struct platform_device *pdev) {
// 内核自动把设备树里的引脚号 3 提取出来,送给这个函数
int pin = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);
// ... 然后直接用即可
}
五、总线的工作流程
第一步:设备登记 (Device Register)
当系统启动或你插上一个新硬件时,内核会创建一个 device 结构体,里面写着:"我叫 my_led,我的引脚是 3"。然后把它扔给总线。
第二步:驱动登记 (Driver Register)
驱动程序加载时,内核创建一个 driver 结构体,里面写着:"我能支持名为 my_led 的硬件"。然后也把它扔给总线。
第三步:匹配 (Match)
这是总线最核心的工作。每当有新的"设备"或"驱动"加入,总线就会自动执行一个 match 函数:
总线问: "驱动啊,这个新来的设备叫
my_led,你要吗?" 驱动看了一眼名字: "名字对上了,我要了!"
第四步:结合 (Probe)
一旦匹配成功,总线就会调用驱动 里的 probe(探查)函数。
总线: "既然你们看对眼了,驱动,这是那个设备的资源(引脚号等),你拿去初始化吧!"
probe 函数的核心任务是:"领资源" + "做初始化"
总结
- 总线模型 解决了"驱动怎么找到设备"的自动化问题。
- 设备树 解决了"硬件描述"的非代码化问题(让代码变纯粹)。