20260102 - Linux驱动设计的思想

一、 核心思想整理:从"写死"到"通用"

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(只记录引脚编号)。
    • 效率极高,不易出错。

二、 代码层面的深度分析

为了让你看懂"分离"是如何在 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,
};

三、 总结:分离的精髓

通过上面的代码分析,我们可以清晰地看到"分离"的效果:

  1. 左边是 board_A_led.c (变化的部分)
    • 这里全是变量数字
    • 负责回答 "Who?" (是哪个引脚?)
    • 将来这部分会演变成 Linux 内核中的 Device Tree (.dts 文件)。
  2. 右边是 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>; 
};
  1. 驱动程序(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 函数的核心任务是:"领资源" + "做初始化"

总结

  • 总线模型 解决了"驱动怎么找到设备"的自动化问题。
  • 设备树 解决了"硬件描述"的非代码化问题(让代码变纯粹)。
相关推荐
List<String> error_P1 天前
STM32窗口看门狗WWDG详解
stm32·单片机·嵌入式硬件·定时器
鑫—萍1 天前
嵌入式开发学习——STM32单片机入门教程
c语言·驱动开发·stm32·单片机·嵌入式硬件·学习·硬件工程
boneStudent1 天前
STM32H750多通道数据采集系统
stm32·单片机·嵌入式硬件
youcans_1 天前
【动手学STM32G4】(13)STM32G431之 TIM+ADC
stm32·单片机·嵌入式硬件·定时器
小灰灰搞电子1 天前
STM32、GD32 ppm协议解析源码分享
stm32·ppm协议
List<String> error_P1 天前
独立看门狗IWDG原理详解
stm32·单片机·嵌入式硬件·定时器
单片机系统设计1 天前
基于STM32的智能防摔报警系统
stm32·单片机·嵌入式硬件·毕业设计·防摔报警·短信报警·号码设置
boneStudent1 天前
基于STM32F745的完整无人机飞控系统
stm32·无人机·cocos2d
boneStudent1 天前
智能电池管理系统(BMS)
stm32·单片机·嵌入式硬件
@good_good_study2 天前
FreeRTOS任务调度
stm32