一、 核心思想整理:从“写死”到“通用”
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 硬件资源。
// led_resource.h// 定义描述硬件资源的结构体structled_resource{intgroup;// GPIO组号,例如 1 代表 GPIO1intpin;// 引脚号,例如 3 代表 GPIO1_3};// 获取资源的函数声明structled_resource*get_led_resource(void);2. 资源层(board_A_led.c)
这个文件只关心数据。它不包含任何寄存器操作,只负责告诉驱动:“我的灯接在哪里”。
// board_A_led.c (针对单板A)#include"led_resource.h"// 定义具体的硬件资源:我的灯接在 GPIO1_3staticstructled_resourceboard_A_led={.group=1,.pin=3,};// 提供给外部获取资源的接口structled_resource*get_led_resource(void){return&board_A_led;}思考:如果你换了板子,灯接在
GPIO5_4,你只需要修改上面这一小段代码,其他所有文件都不用动!
3. 驱动逻辑层(chipY_gpio.c)
这个文件只关心逻辑。它是通用的,不知道也不关心具体的引脚,它只负责“计算和操作”。
// chipY_gpio.c (针对芯片Y的通用驱动)#include"led_resource.h"#include"led_opr.h"// 包含 led_operations 定义// 假设的寄存器基地址 (i.MX6ULL)// 实际代码中需要通过 ioremap 映射,这里仅作逻辑演示#defineCCM_CCGR1_BASE0x20C406C0#defineGPIO1_BASE0x209C0000#defineGPIO5_BASE0x20AC0000staticstructled_resource*p_res;// 初始化函数:通用的,不管你是哪组 GPIOstaticintchipY_gpio_init(intwhich){// 1. 获取资源(关键步骤!)// 驱动去问资源层:"嘿,我们要操作哪个引脚?"p_res=get_led_resource();// 2. 根据资源计算寄存器地址 (逻辑部分)// 无论 p_res->group 是 1 还是 5,这套逻辑都适用unsignedintbase_addr;if(p_res->group==1){base_addr=GPIO1_BASE;// 使能 GPIO1 时钟 (通用逻辑)// ...}elseif(p_res->group==5){base_addr=GPIO5_BASE;// 使能 GPIO5 时钟// ...}// 3. 设置 GPIO 方向为输出 (通用逻辑)// 这里的逻辑是:读取 DIR 寄存器,把对应 pin 位置 1// *GPIO_DIR(base_addr) |= (1 << p_res->pin);return0;}staticintchipY_gpio_ctl(intwhich,charstatus){// 控制函数同理,根据 p_res->group 和 p_res->pin 计算地址和位移// ...return0;}// 定义 operations 结构体,供上层 led_drv.c 调用structled_operationschipY_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 结构体):
staticstructled_resourceboard_A_led={.pin=3};// 写在 C 里2. 设备树时代(DTS 文本):
/* 写在 .dts 文件里,类似 JSON/XML */led@1{compatible="chipY,my-led";gpios=<&gpio13GPIO_ACTIVE_LOW>;};- 驱动程序(Driver):
驱动不再去问“引脚是谁”,而是从内核给的 device 结构体里“领礼物”:
intled_probe(structplatform_device*pdev){// 内核自动把设备树里的引脚号 3 提取出来,送给这个函数intpin=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函数的核心任务是:“领资源”+“做初始化”
总结
- 总线模型解决了“驱动怎么找到设备”的自动化问题。
- 设备树解决了“硬件描述”的非代码化问题(让代码变纯粹)。