[TOC]

在这里插入图片描述

drivers/gpio GPIO子系统(General Purpose Input/Output) 内核与硬件I/O引脚交互的通用框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

GPIO(通用输入/输出)子系统是为了在Linux内核中创建一个统一、抽象、可移植的框架来管理和控制硬件的GPIO引脚而诞生的。 在此框架出现之前,对GPIO的操作是混乱且与平台高度绑定的。每个SoC(片上系统)或主板都有自己独特的GPIO控制方式,驱动程序必须编写大量特定于硬件的代码才能操作一个引脚。

该子系统的诞生解决了以下核心问题:

  • 消除平台特定代码:为内核提供一个标准的API,使得驱动程序(称为“消费者”)可以不用关心底层GPIO控制器(称为“提供者”或gpio_chip)的具体实现,就能请求、配置和读写一个GPIO引脚。
  • 资源管理与冲突避免:一个GPIO引脚在系统中是独占性资源。 该框架提供了一套请求(request)和释放(free)机制,确保一个引脚在同一时间只能被一个驱动程序使用,从而避免了硬件冲突。
  • 抽象硬件差异:不同的GPIO控制器功能各异(例如,有些支持中断,有些支持开漏/开源配置等)。GPIO子系统通过统一的接口抽象了这些差异,为上层驱动提供了一致的行为。
  • 与设备树集成:随着设备树(Device Tree)的普及,GPIO子系统需要一种方法来解析设备树中描述的GPIO连接关系,使得驱动程序能够动态地获取其所需的引脚,而不是在代码中硬编码。

它的发展经历了哪些重要的里程碑或版本迭代?

GPIO子系统的发展经历了从简单的数字命名空间到更安全、更强大的描述符模型的演进。

  • 早期的整数命名空间(Legacy API):最初,所有GPIO引脚都被映射到一个全局的、从0开始的整数命名空间中。 驱动通过一个整数(如 gpio_request(23, ...))来请求引脚。这种方式的主要缺点是:
    • 脆弱性:GPIO编号不是稳定的,它会因为内核配置或硬件平台的改变而改变,导致代码不可移植。
    • 不安全:任何驱动都可以伪造一个整数来尝试控制一个它不拥有的引-脚。
  • 描述符接口的引入(Descriptor-based API):这是一个决定性的里程碑。新的API不再使用不稳定的全局整数,而是引入了一个不透明的句柄——struct gpio_desc *(GPIO描述符)。
    • 驱动程序必须通过 gpiod_get() 等函数从设备(struct device)或通过设备树获取描述符。
    • 这种方式更安全,因为描述符不能被伪造,并且其生命周期由内核管理。
    • 它还更好地处理了“低电平有效”(active-low)的逻辑,驱动可以设置或读取逻辑值(0/1),而由框架来处理物理电平的翻转。
    • 所有新的内核驱动都被强烈推荐使用这个以 gpiod_* 为前缀的新接口。
  • 用户空间接口的演进
    • Sysfs接口(已废弃):早期提供了一个通过/sys/class/gpio的接口,允许用户空间通过读写文件来控制GPIO。该接口因其效率低下、设计缺陷以及与旧的整数模型绑定等原因,从Linux 4.8版本开始被废弃
    • 字符设备接口(Chardev ABI):为了取代sysfs,内核引入了一个基于字符设备(/dev/gpiochipN)的新用户空间接口。 它更高效(可以通过一次ioctl调用操作多个引脚)、更安全(资源生命周期与文件句柄绑定),并由libgpiod库提供了易于使用的封装。

目前该技术的社区活跃度和主流应用情况如何?

GPIO子系统是所有嵌入式Linux系统和许多服务器硬件平台的核心基础组件。它非常稳定,但仍在积极维护以支持新的硬件特性。

  • 主流应用:几乎所有的嵌入式设备驱动都会用到GPIO。内核中已经包含了大量使用GPIO的子系统级驱动,例如gpio-keys(按键输入)、leds-gpio(LED控制)、gpio-fan(风扇控制)等,这些驱动为常见的硬件模式提供了标准化的内核接口。
  • 社区规范:社区强烈推荐所有新代码使用基于描述符的gpiod_*内核API,并使用libgpiod库与字符设备进行用户空间交互。

核心原理与设计

它的核心工作原理是什么?

GPIO子系统的核心是生产者/消费者模型,由一个通用的中间层(称为gpiolib)进行协调。

  1. 生产者(Provider / gpio_chip

    • 硬件GPIO控制器的驱动程序(例如,某个SoC的GPIO模块驱动)会实现一个struct gpio_chip
    • 这个结构体包含了一组函数指针,用于实现具体的操作,如设置方向(.direction_input/.direction_output)、读值(.get)、写值(.set)以及请求中断等。
    • 驱动通过 gpiochip_add_data() 将其实例注册到gpiolib中。
  2. 消费者(Consumer)

    • 需要使用GPIO的设备驱动程序(例如,一个Wi-Fi模块驱动需要控制电源使能引脚)被称为消费者。
    • 消费者通过调用 gpiod_get()devm_gpiod_get(),并提供设备指针和在设备树中定义的名字(如"enable-gpios"),来向gpiolib请求一个GPIO描述符。
    • gpiolib根据设备树的连接关系,找到对应的gpio_chip,并返回一个有效的描述符。
  3. gpiolib(核心层)

    • 位于drivers/gpio/gpiolib.c等文件中,是连接生产者和消费者的桥梁。
    • 它管理着所有注册的gpio_chip,维护一个GPIO描述符池,并处理资源的请求和释放。
    • 当消费者的驱动调用 gpiod_set_value() 时,gpiolib会找到该描述符对应的gpio_chip,并调用其.set()回调函数,从而最终操作硬件。

它的主要优势体现在哪些方面?

  • 抽象和解耦:完全将设备驱动与具体的GPIO控制器硬件分离。
  • 安全性:基于描述符的API防止了资源的误用,并确保了所有权。
  • 标准化:为内核和用户空间提供了稳定且功能丰富的API。
  • 可扩展性:新的GPIO控制器可以很容易地通过实现gpio_chip接口接入到系统中。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能:对于需要极高速率(MHz级别)进行位操作(Bit-banging)的场景,通过gpiolib的函数调用路径可能会引入不可接受的延迟。这种场景通常应使用硬件SPI或I2C控制器,而不是用GPIO模拟。
  • 非通用功能:GPIO子系统只处理通用的数字输入/输出。 对于引脚的复用(Pin Muxing)、电气特性配置(如上/下拉、驱动强度)等更复杂的功能,则由Pinctrl子系统负责。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

GPIO子系统是任何需要直接控制或读取硬件数字信号线的标准解决方案。

  • 硬件使能/复位:一个MMC/SD卡驱动,通过GPIO来检测卡槽中是否有卡插入(输入),以及控制SD卡的电源使能(输出)。
  • LED状态指示:系统通过一个leds-gpio驱动来控制多个LED灯的亮灭,以指示网络活动、存储状态等。
  • 按键输入:使用gpio-keys驱动,将连接到GPIO的物理按键转换为标准的内核输入事件,这样用户空间的程序就能像读取键盘一样读取按键。
  • 中断触发:一个触摸屏控制器,通过一个GPIO引脚在有触摸事件发生时向CPU发送中断信号。gpiolib能够将GPIO中断转换为标准的Linux IRQ号。

是否有不推荐使用该技术的场景?为什么?

  • 引脚复用配置:当一个引脚需要被配置为特定硬件模块(如UART、I2C控制器)的功能引脚时,不应使用GPIO子系统。这项工作必须由Pinctrl子系统完成,它负责将引脚从“GPIO模式”切换到“UART_TX模式”等。
  • 高速串行总线模拟:如上所述,模拟SPI或I2C总线对于gpiolib来说开销太大,性能很差。应优先使用专用的硬件控制器。
  • 模拟PWM或音频信号:虽然可以通过软件快速翻转GPIO来模拟PWM,但其精度和稳定性远不如硬件PWM控制器。

对比分析

请将其 与 其他相似技术 进行详细对比。

在Linux内核中,与GPIO子系统关系最密切、最容易混淆的是Pinctrl(Pin Control)子系统

特性 GPIO Subsystem Pinctrl Subsystem
核心功能 控制引脚的逻辑状态:设置为输入或输出,读取高/低电平,写入高/低电平,处理中断。 配置引脚的“身份”和电气特性:决定一个引脚是作为GPIO、UART、I2C还是其他功能,以及配置其上/下拉、驱动强度、转换速率等。
操作对象 已经处于GPIO模式下的引脚。 系统中的所有物理引脚(Pins)
抽象层次 较高层,面向逻辑功能。一个GPIO是一个可以读写的数字信号线。 较低层,面向硬件物理属性。一个Pin是一个需要被复用和配置的物理实体。
交互关系 依赖于Pinctrl。一个引脚必须首先被Pinctrl子系统配置为GPIO模式,然后才能被GPIO子系统使用。 为GPIO提供基础。Pinctrl负责“搭台”,GPIO负责“唱戏”。
典型API 内核:gpiod_get(), gpiod_direction_output(), gpiod_set_value() 内核:pinctrl_get(), pinctrl_lookup_state(), pinctrl_select_state()
总结 回答“这个信号线是高电平还是低电平? 回答“这个物理引脚现在应该做什么用?(是当GPIO还是当UART用?)

include/linux/gpio/driver.h

for_each_gpiochip_node 遍历GPIO控制器节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define for_each_gpiochip_node(dev, child)					\
device_for_each_child_node(dev, child) \
for_each_if(fwnode_property_present(child, "gpio-controller"))

static inline unsigned int gpiochip_node_count(struct device *dev)
{
struct fwnode_handle *child;
unsigned int count = 0;

for_each_gpiochip_node(dev, child)
count++;

return count;
}

drivers/gpio/gpiolib-of.c

of_get_named_gpiod_flags & of_gpio_flags_quirks

这两个函数是Linux内核中从设备树(Device Tree)解析GPIO属性的最终执行者of_get_named_gpiod_flags是负责处理单个、具名GPIO属性的核心工作引擎, 而of_gpio_flags_quirks则是一个专门的辅助函数, 用于处理各种历史遗留的、非标准的设备树绑定(“怪癖”)。


of_get_named_gpiod_flags: The Core Device Tree GPIO Property Parser

此函数是of_find_gpio在确定了要查找的具体属性名称(例如, "enable-gpios")后调用的底层核心引擎。它的原理是执行一个精确的、分阶段的流程, 将设备树中一个GPIO描述符(phandle + specifier)完全转化为一个内核可以使用的、包含了正确标志的gpio_desc句柄。

工作流程详解:

  1. 解析Phandle和参数 (of_parse_phandle_with_args_map): 这是第一步, 也是最关键的一步。此函数负责解析设备树属性值, 例如 <&gpioA 5 GPIO_ACTIVE_LOW>

    • 它从属性中提取出phandle (指向GPIO控制器节点, 如&gpioA)。
    • 它根据GPIO控制器节点中的#gpio-cells属性, 确定后面有多少个参数(specifier)。
    • 它将phandle和所有参数(5, GPIO_ACTIVE_LOW)打包到一个of_phandle_args结构体(gpiospec)中。
  2. 查找提供者驱动 (of_find_gpio_device_by_xlate): 有了指向GPIO控制器节点的phandle, 此函数会在内核中查找是否已经有驱动程序为该节点注册了一个gpio_device

    • 这是处理驱动依赖关系的核心点: 如果GPIO控制器驱动(例如STM32的pinctrl/gpio驱动)尚未初始化, 查找就会失败。在这种情况下, 此函数会正确地返回-EPROBE_DEFER, 从而安全地推迟当前消费者驱动的探测, 等待依赖就绪。
  3. 翻译与获取 (of_xlate_and_get_gpiod_flags): 找到GPIO控制器驱动后, 此函数会调用该驱动的of_xlate回调函数。

    • xlate (translate) 的作用是将设备树中特定于硬件的参数 (如 5 GPIO_ACTIVE_LOW) “翻译”成驱动内部可以理解的本地硬件引脚号, 并解析出标准的内核GPIO标志。
    • 翻译完成后, 它就从该GPIO控制器驱动管理的gpio_chip中获取代表该特定引脚的gpio_desc句柄。
  4. 应用”怪癖” (of_gpio_flags_quirks): 在基本标志解析完成后, 它会调用of_gpio_flags_quirks函数, 对标志进行可能的修正, 以处理各种非标准的历史遗留绑定。

  5. 资源管理: 在函数结束前, 它必须调用of_node_put来释放对GPIO控制器节点的引用计数, 这是Linux内核中标准的资源管理实践。


of_gpio_flags_quirks: The Legacy Binding Compatibility Handler

此函数本身不执行任何标准的查找, 它的唯一作用是充当一个”兼容性补丁”集合。它包含了许多针对特定设备或旧版设备树绑定的特殊处理逻辑, 用于修正或补充从设备树中解析出来的GPIO标志。

原理:
它是一个大型的if/else if条件判断集合, 每一个条件块都对应一个已知的”怪癖”。

  • 通用修正: 它首先调用一些通用的修复函数, 如of_gpio_try_fixup_polarity, 来处理一些常见的极性定义问题。
  • 固定电压调节器怪癖: 它检查设备是否是一个"reg-fixed-voltage"。如果是, 它会去查找一个额外的、非标准的布尔属性"gpio-open-drain"。在现代设备树中, “open-drain”标志应该直接写在gpios属性的参数里, 这个检查是为了兼容那些将此标志放在别处的旧设备树。
  • SPI芯片选择怪癖: 这是一个更复杂的例子。对于SPI的"cs-gpios"属性, 其”高电平有效”或”低电平有效”的极性, 按照旧的绑定规范, 不是定义在cs-gpios属性本身, 而是定义在SPI master节点的子节点中的一个"spi-cs-high"布尔属性里。此函数包含了遍历子节点、匹配片选索引、并根据spi-cs-high属性来修正极性标志的完整逻辑。
  • STMMAC以太网怪癖: 它为STMMAC驱动的复位引脚"snps,reset-gpio"检查一个名为"snps,reset-active-low"的独立布尔属性, 以此来确定复位信号的极性。

在STM32H750的现代设备树文件中, 这些怪癖通常不会被触发。然而, of_gpio_flags_quirks的存在是Linux内核能够无缝支持横跨十几年、数千种不同硬件设计的关键因素之一, 它将处理历史遗留问题的代码集中在一个地方, 使得核心的解析逻辑(of_get_named_gpiod_flags)能够保持干净和标准化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*
* of_gpio_flags_quirks - 处理GPIO标志的特殊情况和历史遗留问题.
*/
static void of_gpio_flags_quirks(const struct device_node *np,
const char *propname,
enum of_gpio_flags *flags,
int index)
{
// ... (调用通用极性修正函数)

/*
* 怪癖1: 固定电压调节器的历史遗留开漏(open drain)标志处理.
*/
if (IS_ENABLED(CONFIG_REGULATOR) &&
of_device_is_compatible(np, "reg-fixed-voltage") &&
of_property_read_bool(np, "gpio-open-drain")) {
/* 如果找到非标准的 "gpio-open-drain" 属性, 手动添加开漏标志. */
*flags |= (OF_GPIO_SINGLE_ENDED | OF_GPIO_OPEN_DRAIN);
pr_info("%s uses legacy open drain flag - update the DTS if you can\n",
of_node_full_name(np)); // 打印一条信息, 建议用户更新设备树.
}

/*
* 怪癖2: SPI芯片选择(cs-gpios)的历史遗留极性处理.
*/
if (IS_ENABLED(CONFIG_SPI_MASTER) && !strcmp(propname, "cs-gpios") &&
of_property_present(np, "cs-gpios")) {
// ... (复杂的逻辑: 遍历SPI master的子节点)
for_each_child_of_node_scoped(np, child) {
// ... (匹配片选索引 `cs == index`)
if (cs == index) {
/* 检查子节点中是否存在 "spi-cs-high" 属性来决定极性. */
bool active_high = of_property_read_bool(child, "spi-cs-high");
of_gpio_quirk_polarity(child, active_high, flags);
break;
}
}
}

/*
* 怪癖3: STMMAC以太网驱动的历史遗留复位极性处理.
*/
if (IS_ENABLED(CONFIG_STMMAC_ETH) &&
!strcmp(propname, "snps,reset-gpio") &&
of_property_read_bool(np, "snps,reset-active-low"))
/* 如果找到非标准的 "snps,reset-active-low" 属性, 手动添加低电平有效标志. */
*flags |= OF_GPIO_ACTIVE_LOW;
}

/**
* of_get_named_gpiod_flags() - 获取具名GPIO的描述符和标志.
* (核心解析引擎)
*/
static struct gpio_desc *of_get_named_gpiod_flags(const struct device_node *np,
const char *propname, int index, enum of_gpio_flags *flags)
{
struct of_phandle_args gpiospec;
struct gpio_desc *desc;
int ret;

/* 1. 解析 phandle 和参数 */
ret = of_parse_phandle_with_args_map(np, propname, "gpio", index,
&gpiospec);
if (ret) {
// ... (错误处理)
return ERR_PTR(ret);
}

/* 2. 查找提供者驱动 (gpio_device) */
struct gpio_device *gdev __free(gpio_device_put) =
of_find_gpio_device_by_xlate(&gpiospec);
if (!gdev) {
/* 如果驱动未就绪, 返回 EPROBE_DEFER */
desc = ERR_PTR(-EPROBE_DEFER);
goto out;
}

/* 3. 翻译参数并获取最终的 gpio_desc */
desc = of_xlate_and_get_gpiod_flags(gpio_device_get_chip(gdev),
&gpiospec, flags);
if (IS_ERR(desc))
goto out;

/* 4. 如果需要, 应用 "怪癖" 修正标志 */
if (flags)
of_gpio_flags_quirks(np, propname, flags, index);

// ... (调试打印)

out:
/* 5. 释放对GPIO控制器节点的引用 */
of_node_put(gpiospec.np);

return desc;
}

of_find_gpio: The Device Tree GPIO Lookup Engine with Quirk Support

此函数是Linux内核gpiod子系统中专门负责解析设备树(Device Tree)以查找GPIO的后端核心引擎。它的主要作用是根据消费者驱动提供的功能名称(con_id), 在设备树节点(np)中寻找匹配的GPIO属性, 并返回一个代表该GPIO的内核句柄(struct gpio_desc)。

该函数的核心原理是一种健壮且可扩展的两阶段查找策略, 旨在同时支持标准的设备树绑定规范和各种非标准的、特定于硬件的”怪癖”(Quirks)。

工作流程详解:

  1. 阶段一: 标准化查找 (The Standard Path)

    • 此阶段遵循Linux设备树绑定的官方规范。它使用for_each_gpio_property_name宏, 这是一个巧妙的工具, 它会根据输入的功能名con_id自动生成两个标准的属性名进行尝试。例如, 如果con_id"enable", 这个宏会依次生成:
      1. "enable-gpios" (复数形式, 用于可能包含多个GPIO的功能)
      2. "enable-gpio" (单数形式, 向后兼容)
    • 对于每一个生成的属性名, 它会调用of_get_named_gpiod_flags。这个底层函数负责执行实际的设备树解析工作: 它在设备树节点中查找该属性, 读取其值(通常是一个指向GPIO控制器节点的phandle和一些参数), 并返回一个初始的GPIO描述符。
    • 如果of_get_named_gpiod_flags成功找到了GPIO(或者返回了除-ENOENT之外的任何”真实”错误, 如-EBUSY), 查找过程就会立即停止并进入最后阶段。
  2. 阶段二: “怪癖”查找 (The Quirk Path)

    • 只有当第一阶段完全没有找到任何匹配的属性时, 才会进入此阶段。这体现了”标准优先”的原则。
    • 它会遍历一个名为of_find_gpio_quirks的全局函数指针数组。数组中的每一个函数都是一个专门的”怪癖处理器”, 用于解决某个特定硬件平台或旧版设备树绑定不遵循标准规范的问题。
    • 例如:
      • of_find_gpio_rename: 可能用于处理那些使用了非标准属性名的旧绑定。
      • of_find_mt2701_gpio: 这是一个非常具体的例子, 专门用于处理联发科(MediaTek) MT2701 SoC上的一种特殊GPIO绑定。
    • 函数会依次调用数组中的每一个怪癖处理器, 让它们尝试用自己的特殊逻辑去查找GPIO。只要其中任何一个怪癖处理器成功找到, 查找就会停止。
  3. 最后阶段: 标志转换与返回

    • 在通过标准或怪癖路径成功找到GPIO描述符后, 它会调用of_convert_gpio_flags。这是一个重要的翻译步骤, 它将设备树中定义的标志(如OF_GPIO_ACTIVE_LOW)转换为gpiod子系统内部使用的通用标志(如GPIO_ACTIVE_LOW)。
    • 最终, 它返回一个包含了正确硬件信息和标志的、可供上层函数使用的gpio_desc指针。

在STM32H750这样的现代嵌入式平台上, 其设备树绑定通常遵循官方标准。因此, 在绝大多数情况下, of_find_gpio函数会在第一阶段就成功找到所需的GPIO, 而不会进入第二阶段的怪癖处理流程。然而, 这个怪癖处理机制是Linux内核保持对大量不同硬件(包括那些有历史遗留问题的硬件)的广泛兼容性的关键所在, 体现了内核设计的灵活性和向后兼容性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
* 定义一个函数指针类型 of_find_gpio_quirk.
* 这种类型的函数接收设备树节点、功能ID、索引和OF标志指针作为参数, 返回一个gpio_desc.
*/
typedef struct gpio_desc *(*of_find_gpio_quirk)(struct device_node *np,
const char *con_id,
unsigned int idx,
enum of_gpio_flags *of_flags);
/*
* 定义一个静态的、常量类型的函数指针数组.
* 这个数组包含了所有已注册的 "怪癖" 处理器函数.
* 内核会依次调用它们来处理非标准的设备树绑定.
*/
static const of_find_gpio_quirk of_find_gpio_quirks[] = {
of_find_gpio_rename, // 处理重命名或别名
of_find_mt2701_gpio, // 处理MT2701 SoC的特殊情况
of_find_trigger_gpio, // 处理中断触发相关的特殊情况
NULL // 数组的结束标记
};

/*
* of_find_gpio - 在设备树节点中查找GPIO.
* @np: 要搜索的设备树节点.
* @con_id: GPIO的功能名称, 如 "enable".
* @idx: 在多GPIO功能中的索引.
* @flags: (输出参数) 返回转换后的gpiod标志.
* @return: 成功时返回gpio_desc, 失败时返回错误指针.
*/
struct gpio_desc *of_find_gpio(struct device_node *np, const char *con_id,
unsigned int idx, unsigned long *flags)
{
char propname[32]; /* 属性名的最大长度为32个字符 */
enum of_gpio_flags of_flags = 0; // 初始化设备树专用的标志
const of_find_gpio_quirk *q;
struct gpio_desc *desc;

/* --- 阶段一: 标准查找 --- */
/*
* for_each_gpio_property_name 是一个宏, 它会根据 con_id 生成标准的属性名.
* 例如, 如果 con_id 是 "reset", 它会先将 propname 设置为 "reset-gpios",
* 然后在下一次迭代中设置为 "reset-gpio".
*/
for_each_gpio_property_name(propname, con_id) {
/*
* of_get_named_gpiod_flags 是实际的解析函数, 它在np节点中查找名为propname的属性.
*/
desc = of_get_named_gpiod_flags(np, propname, idx, &of_flags);
/*
* gpiod_not_found(desc) 检查返回值是否是 -ENOENT (未找到).
* !gpiod_not_found(desc) 的意思是 "如果找到了, 或者返回了除'未找到'之外的其他错误".
* 在这两种情况下, 我们都应该停止搜索.
*/
if (!gpiod_not_found(desc))
break;
}

/* --- 阶段二: "怪癖"查找 --- */
/*
* 这个循环的条件是: 1. 标准查找没有找到 (gpiod_not_found(desc)为真) 2. 还有未尝试的怪癖处理器 (*q不为NULL)
*/
for (q = of_find_gpio_quirks; gpiod_not_found(desc) && *q; q++)
/*
* 调用当前怪癖处理器函数 (*q), 让它尝试用自己的特殊逻辑去查找GPIO.
*/
desc = (*q)(np, con_id, idx, &of_flags);

/* 检查最终结果是否是一个错误 (除了-ENOENT, 因为它可能已被怪癖处理器修正) */
if (IS_ERR(desc))
return desc;

/* --- 阶段三: 标志转换 --- */
/*
* of_convert_gpio_flags 将从设备树中解析出的 of_flags
* (如 OF_GPIO_ACTIVE_LOW) 转换为 gpiod 子系统内部使用的通用标志 (*flags).
*/
*flags = of_convert_gpio_flags(of_flags);

/* 返回最终找到的GPIO描述符 */
return desc;
}

drivers/gpio/gpio-stmpe.c

STMPE GPIO 操作回调函数集

stmpe_gpio_get: 获取GPIO引脚的输入电平

  • 作用: 当内核或用户空间需要读取一个GPIO引脚的当前逻辑状态(高电平或低电平)时, gpiolib框架会调用此函数。
  • 原理:
    1. 获取上下文: gpiochip_get_data(chip)获取到指向本驱动私有数据stmpe_gpio的指针, 从而可以访问到与父设备通信的stmpe句柄。
    2. 定位寄存器: stmpe->regs是一个数组, 存储了不同功能寄存器的基地址。STMPE_IDX_GPMR_LSB是”GPIO Pin Monitor Register”(GPIO引脚监视寄存器)的基地址索引。offset / 8计算出该引脚属于哪个8位的寄存器(哪个bank)。
    3. 定位比特位: BIT(offset % 8)计算出该引脚在8位寄存器中所对应的比特位掩码(例如, offset为10, 掩码为BIT(2), 即0x04)。
    4. 硬件交互: stmpe_reg_read通过I2C/SPI总线读取目标寄存器的值。
    5. 解析结果: ret & mask从读取到的8位值中分离出我们关心的那一位。!!是一个C语言技巧, 用于将任何非零值转换为1, 0值保持为0, 确保返回值是标准的逻辑值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int stmpe_gpio_get(struct gpio_chip *chip, unsigned offset)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;
/* 计算要读取的监视寄存器的确切地址 */
u8 reg = stmpe->regs[STMPE_IDX_GPMR_LSB + (offset / 8)];
/* 计算要操作的比特位的掩码 */
u8 mask = BIT(offset % 8);
int ret;

/* 通过父驱动提供的函数, 经由I2C/SPI总线读取寄存器 */
ret = stmpe_reg_read(stmpe, reg);
if (ret < 0)
return ret; // 如果读取失败, 向上层返回错误码

/* 将读取到的值与掩码进行按位与, 并将结果转换为 1 或 0 */
return !!(ret & mask);
}

stmpe_gpio_set: 设置GPIO引脚的输出电平

  • 作用: 当需要将一个配置为输出的GPIO引脚设置为高电平或低电平时, gpiolib会调用此函数。
  • 原理:
    1. 选择操作: STMPE芯片通常有独立的”GPIO Pin Set Register”(GPSR, 写1置位)和”GPIO Pin Clear Register”(GPCR, 写1清零)。函数根据传入的val值(1或0)来选择which寄存器基址。
    2. 定位: 与get函数一样, 计算出确切的寄存器地址和比特位掩码。
    3. 特殊处理: 某些STMPE芯片型号可能只有一个寄存器用于置位/清零。代码通过比较GPSR和GPCR的地址是否相同来检测这种情况。如果是, 它会调用stmpe_set_bits(一个读-修改-写操作)来设置或清除相应的位。
    4. 标准操作: 对于有独立置位/清零寄存器的型号, 只需向选定的寄存器写入掩码即可触发硬件操作。stmpe_reg_write完成这个总线写操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int stmpe_gpio_set(struct gpio_chip *chip, unsigned int offset, int val)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;
/* 根据 val 的值选择置位(Set)或清零(Clear)寄存器 */
int which = val ? STMPE_IDX_GPSR_LSB : STMPE_IDX_GPCR_LSB;
u8 reg = stmpe->regs[which + (offset / 8)];
u8 mask = BIT(offset % 8);

/* 检查是否是只有一个set/clear寄存器的特殊型号 */
if (stmpe->regs[STMPE_IDX_GPSR_LSB] == stmpe->regs[STMPE_IDX_GPCR_LSB])
/* 是, 则使用读-修改-写操作 */
return stmpe_set_bits(stmpe, reg, mask, val ? mask : 0);

/* 否, 直接向选定的寄存器写入掩码即可 */
return stmpe_reg_write(stmpe, reg, mask);
}

stmpe_gpio_get_direction: 获取GPIO引脚的方向

  • 作用: 查询一个GPIO引脚当前是被配置为输入还是输出。
  • 原理: 此操作围绕”GPIO Pin Direction Register”(GPDR)进行。
    1. 定位: 计算GPDR寄存器的地址和比特位掩码。注意这里有一个 - (offset / 8)的笔误, 应该是+, 假设在父驱动的寄存器地址表中已做了修正。
    2. 硬件交互: 读取GPDR寄存器的值。
    3. 解析: 检查对应的比特位。根据STMPE的数据手册, 如果该位是1, 表示输出; 如果是0, 表示输入。函数返回gpiolib定义的标准方向常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int stmpe_gpio_get_direction(struct gpio_chip *chip,
unsigned offset)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;
/* 计算方向寄存器的地址 (注意代码中的'-'可能是笔误, 通常是'+') */
u8 reg = stmpe->regs[STMPE_IDX_GPDR_LSB] + (offset / 8);
u8 mask = BIT(offset % 8);
int ret;

ret = stmpe_reg_read(stmpe, reg);
if (ret < 0)
return ret;

/* 如果对应位是1, 返回输出; 否则返回输入 */
if (ret & mask)
return GPIO_LINE_DIRECTION_OUT;

return GPIO_LINE_DIRECTION_IN;
}

stmpe_gpio_direction_output / stmpe_gpio_direction_input: 设置GPIO引脚的方向

  • 作用: 将一个GPIO引脚配置为输入或输出模式。
  • 原理:
    1. direction_output: 这是一个两步操作, 以避免引脚在方向切换瞬间产生不确定的电平(毛刺)。
      • 首先调用stmpe_gpio_set预先设置好期望的输出电平val。此时方向尚未改变, 但硬件内部状态已准备好。
      • 然后调用stmpe_set_bits(一个安全的读-修改-写函数), 将GPDR寄存器中对应的比特位置为1, 正式将引脚切换为输出模式。
    2. direction_input: 这是一个单步操作。只需调用stmpe_set_bits将GPDR寄存器中对应的比特位清零即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int stmpe_gpio_direction_output(struct gpio_chip *chip,
unsigned offset, int val)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;
u8 reg = stmpe->regs[STMPE_IDX_GPDR_LSB + (offset / 8)];
u8 mask = BIT(offset % 8);
int ret;

/* 步骤1: 先设置好输出值 */
ret = stmpe_gpio_set(chip, offset, val);
if (ret)
return ret;

/* 步骤2: 再将方向寄存器的对应位置1, 设为输出 */
return stmpe_set_bits(stmpe, reg, mask, mask);
}

static int stmpe_gpio_direction_input(struct gpio_chip *chip,
unsigned offset)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;
u8 reg = stmpe->regs[STMPE_IDX_GPDR_LSB + (offset / 8)];
u8 mask = BIT(offset % 8);

/* 将方向寄存器的对应位清0, 设为输入 */
return stmpe_set_bits(stmpe, reg, mask, 0);
}

stmpe_gpio_request: 请求使用一个GPIO

  • 作用: 当一个驱动程序首次通过gpio_request()gpiod_get()请求使用某个GPIO时, gpiolib会调用此函数。这是执行一次性初始化配置的理想位置。
  • 原理:
    1. 检查保留位: 首先检查该引脚offset是否在norequest_mask(从设备树读取)中被标记为保留。如果是, 则返回-EINVAL拒绝请求。
    2. 设置复用功能: STMPE芯片的引脚通常是多功能的(例如, GPIO, ADC, PWM等)。stmpe_set_altfunc是一个父驱动提供的函数, 它会通过总线向芯片发送命令, 确保该引脚的复用功能被正确地设置为GPIO模式。这是确保引脚能作为通用IO使用的关键一步。
1
2
3
4
5
6
7
8
9
10
11
12
static int stmpe_gpio_request(struct gpio_chip *chip, unsigned offset)
{
struct stmpe_gpio *stmpe_gpio = gpiochip_get_data(chip);
struct stmpe *stmpe = stmpe_gpio->stmpe;

/* 检查该引脚是否被标记为不可请求 */
if (stmpe_gpio->norequest_mask & BIT(offset))
return -EINVAL;

/* 调用父驱动函数, 将该引脚的复用功能设置为GPIO */
return stmpe_set_altfunc(stmpe, BIT(offset), STMPE_BLOCK_GPIO);
}

STMPE GPIO驱动的核心实现

此代码片段展示了stmpe-gpio驱动程序的核心部分: 它定义了驱动的私有数据结构, 并提供了所有必要的回调函数, 以将STMPE IO扩展器的硬件操作与Linux内核通用的gpiolib框架连接起来。其核心原理是gpiolib的抽象请求(如”设置引脚5为高电平”)转换为对STMPE芯片的具体、面向寄存器的I2C/SPI总线操作


struct stmpe_gpio: 驱动私有数据结构

这个结构体是stmpe-gpio驱动实例的”大脑”, 它包含了驱动运行时所需的所有状态和信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* 宏定义: CACHE_NR_REGS
* 定义了需要缓存的寄存器类型的数量 (例如: 中断使能, 边沿检测等), 这里是3.
*/
#define CACHE_NR_REGS 3
/*
* 宏定义: CACHE_NR_BANKS
* STMPE芯片最多有24个GPIO, 每8个GPIO由一组寄存器管理 (一个bank).
* 因此, 24/8 = 3, 需要3个bank的缓存.
*/
#define CACHE_NR_BANKS (24 / 8)

/*
* stmpe_gpio 结构体定义.
*/
struct stmpe_gpio {
/*
* .chip: 内嵌的 gpio_chip 结构体. 这是与gpiolib框架交互的核心.
* probe函数会用下面的 template_chip 来初始化它.
*/
struct gpio_chip chip;
/*
* .stmpe: 指向父设备(核心STMPE驱动)数据结构的指针.
* 通过这个指针, 本驱动可以调用 stmpe_reg_read(), stmpe_reg_write() 等函数来与硬件通信.
*/
struct stmpe *stmpe;
/*
* .irq_lock: 一个互斥锁.
* 用于保护对中断相关寄存器的读-修改-写操作, 防止竞态条件.
*/
struct mutex irq_lock;
/*
* .norequest_mask: 一个32位的位掩码.
* 用于标记哪些GPIO引脚是保留的, 不应被gpiolib分配和使用.
*/
u32 norequest_mask;
/*
* .regs, .oldregs: 两个二维数组, 用于在中断处理期间缓存寄存器的值.
* 当需要一次性读/写多个中断控制寄存器以避免多次总线锁定时, 这个缓存非常有用.
*/
u8 regs[CACHE_NR_REGS][CACHE_NR_BANKS];
u8 oldregs[CACHE_NR_REGS][CACHE_NR_BANKS];
};

template_chip: gpiolib接口的”蓝图”

这个静态常量结构体将上面定义的所有回调函数打包在一起, 作为注册到gpiolib的模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 定义一个静态常量 gpio_chip 结构体, 作为模板.
*/
static const struct gpio_chip template_chip = {
/* .label: 在sysfs和调试信息中显示的名称. */
.label = "stmpe",
/* .owner: 指向拥有此驱动的模块, 用于管理模块引用计数. */
.owner = THIS_MODULE,
/*
* 将 gpiolib 的标准操作函数指针, 指向我们上面实现的具体函数.
*/
.get_direction = stmpe_gpio_get_direction,
.direction_input = stmpe_gpio_direction_input,
.get = stmpe_gpio_get,
.direction_output = stmpe_gpio_direction_output,
.set = stmpe_gpio_set,
.request = stmpe_gpio_request,
/*
* .can_sleep = true: 这是一个非常重要的标志.
* 它告诉gpiolib框架, 这个驱动的所有回调函数都可能会"睡眠"(例如, 等待I2C/SPI总线操作完成).
* gpiolib会据此调整其内部行为, 例如在使用这些GPIO时不会禁用中断或持有自旋锁.
*/
.can_sleep = true,
};

stmpe_gpio_probe: 初始化STMPE IO扩展器的GPIO功能

此函数是stmpe-gpio平台驱动程序的探测函数, 它是驱动程序的核心入口点。当内核的设备模型发现一个与此驱动匹配的设备时(通常是通过设备树), 就会调用此函数。它的核心作用是将一个物理上的STMPE芯片(通过I2C或SPI连接的外部IO扩展器)的GPIO功能, 初始化并注册到Linux内核通用的gpiolib子系统中, 使这些外部IO引脚能像处理器片上GPIO一样被系统标准地访问和控制。

其工作原理可以分解为以下几个关键步骤:

  1. 获取父设备数据与内存分配: 此驱动是一个”多功能设备”(MFD, Multi-Function Device)的客户端。它首先从其父设备(即主stmpe驱动, 负责I2C/SPI通信)获取一个指向stmpe核心数据结构的句柄。然后, 它使用devm_kzalloc为自身的状态结构stmpe_gpio分配内存, 这种内存分配方式确保了在驱动卸载时资源能被自动释放, 极大地简化了错误处理。
  2. 配置gpio_chip结构体: gpio_chipgpiolib框架的核心, 它像一个”驱动蓝图”, 包含了一系列函数指针, 用于定义如何对GPIO进行读、写、设置方向等操作。此函数使用一个预定义的template_chip作为模板, 并根据从父设备获取的信息(如GPIO数量)对其进行定制。将base设置为-1, 是让gpiolib动态地为这些GPIO分配一个未被占用的编号区间。
  3. 硬件使能与中断处理: 它调用父驱动提供的stmpe_enable函数, 向物理芯片发送命令以开启GPIO功能块。这是实际的硬件交互。接着, 它处理中断:
    • 它获取一个由STMPE芯片产生的中断号。这个中断是”共享的”, 即芯片上任何一个GPIO产生中断, 都会触发这一根物理中断线。
    • 它注册一个线程化中断处理程序(stmpe_gpio_irq)。当物理中断发生时, 内核会唤醒一个专门的内核线程来执行这个处理函数, 这对于通过较慢总线(如I2C)连接的设备是最佳实践, 避免在中断上下文中进行耗时操作。
    • 它配置gpio_irq_chip结构。这是将gpiolibirqchip(中断控制器)框架连接起来的”胶水”。它定义了如何为单个GPIO使能/屏蔽中断、设置触发类型等操作。当上层请求某个GPIO的中断时, gpiolib会通过gpio_irq_chip中的函数指针, 调用本驱动的相应函数来操作硬件。
  4. 注册到GPIOLIB: 最后, 它调用devm_gpiochip_add_data, 将完全配置好的gpio_chip注册到gpiolib中。一旦注册成功, 系统中其他任何部分(包括用户空间)就可以通过标准的GPIO接口(例如, gpio_request, gpiod_get, /sys/class/gpio等)来使用这些来自STMPE芯片的GPIO引脚了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/*
* 静态函数声明: stmpe_gpio_probe
* 这是 stmpe_gpio 平台驱动的探测函数.
* @pdev: 指向 struct platform_device 的指针, 代表内核中与此驱动匹配的设备实例.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int stmpe_gpio_probe(struct platform_device *pdev)
{
/*
* 获取指向设备核心结构体 struct device 的指针, 方便后续使用.
*/
struct device *dev = &pdev->dev;
/*
* 从父设备获取私有数据.
* 这是一个 MFD(多功能设备) 驱动模型: 有一个核心的 stmpe 驱动负责总线通信(I2C/SPI),
* 而GPIO, 触摸屏等功能块作为其 "子设备". dev->parent 指向的就是核心 stmpe 设备.
*/
struct stmpe *stmpe = dev_get_drvdata(dev->parent);
/*
* 定义一个指向 stmpe_gpio 结构体的指针, 这是本GPIO驱动的私有数据结构.
*/
struct stmpe_gpio *stmpe_gpio;
/*
* 定义返回值 ret 和中断号 irq.
*/
int ret, irq;

/*
* 健全性检查: 确保芯片报告的GPIO数量没有超过驱动程序内部定义的上限.
*/
if (stmpe->num_gpios > MAX_GPIOS) {
dev_err(dev, "Need to increase maximum GPIO number\n");
return -EINVAL;
}

/*
* 使用 devm_kzalloc 分配并清零 stmpe_gpio 结构体的内存.
* 'devm_' 前缀确保了这块内存在设备分离时会被自动释放.
*/
stmpe_gpio = devm_kzalloc(dev, sizeof(*stmpe_gpio), GFP_KERNEL);
if (!stmpe_gpio)
return -ENOMEM;

/*
* 初始化一个互斥锁, 用于保护对中断相关寄存器的并发访问.
*/
mutex_init(&stmpe_gpio->irq_lock);

/*
* 在 stmpe_gpio 结构体中保存指向核心 stmpe 结构的指针.
*/
stmpe_gpio->stmpe = stmpe;
/*
* stmpe_gpio->chip 是一个 gpio_chip 结构体, 是 gpiolib 的核心.
* 这里使用一个预定义的 template_chip 作为模板进行初始化.
*/
stmpe_gpio->chip = template_chip;
/*
* 根据从核心驱动获取的信息, 定制 gpio_chip 结构体.
*/
stmpe_gpio->chip.ngpio = stmpe->num_gpios; // 设置GPIO数量
stmpe_gpio->chip.parent = dev; // 设置父设备
stmpe_gpio->chip.base = -1; // 设置为-1, 让gpiolib动态分配GPIO编号基地址

/*
* 如果内核开启了 DEBUG_FS, 则为这个 gpio_chip 设置一个调试输出函数.
*/
if (IS_ENABLED(CONFIG_DEBUG_FS))
stmpe_gpio->chip.dbg_show = stmpe_dbg_show;

/*
* 从设备树中读取 "st,norequest-mask" 属性.
* 这个属性是一个位掩码, 用于指定哪些GPIO引脚不应该被内核请求使用 (例如, 可能被用于特殊功能).
*/
device_property_read_u32(dev, "st,norequest-mask", &stmpe_gpio->norequest_mask);

/*
* 调用核心驱动的函数, 通过总线(I2C/SPI)向芯片发送命令, 使能GPIO功能块.
*/
ret = stmpe_enable(stmpe, STMPE_BLOCK_GPIO);
if (ret)
return ret;

/*
* 注册一个在驱动卸载时会自动执行的清理动作.
* 当驱动卸载时, stmpe_gpio_disable 函数会被调用, 以确保硬件被正确关闭.
*/
ret = devm_add_action_or_reset(dev, stmpe_gpio_disable, stmpe);
if (ret)
return ret;

/*
* 从平台设备数据中获取中断号. 这个中断是STMPE芯片上所有GPIO共享的.
*/
irq = platform_get_irq(pdev, 0);
if (irq > 0) {
/*
* 定义一个指向 gpio_irq_chip 的指针, 这是连接gpiolib和中断子系统的"胶水".
*/
struct gpio_irq_chip *girq;

/*
* 请求一个线程化的中断处理程序.
* - irq: 中断号.
* - NULL: 主中断处理函数, 设为NULL表示当中断发生时, 只唤醒线程.
* - stmpe_gpio_irq: 线程化的中断处理函数, 实际的中断处理逻辑在这里.
* - IRQF_ONESHOT: 标志位, 确保在线程化处理函数完成前, 中断线保持被屏蔽状态.
*/
ret = devm_request_threaded_irq(dev, irq, NULL, stmpe_gpio_irq,
IRQF_ONESHOT, "stmpe-gpio", stmpe_gpio);
if (ret)
return dev_err_probe(dev, ret, "unable to register IRQ handler\n");

/*
* 配置 gpio_chip 内嵌的 irq 成员.
*/
girq = &stmpe_gpio->chip.irq;
/* 设置中断处理的回调函数集 (mask, unmask, set_type等). */
gpio_irq_chip_set_chip(girq, &stmpe_gpio_irq_chip);
/* 本驱动直接处理中断, 不需要父中断处理器. */
girq->parent_handler = NULL;
girq->num_parents = 0;
girq->parents = NULL;
girq->default_type = IRQ_TYPE_NONE; // 默认触发类型为无
girq->handler = handle_simple_irq; // 使用简单的中断流控处理
girq->threaded = true; // 明确表明这是一个线程化中断
girq->init_valid_mask = stmpe_init_irq_valid_mask; // 设置用于初始化有效中断掩码的回调
}

/*
* 调用 devm_gpiochip_add_data 将配置好的 gpio_chip 注册到 gpiolib 核心中.
* 从此, 这些GPIO就可以被系统其他部分使用了.
* 'devm_' 前缀确保了在驱动卸载时会自动调用 gpiochip_remove.
*/
return devm_gpiochip_add_data(dev, &stmpe_gpio->chip, stmpe_gpio);
}

stmpe_gpio: STMPE GPIO驱动的定义与注册

此代码片段的核心作用是定义一个针对”STMPE”系列芯片(通常是I2C/SPI接口的IO扩展器、触摸屏控制器等)上GPIO功能的平台驱动程序, 并通过内核的初始化调用机制, 在系统启动的适当时机将其注册到内核中。

  1. 驱动定义 (stmpe_gpio_driver): struct platform_driver是Linux平台总线模型中用来描述一个驱动程序的核心数据结构。它告诉内核这个驱动的名字、关键的回调函数(如probe), 以及一些行为属性。一旦注册, 内核就会用它的名字("stmpe-gpio")去匹配设备树或板级文件中定义的设备。
  2. 注册函数 (stmpe_gpio_init): 这个函数是驱动注册的入口点。它只做一件事: 调用platform_driver_registerstmpe_gpio_driver这个”驱动蓝图”提交给内核的平台总线核心, 使其变为一个”活”的、可用于设备匹配的驱动程序。
  3. 初始化调用 (subsys_initcall): subsys_initcall是一个宏, 它将stmpe_gpio_init函数的地址放入一个特殊的内存段中。在内核启动过程中, 会有一个专门的阶段来执行这个段里的所有函数指针。这确保了该GPIO驱动在核心子系统初始化之后、依赖它的具体设备驱动(device drivers)初始化之前被注册, 从而保证了正确的初始化顺序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
* 定义一个静态的 platform_driver 结构体.
* 'static' 关键字使得这个变量只在当前文件中可见.
* 这个结构体描述了 "stmpe-gpio" 驱动的核心属性.
*/
static struct platform_driver stmpe_gpio_driver = {
/*
* .driver: 这是一个内嵌的 device_driver 结构体, 包含了通用的驱动属性.
*/
.driver = {
/*
* .suppress_bind_attrs = true:
* 设置为 true 会阻止内核在 sysfs 中为这个驱动自动创建 "bind" 和 "unbind" 文件.
* 这通常用于那些不希望被用户从命令行手动绑定或解绑的驱动,
* 暗示该驱动与其设备的关系是在编译时或通过设备树固定的.
*/
.suppress_bind_attrs = true,
/*
* .name = "stmpe-gpio":
* 这是驱动的唯一名称. 内核的平台总线核心会用这个名字
* 去匹配在设备树中 compatible 字符串或者在板级文件中定义的 platform_device 的名字.
* 当一个名为 "stmpe-gpio" 的设备被注册时, 两者就会匹配成功.
*/
.name = "stmpe-gpio",
},
/*
* .probe = stmpe_gpio_probe:
* 这是一个函数指针, 指向该驱动的探测函数 (probe function).
* 当驱动和设备成功匹配后, 内核会调用 stmpe_gpio_probe 函数.
* 这个函数是驱动的真正入口点, 负责初始化硬件、申请资源、
* 并将GPIO控制器注册到内核的 gpiolib 子系统中.
*/
.probe = stmpe_gpio_probe,
};

/*
* 定义一个静态的初始化函数.
* 'static' 关键字使其仅在当前文件内可见.
* '__init' 宏告诉编译器将这个函数放入一个特殊的内存段 (".init.text").
* 内核在启动过程完成后, 会释放这个段所占用的所有内存, 这是一个重要的内存优化.
*/
static int __init stmpe_gpio_init(void)
{
/*
* 调用 platform_driver_register() 函数, 将 stmpe_gpio_driver 注册到内核的平台总线核心中.
* 从这一刻起, 内核就知道了这个驱动的存在, 并会开始为它寻找匹配的设备.
* 此函数返回一个整型值, 0表示成功, 负数表示错误码.
*/
return platform_driver_register(&stmpe_gpio_driver);
}
/*
* subsys_initcall() 是一个宏, 它将 stmpe_gpio_init 函数注册为一个内核的 "子系统初始化调用".
* 在内核启动过程中, 内核会按照预定义的顺序(core, postcore, arch, subsys, fs, device, late)
* 调用不同级别的初始化函数.
* subsys_initcall 确保了 stmpe_gpio_init 会在一个比较早的、合适的阶段被执行,
* 通常是在核心子系统(如内存管理, VFS)初始化之后, 但在大多数具体设备驱动初始化之前.
*/
subsys_initcall(stmpe_gpio_init);