[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/gpiolib.c

gpiochip_setup_dev: 为单个GPIO控制器完成设备和接口的注册

此函数的核心职责是接收一个代表GPIO控制器(如STM32的GPIOA)的内核对象gdev,并完成以下三件关键事情:

  1. 初始化设备对象: 确保gdev中的struct device成员被正确初始化。
  2. 创建字符设备: 在/dev目录下创建对应的gpiochipN字符设备节点,使用户空间程序可以通过文件I/O操作来访问GPIO。
  3. 创建sysfs接口: 在/sys/class/gpio目录下创建对应的gpiochipN目录和属性文件,提供一种基于文件的、用于管理和调试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
/*
* gpiochip_setup_dev: 对一个gpio_device进行设置.
*
* @gdev: 指向要设置的 gpio_device 结构体的指针.
* @return: 成功时返回0, 失败时返回负值的错误码.
*/
static int gpiochip_setup_dev(struct gdevice *gdev)
{
/*
* fwnode: 指向与此设备关联的固件节点(Firmware Node)的句柄.
* 在基于设备树的系统(如STM32)上, 这通常指向设备树中对应的节点
* (例如, &gpioa 节点). dev_fwnode() 是获取这个句柄的标准函数.
*/
struct fwnode_handle *fwnode = dev_fwnode(&gdev->dev);
/*
* ret: 用于存储函数调用的返回值.
*/
int ret;

/*
* 调用 device_initialize(), 对 gdev 中内嵌的 struct device 对象进行初始化.
* 这一步会设置好设备的引用计数、锁等内部状态, 为后续的注册做准备.
* 如果这个 gdev 之前已经注册过, 这一步可以确保其状态被重置.
*/
device_initialize(&gdev->dev);

/*
* 这是一个处理固件节点状态的特殊情况.
* 如果固件节点存在, 并且它还没有被关联到任何一个'struct device'上,
* 那么我们就安全地清除它的"已初始化"标志.
* 这确保了设备可以被重新初始化和探测.
*/
if (fwnode && !fwnode->dev)
fwnode_dev_initialized(fwnode, false);

/*
* 第一步: 注册字符设备.
* gcdev_register() 是一个辅助函数, 它的核心工作是:
* 1. 从 gpio_devt (在gpiolib_dev_init中分配的设备号范围)中获取一个可用的次设备号.
* 2. 使用这个主/次设备号, 调用 cdev_add() 将这个 gdev 注册为一个字符设备.
* 3. 调用 device_add() 将 gdev->dev 这个设备对象正式添加到内核的设备模型中.
* device_add() 会触发用户空间的udev/mdev, 在/dev目录下创建对应的'gpiochipN'节点.
*/
ret = gcdev_register(gdev, gpio_devt);
if (ret) /* 检查注册是否成功. */
return ret;

/*
* 第二步: 注册sysfs接口.
* gpiochip_sysfs_register() 负责在 /sys/class/gpio/ 目录下创建对应的接口.
* 它会创建一个名为 'gpiochipN' (N是GPIO的基准号, 如gpiochip0)的目录.
* 在这个目录下, 它会创建几个属性文件, 例如:
* - 'base': 只读, 显示这个gpiochip的起始GPIO编号.
* - 'label': 只读, 显示这个gpiochip的标签 (通常来自设备树).
* - 'ngpio': 只读, 显示这个gpiochip管理的GPIO数量 (例如, 16个).
*/
ret = gpiochip_sysfs_register(gdev);
if (ret) /* 检查注册是否成功. */
goto err_remove_device; /* 如果失败, 跳转到错误处理. */

/*
* 使用 dev_dbg() 打印一条调试级别的日志, 宣告注册成功.
* 日志会包含GPIO的编号范围和标签, 这对于调试非常有帮助.
*/
dev_dbg(&gdev->dev, "registered GPIOs %u to %u on %s\n", gdev->base,
gdev->base + gdev->ngpio - 1, gdev->label);

/*
* 所有操作成功, 返回0.
*/
return 0;

/*
* 错误处理标签.
* 当sysfs注册失败时, 代码会跳转到这里.
*/
err_remove_device:
/*
* 必须执行清理操作, 注销之前成功注册的字符设备,
* 以保持系统状态的一致性.
*/
gcdev_unregister(gdev);
/*
* 返回sysfs注册失败时产生的错误码.
*/
return ret;
}

gpiochip_setup_devs: 为已注册的GPIO控制器设置设备节点

此函数的核心作用是遍历当前系统中所有已经注册的GPIO控制器(gpio_device),并为每一个控制器调用gpiochip_setup_dev函数来完成其设备节点的最终设置

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
/*
* gpiochip_setup_devs: 为(一个或多个)gpio设备进行设置.
*/
static void gpiochip_setup_devs(void)
{
/*
* gdev: 一个指向 gpio_device 结构体的指针, 用作循环变量.
* gpio_device 是内核中代表一个完整GPIO控制器(如STM32的GPIOA)的核心对象.
* ret: 用于存储函数调用的返回值.
*/
struct gpio_device *gdev;
int ret;

/*
* guard(srcu)(&gpio_devices_srcu):
* 这是一个C++风格的RAII (Resource Acquisition Is Initialization) 宏,
* 在内核中用于简化锁的管理. 它在这里的作用是:
* 1. 在进入这个作用域时, 自动获取 gpio_devices_srcu 的 SRCU 读锁.
* SRCU (Sleepable Read-Copy-Update) 是一种特殊的读写锁, 它允许读端临界区睡眠.
* 在这里加锁是为了安全地遍历全局的 gpio_devices 链表.
* 2. 在退出这个作用域时 (无论函数是正常返回还是中途退出), 自动释放这个读锁.
* 这就避免了忘记解锁导致死锁的问题.
*/
guard(srcu)(&gpio_devices_srcu);

/*
* list_for_each_entry_srcu: 这是一个专门用于在SRCU保护下安全遍历链表的宏.
* @ gdev: 循环变量.
* @ &gpio_devices: 要遍历的全局链表的头部.
* @ list: 链表节点在 gdev 结构体中的成员名.
* @ srcu_read_lock_held(...): 一个条件, 用于静态检查或在调试时断言我们确实持有了锁.
*
* 这行代码会遍历 gpio_devices 链表中的每一个 gpio_device (即每一个已注册的GPIO控制器).
*/
list_for_each_entry_srcu(gdev, &gpio_devices, list,
srcu_read_lock_held(&gpio_devices_srcu)) {
/*
* 对链表中的每一个 gdev, 调用 gpiochip_setup_dev 函数.
* gpiochip_setup_dev (未在此处显示) 是真正干活的函数. 它的主要工作包括:
* 1. 检查这个gdev是否已经被设置过, 如果是, 就直接返回.
* 2. 使用之前分配的字符设备主设备号和动态分配的次设备号,
* 调用 device_create() 函数.
* 3. device_create() 会触发用户空间的 udev/mdev, 在 /dev 目录下
* 创建一个对应的字符设备节点, 例如 /dev/gpiochip0.
* 4. 它还会设置好 gdev->dev 这个设备对象, 将其与驱动模型完全集成.
*/
ret = gpiochip_setup_dev(gdev);
/*
* 检查 gpiochip_setup_dev 是否成功.
*/
if (ret)
/*
* 如果失败, 使用 dev_err() 打印一条错误日志.
* &gdev->dev 是与这个 gdev 关联的设备对象,
* dev_err 会自动在日志中包含设备名, 方便调试.
*/
dev_err(&gdev->dev,
"Failed to initialize gpio device (%d)\n", ret);
}
}

gpiolib_dev_init: 初始化GPIO设备库

此函数在内核启动的早期阶段被调用,其核心职责是建立Linux内核GPIO子系统的设备驱动模型框架。它本身不注册任何具体的GPIO硬件,而是搭建一个“舞台”,让后续具体的GPIO控制器驱动(如STM32的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
/*
* 定义一个静态的 'device_driver' 结构体实例, 名为 gpio_stub_drv.
* 这是一个 "存根驱动" (stub driver). 它的作用是在没有真正的GPIO设备注册时,
* 作为一个占位符存在于GPIO总线上. 这有助于简化总线管理逻辑.
*/
static struct device_driver gpio_stub_drv = {
/*
* .name: 驱动的名称. 这个名字会出现在 sysfs 中.
*/
.name = "gpio_stub_drv",
/*
* .bus: 指定这个驱动属于哪个总线. 这里它被绑定到 gpio_bus_type.
*/
.bus = &gpio_bus_type,
/*
* .probe: 当一个设备与这个驱动匹配时, 内核会调用 .probe 函数.
* (gpio_stub_drv_probe 的定义不在此处, 但它通常只做少量工作或什么都不做).
*/
.probe = gpio_stub_drv_probe,
};

/*
* gpiolib_dev_init: GPIO设备库的初始化函数.
* 标记为 __init, 表示它仅在内核启动期间执行.
*/
static int __init gpiolib_dev_init(void)
{
/*
* ret: 用于存储函数调用的返回值.
*/
int ret;

/*
* 第一步: 注册GPIO总线类型.
* bus_register() 会在 /sys/bus/ 目录下创建一个名为 "gpio" 的新目录.
* 这就创建了一条虚拟的 "GPIO总线". 所有后续的GPIO控制器设备和驱动,
* 都会被 "挂载" 到这条总线上. 这是建立标准设备模型的第一步.
*/
ret = bus_register(&gpio_bus_type);
if (ret < 0) { /* 检查注册是否成功. */
/* 如果失败, 打印错误日志. */
pr_err("gpiolib: could not register GPIO bus type\n");
return ret;
}

/*
* 第二步: 注册GPIO存根驱动.
* driver_register() 会将我们上面定义的 gpio_stub_drv 注册到内核中,
* 并将其与 gpio_bus_type 关联.
*/
ret = driver_register(&gpio_stub_drv);
if (ret < 0) { /* 检查注册是否成功. */
pr_err("gpiolib: could not register GPIO stub driver\n");
/*
* 如果失败, 必须执行清理操作: 注销之前成功注册的总线,
* 以保持系统状态的一致性.
*/
bus_unregister(&gpio_bus_type);
return ret;
}

/*
* 第三步: 为GPIO字符设备分配一个主次设备号范围.
* 现代Linux内核推荐使用字符设备接口(/dev/gpiochipN)来访问GPIO.
* alloc_chrdev_region() 会向内核申请一段连续的、未被使用的设备号.
* @ &gpio_devt: 用于存储分配到的起始设备号 (主设备号+次设备号).
* @ 0: 起始的次设备号, 0表示由内核动态选择.
* @ GPIO_DEV_MAX: 希望分配的设备号数量, 即最多支持多少个GPIO控制器.
* @ GPIOCHIP_NAME: 与这个设备号范围关联的名称 (通常是 "gpio").
*/
ret = alloc_chrdev_region(&gpio_devt, 0, GPIO_DEV_MAX, GPIOCHIP_NAME);
if (ret < 0) { /* 检查分配是否成功. */
pr_err("gpiolib: failed to allocate char dev region\n");
/*
* 如果失败, 必须按相反的顺序执行清理操作.
*/
driver_unregister(&gpio_stub_drv);
bus_unregister(&gpio_bus_type);
return ret;
}

/*
* 设置一个全局标志, 表示GPIO库的核心基础设施已经初始化完毕.
* 其他部分的代码可能会检查这个标志.
*/
gpiolib_initialized = true;
/*
* 调用 gpiochip_setup_devs(), 这个函数会进一步设置字符设备接口
* 所需的类(class)等, 准备好动态创建/dev/gpiochipN节点的条件.
*/
gpiochip_setup_devs();

/*
* 这部分代码用于处理动态设备树(Dynamic Device Tree)的通知.
* 它允许在系统运行时, 如果设备树中的GPIO相关节点发生变化(例如通过overlay),
* GPIO子系统能够收到通知并做出相应的调整.
* 这对于像树莓派这样支持DT overlay的系统很重要, 对于固件固定的STM32系统则不太常用.
*/
#if IS_ENABLED(CONFIG_OF_DYNAMIC) && IS_ENABLED(CONFIG_OF_GPIO)
/*
* 注册一个通知回调函数(gpio_of_notifier),
* 当设备树发生重新配置时, 内核会调用它.
* WARN_ON 确保如果注册失败, 会打印一个警告, 因为这通常不应该发生.
*/
WARN_ON(of_reconfig_notifier_register(&gpio_of_notifier));
#endif /* CONFIG_OF_DYNAMIC && CONFIG_OF_GPIO */

/*
* 返回0或最后一次成功的操作的返回值, 表示初始化成功.
*/
return ret;
}
/*
* 使用 core_initcall() 将 gpiolib_dev_init 注册为一个核心初始化调用.
* 这确保了GPIO的基础设施会在所有具体的GPIO控制器驱动(如STM32的GPIO驱动)
* 尝试注册自己之前, 就已经准备就绪.
*/
core_initcall(gpiolib_dev_init);

gpiolib 总线与设备类型定义

此代码片段定义了Linux内核gpiolib子系统用于融入内核标准设备模型(Device Model)的两个核心数据结构。它的核心原理是创建一个名为 “gpio” 的逻辑总线(logical bus), 并为所有注册的GPIO控制器(gpio_chip)定义一个统一的设备类型(gpio_chip)。这使得内核可以将每一个GPIO控制器都视为一个标准化的”设备”, 并通过一个虚拟的”总线”来管理它们, 从而能够复用设备模型提供的所有成熟机制, 如驱动绑定、电源管理和sysfs接口。


gpio_dev_type: GPIO芯片的设备类型

这是一个struct device_type实例, 它为所有由gpiolib创建的struct gpio_device对象提供了一组通用的属性。

原理与作用:
device_type的主要作用是为一类设备提供共享的特性, 最重要的是统一的释放(release)回调函数

  • .name = "gpio_chip": 为这类设备指定了一个内部名称 “gpio_chip”。这个名字主要用于调试和内核内部识别。
  • .release = gpiodev_release: 这是此结构体最关键的部分。它指定了一个回调函数gpiodev_release。当一个gpio_device对象的最后一个引用被释放(其引用计数降为0)时, 内核的设备模型会自动调用这个函数。gpiodev_release函数内部会负责清理和释放与该gpio_device对象相关的所有内存和资源。这是一种健壮的、自动化的资源管理机制, 对于防止内存泄漏至关重要, 在像STM32H750这样内存资源宝贵的嵌入式系统中尤其重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 定义一个静态的、常量类型的 struct device_type 实例.
* "static"表示其作用域仅限于当前文件. "const"表示其内容在编译后是只读的.
*/
static const struct device_type gpio_dev_type = {
/*
* .name: 指定此设备类型的名称为 "gpio_chip".
*/
.name = "gpio_chip",
/*
* .release: 指定一个回调函数 gpiodev_release.
* 当内核要销毁一个此类型的设备对象时, 这个函数会被自动调用以释放资源.
*/
.release = gpiodev_release,
};

gpio_bus_match: GPIO总线的匹配规则

这是一个自定义的match函数, 它定义了在”gpio”总线上, 一个设备和一个驱动程序应该如何被视为”兼容”的。

原理与作用:
标准的总线(如I2C, SPI)通常根据设备和驱动的名称或ID来进行匹配。但”gpio”总线是一个逻辑上的虚拟总线, 其匹配规则也比较特殊。

  • struct fwnode_handle *fwnode = dev_fwnode(dev);: 获取与设备dev关联的固件节点(通常是设备树节点)。
  • if (fwnode && fwnode->dev != dev): 这是匹配的核心逻辑。一个物理GPIO控制器在内核中可能对应多个struct device对象(例如, 一个platform_devicegpiolib创建的gpio_device)。fwnode->dev通常指向最主要的那个设备对象(即platform_device)。这个判断的意图是: 只有当这个gpio_device是其固件节点所代表的主要设备时, 才允许匹配。它防止了通用的”gpio总线驱动”错误地绑定到一个已经被更具体的平台驱动所拥有的硬件上, 是一种避免潜在逻辑冲突的保护机制。
  • return 1; 表示匹配成功。
  • return 0; 表示匹配失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* gpio_bus_match: "gpio"总线的自定义匹配函数.
* @dev: 要进行匹配的设备.
* @drv: 要进行匹配的驱动.
* @return: 1 表示匹配成功, 0 表示失败.
*/
static int gpio_bus_match(struct device *dev, const struct device_driver *drv)
{
/* 获取设备的固件节点句柄 (fwnode). */
struct fwnode_handle *fwnode = dev_fwnode(dev);

/*
* 这是一个特殊的匹配逻辑, 用于处理一个硬件节点可能对应多个设备对象的情况.
* 如果该固件节点存在, 并且它关联的主要设备不是当前正在匹配的这个设备,
* 那么就认为不匹配.
*/
if (fwnode && fwnode->dev != dev)
return 0;

/* 在其他情况下, 都认为匹配成功. */
return 1;
}

gpio_bus_type: “gpio” 逻辑总线

这是一个struct bus_type实例, 它在内核中注册了一个全新的、名为”gpio”的总线。

原理与作用:
注册这个结构体会在sysfs中创建/sys/bus/gpio/目录。所有被gpiolib注册的GPIO控制器都会作为设备出现在这个总线上。这提供了一个统一的场所来管理和查看系统中的所有GPIO控制器。

  • .name = "gpio": 定义了总线的名称, 这也是sysfs中目录的名称。
  • .match = gpio_bus_match: 将上面定义的自定义匹配函数gpio_bus_match指定为本总线的官方匹配规则。当任何驱动或设备尝试在此总线上进行绑定时, 内核都会调用这个函数来做决定。
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* 定义一个静态的、常量类型的 struct bus_type 实例, 用于描述 "gpio" 总线.
*/
static const struct bus_type gpio_bus_type = {
/*
* .name: 总线的名称, 将会创建 /sys/bus/gpio 目录.
*/
.name = "gpio",
/*
* .match: 指定此总线的设备-驱动匹配函数.
*/
.match = gpio_bus_match,
};

gpiochip_find_base_unlocked: 动态查找可用的GPIO编号基地址

此函数是Linux内核gpiolib子系统中实现GPIO控制器编号动态分配的核心算法。当一个新的GPIO控制器驱动请求动态分配其GPIO编号基地址时(gpio_chip->base = -1), gpiochip_add_data函数会在持有锁的情况下调用此函数。它的核心原理是以一种”贪心算法”(Greedy Algorithm)的思路, 线性扫描一个全局的、已注册的GPIO设备链表, 以寻找第一个足够大的、未被占用的连续编号”空隙”

工作流程详解:

  1. 初始化: 函数从一个预定义的动态分配起始点GPIO_DYNAMIC_BASE开始搜索。这个值通常足够大, 以避开为特殊硬件静态预留的低地址编号。
  2. 遍历已注册设备链表: 它使用list_for_each_entry_srcu宏来安全地遍历gpio_devices这个全局链表。这个链表按照GPIO基地址从小到大的顺序维护了所有已注册的GPIO控制器。_srcu版本的宏使用了”读-拷贝-更新”(Read-Copy-Update)的变体, 即使在遍历过程中有其他CPU在并发地修改链表(虽然此函数被调用时已持有锁, 但这个宏是通用的), 也能保证遍历的安全性。
  3. 寻找空隙 (核心算法): 在循环的每一步, 它会比较当前搜索的起始点base和正在检查的已注册设备gdev的范围:
    • 找到空隙 (成功): 如果当前gdev的基地址 (gdev->base) 大于或等于 base + ngpio (当前搜索点 + 需要的引脚数量), 这意味着在basegdev之间有一个足够大的空隙。循环立即break
    • 未找到空隙 (继续搜索): 如果没有找到空隙, 函数会将搜索的起始点base更新为当前gdev范围的末尾之后 (base = gdev->base + gdev->ngpio;)。然后继续下一次循环, 检查这个新的base与下一个已注册设备之间的关系。
  4. 边界检查: 在每次更新base后, 都会检查新的base是否超出了预定义的动态分配的最大范围GPIO_DYNAMIC_MAX。如果超出, 说明不可能再找到空隙了, 循环也会break
  5. 返回结果:
    • 如果循环结束后, base仍然在有效的动态分配范围内, 说明找到了一个可用的基地址, 函数将其返回。
    • 如果超出了范围, 说明GPIO编号空间已满, 函数返回-ENOSPC(“设备上没有空间”)错误。

为什么这个机制很重要?
在早期的Linux内核中, GPIO控制器的基地址通常由驱动程序或板级文件静态硬编码。这种方式非常容易导致冲突, 特别是在一个平台上集成了来自不同供应商的、可热插拔的模块化硬件时。动态分配机制彻底解决了这个问题。它使得内核可以像DHCP服务器分配IP地址一样, 自动地为新加入的GPIO控制器找到一个不与任何现有设备冲突的、唯一的编号范围。这对于构建可扩展、可维护的嵌入式Linux系统, 尤其是在STM32这样拥有众多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
/*
* gpiochip_find_base_unlocked: 在未锁定的上下文中(但调用者必须持有锁), 动态地查找GPIO基地址.
* @ngpio: 新的GPIO控制器需要的引脚数量.
* @return: 成功时返回一个可用的基地址, 失败时返回 -ENOSPC.
*/
static int gpiochip_find_base_unlocked(u16 ngpio)
{
/*
* 从预定义的动态GPIO编号起始地址开始搜索.
* 这个值通常是256或512, 以避开静态分配的低地址区域.
*/
unsigned int base = GPIO_DYNAMIC_BASE;
/* 定义一个gdev指针, 用于遍历已注册的GPIO设备. */
struct gpio_device *gdev;

/*
* 安全地遍历全局的 gpio_devices 链表. 这个链表按照基地址排序.
* lockdep_is_held() 是一个锁调试断言, 确保调用此函数时确实持有了 gpio_devices_lock.
*/
list_for_each_entry_srcu(gdev, &gpio_devices, list,
lockdep_is_held(&gpio_devices_lock)) {
/*
* 核心的空隙查找逻辑:
* 检查当前已注册设备 gdev 的基地址, 是否大于等于 (我们当前寻找的基地址 base + 我们需要的数量 ngpio).
* 如果是, 说明在 base 和 gdev->base 之间有一个足够大的空隙.
*/
if (gdev->base >= base + ngpio)
break; /* 找到了, 跳出循环. */

/*
* 如果没有找到空隙, 更新我们的搜索起点:
* 将 base 设置为当前设备 gdev 占用范围的末尾之后.
*/
base = gdev->base + gdev->ngpio;

/* 确保我们的搜索起点不会回退到动态范围的起始点之下. */
if (base < GPIO_DYNAMIC_BASE)
base = GPIO_DYNAMIC_BASE;

/* 检查更新后的 base 是否还有可能容纳 ngpio 个引脚而不超出最大范围. */
if (base > GPIO_DYNAMIC_MAX - ngpio)
break; /* 不可能了, 跳出循环. */
}

/*
* 循环结束后, 检查最终的 base 是否仍在合法范围内.
*/
if (base <= GPIO_DYNAMIC_MAX - ngpio) {
pr_debug("%s: found new base at %d\n", __func__, base);
return base; /* 成功, 返回找到的基地址. */
} else {
pr_err("%s: cannot find free range\n", __func__);
return -ENOSPC; /* 失败, 返回"无可用空间"错误. */
}
}

gpiodev_add_to_list_unlocked: 将GPIO设备插入全局排序列表

此函数是Linux内核gpiolib子系统内部一个至关重要的列表管理函数。它的核心原理是以一种原子性的、保证排序的方式, 将一个新初始化的GPIO设备(gdev)插入到一个全局的、按GPIO编号基地址排序的设备链表(gpio_devices)中。在插入的同时, 它还必须严格执行冲突检测, 确保新设备的GPIO编号范围不会与任何已存在的设备发生重叠。

这个函数是GPIO控制器能够被内核动态、安全地添加和管理的基础。它的命名后缀_unlocked是一个明确的约定, 意味着调用此函数的代码必须已经持有了保护该链表的gpio_devices_lock, lockdep_assert_held宏在函数开头就强制检查了这一前提条件。

工作流程与算法详解:

该函数的算法是一个为链表插入优化的”寻找间隙”过程:

  1. 空列表处理 (最快路径): 如果全局链表gpio_devices是空的, 说明这是第一个被注册的GPIO控制器。函数直接将其添加到链表尾部并成功返回。

  2. 头部插入优化 (次快路径): 函数检查新设备gdev是否可以被完整地插入到链表的最前端。它比较gdev的结束地址与链表中第一个设备next的起始地址。如果gdev的范围在next之前且无重叠, 就将其插入到链表头部。

  3. 尾部插入优化 (次快路径): 类似地, 函数检查gdev是否可以被完整地插入到链表的最后端。它比较gdev的起始地址与链表中最后一个设备prev的结束地址。如果gdev的范围在prev之后且无重叠, 就将其插入到链表尾部。这两种优化(头部和尾部)覆盖了系统启动时设备按顺序注册的绝大多数情况, 避免了昂贵的完整链表遍历。

  4. 中间插入 (通用路径): 如果以上优化都不适用, 函数就必须遍历整个链表来寻找一个可以容纳gdev的”间隙”。它使用list_for_each_entry_safe同时追踪前一个节点prev和下一个节点next, 并检查是否存在一个位置, 使得gdev的范围恰好在prev之后且在next之前。如果找到这样的间隙, 就执行插入并成功返回。

  5. 冲突检测与失败: 如果函数遍历完整个链表都没有找到任何可以插入的间隙(即不满足上述任何一个插入条件), 这就确定地意味着gdev的GPIO编号范围与一个或多个已存在的设备发生了重叠。在这种情况下, 函数不会执行任何插入操作, 而是直接返回-EBUSY错误码, 明确地告知上层调用者发生了冲突。

RCU (读-拷贝-更新) 的使用:
此函数使用了list_add_rculist_add_tail_rcu_rcu后缀表明这个链表是受RCU机制保护的。这是一种高级的同步技术, 它允许其他代码在不获取任何锁的情况下安全地并发读取gpio_devices链表, 极大地提高了系统的并发性能。只有在写入(添加或删除节点)时才需要获取锁。

在STM32H750上的应用:
当STM32驱动程序一个接一个地注册其GPIO Bank (GPIOA, GPIOB, GPIOC…)时, gpiochip_add_data内部就会调用此函数。

  • 注册GPIOA时, 会命中”空列表”路径。
  • 注册GPIOB时, 会命中”尾部插入”路径。
  • 注册GPIOC时, 同样会命中”尾部插入”路径。
    …以此类推。这使得STM32众多GPIO Bank的注册过程非常高效。
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
/*
* gpiodev_add_to_list_unlocked: 将一个新的chip添加到全局chips列表中, 保持列表
* 按范围(即[base, base + ngpio - 1])排序.
*
* 返回:
* -EBUSY 如果新的chip与某个其他chip的整数空间重叠.
*/
static int gpiodev_add_to_list_unlocked(struct gpio_device *gdev)
{
struct gpio_device *prev, *next;

/* 这是一个锁调试断言, 确保调用者确实持有了 gpio_devices_lock 锁. */
lockdep_assert_held(&gpio_devices_lock);

/* 情况1: 列表为空. 这是第一个被添加的设备. */
if (list_empty(&gpio_devices)) {
/* 直接添加到链表尾部. _rcu版本确保了对并发RCU读者的安全. */
list_add_tail_rcu(&gdev->list, &gpio_devices);
return 0;
}

/* 情况2: 尝试在头部插入 (优化). */
next = list_first_entry(&gpio_devices, struct gpio_device, list);
/* 检查新设备的范围是否完全在第一个设备之前. */
if (gdev->base + gdev->ngpio <= next->base) {
/* 使用_rcu版本添加到链表头部. */
list_add_rcu(&gdev->list, &gpio_devices);
return 0;
}

/* 情况3: 尝试在尾部插入 (优化). */
prev = list_last_entry(&gpio_devices, struct gpio_device, list);
/* 检查新设备的范围是否完全在最后一个设备之后. */
if (prev->base + prev->ngpio <= gdev->base) {
list_add_tail_rcu(&gdev->list, &gpio_devices);
return 0;
}

/* 情况4: 在中间插入 (通用路径). */
/* _safe版本可以安全地处理循环中对链表的修改, 这里用于方便地获取prev和next. */
list_for_each_entry_safe(prev, next, &gpio_devices, list) {
/* &next->list == &gpio_devices 表示prev已经是最后一个元素了. */
if (&next->list == &gpio_devices)
break;

/*
* 检查是否找到了一个间隙:
* 新设备的起始地址在prev之后, 并且新设备的结束地址在next之前.
*/
if (prev->base + prev->ngpio <= gdev->base
&& gdev->base + gdev->ngpio <= next->base) {
/* 在prev之后插入新设备. */
list_add_rcu(&gdev->list, &prev->list);
return 0;
}
}

/*
* 如果执行到这里, 说明遍历了整个列表都没有找到可以插入的间隙.
* 这意味着存在地址范围重叠.
* 在返回错误前, 等待所有正在进行的RCU读取操作完成.
*/
synchronize_srcu(&gpio_devices_srcu);

/* 返回 "设备或资源繁忙" 错误. */
return -EBUSY;
}

GPIO有效引脚掩码(Valid Mask)与引脚范围管理系列函数

此代码片段展示了Linux内核gpiolib子系统中一组用于管理GPIO控制器引脚有效性和范围映射的函数。它们共同构成了一个强大而灵活的系统, 其核心原理是通过静态的设备树声明和/或动态的驱动回调, 创建一个精确的”有效引脚掩码”(valid mask), 并将GPIO编号范围与pinctrl子系统关联起来。这使得gpiolib能够安全地处理具有非连续、复杂引脚布局的硬件, 并确保了不同子系统间的正确协作。


1. 有效引脚掩码的生命周期管理

这组函数负责创建、填充和释放valid_mask位图。

gpiochip_allocate_mask & gpiochip_free_mask / gpiochip_free_valid_mask

这是位图最基本的内存管理。allocate负责分配内存并设定一个”全部有效”的初始状态, free则负责释放。

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
/* gpiochip_allocate_mask: 分配一个有效掩码位图. */
static unsigned long *gpiochip_allocate_mask(struct gpio_chip *gc)
{
unsigned long *p;

/* 使用内核位图API分配一块能容纳 ngpio 个比特的内存. */
p = bitmap_alloc(gc->ngpio, GFP_KERNEL);
if (!p)
return NULL;

/* 关键的初始状态: 默认假设所有GPIO引脚都是有效的, 将所有位都设置为1. */
bitmap_fill(p, gc->ngpio);

return p;
}

/* gpiochip_free_mask: 释放一个掩码位图. */
static void gpiochip_free_mask(unsigned long **p)
{
bitmap_free(*p); /* 释放位图内存. */
*p = NULL; /* 将指针设为NULL, 防止悬挂指针. */
}

/* gpiochip_free_valid_mask: devm框架使用的清理函数, 释放有效掩码. */
static void gpiochip_free_valid_mask(struct gpio_chip *gc)
{
gpiochip_free_mask(&gc->gpiodev->valid_mask);
}

2. 通过设备树声明无效引脚 (静态方式)

这组函数实现了通过设备树中的gpio-reserved-ranges属性来声明无效引脚范围。

gpiochip_count_reserved_ranges & gpiochip_apply_reserved_ranges

count函数检查属性是否存在且格式正确, apply函数则读取属性内容并在valid_mask中”打孔”。

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
/* gpiochip_count_reserved_ranges: 计算 "gpio-reserved-ranges" 属性中的条目数量. */
static unsigned int gpiochip_count_reserved_ranges(struct gpio_chip *gc)
{
struct device *dev = &gc->gpiodev->dev;
int size;

/* 属性格式是成对的u32值: [起始偏移, 数量, ...], 所以总数必须是偶数. */
size = device_property_count_u32(dev, "gpio-reserved-ranges");
if (size > 0 && size % 2 == 0)
return size;

return 0;
}

/* gpiochip_apply_reserved_ranges: 读取保留范围属性并将其应用到有效掩码上. */
static int gpiochip_apply_reserved_ranges(struct gpio_chip *gc)
{
// ... (代码逻辑: 1. 检查是否有保留范围. 2. 分配临时内存读取属性. 3. 读取属性.) ...
// ... (循环读取[start, count]对)
while (size) {
u32 count = ranges[--size]; // 获取数量
u32 start = ranges[--size]; // 获取起始偏移

/* 安全检查, 确保范围不会超出芯片的能力. */
if (start >= gc->ngpio || start + count > gc->ngpio)
continue;

/* 核心操作: 在valid_mask位图中, 从start位开始, 清除count个位 (将它们设为0). */
bitmap_clear(gc->gpiodev->valid_mask, start, count);
}
// ... (释放临时内存) ...
return 0;
}

3. 有效掩码的构建与查询

这组函数是上层API, 用于协调掩码的构建过程并提供查询接口。

gpiochip_init_valid_mask

这是gpiochip_add_data调用的主协调函数。

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
/* gpiochip_init_valid_mask: 为一个gpio_chip初始化其有效引脚掩码. */
static int gpiochip_init_valid_mask(struct gpio_chip *gc)
{
int ret;

/* 只有在驱动定义了"保留范围"(静态方式)或提供了自定义初始化函数(动态方式)时, 才需要创建掩码. */
if (!(gpiochip_count_reserved_ranges(gc) || gc->init_valid_mask))
return 0;

/* 1. 分配一个全1的掩码. */
gc->gpiodev->valid_mask = gpiochip_allocate_mask(gc);
if (!gc->gpiodev->valid_mask)
return -ENOMEM;

/* 2. 应用设备树中定义的静态保留范围, 在掩码上"打孔". */
ret = gpiochip_apply_reserved_ranges(gc);
if (ret)
return ret;

/* 3. 如果驱动提供了自定义的回调函数, 调用它, 给予驱动最后一次动态修改掩码的机会. */
if (gc->init_valid_mask)
return gc->init_valid_mask(gc,
gc->gpiodev->valid_mask,
gc->ngpio);

return 0;
}

gpiochip_query_valid_mask & gpiochip_line_is_valid

这两个函数提供了对已构建好的valid_mask的查询能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* gpiochip_query_valid_mask: 返回整个有效掩码位图的指针. */
const unsigned long *gpiochip_query_valid_mask(const struct gpio_chip *gc)
{
return gc->gpiodev->valid_mask;
}

/* gpiochip_line_is_valid: 检查单个引脚是否有效. */
bool gpiochip_line_is_valid(const struct gpio_chip *gc,
unsigned int offset)
{
/* 在芯片完全注册前(例如处理pin hog时), 默认所有引脚有效. */
if (!gc->gpiodev)
return true;

/* 快速路径优化: 如果没有掩码(NULL), 说明所有引脚都有效. */
if (likely(!gc->gpiodev->valid_mask))
return true;

/* 慢速路径: 检查掩码位图中对应的位是否为1. */
return test_bit(offset, gc->gpiodev->valid_mask);
}

4. GPIO范围与Pinctrl的关联

gpiochip_add_pin_ranges

此函数负责将GPIO控制器的编号范围与pinctrl子系统关联起来。

原理与作用:
它的作用是调用驱动提供的add_pin_ranges回调函数, 在这个回调中驱动通常会手动调用pinctrl_add_gpio_range来注册映射关系。但是, 这个机制很大程度上是遗留(legacy)的

  • 现代方法: 现代内核强烈推荐使用设备树中的gpio-ranges属性来完成这个映射。内核的OF(Open Firmware)解析器会自动处理这个属性, 无需驱动操心。
  • 函数行为: 因此, 此函数首先会检查设备树中是否存在gpio-ranges属性。如果存在, 它会立即返回成功, 有意地跳过调用驱动的add_pin_ranges回调, 以避免重复或冲突的映射。只有在gpio-ranges属性不存在时, 它才会调用那个遗留的回调函数, 以提供向后兼容性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* gpiochip_add_pin_ranges: 为gpio_chip添加引脚范围. */
static int gpiochip_add_pin_ranges(struct gpio_chip *gc)
{
/*
* 关键检查: 如果设备树平台已经使用了标准的"gpio-ranges"属性,
* 那么就不应该再调用遗留的回调函数.
*/
if (device_property_present(&gc->gpiodev->dev, "gpio-ranges"))
return 0;

/* 如果驱动提供了遗留的回调函数, 则调用它. */
if (gc->add_pin_ranges)
return gc->add_pin_ranges(gc);

return 0;
}

GPIO层次化中断域(Hierarchical IRQ Domain)实现

此代码片段是Linux内核gpiolib中用于实现和管理层次化中断域的核心。其根本原理是为那些级联(cascaded)在另一个主中断控制器之下的GPIO控制器, 创建一个”子域”(child domain), 并提供一套完整的操作函数集来管理这个子域的生命周期, 包括它的创建、中断翻译、分配和释放

这个模型对于像STM32这样具有分层中断结构的复杂SoC是必不可少的。在STM32中, GPIO引脚 -> EXTI控制器 -> NVIC(CPU)构成了一个清晰的中断层次。这组函数就是在软件层面精确地建模这种硬件上的父子级联关系, 使得内核能够正确地将一个来自特定GPIO引脚的中断请求, 逐级翻译并路由到CPU。


1. 域的创建与配置

这组函数负责判断是否需要创建层次化域, 并执行创建和配置过程。

gpiochip_hierarchy_is_hierarchical

一个简单的谓词函数, 用于判断是否应使用层次化模型。

1
2
3
4
5
6
7
8
9
10
/* gpiochip_hierarchy_is_hierarchical: 判断一个gpio_chip是否被配置为层次化中断. */
static bool gpiochip_hierarchy_is_hierarchical(struct gpio_chip *gc)
{
/*
* !! 是一个C语言技巧, 将任何非NULL指针转换为true(1), NULL指针转换为false(0).
* 它的逻辑是: 如果驱动已经为 gc->irq.parent_domain 提供了有效的父域指针,
* 那么就认为它工作在层次化模式下.
*/
return !!gc->irq.parent_domain;
}

gpiochip_hierarchy_setup_domain_ops

为子域的irq_domain_ops结构体填充一组标准的、预设的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* gpiochip_hierarchy_setup_domain_ops: 为层次化域的操作函数集(ops)进行设置. */
static void gpiochip_hierarchy_setup_domain_ops(struct irq_domain_ops *ops)
{
/* 当一个中断被请求时, 调用此函数来锁定GPIO引脚, 防止其被用作他途. */
ops->activate = gpiochip_irq_domain_activate;
/* 当中断被释放时, 调用此函数来解锁GPIO引脚. */
ops->deactivate = gpiochip_irq_domain_deactivate;
/* 当需要将硬件中断号映射到Linux IRQ号时, 调用此核心分配函数. */
ops->alloc = gpiochip_hierarchy_irq_domain_alloc;

/*
* 对于层次化芯片, 我们只允许驱动有选择地覆盖 translate 和 free 函数.
* 默认的 translate 函数能够处理标准的设备树绑定.
* 默认的 free 函数能够处理通用的IRQ释放.
*/
if (!ops->translate)
ops->translate = gpiochip_hierarchy_irq_domain_translate;
if (!ops->free)
ops->free = irq_domain_free_irqs_common;
}

gpiochip_hierarchy_create_domain

创建层次化中断域的主函数。

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
/* gpiochip_hierarchy_create_domain: 为一个gpio_chip创建一个层次化的中断域(子域). */
static struct irq_domain *gpiochip_hierarchy_create_domain(struct gpio_chip *gc)
{
struct irq_domain *domain;

/* 健全性检查: 驱动必须提供核心的回调函数和固件节点. */
if (!gc->irq.child_to_parent_hwirq || !gc->irq.fwnode) {
chip_err(gc, "missing irqdomain vital data\n");
return ERR_PTR(-EINVAL);
}

/* 为驱动未提供的可选回调函数设置无操作的默认值, 简化驱动编写. */
if (!gc->irq.child_offset_to_irq)
/*
static unsigned int gpiochip_child_offset_to_irq_noop(struct gpio_chip *gc,
unsigned int offset)
{
return offset;
}
*/
gc->irq.child_offset_to_irq = gpiochip_child_offset_to_irq_noop;
if (!gc->irq.populate_parent_alloc_arg)
gc->irq.populate_parent_alloc_arg =
gpiochip_populate_parent_fwspec_twocell;

/* 为子域配置标准的操作函数集. */
gpiochip_hierarchy_setup_domain_ops(&gc->irq.child_irq_domain_ops);

/* 调用内核通用的层次化域创建函数, 实际创建域并建立父子链接. */
domain = irq_domain_create_hierarchy(
gc->irq.parent_domain, /* 父域 */
0, /* flags */
gc->ngpio, /* 子域大小 */
gc->irq.fwnode, /* 子域的固件节点 */
&gc->irq.child_irq_domain_ops, /* 子域的操作函数集 */
gc); /* 私有数据 */

if (!domain)
return ERR_PTR(-ENOMEM);

/* 为irq_chip本身设置用于层次化操作的钩子 (主要是为了兼容非设备树的旧系统). */
gpiochip_set_hierarchical_irqchip(gc, gc->irq.chip);

return domain;
}

2. 中断域操作回调 (irq_domain_ops) 的实现

这组函数是gpiochip_hierarchy_setup_domain_ops所设置的回调, 它们定义了子域的具体行为。

gpiochip_hierarchy_irq_domain_translate

负责解析来自设备树的中断请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* gpiochip_hierarchy_irq_domain_translate: 层次化域的translate回调. */
static int gpiochip_hierarchy_irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
unsigned long *hwirq,
unsigned int *type)
{
/* 如果是标准的设备树节点, 使用内核提供的标准twocell/threecell翻译函数. */
if (is_of_node(fwspec->fwnode))
return irq_domain_translate_twothreecell(d, fwspec, hwirq, type);

/* 这是为了兼容旧的、非设备树的板级文件. */
if (is_fwnode_irqchip(fwspec->fwnode)) {
// ... (处理旧格式) ...
}
return -EINVAL;
}

gpiochip_hierarchy_irq_domain_alloc

这是最核心的函数, 负责将一个子域的硬件中断(hwirq)映射到一个Linux IRQ, 并递归地向父域申请资源。

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
/* gpiochip_hierarchy_irq_domain_alloc: 层次化域的alloc回调. */
static int gpiochip_hierarchy_irq_domain_alloc(struct irq_domain *d,
unsigned int irq,
unsigned int nr_irqs,
void *data)
{
struct gpio_chip *gc = d->host_data; // 获取私有数据, 即gpio_chip
irq_hw_number_t hwirq; // 子域的硬件中断号 (即GPIO偏移)
unsigned int type = IRQ_TYPE_NONE;
struct irq_fwspec *fwspec = data; // 来自设备树的请求参数
union gpio_irq_fwspec gpio_parent_fwspec = {}; // 准备传递给父域的参数
unsigned int parent_hwirq; // 父域的硬件中断号 (即EXTI线号)
unsigned int parent_type;
struct gpio_irq_chip *girq = &gc->irq;
int ret;

/* 步骤1: 将设备树请求翻译成本地域的hwirq和type. */
ret = gc->irq.child_irq_domain_ops.translate(d, fwspec, &hwirq, &type);
if (ret)
return ret;

/* 步骤2: 核心翻译. 调用驱动提供的回调, 将子域hwirq翻译成父域的parent_hwirq. */
ret = girq->child_to_parent_hwirq(gc, hwirq, type,
&parent_hwirq, &parent_type);
if (ret) {
chip_err(gc, "can't look up hwirq %lu\n", hwirq);
return ret;
}

/* 步骤3: 设置本地域的映射关系. 将Linux IRQ(irq)与本地hwirq和irq_chip关联起来. */
irq_domain_set_info(d, irq, hwirq, gc->irq.chip, gc,
girq->handler, NULL, NULL);
irq_set_probe(irq); // 标记此IRQ需要探测

/* 步骤4: 准备向父域申请资源的参数. */
ret = girq->populate_parent_alloc_arg(gc, &gpio_parent_fwspec,
parent_hwirq, parent_type);
if (ret)
return ret;

/* 步骤5: 递归向上, 向父域申请中断. 这会触发父域的alloc回调. */
ret = irq_domain_alloc_irqs_parent(d, irq, 1, &gpio_parent_fwspec);
if (ret)
chip_err(gc, "failed to allocate parent hwirq %d for hwirq %lu\n",
parent_hwirq, hwirq);

return ret;
}

gpiochip_irq_domain_activate / deactivate

在请求/释放中断时, 锁定/解锁对应的GPIO引脚, 防止其被用作普通IO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* gpiochip_irq_domain_activate: activate回调, 锁定GPIO引脚作为IRQ. */
static int gpiochip_irq_domain_activate(struct irq_domain *domain,
struct irq_data *data, bool reserve)
{
struct gpio_chip *gc = domain->host_data;
unsigned int hwirq = irqd_to_hwirq(data); // 从irq_data获取hwirq

/* 调用gpiolib内部函数, 将该引脚标记为"已被IRQ使用". */
return gpiochip_lock_as_irq(gc, hwirq);
}

/* gpiochip_irq_domain_deactivate: deactivate回调, 解锁作为IRQ的GPIO引脚. */
static void gpiochip_irq_domain_deactivate(struct irq_domain *domain,
struct irq_data *data)
{
struct gpio_chip *gc = domain->host_data;
unsigned int hwirq = irqd_to_hwirq(data);

/* 调用gpiolib内部函数, 解除引脚的"已被IRQ使用"标记. */
return gpiochip_unlock_as_irq(gc, hwirq);
}

3. 其他辅助与遗留(Legacy)支持函数

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
/* gpiochip_set_hierarchical_irqchip: (主要用于遗留系统) 设置层次化irqchip. */
static void gpiochip_set_hierarchical_irqchip(struct gpio_chip *gc,
struct irq_chip *irqchip)
{
/* 如果使用设备树, 内核会动态处理映射, 无需此函数做任何事. */
if (is_of_node(gc->irq.fwnode))
return;

/*
* 下面的代码是为了兼容旧的、不使用设备树的板级文件.
* 它会尝试为所有可能的引脚预先分配中断.
* 在现代内核和STM32开发中, 这部分代码通常不会被执行.
*/
// ... (legacy code) ...
}

/* gpiochip_populate_parent_fwspec_*: 用于准备传递给父域中断分配函数的参数的默认实现. */
int gpiochip_populate_parent_fwspec_twocell(struct gpio_chip *gc,
union gpio_irq_fwspec *gfwspec,
unsigned int parent_hwirq,
unsigned int parent_type)
{
struct irq_fwspec *fwspec = &gfwspec->fwspec;

fwspec->fwnode = gc->irq.parent_domain->fwnode; // 使用父域的节点
fwspec->param_count = 2; // 父域需要2个参数
fwspec->param[0] = parent_hwirq; // 第一个参数是父hwirq
fwspec->param[1] = parent_type; // 第二个参数是触发类型

return 0;
}

GPIO简单中断域(Simple IRQ Domain)创建函数

此代码片段展示了gpiolib中用于创建简单(或称”扁平”, flat)中断域的机制。其核心原理是为那些硬件结构较为简单、其GPIO中断线可以直接一对一映射到主中断控制器上的GPIO控制器, 提供一个简化的、非层次化的中断域创建流程

这与我们之前讨论的gpiochip_hierarchy_create_domain形成了鲜明对比。层次化模型用于处理像STM32这样具有”GPIO -> EXTI -> NVIC”多级级联关系的复杂硬件, 而简单模型则适用于那些GPIO引脚中断可以直接被系统顶级中断控制器(如GIC on multi-core ARM, or NVIC on Cortex-M)识别的硬件


gpiochip_irq_map: 映射一个Linux IRQ到GPIO引脚

此函数是irq_domain_ops最核心的回调函数。它的作用是建立一个从全局唯一的Linux IRQ号(irq)到特定GPIO控制器上一个本地硬件中断号(hwirq, 即引脚偏移量)的完整映射关系。它负责完成所有必要的软件配置, 为即将到来的中断做好准备。

原理与工作流程:

  1. 获取上下文: 从中断域的私有数据d->host_data中获取到对应的gpio_chip结构体, 这是所有操作的基础。
  2. 有效性检查: 调用gpiochip_irqchip_irq_valid检查hwirq(引脚偏移)是否在该gpio_chip的中断有效掩码内。如果一个引脚不支持中断, 则映射失败。
  3. 关联核心数据:
    • irq_set_chip_data(irq, gc): 将gpio_chip本身设置为该IRQ的”chip data”。这使得中断处理代码在处理这个IRQ时, 可以快速地回溯到管理它的GPIO控制器。
    • irq_set_chip_and_handler(irq, gc->irq.chip, gc->irq.handler): 这是关键的一步。它将驱动提供的irq_chip结构体(包含ack, mask, unmask等底层硬件操作函数)和中断流处理器(handler)与该IRQ关联起来。从此, 当这个IRQ触发时, 内核就知道应该调用哪个函数来处理它。
  4. 设置父IRQ (可选): 如果该GPIO控制器的中断是级联在一个父IRQ之下的(即使是在简单模型中也可能出现这种情况), 此函数会调用irq_set_parent来建立这种级联关系。
  5. 设置默认触发类型: 如果驱动程序在gpio_chip中指定了default_type (如边沿触发、电平触发), 此函数会调用irq_set_irq_type来将这个默认配置应用到硬件上。
  6. 配置特殊属性: 它还会设置一些额外的属性, 如lockdep锁分类(用于调试死锁)和nested_thread标志(用于支持嵌套的线程化中断处理器)。
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
/**
* gpiochip_irq_map() - 将一个IRQ映射到一个GPIO irqchip中
* @d: 此irqchip使用的irqdomain
* @irq: 此GPIO irqchip irq使用的全局irq号
* @hwirq: 此gpiochip上的本地IRQ/GPIO线偏移量
*/
static int gpiochip_irq_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hwirq)
{
struct gpio_chip *gc = d->host_data; // 获取gpio_chip上下文
int ret = 0;

/* 检查此hwirq(引脚)是否支持中断. */
if (!gpiochip_irqchip_irq_valid(gc, hwirq))
return -ENXIO; // 不支持则返回"无此设备或地址"错误

/* 将gpio_chip本身设置为此IRQ的私有数据. */
irq_set_chip_data(irq, gc);

/* 为锁调试器设置特殊的锁分类, 以避免误报. */
irq_set_lockdep_class(irq, gc->irq.lock_key, gc->irq.request_key);

/*
* 核心关联: 将驱动提供的irq_chip和中断流处理器(handler)与此IRQ号绑定.
* 当此IRQ触发时, 内核将调用 gc->irq.handler.
* 在handler内部, 会再调用 gc->irq.chip 中的函数来操作硬件.
*/
irq_set_chip_and_handler(irq, gc->irq.chip, gc->irq.handler);

/* 如果驱动使用了嵌套线程化中断处理器, 则设置相应标志. */
if (gc->irq.threaded)
irq_set_nested_thread(irq, 1);

/* 标记此IRQ不需要在启动时被内核自动探测. */
irq_set_noprobe(irq);

/* 如果有父IRQ, 建立级联关系. */
if (gc->irq.num_parents == 1) // 简单级联, 只有一个父IRQ
ret = irq_set_parent(irq, gc->irq.parents[0]);
else if (gc->irq.map) // 复杂级联, 每个hwirq可能对应不同的父IRQ
ret = irq_set_parent(irq, gc->irq.map[hwirq]);
if (ret < 0)
return ret;

/* 如果驱动指定了默认触发类型, 则应用它. */
if (gc->irq.default_type != IRQ_TYPE_NONE)
irq_set_irq_type(irq, gc->irq.default_type);

return 0;
}

gpiochip_irq_unmap: 解除一个Linux IRQ与GPIO引脚的映射

此函数是gpiochip_irq_map的逆操作。当一个IRQ被释放时, 内核会调用此函数来清除所有在map阶段建立的软件关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* gpiochip_irq_unmap() - 解除一个IRQ与gpiochip的映射
* @d: irqdomain
* @irq: 要解除映射的全局irq号
*/
static void gpiochip_irq_unmap(struct irq_domain *d, unsigned int irq)
{
struct gpio_chip *gc = d->host_data; // 获取gpio_chip上下文

/* 清除嵌套线程标志. */
if (gc->irq.threaded)
irq_set_nested_thread(irq, 0);

/*
* 解除核心关联: 将irq_chip和handler都设为NULL.
* 这样, 即使这个IRQ号意外触发, 内核也不会再调用旧的处理函数.
*/
irq_set_chip_and_handler(irq, NULL, NULL);

/* 清除私有数据. */
irq_set_chip_data(irq, NULL);
}

gpiochip_irq_select: 选择一个中断控制器

此回调函数用于一个高级场景: 当一个设备在设备树中描述的中断可以由多个不同的中断控制器来提供服务时, 内核会调用.select回调来让驱动程序判断自己是否是那个”正确”的控制器。

原理与作用:
它的主要作用是进行匹配。内核将从设备树中解析出的中断请求fwspec传递给它, 它需要将fwspec中的信息与自身irq_domain的信息进行比较。

  • 对于使用设备树的现代系统, 它可能会调用of_gpiochip_instance_match来进行更复杂的匹配。
  • 对于简单的、非层次化的域, 它主要比较fwspec中的固件节点(fwnode)是否与自身域的固件节点(d->fwnode)相同, 并可能比较总线令牌(bus_token)。如果匹配, 就返回true, 表示”这个中断请求是给我的”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* gpiochip_irq_select() - 选择一个中断控制器来处理中断请求
* @d: irqdomain
* @fwspec: 从设备树解析出的中断请求描述符
* @bus_token: 总线令牌
*/
static int gpiochip_irq_select(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token)
{
struct fwnode_handle *fwnode = fwspec->fwnode;
struct gpio_chip *gc = d->host_data;
unsigned int index = fwspec->param[0];

/* 对于三单元格的设备树中断描述, 使用一个更复杂的匹配函数. */
if (fwspec->param_count == 3 && is_of_node(fwnode))
return of_gpiochip_instance_match(gc, index);

/*
* 对于两单元格的描述, 进行简单的匹配:
* 请求的固件节点是否与本域的固件节点相同? 并且总线令牌是否匹配?
*/
return (fwnode && (d->fwnode == fwnode) && (d->bus_token == bus_token));
}

gpiochip_domain_ops: 简单域的操作函数集

这是一个静态的irq_domain_ops结构体实例, 它为所有通过”简单模型”创建的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
/*
* 定义一个静态的、常量类型的 struct irq_domain_ops 实例.
* 这是为所有"简单"GPIO中断域提供的标准操作集.
*/
static const struct irq_domain_ops gpiochip_domain_ops = {
/*
* .map: 指向 gpiochip_irq_map 函数.
* 作用: 这是最重要的回调函数. 当内核需要将一个硬件中断号(hwirq, 在此上下文中就是GPIO的偏移量)
* 映射到一个Linux IRQ号(virq)时, 此函数被调用. 它的职责是:
* 1. 将virq与hwirq关联起来.
* 2. 调用gpio_chip提供的irq_chip回调函数(如.irq_ack, .irq_mask)来配置硬件.
* 3. 设置该IRQ的中断处理器(handler).
*/
.map = gpiochip_irq_map,
/*
* .unmap: 指向 gpiochip_irq_unmap 函数.
* 作用: 这是 .map 的逆操作. 当一个映射被销毁时, 此函数被调用以释放硬件资源和软件关联.
*/
.unmap = gpiochip_irq_unmap,
/*
* .select: 指向 gpiochip_irq_select 函数.
* 作用: (可选) 当一个IRQ可以由多个芯片或配置提供时, 此函数用于在它们之间进行选择.
*/
.select = gpiochip_irq_select,
/*
* .xlate: 指向 irq_domain_xlate_twothreecell 函数.
* 作用: "Translate", 即翻译. 此函数用于解析设备树(Device Tree)中的 "interrupts" 属性.
* 内核提供了几个标准的xlate函数, irq_domain_xlate_twothreecell 用于处理最常见的两种格式:
* - 两单元格(twocell): interrupts = <hwirq trigger_type>; (例如 <5 IRQ_TYPE_EDGE_RISING>)
* - 三单元格(threecell): interrupts = <hwirq trigger_type priority>;
* 它的职责是从设备树的这两个或三个单元格中提取出硬件中断号(hwirq)和触发类型等信息.
*/
.xlate = irq_domain_xlate_twothreecell,
};

gpiochip_simple_create_domain: 创建一个简单中断域

这是一个内部函数, 它封装了内核通用的irq_domain_create_simple函数, 为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
/*
* gpiochip_simple_create_domain: 为一个gpio_chip创建一个简单的中断域.
* @gc: 目标gpio_chip.
* @return: 成功时返回一个指向新创建的 irq_domain 的指针, 失败时返回一个错误指针.
*/
static struct irq_domain *gpiochip_simple_create_domain(struct gpio_chip *gc)
{
/* 获取与该gpiolib设备关联的固件节点(设备树节点). */
struct fwnode_handle *fwnode = dev_fwnode(&gc->gpiodev->dev);
struct irq_domain *domain;

/*
* 调用内核通用的 irq_domain_create_simple 函数来完成实际的创建工作.
* 这个函数适用于创建线性的、扁平的、一对一映射的中断域.
* 参数解释:
* - fwnode: 该中断控制器的固件节点.
* - gc->ngpio: 域的大小, 即该控制器管理的GPIO(中断)数量.
* - gc->irq.first: 硬件中断号(hwirq)的起始值. 对于简单模型, 这通常是0.
* - &gpiochip_domain_ops: 为这个新域指定上面定义好的标准操作函数集.
* - gc: 将gpio_chip自身作为私有数据(host_data)传递给中断域, 以便在回调函数中可以访问到它.
*/
domain = irq_domain_create_simple(fwnode, gc->ngpio, gc->irq.first,
&gpiochip_domain_ops, gc);
/*
* irq_domain_create_simple 在成功时返回域指针, 失败时返回NULL.
* 这里将其转换为内核标准的错误指针格式.
*/
if (!domain)
return ERR_PTR(-EINVAL);

return domain;
}

gpiolib中断资源管理与使能/禁用函数

此代码片段展示了Linux内核gpiolib子系统中用于管理GPIO引脚作为中断资源的一组核心函数。这组函数与irqchip子系统紧密协作, 其根本原理是在一个GPIO引脚被用作中断源的整个生命周期中, 对其进行状态跟踪和访问控制。这包括:

  1. 锁定/解锁: 当引脚被请求为中断时, 将其”锁定”, 防止它同时被用作普通的输入/输出引脚, 从而避免了功能冲突。
  2. 模块引用计数: 确保提供GPIO控制器功能的内核模块在使用期间不会被意外卸载。
  3. 使能/禁用状态跟踪: 在引脚的描述符中维护一个软件状态位, 记录该中断是处于使能还是禁用状态。

gpiochip_lock_as_irq: 将GPIO引脚锁定为中断模式

当一个驱动程序请求一个GPIO引脚作为中断源时(通常在request_irq的调用链深处), irqchip子系统的request_resources回调函数(即gpiolibgpiochip_reqres_irq)最终会调用此函数。

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
/**
* gpiochip_lock_as_irq() - 锁定一个GPIO以用作IRQ
* @gc: GPIO所属的chip
* @offset: 要锁定为IRQ的GPIO的偏移量
*
* 这个函数由那些希望锁定某条GPIO线用于IRQ的GPIO驱动直接使用.
*
* 返回:
* 0 on success, or negative errno on failure.
*/
int gpiochip_lock_as_irq(struct gpio_chip *gc, unsigned int offset)
{
struct gpio_desc *desc;

/* 步骤1: 获取该引脚的软件描述符. */
desc = gpiochip_get_desc(gc, offset);
if (IS_ERR(desc))
return PTR_ERR(desc); // 如果偏移无效, 返回错误.

/*
* 步骤2 (可选的硬件同步):
* 如果GPIO控制器的操作不能休眠(gc->can_sleep为false), 并且驱动提供了get_direction回调,
* 这通常意味着寄存器访问非常快, 而且引脚方向可能被硬件或其他未知因素改变.
* 在这种情况下, 函数会主动调用gpiod_get_direction()来从硬件回读当前的方向,
* 以确保描述符中的软件状态与硬件的实际状态同步.
*/
if (!gc->can_sleep && gc->get_direction) {
int dir = gpiod_get_direction(desc);

if (dir < 0) {
chip_err(gc, "%s: cannot get GPIO direction\n",
__func__);
return dir;
}
}

/*
* 步骤3: 关键的健全性检查.
* 一个引脚要能作为中断输入, 它本身必须是输入模式, 或者是一种特殊的"开漏"(open drain)输出模式.
* 开漏输出可以驱动低电平, 但在高电平状态下呈高阻态, 因此可以安全地感知外部信号的上拉或下拉.
* 如果一个引脚被配置为标准的推挽(push-pull)输出模式, 那么它无法可靠地接收外部中断信号.
* test_bit()用于原子地检查描述符中的标志位.
*/
if (test_bit(FLAG_IS_OUT, &desc->flags) &&
!test_bit(FLAG_OPEN_DRAIN, &desc->flags)) {
chip_err(gc,
"%s: tried to flag a GPIO set as output for IRQ\n",
__func__);
return -EIO; // 返回"IO错误"
}

/*
* 步骤4: 核心操作.
* 使用set_bit()原子地设置两个标志位:
* FLAG_USED_AS_IRQ: 这是"锁定"标志, 表明该引脚现在被中断子系统独占.
* FLAG_IRQ_IS_ENABLED: 将该中断的软件使能状态初始化为"使能".
*/
set_bit(FLAG_USED_AS_IRQ, &desc->flags);
set_bit(FLAG_IRQ_IS_ENABLED, &desc->flags);

return 0; // 成功
}
/* 将此函数导出, 使其对内核其他部分(主要是irqchip驱动)可用. */
EXPORT_SYMBOL_GPL(gpiochip_lock_as_irq);

gpiochip_unlock_as_irq: 解锁一个用作中断的GPIO引脚

当一个驱动程序释放一个中断时(在free_irq的调用链中), irqchip子系统的release_resources回调函数(即gpiolibgpiochip_relres_irq)会调用此函数。

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
/**
* gpiochip_unlock_as_irq() - 解锁一个用作IRQ的GPIO
* @gc: GPIO所属的chip
* @offset: 要解锁的GPIO的偏移量
*
* 这个函数由那些希望表明某条GPIO不再被专门用于IRQ的GPIO驱动直接使用.
*/
void gpiochip_unlock_as_irq(struct gpio_chip *gc, unsigned int offset)
{
struct gpio_desc *desc;

/* 步骤1: 获取该引脚的软件描述符. */
desc = gpiochip_get_desc(gc, offset);
if (IS_ERR(desc))
return; // 如果偏移无效, 静默返回.

/*
* 步骤2: 核心操作.
* 使用clear_bit()原子地清除两个标志位, 解除锁定并重置状态.
* 此后, 该引脚就可以被重新配置为普通的输入或输出引脚了.
*/
clear_bit(FLAG_USED_AS_IRQ, &desc->flags);
clear_bit(FLAG_IRQ_IS_ENABLED, &desc->flags);
}
/* 将此函数导出. */
EXPORT_SYMBOL_GPL(gpiochip_unlock_as_irq);

gpiochip_reqres_irq / gpiochip_relres_irq: 请求/释放中断资源

这两个函数是irqchip子系统的request_resourcesrelease_resources回调函数的标准实现。它们负责一个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
/**
* gpiochip_reqres_irq - 请求一个GPIO引脚作为中断资源
* @gc: 目标gpio_chip
* @offset: 该chip上的引脚偏移量
*/
int gpiochip_reqres_irq(struct gpio_chip *gc, unsigned int offset)
{
int ret;

/*
* 步骤1: 增加模块引用计数.
* try_module_get()会尝试增加提供该GPIO控制器的内核模块(owner)的引用计数.
* 这可以防止在中断正在使用时, 该模块被`rmmod`命令卸载, 从而避免了使用悬挂指针导致的系统崩溃.
*/
if (!try_module_get(gc->gpiodev->owner))
return -ENODEV;

/*
* 步骤2: 将引脚锁定为中断模式.
* gpiochip_lock_as_irq()会在该引脚的描述符中设置一个FLAG_USED_AS_IRQ标志.
* 任何后续尝试通过gpiod_direction_*()等函数来改变该引脚方向的操作,
* 都会因为检查到这个标志而失败.
*/
ret = gpiochip_lock_as_irq(gc, offset);
if (ret) {
/*
* 错误处理: 如果锁定失败, 必须撤销第一步的操作.
* module_put()会减少模块的引用计数.
*/
chip_err(gc, "unable to lock HW IRQ %u for IRQ\n", offset);
module_put(gc->gpiodev->owner);
return ret;
}
return 0; // 成功
}
EXPORT_SYMBOL_GPL(gpiochip_reqres_irq);

/**
* gpiochip_relres_irq - 释放一个作为中断资源的GPIO引脚
* @gc: 目标gpio_chip
* @offset: 该chip上的引脚偏移量
*/
void gpiochip_relres_irq(struct gpio_chip *gc, unsigned int offset)
{
/*
* 步骤1: 解锁引脚.
* gpiochip_unlock_as_irq()会清除FLAG_USED_AS_IRQ标志,
* 使得该引脚可以被重新用作普通的GPIO.
*/
gpiochip_unlock_as_irq(gc, offset);
/*
* 步骤2: 减少模块引用计数.
* 与try_module_get()配对, 表明我们已经不再需要这个模块提供的中断服务了.
*/
module_put(gc->gpiodev->owner);
}
EXPORT_SYMBOL_GPL(gpiochip_relres_irq);

gpiochip_enable_irq / gpiochip_disable_irq: 使能/禁用中断(软件状态)

这两个函数通常被irq_chipmask/unmask回调函数的包装器所调用。它们只负责在gpiolib的软件层面更新中断的使能状态, 而不直接操作硬件。硬件的屏蔽/解屏蔽操作由irq_chip回调本身负责。

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
/**
* gpiochip_disable_irq - (在gpiolib层面)禁用一个GPIO中断
* @gc: 目标gpio_chip
* @offset: 该chip上的引脚偏移量
*/
void gpiochip_disable_irq(struct gpio_chip *gc, unsigned int offset)
{
/* 获取该引脚的描述符(gpio_desc). */
struct gpio_desc *desc = gpiochip_get_desc(gc, offset);

/*
* 检查描述符是否有效, 并使用WARN_ON确保该引脚确实被配置为中断模式.
* 如果一个引脚没有被配置为中断, 那么禁用它是一个逻辑错误, WARN_ON会打印警告.
*/
if (!IS_ERR(desc) &&
!WARN_ON(!test_bit(FLAG_USED_AS_IRQ, &desc->flags)))
/* 核心操作: 清除描述符中的FLAG_IRQ_IS_ENABLED标志位. */
clear_bit(FLAG_IRQ_IS_ENABLED, &desc->flags);
}
EXPORT_SYMBOL_GPL(gpiochip_disable_irq);

/**
* gpiochip_enable_irq - (在gpiolib层面)使能一个GPIO中断
* @gc: 目标gpio_chip
* @offset: 该chip上的引脚偏移量
*/
void gpiochip_enable_irq(struct gpio_chip *gc, unsigned int offset)
{
/* 获取该引脚的描述符. */
struct gpio_desc *desc = gpiochip_get_desc(gc, offset);

if (!IS_ERR(desc) &&
!WARN_ON(!test_bit(FLAG_USED_AS_IRQ, &desc->flags))) {
/*
* 关键的健全性检查:
* 一个引脚在用作中断时, 通常必须是输入模式.
* 唯一的例外是它被配置为"开漏"(open drain)输出, 此时它可以同时驱动低电平并感知外部信号.
* 如果一个推挽(push-pull)输出引脚被用作中断, 这是一个硬件配置错误, WARN_ON会打印警告.
*/
WARN_ON(test_bit(FLAG_IS_OUT, &desc->flags) &&
!test_bit(FLAG_OPEN_DRAIN, &desc->flags));
/* 核心操作: 设置描述符中的FLAG_IRQ_IS_ENABLED标志位. */
set_bit(FLAG_IRQ_IS_ENABLED, &desc->flags);
}
}
EXPORT_SYMBOL_GPL(gpiochip_enable_irq);

gpiolib中断控制器(irqchip)的实现

此代码片段是Linux内核gpiolib子系统与irqchip子系统集成的核心部分。它的根本原理是提供一套标准的适配器(Adapter)和包装器(Wrapper)函数, 将一个驱动程序提供的、特定于硬件的gpio_chip对象, 封装成一个内核可以统一识别和管理的标准irq_chip对象。这使得任何一个GPIO控制器, 无论其硬件实现如何, 都能作为中断源无缝地融入内核的中断处理框架, 从而实现了gpio_to_irq()这个关键功能。


1. gpio_to_irq的核心实现

gpiochip_to_irq是驱动程序和内核其他部分将一个GPIO引脚坐标(控制器+偏移)翻译成一个全局Linux IRQ号所调用的核心API。

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
/**
* gpiochip_to_irq - 将一个gpiochip上的偏移量翻译成Linux IRQ号
* @gc: 目标gpio_chip
* @offset: 该chip上的引脚偏移量
* @return: 成功时返回Linux IRQ号, 失败时返回负的错误码
*/
static int gpiochip_to_irq(struct gpio_chip *gc, unsigned int offset)
{
struct irq_domain *domain = gc->irq.domain; // 获取与该chip关联的中断域

/*
* 关键的竞态条件防护: 检查irq子系统是否已完全初始化.
* 如果一个驱动尝试获取IRQ, 但gpiochip的irqchip部分尚未通过
* gpiochip_irqchip_add_allocated_domain()注册完毕, gc->irq.initialized会是false.
* 返回-EPROBE_DEFER是标准的处理方法, 请求内核稍后重试该驱动的探测.
*/
if (!gc->irq.initialized)
return -EPROBE_DEFER;

/* 检查该引脚是否在硬件上支持中断. */
if (!gpiochip_irqchip_irq_valid(gc, offset))
return -ENXIO; // 不支持则返回"无此设备或地址"

#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/*
* 如果这是一个层次化域 (例如STM32的GPIO域是EXTI域的子域).
*/
if (irq_domain_is_hierarchy(domain)) {
struct irq_fwspec spec; // 创建一个通用的中断请求描述符

spec.fwnode = domain->fwnode; // 使用子域的固件节点
spec.param_count = 2; // 指定请求包含2个参数
/* 第一个参数: 本地硬件中断号(hwirq), 即引脚偏移 */
spec.param[0] = gc->irq.child_offset_to_irq(gc, offset);
/* 第二个参数: 触发类型, 此时未知, 设为NONE */
spec.param[1] = IRQ_TYPE_NONE;

/*
* 调用irq_create_fwspec_mapping(), 这个函数会触发整个层次化域的
* 分配(alloc)流程, 逐级向上申请和翻译, 最终创建一个完整的映射.
*/
return irq_create_fwspec_mapping(&spec);
}
#endif

/*
* 对于简单的(非层次化的)域, 直接调用irq_create_mapping.
* 这个函数会使用domain->ops->map回调(即gpiochip_irq_map)来建立映射.
*/
return irq_create_mapping(domain, offset);
}

2. irq_chip回调函数的包装器

gpiochip_set_irq_hooks函数通过巧妙的指针交换, 将gpiolib自己的通用逻辑”注入”到驱动提供的irq_chip回调中。下面是被注入的包装器函数, 它们在调用驱动原始的回调函数的同时, 执行了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
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
/* gpiochip_irq_reqres: "request_resources"回调的包装器. */
int gpiochip_irq_reqres(struct irq_data *d)
{
/* 从irq_data中获取gpio_chip上下文 */
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
/* 从irq_data中获取硬件中断号(引脚偏移) */
unsigned int hwirq = irqd_to_hwirq(d);

/* 调用gpiolib内部函数, 将该引脚锁定为IRQ模式, 防止被用作普通GPIO. */
return gpiochip_reqres_irq(gc, hwirq);
}
EXPORT_SYMBOL(gpiochip_irq_reqres);

/* gpiochip_irq_relres: "release_resources"回调的包装器. */
void gpiochip_irq_relres(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
unsigned int hwirq = irqd_to_hwirq(d);

/* 解锁引脚, 使其可以被重新用作普通GPIO. */
gpiochip_relres_irq(gc, hwirq);
}
EXPORT_SYMBOL(gpiochip_irq_relres);

/* gpiochip_irq_mask: "irq_mask"回调的包装器. */
static void gpiochip_irq_mask(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
unsigned int hwirq = irqd_to_hwirq(d);

/* 如果驱动提供了自己的irq_mask实现, 先调用它. */
if (gc->irq.irq_mask)
gc->irq.irq_mask(d);
/* 然后, 调用gpiolib的通用disable函数. */
gpiochip_disable_irq(gc, hwirq);
}

/* gpiochip_irq_unmask: "irq_unmask"回调的包装器. */
static void gpiochip_irq_unmask(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
unsigned int hwirq = irqd_to_hwirq(d);

/* 先调用gpiolib的通用enable函数. */
gpiochip_enable_irq(gc, hwirq);
/* 如果驱动提供了自己的irq_unmask实现, 再调用它. */
if (gc->irq.irq_unmask)
gc->irq.irq_unmask(d);
}

/* gpiochip_irq_enable: "irq_enable"回调的包装器 (用于旧式API). */
static void gpiochip_irq_enable(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
unsigned int hwirq = irqd_to_hwirq(d);

gpiochip_enable_irq(gc, hwirq);
/* 调用驱动原始的irq_enable. */
gc->irq.irq_enable(d);
}

/* gpiochip_irq_disable: "irq_disable"回调的包装器 (用于旧式API). */
static void gpiochip_irq_disable(struct irq_data *d)
{
struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
unsigned int hwirq = irqd_to_hwirq(d);

/* 调用驱动原始的irq_disable. */
gc->irq.irq_disable(d);
gpiochip_disable_irq(gc, hwirq);
}

3. irqchip的安装与初始化

这组函数负责将上述所有部分组装起来, 完成irqchipgpiolib中的注册。

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
/* gpiochip_set_irq_hooks: 为gpio_chip的irqchip安装包装器钩子. */
static void gpiochip_set_irq_hooks(struct gpio_chip *gc)
{
struct irq_chip *irqchip = gc->irq.chip;

/*
* 如果irq_chip被标记为"不可变的"(immutable), 说明其回调函数不应被修改,
* 直接返回. 这是现代驱动推荐的做法.
*/
if (irqchip->flags & IRQCHIP_IMMUTABLE)
return;

/* 否则, 打印一条警告, 建议驱动作者修复它. */
chip_warn(gc, "not an immutable chip, please consider fixing it!\n");

/* 如果驱动没有提供资源管理回调, 则安装gpiolib的默认实现. */
if (!irqchip->irq_request_resources &&
!irqchip->irq_release_resources) {
irqchip->irq_request_resources = gpiochip_irq_reqres;
irqchip->irq_release_resources = gpiochip_irq_relres;
}

if (WARN_ON(gc->irq.irq_enable))
return;

// ... (检查共享irqchip的警告) ...

/*
* 核心的指针交换"包装"逻辑:
* 1. 保存驱动原始的回调函数指针到gc->irq的私有字段中.
* 2. 用gpiolib自己的包装器函数覆盖irqchip中的回调函数指针.
*/
if (irqchip->irq_disable) {
gc->irq.irq_disable = irqchip->irq_disable;
irqchip->irq_disable = gpiochip_irq_disable;
} else {
gc->irq.irq_mask = irqchip->irq_mask;
irqchip->irq_mask = gpiochip_irq_mask;
}
if (irqchip->irq_enable) {
gc->irq.irq_enable = irqchip->irq_enable;
irqchip->irq_enable = gpiochip_irq_enable;
} else {
gc->irq.irq_unmask = irqchip->irq_unmask;
irqchip->irq_unmask = gpiochip_irq_unmask;
}
}

/* gpiochip_irqchip_add_allocated_domain: 将一个已分配的域与gpio_chip关联. */
static int gpiochip_irqchip_add_allocated_domain(struct gpio_chip *gc,
struct irq_domain *domain,
bool allocated_externally)
{
if (!domain)
return -EINVAL;

/*
* 警告并覆盖驱动中可能存在的旧的、已废弃的to_irq函数指针.
* 现代驱动不应再使用它.
*/
if (gc->to_irq)
chip_warn(gc, "to_irq is redefined in %s and you shouldn't rely on it\n", __func__);

/*
* 将gpiolib自己的to_irq实现和已创建的domain关联到gpio_chip.
*/
gc->to_irq = gpiochip_to_irq;
gc->irq.domain = domain;
gc->irq.domain_is_allocated_externally = allocated_externally;

/*
* 使用内存屏障, 防止编译器将下面的initialized赋值操作重排到前面的指针赋值之前.
* 这是确保竞态条件防护有效的关键.
*/
barrier();

/*
* 最后, 设置initialized标志为true, 向外界宣告irqchip已准备就绪.
*/
gc->irq.initialized = true;

return 0;
}

gpiochip_add_irqchip: 为GPIO控制器添加中断控制器功能

此函数是Linux内核gpiolibirqchip两大子系统之间的核心桥梁。它的根本原理是将一个已经注册的GPIO控制器(gpio_chip)进一步封装和注册, 使其在内核中也扮演一个标准的中断控制器(irqchip)的角色。完成此函数的调用后, gpiolib就具备了将一个GPIO引脚号翻译成一个全局Linux IRQ号的能力(即gpio_to_irq()功能得以实现), 并且能够处理来自该引脚的中断请求。

这是一个复杂但设计精巧的注册过程, 其工作流程如下:

  1. 前提检查与配置: 函数首先进行一系列健全性检查。例如, 如果驱动使用了”链式中断处理器”(parent_handler), 那么该GPIO控制器的操作函数就绝不能休眠, 因为链式处理器通常在原子上下文中被调用。它还会警告并修正一个不推荐的做法: 在设备树系统中使用驱动硬编码的默认中断触发类型, 因为这应该由设备树来描述。

  2. 创建中断域 (irq_domain) - 核心逻辑: 这是函数最关键的一步。它会根据GPIO控制器的特性, 选择两种方式之一来创建其中断域:

    • 层次化域 (Hierarchical Domain): 这是为像STM32这样复杂的SoC设计的。在这种模型中, GPIO控制器本身并不是顶级中断控制器, 而是作为一个次级(或三级)控制器, 级联(cascaded)在另一个主中断控制器之下(例如, STM32的EXTI)。函数会调用gpiochip_hierarchy_create_domain来创建一个子域(child domain), 并将其与驱动指定的**父域(parent domain)**关联起来。
    • 简单域 (Simple Domain): 对于一些简单的硬件, GPIO控制器可能就是主中断源, 或者其级联关系非常简单。在这种情况下, 函数会创建一个独立的、非层次化的中断域。
  3. 设置链式中断处理器 (可选): 如果GPIO控制器是一个”中断解复用器”(即它自己有一条中断线连接到父中断控制器, 当其任何一个引脚中断时, 这条线都会触发), 此函数会调用irq_set_chained_handler_and_data。这个调用会将父中断控制器上的那个IRQ配置为: 当它触发时, 不去执行一个普通的中断服务程序, 而是直接调用本GPIO控制器驱动提供的parent_handler函数。这个parent_handler的职责就是去查询自己内部的寄存器, 找出到底是哪个GPIO引脚真正触发了中断。

  4. 最终注册与激活: 在创建好irq_domain并设置好所有链接后, 函数会调用gpiochip_irqchip_add_allocated_domain将这个域与gpio_chip正式绑定。从此, gpiolibirqchip两大子系统就完全关联起来了。

在STM32H750上的应用:
STM32的中断系统是典型的层次化结构:
GPIO Pin -> GPIO Bank -> EXTI Controller -> NVIC (CPU Interrupt Controller)

因此, 当STM32的GPIO驱动调用gpiochip_add_irqchip时, 总是会走”层次化域”的路径:

  • STM32的EXTI驱动会首先注册一个代表EXTI的irq_domain
  • gpiolib为GPIOA这个Bank注册irqchip时, gpiochip_add_irqchip会创建一个新的irq_domain, 并将其parent指针指向EXTI的域。
  • 当上层驱动调用gpio_to_irq()请求PA5的中断时, 这个两级域的层次结构就会被用来进行翻译, 最终返回一个由NVIC管理的、全局唯一的Linux IRQ号。
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
/*
* gpiochip_add_irqchip: 为一个GPIO chip添加一个IRQ chip功能.
* @gc: 要添加IRQ chip功能的GPIO chip.
* @lock_key: 用于锁调试的锁分类键 (IRQ锁).
* @request_key: 用于锁调试的锁分类键 (IRQ请求锁).
*
* 返回: 成功时返回0, 失败时返回负的错误码.
*/
static int gpiochip_add_irqchip(struct gpio_chip *gc,
struct lock_class_key *lock_key,
struct lock_class_key *request_key)
{
struct fwnode_handle *fwnode = dev_fwnode(&gc->gpiodev->dev);
struct irq_chip *irqchip = gc->irq.chip;
struct irq_domain *domain;
unsigned int type;
unsigned int i;
int ret;

/* 如果驱动没有提供 irqchip 结构体, 说明它不支持中断, 直接成功返回. */
if (!irqchip)
return 0;

/*
* 健全性检查: 如果驱动使用了链式中断处理器, 那么它的操作函数绝不能休眠.
* 因为链式处理器在原子上下文中被调用, 睡眠会导致系统死锁.
*/
if (gc->irq.parent_handler && gc->can_sleep) {
chip_err(gc, "you cannot have chained interrupts on a chip that may sleep\n");
return -EINVAL;
}

type = gc->irq.default_type;

/*
* 健全性检查: 在使用设备树(fwnode存在)的系统中, 不应在驱动中硬编码默认触发类型.
* 触发类型应该由设备树描述. 如果驱动这么做了, 打印警告并忽略该默认值.
*/
if (WARN(fwnode && type != IRQ_TYPE_NONE,
"%pfw: Ignoring %u default trigger\n", fwnode, type))
type = IRQ_TYPE_NONE;

gc->irq.default_type = type;
gc->irq.lock_key = lock_key;
gc->irq.request_key = request_key;

/*
* 核心逻辑: 根据驱动配置, 创建中断域.
* 如果提供了父域, 则构建一个层次化域.
*/
if (gpiochip_hierarchy_is_hierarchical(gc)) {
domain = gpiochip_hierarchy_create_domain(gc);
} else { /* 否则, 创建一个简单的、非层次化的域. */
domain = gpiochip_simple_create_domain(gc);
}
if (IS_ERR(domain))
return PTR_ERR(domain);

/* 如果驱动配置了链式中断处理器. */
if (gc->irq.parent_handler) {
/* 遍历所有父中断线. */
for (i = 0; i < gc->irq.num_parents; i++) {
void *data;

/* 获取传递给处理器的私有数据. */
if (gc->irq.per_parent_data)
data = gc->irq.parent_handler_data_array[i];
else
data = gc->irq.parent_handler_data ?: gc;

/*
* 将父IRQ配置为链式中断模式.
* 当父IRQ触发时, 会直接调用我们提供的 parent_handler 函数.
*/
irq_set_chained_handler_and_data(gc->irq.parents[i],
gc->irq.parent_handler,
data);
}
}

/* 设置gpiolib内部用于中断处理的钩子函数. */
gpiochip_set_irq_hooks(gc);

/* 将新创建的域与gpiochip正式关联起来. */
ret = gpiochip_irqchip_add_allocated_domain(gc, domain, false);
if (ret)
return ret;

/* 为ACPI平台请求中断(在非ACPI系统上此函数为空操作). */
acpi_gpiochip_request_interrupts(gc);

return 0;
}

gpiochip_setup_dev: 创建GPIO控制器的用户空间接口

此函数是gpiolib注册流程的最后一步, 也是至关重要的一步。它的核心原理是将一个已经在内核内部完全初始化好的gpio_device对象”发布”(publish)给系统的更高层和用户空间, 主要通过两种机制来完成: 注册一个字符设备(character device)创建其sysfs接口

这个函数是连接内核内部的gpiolib世界和外部的用户空间世界的桥梁。在此函数成功执行之前, GPIO控制器只存在于内核的内存中; 在此函数执行之后, 它就成为了一个用户空间工具(如libgpiodgpiodetect, gpioinfo命令)和udev/mdev系统可以看见并与之交互的实体。

工作流程详解:

  1. 设备对象初始化: 它首先调用device_initialize, 对内核的gpio_device内部嵌入的struct device对象进行标准化的最后准备。此时, 设备对象已准备就绪, 但尚未对系统可见。
  2. 字符设备注册: 这是最关键的一步。它调用gcdev_register(GPIO Character Device Register), 将该GPIO控制器注册为一个字符设备
    • 内核会从gpiolib的动态主设备号gpio_devt中为这个新设备分配一个唯一的设备号(major:minor)。
    • 这个注册操作会触发udevmdev守护进程, 在/dev/目录下自动创建一个对应的设备节点, 例如/dev/gpiochip0
    • 现代的Linux GPIO用户空间工具(基于libgpiod)就是通过open()这个字符设备节点来与内核中的GPIO控制器进行交互的, 这种方式取代了旧的、已被废弃的通过sysfs/sys/class/gpio/export接口来控制引脚的方法。
  3. Sysfs接口注册: 它接着调用gpiochip_sysfs_register, 在/sys/class/gpio/目录下创建一个名为gpiochipN (N是该控制器的ID号)的符号链接, 指向其在/sys/devices/下的真实设备目录。同时, 它还会创建一些用于描述该控制器属性的只读文件, 例如:
    • label: 包含该控制器的名称(例如 “GPIOA”)。
    • base: 该控制器在全局GPIO编号空间中的起始编号。
    • ngpio: 该控制器管理的引脚数量。
      这些sysfs文件主要用于系统状态的查看、调试和诊断。
  4. 错误处理: 函数包含了健壮的错误处理逻辑。如果在注册sysfs接口时失败, 它会跳转到err_remove_device标签, 调用gcdev_unregister撤销已经成功的字符设备注册, 从而保证了系统状态的一致性, 不会留下一个”半注册”的设备。

在STM32H750上的应用:
当STM32驱动为每一个GPIO Bank(如GPIOA, GPIOB)调用gpiochip_add_data并成功完成所有内部初始化后, gpiochip_setup_dev就会被调用。

  • 为GPIOA调用此函数后, 系统中就会出现/dev/gpiochip0设备节点和/sys/class/gpio/gpiochip0符号链接。
  • 在Linux终端中运行gpioinfo命令, 你会看到一行输出, 显示”gpiochip0 [GPIOA] 16 lines”, 这些信息就是通过读写/dev/gpiochip0和解析其sysfs属性而获得的。
  • 同样地, 为GPIOB调用后, 就会出现/dev/gpiochip1/sys/class/gpio/gpiochip1。这个过程对所有使能的GPIO Bank依次重复。
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
/*
* gpiochip_setup_dev: 为一个gpio_device设置其设备模型和用户空间接口.
*/
static int gpiochip_setup_dev(struct gpio_device *gdev)
{
/* 获取与该gpiolib设备关联的固件节点(设备树节点)句柄. */
struct fwnode_handle *fwnode = dev_fwnode(&gdev->dev);
int ret;

/*
* 对gdev内部嵌入的struct device对象进行标准初始化.
* 此时该设备对象已在内存中准备好, 但尚未注册到内核设备模型中.
*/
device_initialize(&gdev->dev);

/*
* 一个微妙的检查: 如果一个固件节点(fwnode)没有被一个主要设备所拥有
* (fwnode->dev为NULL), 那么我们可以安全地清除它的"已初始化"标志.
* 这允许我们将这个新的gpio_device作为该fwnode的主要用户接口.
*/
if (fwnode && !fwnode->dev)
fwnode_dev_initialized(fwnode, false);

/*
* 步骤1: 注册一个字符设备.
* 这使得用户空间可以通过 /dev/gpiochipN 节点与此控制器交互.
* gpio_devt 是gpiolib子系统的主设备号.
*/
ret = gcdev_register(gdev, gpio_devt);
if (ret)
return ret;

/*
* 步骤2: 注册sysfs接口.
* 这会创建 /sys/class/gpio/gpiochipN 等目录和属性文件.
*/
ret = gpiochip_sysfs_register(gdev);
if (ret)
goto err_remove_device; /* 如果失败, 跳转到错误处理路径. */

/*
* 打印一条调试信息, 宣告该GPIO控制器已成功注册.
*/
dev_dbg(&gdev->dev, "registered GPIOs %u to %u on %s\n", gdev->base,
gdev->base + gdev->ngpio - 1, gdev->label);

return 0; /* 成功 */

/* 错误处理路径 */
err_remove_device:
/*
* 撤销操作: 如果sysfs注册失败, 必须将已经成功注册的字符设备注销掉,
* 以保持系统状态的一致性.
*/
gcdev_unregister(gdev);
return ret;
}

gpiochip_add_data: 将一个GPIO控制器注册到内核

此函数是Linux内核gpiolib子系统的心脏。一个设备驱动程序(例如STM32的pinctrl驱动)在准备好一个描述其硬件能力的struct gpio_chip结构体之后, 会调用此函数, 将其正式注册并”激活”, 使其成为一个对整个内核可用的、功能完备的GPIO控制器。

它的核心原理是一个精心设计的、分阶段的”构造”过程, 它将一个驱动提供的、半成品的gpio_chip蓝图, 实例化为一个内核内部的、标准化的gpio_device对象, 并将其与内核的各大关键子系统(设备模型、中断系统、pinctrl系统、设备树)一一链接起来。

1
2
3
4
/* 这两个宏为调用者提供了更简洁的API, 它们会自动将用于高级锁分类的key参数设置为NULL. */
#define gpiochip_add_data(gc, data) gpiochip_add_data_with_key(gc, data, NULL, NULL)
#define devm_gpiochip_add_data(dev, gc, data) \
devm_gpiochip_add_data_with_key(dev, gc, data, NULL, NULL)

gpiochip_add_data_with_key: 核心注册函数

工作流程概览:

  1. 内部对象创建: 函数首先为内核创建一个内部的gpio_device结构体, 这是gpiolib核心用来管理控制器的标准容器。它还会为此控制器分配一个全局唯一的ID号(例如, 0, 1, 2…), 并生成对应的设备名(如 “gpiochip0”)。
  2. 描述符分配: 它为该控制器的每一个引脚都分配一个struct gpio_desc描述符。这是现代内核中代表单个GPIO引脚的标准方式。
  3. 全局编号空间分配: 它会为该控制器在Linux全局GPIO编号空间中分配一段连续的编号。虽然这种全局编号机制已不被推荐(现代驱动应使用描述符), 但为了兼容旧的API和sysfs接口, 这一步仍然是必需的。此函数支持动态分配(推荐, gc->base = -1)和静态分配(已废弃)两种模式。
  4. 子系统集成: 这是最关键的部分, 它像接线员一样, 将这个新创建的gpio_device连接到各个相关子系统:
    • pinctrl: 调用gpiochip_add_pin_ranges来注册GPIO编号范围。
    • 设备树(OF): 调用of_gpiochip_add来解析设备树中与GPIO相关的属性。
    • 中断(IRQ): 调用gpiochip_add_irqchip将该GPIO控制器注册为一个irqchip(中断控制器), 这使得gpio_to_irq()功能得以实现。
  5. sysfs设备创建: 最后, 它调用gpiochip_setup_dev/sys/class/gpio/目录下创建对应的gpiochipN设备节点, 使得用户空间工具(如gpiodetect, udev)可以看到并与之交互。
  6. 错误处理: 此函数拥有一个非常健壮的错误处理机制。它使用了一系列的goto标签, 如果在上述任何一个阶段失败, 程序会跳转到对应的标签, 并以与注册相反的顺序, 精确地撤销所有已经成功完成的步骤, 确保不会有任何资源泄漏或状态不一致, 保证了系统的稳定性。

在STM32H750上的应用:
stm32_gpiolib_register_bank函数为STM32的某一个GPIO Bank(例如, GPIOA)准备好其gpio_chip结构体后, 它就会调用gpiochip_add_data (通过宏)。这个调用会触发上述所有流程, 最终结果是:

  • GPIOA被注册为gpiochipN
  • 它所管理的16个引脚(PA0-PA15)在内核中都有了对应的gpio_desc
  • 它会被分配一段GPIO编号(例如, 0-15)。
  • gpio_to_irq功能被激活, 可以将PA5的中断请求转换为一个全局的Linux IRQ号。
  • 其他驱动程序从此可以通过gpio_request(5, ...)来申请并使用PA5。

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
int gpiochip_add_data_with_key(struct gpio_chip *gc, void *data,
struct lock_class_key *lock_key,
struct lock_class_key *request_key)
{
struct gpio_device *gdev; // 内核内部的GPIO设备表示
unsigned int desc_index;
int base = 0; // GPIO编号基地址
int ret;

// ... (一些合法性检查) ...
/* Only allow one set() and one set_multiple(). */
if ((gc->set && gc->set_rv) ||
(gc->set_multiple && gc->set_multiple_rv))
return -EINVAL;

/* 步骤1: 分配并填充内核内部的gpio_device结构体 */
gdev = kzalloc(sizeof(*gdev), GFP_KERNEL);
if (!gdev)
return -ENOMEM;

gdev->dev.type = &gpio_dev_type; // 设置设备类型
gdev->dev.bus = &gpio_bus_type; // 设置所属总线
gdev->dev.parent = gc->parent; // 设置父设备
rcu_assign_pointer(gdev->chip, gc); // 安全地将驱动的gpio_chip关联到内核的gpio_device

gc->gpiodev = gdev; // 反向关联, 从gpio_chip可以找到gpio_device
/* gc->gpiodev->data = data; */
gpiochip_set_data(gc, data); // 将驱动的私有数据与chip关联

device_set_node(&gdev->dev, gpiochip_choose_fwnode(gc)); // 关联设备树节点

/* 使用IDA(ID Allocator)分配一个唯一的、动态的ID号 */
ret = ida_alloc(&gpio_ida, GFP_KERNEL);
if (ret < 0)
goto err_free_gdev;
gdev->id = ret;

/* 使用ID号生成设备名, 例如 "gpiochip0" */
ret = dev_set_name(&gdev->dev, GPIOCHIP_NAME "%d", gdev->id);
if (ret)
goto err_free_ida;

// ... (设置owner模块) ...
if (gc->parent && gc->parent->driver)
gdev->owner = gc->parent->driver->owner;
else if (gc->owner)
/* TODO: remove chip->owner */
gdev->owner = gc->owner;
else
gdev->owner = THIS_MODULE;

/* 步骤2: 分配GPIO描述符数组 */
ret = gpiochip_get_ngpios(gc, &gdev->dev);
if (ret)
goto err_free_dev_name;

gdev->descs = kcalloc(gc->ngpio, sizeof(*gdev->descs), GFP_KERNEL);
if (!gdev->descs) {
ret = -ENOMEM;
goto err_free_dev_name;
}

gdev->label = kstrdup_const(gc->label ?: "unknown", GFP_KERNEL);
// ... (填充gdev的其他字段: ngpio, can_sleep等) ...

/* 步骤3: 锁定并分配全局GPIO编号空间 */
scoped_guard(mutex, &gpio_devices_lock) {
base = gc->base;
if (base < 0) { // base为-1, 请求动态分配
base = gpiochip_find_base_unlocked(gc->ngpio);
if (base < 0) {
ret = base;
goto err_free_label; // 分配失败
}
gc->base = base; // 将动态分配的基地址回写到驱动的chip结构体中
} else { // 驱动请求了静态基地址, 已不推荐
dev_warn(&gdev->dev,
"Static allocation of GPIO base is deprecated, use dynamic allocation.\n");
}

gdev->base = base; // 保存基地址

/* 将gdev添加到全局列表中, 此函数会检查编号空间是否冲突 */
ret = gpiodev_add_to_list_unlocked(gdev);
if (ret) {
chip_err(gc, "GPIO integer space overlap, cannot add chip\n");
goto err_free_label;
}
} // 锁在此处自动释放

// ... (初始化各种锁和通知链) ...
rwlock_init(&gdev->line_state_lock);
RAW_INIT_NOTIFIER_HEAD(&gdev->line_state_notifier);
BLOCKING_INIT_NOTIFIER_HEAD(&gdev->device_notifier);

ret = init_srcu_struct(&gdev->srcu);
if (ret)
goto err_remove_from_list;

ret = init_srcu_struct(&gdev->desc_srcu);
if (ret)
goto err_cleanup_gdev_srcu;

#ifdef CONFIG_PINCTRL
INIT_LIST_HEAD(&gdev->pin_ranges);
#endif

/* 步骤4: 子系统集成 */
if (gc->names) // 如果驱动提供了引脚名, 则设置它们
gpiochip_set_desc_names(gc);

// ... (初始化valid_mask等) ...
ret = gpiochip_init_valid_mask(gc);
if (ret)
goto err_cleanup_desc_srcu;

/* 遍历所有描述符, 初始化其默认方向 */
for (desc_index = 0; desc_index < gc->ngpio; desc_index++) {
struct gpio_desc *desc = &gdev->descs[desc_index];
desc->gdev = gdev;
// ... (根据驱动提供的回调函数, 设置引脚的初始输入/输出状态) ...
/*
* We would typically want to use gpiochip_get_direction() here
* but we must not check the return value and bail-out as pin
* controllers can have pins configured to alternate functions
* and return -EINVAL. Also: there's no need to take the SRCU
* lock here.
*/
if (gc->get_direction && gpiochip_line_is_valid(gc, desc_index))
assign_bit(FLAG_IS_OUT, &desc->flags,
!gc->get_direction(gc, desc_index));
else
assign_bit(FLAG_IS_OUT,
&desc->flags, !gc->direction_input);
}

/* 与设备树(OF)子系统集成 */
ret = of_gpiochip_add(gc);
if (ret)
goto err_free_valid_mask;

/* 添加pin ranges, 与pinctrl子系统集成 */
ret = gpiochip_add_pin_ranges(gc);
if (ret)
goto err_remove_of_chip;

// ... (添加ACPI和machine-specific的支持) ...
acpi_gpiochip_add(gc);
machine_gpiochip_add(gc);

/* 与中断(IRQ)子系统集成 */
ret = gpiochip_irqchip_init_valid_mask(gc); // 初始化IRQ的valid_mask
if (ret)
goto err_free_hogs;

/*
static int gpiochip_irqchip_init_hw(struct gpio_chip *gc)
{
struct gpio_irq_chip *girq = &gc->irq;

if (!girq->init_hw)
return 0;

return girq->init_hw(gc);
}
*/
ret = gpiochip_irqchip_init_hw(gc); // 初始化IRQ硬件
if (ret)
goto err_remove_irqchip_mask;

ret = gpiochip_add_irqchip(gc, lock_key, request_key); // 正式添加irqchip
if (ret)
goto err_remove_irqchip_mask;

/* 步骤5: 创建sysfs设备节点 */
if (gpiolib_initialized) {
ret = gpiochip_setup_dev(gdev);
if (ret)
goto err_remove_irqchip;
}
return 0; // 成功

/* 步骤6: 错误处理的级联清理路径 */
err_remove_irqchip:
gpiochip_irqchip_remove(gc);
err_remove_irqchip_mask:
gpiochip_irqchip_free_valid_mask(gc);
err_free_hogs:
// ... (逐层向上, 撤销所有已成功的操作) ...
err_free_gdev:
kfree(gdev);
err_print_message:
if (ret != -EPROBE_DEFER) {
pr_err("%s: GPIOs %d..%d (%s) failed to register, %d\n", __func__,
base, base + (int)gc->ngpio - 1,
gc->label ? : "generic", ret);
}
return ret;
}
EXPORT_SYMBOL_GPL(gpiochip_add_data_with_key);

gpiod_find_by_fwnode: 与固件无关的 GPIO 查找调度程序

此函数是Linux内核gpiod子系统中一个至关重要的内部调度函数。它的核心原理是充当一个抽象层, 将一个来自上层API的、基于通用固件句柄(fwnode)的GPIO查找请求, 路由到与该固件类型相匹配的、特定于技术的后端解析函数

这个函数是实现驱动程序跨平台可移植性的关键。一个编写良好的驱动程序不应该关心它所运行的系统是使用设备树(Device Tree)还是ACPI来描述硬件, 它只知道设备有一个fwnode。此函数正是负责处理这种差异的中间人。

其工作流程非常直接, 作为一个多路分发器:

  1. 接收通用句柄: 它接收一个fwnode_handle作为输入。这是一个通用的、不透明的句柄, 可以代表设备树节点、ACPI设备节点, 甚至是纯软件定义的节点。
  2. 识别句柄类型: 它使用一系列的类型检查函数 (is_of_node, is_acpi_node, is_software_node) 来确定fwnode的真实 underlying 类型。
  3. 分派到专用后端:
    • 如果fwnode是一个设备树节点, 它就调用of_find_gpioof_find_gpio是专门为设备树设计的后端, 它知道如何去解析设备树节点中的<con_id>-gpios属性(例如enable-gpios), 并将设备树的phandle和specifier转换为内核的gpio_desc
    • 如果fwnode是一个ACPI节点, 它就调用acpi_find_gpioacpi_find_gpio则知道如何去解析ACPI表中的_CRS(Current Resource Settings)资源, 找到匹配的GpioIoGpioInt条目来获取GPIO信息。
    • 如果fwnode是一个软件节点, 它就调用swnode_find_gpio, 该函数用于处理在代码中定义的、用于模拟固件描述的软件节点层次结构。
  4. 返回结果: 它将专用后端函数的返回值(一个gpio_desc指针或一个错误码)直接向上传递给调用者(gpiod_find_and_request)。如果fwnode的类型不被识别, 它会返回初始设置的默认错误码-ENOENT(“No such entity”)。

在STM32H750这样的嵌入式系统上, 固件几乎总是设备树(Device Tree)。因此, 当一个驱动程序为STM32平台上的设备请求GPIO时, gpiod_find_by_fwnode的执行路径将是通过is_of_node()检查, 最终调用of_find_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
/*
* gpiod_find_by_fwnode - 通过固件节点查找GPIO描述符
* @fwnode: 要查找的固件节点 (一个通用的句柄)
* @consumer: 请求GPIO的设备
* @con_id: GPIO的功能名称 (如 "enable", "reset")
* @idx: 在多GPIO功能中的索引
* @flags: 用于ACPI查找的GPIO标志
* @lookupflags: 用于设备树查找的GPIO标志
* @return: 成功时返回有效的gpio_desc, 失败时返回错误指针.
*/
static struct gpio_desc *gpiod_find_by_fwnode(struct fwnode_handle *fwnode,
struct device *consumer,
const char *con_id,
unsigned int idx,
enum gpiod_flags *flags,
unsigned long *lookupflags)
{
const char *name = function_name_or_default(con_id);
/*
* 初始化 desc 为 -ENOENT ("No such entity") 错误.
* 这是一个安全的默认值, 如果 fwnode 类型不匹配或查找失败, 将返回此错误.
*/
struct gpio_desc *desc = ERR_PTR(-ENOENT);

/* --- 调度逻辑开始 --- */

/* 检查 fwnode 是否是一个设备树(Open Firmware)节点? */
if (is_of_node(fwnode)) {
dev_dbg(consumer, "using DT '%pfw' for '%s' GPIO lookup\n", fwnode, name);
/*
* 如果是, 调用专门处理设备树的后端函数 of_find_gpio.
* to_of_node() 宏用于将通用的 fwnode_handle 安全地转换为 device_node 指针.
*/
desc = of_find_gpio(to_of_node(fwnode), con_id, idx, lookupflags);
} else if (is_acpi_node(fwnode)) {
/* 否则, 检查 fwnode 是否是一个ACPI节点? */
dev_dbg(consumer, "using ACPI '%pfw' for '%s' GPIO lookup\n", fwnode, name);
/*
* 如果是, 调用专门处理ACPI的后端函数 acpi_find_gpio.
*/
desc = acpi_find_gpio(fwnode, con_id, idx, flags, lookupflags);
} else if (is_software_node(fwnode)) {
/* 否则, 检查 fwnode 是否是一个软件节点? */
dev_dbg(consumer, "using swnode '%pfw' for '%s' GPIO lookup\n", fwnode, name);
/*
* 如果是, 调用专门处理软件节点的后端函数 swnode_find_gpio.
*/
desc = swnode_find_gpio(fwnode, con_id, idx, lookupflags);
}

/*
* 返回其中一个后端函数的结果, 或者返回初始的 -ENOENT 错误.
*/
return desc;
}

gpiod_add_lookup_tables: 注册GPIO查找表

此函数的核心作用是将一个或多个GPIO查找表(gpiod_lookup_table)注册到内核的全局GPIO查找列表gpio_lookup_list。这个机制是Linux内核GPIO子系统的一种**非设备树(non-Device-Tree)**的配置方法, 它允许板级支持文件(Board Support Package, BSP)以编程方式、在C代码中定义哪个设备的哪个功能性引脚(例如, “sd-power-gpio”)对应于哪个物理GPIO引脚(例如, GPIOC的第5脚)。

该函数的原理非常直接, 并且以线程安全为核心:

  1. 获取全局锁: 它首先获取一个全局互斥锁gpio_lookup_lock。这个锁保护着全局的gpio_lookup_list链表。
  2. 添加到全局链表: 在锁的保护下, 它遍历调用者传入的查找表数组, 并使用list_add_tail将每一个查找表中的list成员(一个struct list_head)添加到gpio_lookup_list链表的末尾。
  3. 释放锁: 遍历完成后, 锁被自动释放。

当系统中的某个驱动程序(消费者)稍后调用gpiod_get()来请求一个GPIO时, 内核的GPIO核心代码就会遍历这个gpio_lookup_list全局链表, 查找是否有哪个已注册的表项能够匹配该消费者设备的名称和它请求的GPIO功能名称。如果找到匹配项, 内核就能够知道要分配哪个具体的物理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
/**
* gpiod_add_lookup_tables() - 注册GPIO设备消费者
* @tables: 要注册的消费者表(gpiod_lookup_table)的指针数组
* @n: 数组中的表的数量
*/
void gpiod_add_lookup_tables(struct gpiod_lookup_table **tables, size_t n)
{
/*
* 定义一个无符号整型变量 i, 用作循环计数器.
*/
unsigned int i;

/*
* guard(mutex)(&gpio_lookup_lock);
* 这是一个现代C语言的宏, 用于实现作用域范围的锁 (scoped lock), 类似于C++的 std::lock_guard.
* 它会在进入其作用域(由花括号或下一条语句定义)时自动获取 gpio_lookup_lock 互斥锁,
* 并在退出作用域时自动释放该锁.
* gpio_lookup_lock 是一个全局互斥锁, 用于保护全局的 gpio_lookup_list 链表, 防止并发访问导致的数据损坏.
* 即使在单核抢占式系统上, 这个锁也是必需的, 以防止任务在修改链表时被抢占.
*/
guard(mutex)(&gpio_lookup_lock);

/*
* 开始一个 for 循环, 遍历调用者提供的所有查找表.
*/
for (i = 0; i < n; i++)
/*
* 调用内核标准的链表操作函数 list_add_tail.
* &tables[i]->list: 获取第 i 个 gpiod_lookup_table 结构体中的 list 成员的地址.
* 这个 list 成员是一个 struct list_head, 是链表节点.
* &gpio_lookup_list: 这是内核中用于存储所有GPIO查找表的全局链表的头节点.
*
* 整个语句的作用是: 将当前遍历到的查找表安全地添加到全局查找链表的末尾.
* 添加到尾部可以保持注册的顺序.
*/
list_add_tail(&tables[i]->list, &gpio_lookup_list);
}
/* 在这个函数中, guard(mutex) 的作用域覆盖了整个 for 循环.
* 当 for 循环结束, 函数即将返回时, 宏会自动展开代码以释放 gpio_lookup_lock 互斥锁.
*/

gpiod_find: 传统平台 GPIO 查找引擎

此函数是Linux内核gpiod子系统中负责执行基于平台查找表(platform lookup table)的GPIO查找的底层核心函数。它的核心原理是充当一个备用/回退机制, 用于那些没有使用现代固件描述(如设备树或ACPI)的系统。在这种旧式系统中, 硬件布线信息不是在设备树中描述, 而是通过C代码中的静态查找表(通常在”board file”中定义)来提供的。

gpiod_find是连接消费者驱动程序和这些静态C语言查找表之间的桥梁。

工作流程详解:

  1. 同步与安全: 函数的第一步是获取一个全局互斥锁gpio_lookup_lock。这至关重要, 因为这些静态查找表可以在系统运行时被动态地添加或移除。这个锁确保了在函数遍历查找表的过程中, 查找表本身不会被另一个任务或CPU核心并发地修改, 从而防止了竞态条件和数据损坏。

  2. 查找正确的表 (gpiod_find_lookup_table): 系统中可能存在多个查找表, 每个表可能与特定的设备或总线相关联。此函数首先会根据传入的dev参数, 找到与该消费者设备最匹配的那个查找表。

  3. 遍历与匹配: 找到正确的表之后, 函数会遍历表中的每一个条目 (struct gpiod_lookup)。对于每个条目, 它会执行精确的匹配逻辑:

    • 索引 (idx): 必须与请求的索引完全匹配。
    • 功能ID (con_id): 如果表中的条目定义了con_id, 那么请求的con_id也必须存在且完全相同。如果表中条目没有定义con_id(即为NULL), 它可以匹配任何功能名称, 这通常用于只有一个GPIO的简单设备。
  4. 两种查找方式: 匹配成功后, 它会根据表中条目的内容, 采用两种方式之一来定位GPIO:

    • 方式A: 按全局名称查找 (罕见): 如果表条目中的chip_hwnum被设置为一个特殊值U16_MAX, 这意味着条目中的key字符串不是一个GPIO芯片的标签, 而是一个全局唯一的GPIO线路名称。函数会调用gpio_name_to_desc在整个系统中搜索这个名称。
    • 方式B: 按芯片标签和硬件编号查找 (常见): 这是最主要的方式。表条目中的key字符串是GPIO控制器芯片的label(例如, "gpio-a")。函数会:
      a. 调用gpio_device_find_by_label来查找与该标签匹配的、已经注册的gpio_device
      b. 从gpio_device中获取其硬件编号chip_hwnum
      c. 进行范围检查, 确保请求的硬件编号没有超出该芯片的引脚总数。
      d. 最终从该芯片获取代表特定引脚的gpio_desc
  5. 健壮的依赖处理 (-EPROBE_DEFER): 这是此函数设计中非常关键的一点。在上述查找过程中(无论是按名称还是按标签), 如果依赖的GPIO控制器驱动程序尚未被内核探测和初始化, 那么查找就会失败。此时, gpiod_find不会返回一个硬性的”未找到”错误, 而是会返回-EPROBE_DEFER。这个特殊的返回值会通知上层调用者和内核驱动模型:”我的一个依赖项还没准备好, 请稍后重试探测我这个消费者驱动”。这是自动解决驱动加载顺序问题的核心机制。

与STM32H750的关系

对于一个使用现代设备树的STM32H750系统, gpiod_find函数通常不会被执行

  • GPIO的查找会由gpiod_find_and_request首先调用gpiod_find_by_fwnode, 然后分派到of_find_gpio来处理。
  • 只有在of_find_gpio完全没有在设备树中找到任何匹配的GPIO属性, 并且上层调用者(gpiod_find_and_request)的platform_lookup_allowed参数为true时, gpiod_find才会作为最后的手段被调用。
  • 因此, 在一个配置正确的STM32设备树系统中, gpiod_find的执行通常意味着设备树配置存在问题或不完整。
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
static struct gpio_desc *gpiod_find(struct device *dev, const char *con_id,
unsigned int idx, unsigned long *flags)
{
struct gpio_desc *desc = ERR_PTR(-ENOENT); // 默认返回 "未找到"
struct gpiod_lookup_table *table;
struct gpiod_lookup *p;
struct gpio_chip *gc;

/* 1. 获取互斥锁, 保护全局查找表 */
guard(mutex)(&gpio_lookup_lock);

/* 2. 找到与此设备关联的查找表 */
table = gpiod_find_lookup_table(dev);
if (!table)
return desc; // 没有表, 直接返回 "未找到"

/* 3. 遍历表中的每一个条目 */
for (p = &table->table[0]; p->key; p++) {
/* --- 匹配逻辑 --- */
if (p->idx != idx) // 索引必须精确匹配
continue;
if (p->con_id && (!con_id || strcmp(p->con_id, con_id))) // 功能ID必须精确匹配 (如果表中定义了的话)
continue;

/* --- 4. 两种查找方式 --- */
if (p->chip_hwnum == U16_MAX) {
/* 方式A: 按全局线路名称查找 */
desc = gpio_name_to_desc(p->key);
if (desc) {
*flags = p->flags;
return desc; // 找到了, 返回
}

/* 没找到, 可能是因为对应的GPIO驱动还没注册, 推迟探测 */
dev_warn(dev, "cannot find GPIO line %s, deferring\n",
p->key);
return ERR_PTR(-EPROBE_DEFER);
}

/* 方式B: 按芯片标签和硬件编号查找 */
struct gpio_device *gdev __free(gpio_device_put) =
gpio_device_find_by_label(p->key);
if (!gdev) {
/* GPIO芯片驱动还没注册, 推迟探测 */
dev_warn(dev, "cannot find GPIO chip %s, deferring\n",
p->key);
return ERR_PTR(-EPROBE_DEFER);
}

gc = gpio_device_get_chip(gdev);

/* 范围检查, 防止访问越界 */
if (gc->ngpio <= p->chip_hwnum) {
dev_err(dev,
"requested GPIO %u (%u) is out of range [0..%u] for chip %s\n",
idx, p->chip_hwnum, gc->ngpio - 1,
gc->label);
return ERR_PTR(-EINVAL);
}

/* 从找到的芯片中获取指定硬件编号的描述符 */
desc = gpio_device_get_desc(gdev, p->chip_hwnum);
*flags = p->flags;

return desc; // 找到了, 返回
}

/* 遍历完整个表都没找到匹配项 */
return desc;
}

gpiod_request 和 gpiod_request_commit: 安全地请求并独占一个GPIO

这两个函数协同工作, 共同构成了Linux内核gpiolib框架中用于”请求”或”声明”一个GPIO引脚的核心API。当一个设备驱动程序需要使用某个GPIO引脚时, 它必须先调用gpiod_request来获得对该引脚的独占访问权。这个过程确保了不会有多个驱动程序试图同时控制同一个物理引脚, 从而避免了硬件冲突。

gpiod_request_commit是执行实际工作的内部核心函数, 而gpiod_request则是一个安全封装, 它在调用核心函数之前处理了至关重要的模块生命周期管理。


gpiod_request_commit: 执行请求的核心逻辑

此函数负责执行所有将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
/*
* 这些"可选的"分配调用有助于防止驱动程序之间互相干扰,
* 并在debugfs中提供更好的诊断信息.
* 它们被调用的频率甚至比"设置方向"的调用还要低.
*/
/*
* 静态函数声明: gpiod_request_commit
* 这是执行GPIO请求的内部核心函数.
* @desc: 指向 struct gpio_desc 的指针, 这是代表一个GPIO引脚的核心描述符.
* @label: 一个字符串, 用于为此GPIO的使用场景提供一个描述性标签 (例如 "sd-card-detect").
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int gpiod_request_commit(struct gpio_desc *desc, const char *label)
{
/*
* 定义一个无符号整型 offset, 用于存储引脚在GPIO控制器内的硬件编号.
*/
unsigned int offset;
/*
* 定义整型变量 ret, 用于存储返回值.
*/
int ret;

/*
* CLASS(gpio_chip_guard, guard)(desc);
* 这是一个自定义宏, 用于安全地获取与此GPIO描述符关联的 gpio_chip (guard.gc).
* gpio_chip 代表了物理上的GPIO控制器硬件 (例如 STM32H7上的 GPIOC).
*/
CLASS(gpio_chip_guard, guard)(desc);
/*
* 如果无法找到关联的GPIO控制器, 返回设备未找到错误.
*/
if (!guard.gc)
return -ENODEV;

/*
* 这是确保独占性的核心操作.
* test_and_set_bit 是一个原子操作, 它会:
* 1. 测试 desc->flags 中的 FLAG_REQUESTED 位是否已经被设置.
* 2. 无论测试结果如何, 都将该位设置为1.
* 3. 返回该位在操作之前的原始值.
* 因此, 如果该引脚已经被其他驱动请求 (FLAG_REQUESTED=1), 此函数返回true,
* 我们立即返回 -EBUSY (设备或资源忙), 表示请求失败.
* 由于操作是原子的, 即使在单核抢占式系统上, 也能防止两个任务之间的竞态条件.
*/
if (test_and_set_bit(FLAG_REQUESTED, &desc->flags))
return -EBUSY;

/*
* 获取此引脚在其GPIO控制器内的硬件偏移量/编号.
*/
offset = gpio_chip_hwgpio(desc);
/*
* 检查此编号对于该控制器是否有效.
*/
if (!gpiochip_line_is_valid(guard.gc, offset))
return -EINVAL;

/*
* 注意: gpio_request() 可以在系统早期启动阶段被调用,
* 此时中断可能还未启用, 这对于非休眠的(片上SOC)GPIO是允许的.
*/

/*
* 检查底层的GPIO控制器驱动是否提供了 .request 回调函数.
*/
if (guard.gc->request) {
/*
* 如果提供了, 就调用它. 这允许硬件驱动执行任何特定于硬件的请求时设置.
* 例如, 某些硬件可能需要在这里启用引脚的数字功能.
*/
ret = guard.gc->request(guard.gc, offset);
/* 内核API约定错误码为负值. 如果驱动返回了正值, 将其标准化为-EBADE. */
if (ret > 0)
ret = -EBADE;
if (ret)
/* 如果硬件驱动的 .request 失败, 跳转到错误处理代码. */
goto out_clear_bit;
}

/*
* 如果硬件驱动提供了 .get_direction 回调, 就调用它来读取引脚的当前方向(输入/输出).
* 这可以使软件状态与硬件的实际状态同步.
*/
if (guard.gc->get_direction)
gpiod_get_direction(desc);

/*
* 调用 desc_set_label 为此GPIO设置描述性标签. 这在调试时非常有用 (例如在 /sys/kernel/debug/gpio 中).
* 如果调用者没有提供标签, 则使用 "?" 作为默认值.
*/
ret = desc_set_label(desc, label ? : "?");
if (ret)
/* 如果设置标签失败 (例如内存不足), 跳转到错误处理. */
goto out_clear_bit;

/*
* 所有步骤都成功, 返回 0.
*/
return 0;

/*
* 错误处理标签.
*/
out_clear_bit:
/*
* 清除 FLAG_REQUESTED 标志位.
* 这是至关重要的回滚操作: 因为我们在函数开头成功设置了该位,
* 所以在任何后续步骤失败时, 都必须将其清除, 以便该引脚可以被再次请求.
*/
clear_bit(FLAG_REQUESTED, &desc->flags);
/*
* 返回导致失败的错误码.
*/
return ret;
}

gpiod_request: 安全的公共API封装

此函数是在驱动程序中应该被调用的标准API。它在gpiod_request_commit的基础上, 增加了对内核模块生命周期的管理

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
/*
* gpiod_request: 请求一个GPIO描述符以供独占使用.
* @desc: 要请求的GPIO描述符.
* @label: 描述此GPIO用途的标签.
* @return: 成功时返回 0, 失败时返回错误码.
*/
int gpiod_request(struct gpio_desc *desc, const char *label)
{
/*
* 将默认返回值设置为 -EPROBE_DEFER.
* 这是一个重要的默认值. 如果try_module_get失败, 意味着GPIO控制器模块
* 可能正在卸载, 或者尚未完全准备好. 返回-EPROBE_DEFER会告诉调用者
* (通常是一个正在probe的驱动), 它的一个依赖项未就绪, 应该稍后重试.
*/
int ret = -EPROBE_DEFER;

/*
* 验证传入的 desc 指针是否有效, 防止空指针解引用.
*/
VALIDATE_DESC(desc);

/*
* 这是此封装函数的核心价值所在.
* GPIO控制器驱动本身可能是一个可加载的内核模块.
* try_module_get() 会尝试增加该模块的引用计数.
* 这可以防止内核在另一个驱动正在使用其提供的GPIO时, 将该模块卸载掉 (例如用户执行 `rmmod`).
*/
if (try_module_get(desc->gdev->owner)) {
/*
* 如果成功获取了模块的引用, 就调用核心函数来执行实际的请求操作.
*/
ret = gpiod_request_commit(desc, label);
/*
* 检查请求是否失败.
*/
if (ret)
/*
* 如果请求失败了 (例如引脚已被占用), 我们必须撤销之前对模块的引用计数增加.
* module_put() 会减少模块的引用计数.
*/
module_put(desc->gdev->owner);
else
/*
* 如果请求成功, 我们再增加 gpio_device 的引用计数.
* 这是一个更细粒度的引用, 确保gpio_device本身在被使用时不会被释放.
*/
gpio_device_get(desc->gdev);
}

/*
* 如果最终结果是错误, 打印一条调试信息.
*/
if (ret)
gpiod_dbg(desc, "%s: status %d\n", __func__, ret);

/*
* 返回最终的状态码.
*/
return ret;
}

gpiochip_* 静态函数: gpiolib 核心到硬件驱动的安全调度层

这四个静态函数是Linux gpiolib 框架内部的核心组件。它们共同构成了一个安全调度层(Safe Dispatch Layer)网关(Gateway), 其核心作用是将上层 gpiolib 的通用、硬件无关的请求, 安全地分发到下层具体的、硬件相关的gpio_chip驱动程序所实现的回调函数中

这些函数的设计原理体现了Linux内核驱动框架的几个核心思想:

  1. 抽象与封装: 上层驱动(如gpiod_direction_output)不需要知道底层硬件是STM32、NXP还是TI的芯片。它们只与通用的gpio_desc交互。而这一组gpiochip_*函数就是实现这种抽象的关键环节, 它们负责调用与gpio_desc关联的那个具体硬件驱动(gpio_chip)的实现。
  2. 健壮性与错误检查: 每一个函数都内置了关键的检查:
    • lockdep_assert_held: 这是一个锁调试断言, 确保调用者已经持有了适当的锁(在这里是SRCU读锁)。SRCU(Sleepable Read-Copy Update)是一种高级锁机制, 即使在单核系统上, 它也能确保在一个驱动正在使用某个gpio_chip时, 提供该gpio_chip的内核模块不会被中途卸载, 从而防止了悬空指针等严重问题。
    • WARN_ON: 这是一个运行时检查, 用于确保底层的gpio_chip驱动程序确实实现了它应该实现的回调函数。如果一个上层函数试图调用一个gpio_chip驱动没有提供的功能(例如, 在一个只支持输入的芯片上调用.set), 内核会打印一个警告, 这极大地帮助了驱动开发者的调试。
  3. API约定强制: 内核API约定错误码必须是负的errno值。这些函数会检查底层驱动的返回值, 如果驱动错误地返回了一个正值, 它们会将其规范化为-EBADE(错误的交换描述符), 从而保证了整个内核API的一致性。

在STM32H750的上下文中, 当gpiolib核心需要操作一个GPIO时(例如GPIOC的第5脚), gc参数就会是一个指向代表STM32 GPIOC端口的gpio_chip结构体的指针。而gc->setgc->direction_input等函数指针, 则会指向在ST的pinctrl-stm32.c驱动中实现的、真正通过读写GPIOC->MODER, GPIOC->ODR, GPIOC->BSRR等寄存器来操作硬件的函数。


gpiochip_set: 设置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
/*
* 静态函数: gpiochip_set
* 作用: 调用底层硬件驱动的 .set 回调函数, 来设置一个输出引脚的物理电平.
* @gc: 指向 struct gpio_chip 的指针, 代表硬件GPIO控制器.
* @offset: 要操作的引脚在控制器内的硬件编号 (0-15).
* @value: 要设置的物理电平 (0 或 1).
* @return: 成功时返回0, 失败时返回负的错误码.
*/
static int gpiochip_set(struct gpio_chip *gc, unsigned int offset, int value)
{
int ret;

/*
* 锁调试断言: 确保调用者已经持有了SRCU读锁.
* 这可以防止在执行此函数期间, gc 所属的 gpio_device 被释放或其驱动模块被卸载.
*/
lockdep_assert_held(&gc->gpiodev->srcu);

/*
* 检查并警告: 如果底层驱动没有实现 .set 回调函数, 这是一个驱动程序错误.
* WARN_ON 会打印一个内核警告信息, 并返回true.
* unlikely() 是一个编译器提示, 告诉编译器这个分支很少会进入, 以便进行优化.
*/
if (WARN_ON(unlikely(!gc->set)))
return -EOPNOTSUPP; // 返回 "操作不支持" 错误.

/*
* 核心调度操作: 调用 gc->set 指针所指向的函数.
* 这会执行具体硬件驱动中的代码, 真正地去写硬件寄存器 (例如STM32的ODR或BSRR).
*/
ret = gc->set(gc, offset, value);
/*
* 错误码规范化: 如果驱动错误地返回了一个正数, 将其转换为一个标准的负错误码.
*/
if (ret > 0)
ret = -EBADE;

/*
* 返回最终结果.
*/
return ret;
}

gpiochip_get_direction: 获取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
/*
* 静态函数: gpiochip_get_direction
* 作用: 调用底层硬件驱动的 .get_direction 回调函数, 来查询引脚的当前方向 (输入/输出).
* @gc: 指向 struct gpio_chip 的指针.
* @offset: 引脚的硬件编号.
* @return: 成功时返回 GPIO_LINE_DIRECTION_IN 或 GPIO_LINE_DIRECTION_OUT, 失败时返回负的错误码.
*/
static int gpiochip_get_direction(struct gpio_chip *gc, unsigned int offset)
{
int ret;

/*
* 锁调试断言: 确保SRCU读锁已被持有.
*/
lockdep_assert_held(&gc->gpiodev->srcu);

/*
* 检查并警告: 确保底层驱动实现了 .get_direction 回调.
*/
if (WARN_ON(!gc->get_direction))
return -EOPNOTSUPP;

/*
* 核心调度操作: 调用硬件驱动的函数来读取方向寄存器 (例如STM32的MODER).
*/
ret = gc->get_direction(gc, offset);
/*
* 如果驱动返回了负的错误码, 直接将其返回.
*/
if (ret < 0)
return ret;

/*
* 返回值验证: 确保驱动返回的是两个标准方向常量之一.
* 如果不是, 说明驱动实现有误.
*/
if (ret != GPIO_LINE_DIRECTION_OUT && ret != GPIO_LINE_DIRECTION_IN)
ret = -EBADE; // 将返回值修正为错误码.

return ret;
}

gpiochip_direction_input: 设置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
/*
* 静态函数: gpiochip_direction_input
* 作用: 调用底层硬件驱动的 .direction_input 回调函数, 将引脚配置为输入模式.
* @gc: 指向 struct gpio_chip 的指针.
* @offset: 引脚的硬件编号.
* @return: 成功时返回0, 失败时返回负的错误码.
*/
static int gpiochip_direction_input(struct gpio_chip *gc, unsigned int offset)
{
int ret;

/*
* 锁调试断言: 确保SRCU读锁已被持有.
*/
lockdep_assert_held(&gc->gpiodev->srcu);

/*
* 检查并警告: 确保底层驱动实现了 .direction_input 回调.
*/
if (WARN_ON(!gc->direction_input))
return -EOPNOTSUPP;

/*
* 核心调度操作: 调用硬件驱动的函数来设置方向寄存器 (例如STM32的MODER).
*/
ret = gc->direction_input(gc, offset);
/*
* 错误码规范化.
*/
if (ret > 0)
ret = -EBADE;

return ret;
}

gpiochip_direction_output: 设置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
/*
* 静态函数: gpiochip_direction_output
* 作用: 调用底层硬件驱动的 .direction_output 回调函数, 将引脚配置为输出模式并设置初始电平.
* @gc: 指向 struct gpio_chip 的指针.
* @offset: 引脚的硬件编号.
* @value: 初始的物理输出电平 (0 或 1).
* @return: 成功时返回0, 失败时返回负的错误码.
*/
static int gpiochip_direction_output(struct gpio_chip *gc, unsigned int offset,
int value)
{
int ret;

/*
* 锁调试断言: 确保SRCU读锁已被持有.
*/
lockdep_assert_held(&gc->gpiodev->srcu);

/*
* 检查并警告: 确保底层驱动实现了 .direction_output 回调.
*/
if (WARN_ON(!gc->direction_output))
return -EOPNOTSUPP;

/*
* 核心调度操作: 调用硬件驱动的函数来设置方向和初始值.
* 这通常是一个原子操作, 同时写入方向寄存器(MODER)和输出数据寄存器(ODR/BSRR).
*/
ret = gc->direction_output(gc, offset, value);
/*
* 错误码规范化.
*/
if (ret > 0)
ret = -EBADE;

return ret;
}

gpiod_direction_input及相关函数: 设置GPIO为输入模式

这一组函数是gpiod_direction_output函数的逻辑对应面, 它们共同构成了将一个GPIO引脚配置为输入模式的标准实现。同样, 它们也采用了层次化设计, 从一个简单的公共API深入到一个能够智能适应不同硬件能力的内部核心函数。

其核心原理是优先使用硬件驱动提供的专用回调函数来将引脚设置为输入, 如果专用函数不存在, 则通过查询引脚当前状态来推断其是否可用作输入, 最终在成功后更新gpiolib的内部软件状态标志并应用任何必要的偏置(如上拉/下拉电阻)


gpiod_direction_input_nonotify: 核心逻辑与硬件适配

这是执行所有实际工作的核心函数。它负责与底层gpio_chip驱动交互, 并处理了各种可能的硬件驱动实现方式。

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
/*
* gpiod_direction_input_nonotify: 设置方向为输入, 但不发送uapi通知.
* 这是执行实际工作的内部核心函数.
* @desc: GPIO描述符
* @return: 0表示成功, 负值表示错误.
*/
int gpiod_direction_input_nonotify(struct gpio_desc *desc)
{
int ret = 0, dir;

/* CLASS宏: 安全地获取与此GPIO描述符关联的gpio_chip (硬件控制器). */
CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV;

/*
* 驱动实现完整性检查 (Driver Contract Sanity Check):
* 如果一个芯片是仅输出的, 那么没有 .get() 和 .direction_input() 是合法的.
* 但是, 如果你指定了 .direction_input() 却不支持 .get() 操作, 那就说不通了.
* 因为设置一个引脚为输入的全部意义就在于能够读取它的值.
* 这是一个针对驱动程序实现逻辑一致性的检查.
*/
if (!guard.gc->get && guard.gc->direction_input) {
gpiod_warn(desc,
"%s: missing get() but have direction_input()\n",
__func__);
return -EIO;
}

/*
* --- 核心的硬件能力适配逻辑 ---
* 这是一个三层回退(fallback)策略, 以适应不同能力的硬件驱动.
*/
/*
* 路径1 (首选): 硬件驱动提供了专用的 .direction_input() 回调.
* 这是最理想、最明确的情况.
*/
if (guard.gc->direction_input) {
/*
* 调用gpiochip_direction_input, 它会安全地调度到底层驱动的实现.
* 对于STM32, 这会写入相应引脚的MODER寄存器, 将其配置为输入模式.
*/
ret = gpiochip_direction_input(guard.gc,
gpio_chip_hwgpio(desc));
} else if (guard.gc->get_direction) {
/*
* 路径2 (回退): 驱动没有专用的设置函数, 但可以查询当前方向.
* 这通常用于那些方向固定为输入, 或者方向不可更改的硬件.
*/
dir = gpiochip_get_direction(guard.gc, gpio_chip_hwgpio(desc));
if (dir < 0)
return dir; // 查询出错

/*
* 关键检查: 如果引脚当前不是输入模式, 而我们又没有办法改变它,
* 那么就必须报错.
*/
if (dir != GPIO_LINE_DIRECTION_IN) {
gpiod_warn(desc,
"%s: missing direction_input() operation and line is output\n",
__func__);
return -EIO;
}
}
/*
* 路径3 (隐式回退): 驱动既没有 .direction_input 也没有 .get_direction.
* 在这种情况下, 代码会直接 "fall through",
* 并 "默默地假设" 该引脚已经是输入模式了. 这适用于最简单的、
* 默认就是输入且不可配置的硬件.
*/
if (ret == 0) {
/*
* --- 成功后的状态更新 ---
* 只有在硬件操作成功(或被假定成功)后, 才执行以下步骤.
*/
/*
* 1. 更新软件状态: 清除 FLAG_IS_OUT 标志.
* 这是至关重要的, gpiolib 现在从软件层面知道此引脚是输入模式,
* 这会禁止后续对它调用 gpiod_set_value() 等输出操作.
*/
clear_bit(FLAG_IS_OUT, &desc->flags);
/*
* 2. 应用偏置: 调用 gpio_set_bias.
* 对于输入引脚, 设置正确的上拉或下拉电阻通常是必需的,
* 以确保在外部没有驱动时, 引脚有一个确定的默认电平.
*/
ret = gpio_set_bias(desc);
}

/* 记录追踪事件, 用于内核调试. */
trace_gpio_direction(desc_to_gpio(desc), 1, ret);

return ret;
}

gpiod_direction_input: 公共API封装

这个函数是暴露给驱动程序使用的标准顶层API。它的作用很简单: 调用核心逻辑函数, 并在成功后向用户空间发送状态变更通知。

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
/**
* gpiod_direction_input - 设置GPIO方向为输入
* @desc: 要设置为输入的GPIO
*
* 将传入的GPIO的方向设置为输入, 以便可以安全地对其调用 gpiod_get_value().
*
* 返回:
* 成功时返回0, 失败时返回负的errno.
*/
int gpiod_direction_input(struct gpio_desc *desc)
{
int ret;

/* 标准的安全检查宏, 确保desc指针有效. */
VALIDATE_DESC(desc);

/* 调用执行所有实际工作的核心函数. */
ret = gpiod_direction_input_nonotify(desc);
/*
* 如果核心函数成功返回0, 就调用gpiod_line_state_notify.
* 这个函数会通过netlink套接字发送一个事件,
* 通知用户空间(例如gpiomon等工具)这个引脚的配置已经改变.
*/
if (ret == 0)
gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG);

return ret;
}
/* 将此函数导出, 使其对其他内核模块可用. */
EXPORT_SYMBOL_GPL(gpiod_direction_input);

gpiod_direction_output及相关函数: 设置GPIO为输出模式的层次化实现

这一组函数共同构成了Linux gpiolib框架中将一个GPIO引脚配置为输出模式的完整实现。它们采用了一种层次化的设计, 从一个易于使用的高层逻辑API, 逐层深入到底层的硬件交互, 每一层都增加了特定的功能, 如安全检查、逻辑值转换、硬件能力适配和软件仿真。


gpiod_direction_output_raw_commit: 执行硬件配置的底层核心

这是整个功能链的最底层和最核心的函数。它的作用是直接与底层的gpio_chip驱动程序交互, 发出将引脚设置为输出模式并赋予初始值的硬件命令。它的原理是适配不同的硬件驱动能力, 并原子性地更新软件状态

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
/*
* 静态函数声明: gpiod_direction_output_raw_commit
* 这是设置GPIO为输出的内部核心实现.
* @desc: GPIO描述符
* @value: 要设置的初始*物理*电平 (0或1)
* @return: 0表示成功, 负值表示错误.
*/
static int gpiod_direction_output_raw_commit(struct gpio_desc *desc, int value)
{
/* val: 将value规范化为0或1. ret: 返回值. dir: 方向. */
int val = !!value, ret = 0, dir;

/* CLASS宏: 安全地获取与此GPIO描述符关联的gpio_chip (硬件控制器). */
CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV;

/*
* 关键的驱动能力检查:
* 如果一个gpiochip是仅输出的, 那么它不提供.direction_output()是可以接受的,
* 但如果它甚至连.set()操作都没有, 那么驱动输出线就非常棘手了.
* 确保驱动至少实现了这两个回调之一.
*/
if (!guard.gc->set && !guard.gc->direction_output) {
gpiod_warn(desc,
"%s: missing set() and direction_output() operations\n",
__func__);
return -EIO;
}

/*
* 优先路径: 如果硬件驱动提供了.direction_output()回调...
* 这是首选方式, 因为它允许在一个原子操作中设置方向和初始值.
*/
if (guard.gc->direction_output) {
ret = gpiochip_direction_output(guard.gc,
gpio_chip_hwgpio(desc), val);
} else {
/*
* 备用路径: 驱动没有提供组合的回调.
* 检查我们是否可以查询当前的方向.
*/
if (guard.gc->get_direction) {
dir = gpiochip_get_direction(guard.gc,
gpio_chip_hwgpio(desc));
if (dir < 0)
return dir; // 查询出错

/* 如果引脚当前不是输出模式, 我们又无法改变它, 这是一个错误. */
if (dir != GPIO_LINE_DIRECTION_OUT) {
gpiod_warn(desc,
"%s: missing direction_output() operation\n",
__func__);
return -EIO;
}
}
/*
* 如果我们不能主动设置方向, 我们就假定它是一个仅输出的芯片,
* 直接驱动输出线到期望的值.
*/
ret = gpiochip_set(guard.gc, gpio_chip_hwgpio(desc), val);
if (ret)
return ret;
}

/* 如果硬件操作成功 (ret == 0), 更新软件状态标志.
* 设置FLAG_IS_OUT, 表明此引脚现在是输出模式.
* 这个标志对于后续的gpiod_set_value()调用至关重要.
*/
if (!ret)
set_bit(FLAG_IS_OUT, &desc->flags);

/* 记录追踪事件, 用于内核调试. */
trace_gpio_value(desc_to_gpio(desc), 0, val);
trace_gpio_direction(desc_to_gpio(desc), 0, ret);
return ret;
}

gpiod_direction_output_nonotify: 逻辑层核心 (处理特殊模式和安全检查)

这个函数是整个逻辑的核心。它的作用是处理所有与软件相关的复杂性, 包括逻辑电平转换、开漏/开源模式的硬件支持或软件仿真, 以及关键的安全检查

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

int gpiod_direction_input_nonotify(struct gpio_desc *desc)
{
int ret = 0, dir;

CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV;

/*
* It is legal to have no .get() and .direction_input() specified if
* the chip is output-only, but you can't specify .direction_input()
* and not support the .get() operation, that doesn't make sense.
*/
if (!guard.gc->get && guard.gc->direction_input) {
gpiod_warn(desc,
"%s: missing get() but have direction_input()\n",
__func__);
return -EIO;
}

/*
* If we have a .direction_input() callback, things are simple,
* just call it. Else we are some input-only chip so try to check the
* direction (if .get_direction() is supported) else we silently
* assume we are in input mode after this.
*/
if (guard.gc->direction_input) {
ret = gpiochip_direction_input(guard.gc,
gpio_chip_hwgpio(desc));
} else if (guard.gc->get_direction) {
dir = gpiochip_get_direction(guard.gc, gpio_chip_hwgpio(desc));
if (dir < 0)
return dir;

if (dir != GPIO_LINE_DIRECTION_IN) {
gpiod_warn(desc,
"%s: missing direction_input() operation and line is output\n",
__func__);
return -EIO;
}
}
if (ret == 0) {
clear_bit(FLAG_IS_OUT, &desc->flags);
ret = gpio_set_bias(desc);
}

trace_gpio_direction(desc_to_gpio(desc), 1, ret);

return ret;
}

/*
* gpiod_direction_output_nonotify: 设置方向为输出, 但不发送uapi通知.
* @desc: GPIO描述符
* @value: 初始的 *逻辑* 电平 (考虑ACTIVE_LOW)
* @return: 0表示成功, 负值表示错误.
*/
int gpiod_direction_output_nonotify(struct gpio_desc *desc, int value)
{
unsigned long flags;
int ret;

/* 使用READ_ONCE安全地读取标志, 防止并发问题. */
flags = READ_ONCE(desc->flags);

/*
* 核心的逻辑到物理电平转换:
* 如果FLAG_ACTIVE_LOW被设置, 则反转逻辑值.
*/
if (test_bit(FLAG_ACTIVE_LOW, &flags))
value = !value;
else
value = !!value; /* 否则, 仅规范化为0或1. */

/*
* 关键的安全检查:
* 如果一个GPIO被用作一个已使能的中断, 绝对不能将其设置为输出模式.
* 这会造成硬件冲突.
*/
if (test_bit(FLAG_USED_AS_IRQ, &flags) &&
test_bit(FLAG_IRQ_IS_ENABLED, &flags)) {
gpiod_err(desc,
"%s: tried to set a GPIO tied to an IRQ as output\n",
__func__);
return -EIO;
}

/*
* 处理开漏(Open Drain)模式:
*/
if (test_bit(FLAG_OPEN_DRAIN, &flags)) {
/* 首先, 尝试让硬件直接支持开漏模式. */
ret = gpio_set_config(desc, PIN_CONFIG_DRIVE_OPEN_DRAIN);
if (!ret)
goto set_output_value; /* 硬件支持, 直接去设置值. */
/*
* 硬件不支持, 进行软件仿真:
* 仿真开漏时, 如果要输出高电平(value=1), 我们不能主动驱动线路,
* 而是应该将其设置为输入模式(高阻态), 依靠外部上拉电阻.
*/
if (value)
goto set_output_flag;
} else if (test_bit(FLAG_OPEN_SOURCE, &flags)) { /* 开源模式处理, 逻辑与开漏相反. */
ret = gpio_set_config(desc, PIN_CONFIG_DRIVE_OPEN_SOURCE);
if (!ret)
goto set_output_value;
/* 仿真开源时, 输出低电平(value=0)需要设置为输入模式. */
if (!value)
goto set_output_flag;
} else {
/* 标准的推挽(Push-Pull)模式, 这是一个建议性设置. */
gpio_set_config(desc, PIN_CONFIG_DRIVE_PUSH_PULL);
}

set_output_value:
/* 设置任何已配置的偏置(上下拉电阻). */
ret = gpio_set_bias(desc);
if (ret)
return ret;
/* 调用底层核心函数, 使用已转换为物理值的value来配置硬件. */
return gpiod_direction_output_raw_commit(desc, value);

set_output_flag:
/* 软件仿真路径: */
ret = gpiod_direction_input_nonotify(desc); /* 将引脚实际设置为输入模式. */
if (ret)
return ret;
/*
* 这是仿真的关键技巧:
* 当我们通过不主动驱动线路来仿真开漏/开源功能时(将模式设置为输入),
* 我们仍然需要设置IS_OUT软件标志, 否则我们将无法再设置线路的值.
*/
set_bit(FLAG_IS_OUT, &desc->flags);
return 0;
}

gpiod_direction_outputgpiod_direction_output_raw: 公共API

这两个函数是暴露给驱动程序使用的顶层API。它们非常相似, 都是简单地调用它们各自的_nonotify_commit版本, 然后在成功后发送一个通知, 通常用于更新用户空间的状态。

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
/**
* gpiod_direction_output - 设置GPIO方向为输出 (使用逻辑值)
* 这是推荐使用的标准API.
*/
int gpiod_direction_output(struct gpio_desc *desc, int value)
{
int ret;

VALIDATE_DESC(desc);

/* 调用逻辑核心函数. */
ret = gpiod_direction_output_nonotify(desc, value);
/* 如果成功, 发送配置变更通知. */
if (ret == 0)
gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG);

return ret;
}
EXPORT_SYMBOL_GPL(gpiod_direction_output);

/**
* gpiod_direction_output_raw - 设置GPIO方向为输出 (使用物理值)
* 这是一个更底层的API, 绕过了ACTIVE_LOW处理.
*/
int gpiod_direction_output_raw(struct gpio_desc *desc, int value)
{
int ret;

VALIDATE_DESC(desc);

/* 直接调用底层核心函数. */
ret = gpiod_direction_output_raw_commit(desc, value);
/* 如果成功, 发送配置变更通知. */
if (ret == 0)
gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG);

return ret;
}
EXPORT_SYMBOL_GPL(gpiod_direction_output_raw);

gpiod_set_transitory及相关函数: 配置GPIO状态的持久性

这一组函数共同实现了一个功能: 配置一个GPIO引脚的状态在系统低功耗(挂起/suspend)或复位事件中是否应该被保持(持久化, persistent)还是可以丢失(瞬态的, transitory)。这对于电源管理至关重要, 例如, 一个用于唤醒系统的引脚必须保持其状态, 而一个用于点亮LED的引脚则可以在系统睡眠时被关闭。

这个功能通过一个从高层API到底层硬件驱动调用的函数链来实现。我们将从最高层的gpiod_set_transitory开始, 逐层深入。


gpiod_set_transitory: 设置引脚状态是否为瞬态的公共API

这是驱动程序应该调用的顶层函数。它的核心原理是实现了一个两级状态管理: (1) 它无条件地更新内核gpiolib框架中关于该引脚的软件状态标志; (2) 然后, 它”尽力而为”(best-effort)地尝试将这个配置应用到底层的物理硬件上

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
/**
* gpiod_set_transitory - 在挂起或复位时丢失或保留GPIO状态
* @desc: 要为其配置持久性的GPIO的描述符
* @transitory: true表示在挂起或复位时丢失状态, false表示持久化
*
* 返回:
* 成功时返回0, 否则返回一个负的错误码.
*/
int gpiod_set_transitory(struct gpio_desc *desc, bool transitory)
{
/*
* 验证传入的 desc 指针是否有效, 这是一个防止空指针解引用的标准安全检查宏.
*/
VALIDATE_DESC(desc);
/*
* 第一步, 也是最重要的一步: 更新软件状态.
* assign_bit 是一个辅助宏, 它会根据 'transitory' 的值 (true 或 false)
* 来设置或清除 desc->flags 中的 FLAG_TRANSITORY 位.
* 这一步是无条件的, 它确保了 gpiolib 的软件层面始终知道该引脚期望的持久性策略,
* 即使底层硬件不支持配置, 其他内核代码也可以查询这个标志.
*/
assign_bit(FLAG_TRANSITORY, &desc->flags, transitory);

/*
* 第二步: 尝试将配置应用到硬件.
* 调用下一层辅助函数 gpio_set_config_with_argument_optional.
* - 第一个参数是引脚描述符.
* - 第二个参数 PIN_CONFIG_PERSIST_STATE 是一个枚举, 告诉下层函数我们要配置的是持久性.
* - 第三个参数 !transitory 是一个逻辑非操作. 这是因为下层函数期望的参数是"是否持久化",
* 而本函数的输入是"是否为瞬态", 两者逻辑相反.
*/
return gpio_set_config_with_argument_optional(desc,
PIN_CONFIG_PERSIST_STATE,
!transitory);
}

gpio_set_config_with_argument_optional: “可选地”应用配置

此函数是gpiod_set_transitory的直接辅助函数。它的核心作用是尝试应用一个配置, 但如果底层硬件明确表示”不支持”该功能, 则将其视为成功, 而不是一个错误

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
/*
* 静态函数声明: gpio_set_config_with_argument_optional
* @desc: GPIO描述符
* @mode: 要配置的参数类型 (enum pin_config_param)
* @argument: 要设置的参数值
* @return: 0表示成功或功能不支持, 其他负值表示真实错误.
*/
static int gpio_set_config_with_argument_optional(struct gpio_desc *desc,
enum pin_config_param mode,
u32 argument)
{
struct device *dev = &desc->gdev->dev;
int gpio = gpio_chip_hwgpio(desc);
int ret;

/*
* 调用下一层的函数, 实际尝试去设置配置.
*/
ret = gpio_set_config_with_argument(desc, mode, argument);
/*
* 这是本函数的关键逻辑.
* 如果返回值不是 -ENOTSUPP (不支持), 那么就直接返回这个结果.
* 这意味着, 如果操作成功(ret=0)或发生了其他真实错误(如-EINVAL), 都将结果向上传递.
*/
if (ret != -ENOTSUPP)
return ret;

/*
* 如果代码执行到这里, 意味着 ret == -ENOTSUPP, 即硬件不支持此配置.
* 函数会根据配置模式打印一条可选的调试信息.
*/
switch (mode) {
case PIN_CONFIG_PERSIST_STATE:
dev_dbg(dev, "Persistence not supported for GPIO %d\n", gpio);
break;
default:
break;
}

/*
* 最重要的一点: 即使硬件不支持, 此函数也返回 0 (成功).
* 这使得上层API gpiod_set_transitory 能够实现 "best-effort" 的行为.
*/
return 0;
}

gpio_set_config_with_argumentgpio_do_set_config: 打包并分发配置

gpio_set_config_with_argument是一个简单的转换器, 而gpio_do_set_config是最终与硬件驱动程序交互的网关。

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
static int gpio_set_config(struct gpio_desc *desc, enum pin_config_param mode)
{
return gpio_set_config_with_argument(desc, mode, 0);
}

/*
* 静态函数声明: gpio_set_config_with_argument
* 作用: 将高级的 (mode, argument) 参数打包成一个底层的 unsigned long 配置值.
*/
static int gpio_set_config_with_argument(struct gpio_desc *desc,
enum pin_config_param mode,
u32 argument)
{
unsigned long config;

/*
* 使用 pinconf_to_config_packed 宏将 mode 和 argument 打包成一个单一的 config 值.
* 这是 pinconf (引脚配置) 框架的一部分, 用于在不同子系统间传递配置.
*/
config = pinconf_to_config_packed(mode, argument);
/*
* 调用最终的执行函数.
*/
return gpio_do_set_config(desc, config);
}


/*
* gpio_do_set_config: 最终执行设置配置的函数.
* @desc: GPIO描述符
* @config: 已打包的配置值
* @return: 0表示成功, 负值表示错误.
*/
int gpio_do_set_config(struct gpio_desc *desc, unsigned long config)
{
int ret;

/*
* CLASS宏: 安全地获取与此GPIO描述符关联的 gpio_chip (硬件控制器).
*/
CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV; // 如果没有关联的控制器, 返回错误.

/*
* 检查底层硬件驱动 (gpio_chip) 是否实现了 .set_config 回调函数.
*/
if (!guard.gc->set_config)
return -ENOTSUPP; // 如果没有实现, 返回"不支持"错误.

/*
* 调用硬件驱动的 .set_config 函数, 将配置应用到物理硬件.
* gpio_chip_hwgpio(desc) 获取引脚在控制器内的硬件编号.
* 这是通用 gpiolib 框架与具体硬件驱动 (如STM32 GPIO驱动) 之间的接口点.
*/
ret = guard.gc->set_config(guard.gc, gpio_chip_hwgpio(desc), config);
if (ret > 0)
ret = -EBADE; // 标准化内核错误码 (错误码应为负值).

#ifdef CONFIG_GPIO_CDEV
/*
* 这是一个特殊情况, 用于支持通过字符设备(/dev/gpiochipX)访问GPIO.
* 如果配置的是输入去抖动, 需要将去抖周期存入描述符中, 以便用户空间可以读回它.
* WRITE_ONCE 用于并发安全的写入.
*/
if (!ret && pinconf_to_config_param(config) == PIN_CONFIG_INPUT_DEBOUNCE)
WRITE_ONCE(desc->debounce_period_us,
pinconf_to_config_argument(config));
#endif
return ret;
}

gpiod_configure_flags: 集中式GPIO配置核心辅助函数

此函数是Linux gpiolib框架内部的一个核心辅助函数。它的主要作用是提供一个统一的接口, 用于将从两个不同来源获取的配置标志——静态的板级描述(lflags)和动态的驱动程序请求(dflags)——应用到一个GPIO引脚描述符(desc)上。它负责处理引脚的所有关键电气特性(如逻辑电平、开漏/开源、上下拉)以及其工作方向(输入/输出)。

该函数的原理可以分解为两个主要阶段:

  1. 应用静态电气特性 (lflags): 函数首先处理从设备树、ACPI或板级文件中解析出的标志(lflags)。这些标志定义了引脚在特定硬件设计中的固有电气属性。

    • 逻辑电平 (Active-Low): 如果GPIO_ACTIVE_LOW被设置, 它会在此引脚的内部标志中设置FLAG_ACTIVE_LOW。这会反转该引脚的逻辑含义, 即物理低电平被软件视为”1”或”激活”, 反之亦然。
    • 输出模式 (Open-Drain/Source): 它会设置FLAG_OPEN_DRAINFLAG_OPEN_SOURCE。开漏(Open-Drain)模式意味着引脚只能主动将线路拉低至地, 或进入高阻态(浮空); 它不能主动输出高电平。这对于I2C等多主设备总线至关重要。
    • 偏置/上下拉 (Bias/Pull): 它会解析GPIO_PULL_UP, GPIO_PULL_DOWN, 或GPIO_PULL_DISABLE标志, 并在内部设置相应的FLAG_PULL_UP, FLAG_PULL_DOWN, 或FLAG_BIAS_DISABLE。在设置前, 它会执行一个关键的完整性检查, 确保设备树中没有定义相互冲突的拉电阻配置(例如, 不能同时上拉和下拉)。
    • 瞬态值 (Transitory): 它会处理GPIO_TRANSITORY标志, 表明此引脚的状态在系统睡眠/挂起期间无需被保持, 这是一种电源管理优化。
  2. 应用动态方向和初始值 (dflags): 在处理完静态标志后, 函数会检查调用者(通常是设备驱动)传入的dflags

    • 方向设置: 如果dflags中包含GPIOD_FLAGS_BIT_DIR_SET标志, 意味着驱动程序希望明确设置引脚的方向。
    • 如果GPIOD_FLAGS_BIT_DIR_OUT被设置, 它会调用gpiod_direction_output_nonotify将引脚配置为输出模式。同时, 它会检查GPIOD_FLAGS_BIT_DIR_VAL标志, 以此决定引脚的初始输出电平是高还是低。
    • 否则, 它会调用gpiod_direction_input_nonotify将引脚配置为输入模式。
    • 如果dflags包含GPIOD_FLAGS_BIT_DIR_SET, 函数在完成第一阶段后就会直接返回成功。这允许驱动程序只获取一个GPIO句柄并应用其静态电气特性, 而不立即改变其方向。

一个值得注意的细节是该函数如何处理OPEN_DRAIN的动态请求: 它允许驱动程序通过dflags来强制设置开漏模式, 但前提是lflags中没有指定。然而, 它会打印一条警告, 强调这种配置应该在设备树等板级描述中定义, 驱动程序强制指定是一种不规范的备用手段。

在STM32H750这样的系统中, 当一个设备驱动(例如I2C驱动)调用devm_gpiod_get_optional()或类似函数时, gpiolib核心最终会调用gpiod_configure_flagslflags参数会携带从STM32H750设备树中解析出的标志(如GPIO_ACTIVE_LOW), 而dflags则携带驱动自身指定的标志(如GPIOD_OUT_HIGH)。此函数随后会将这些抽象的标志转换为对底层STM32 GPIO驱动gpio_chip的回调函数(如.direction_output)的调用, 最终实现对STM32 GPIO端口的MODER, PUPDR, OTYPER, ODR等寄存器的精确配置。

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
/**
* gpiod_configure_flags - 用于配置一个给定GPIO的辅助函数
* @desc: 将被赋值的gpio描述符
* @con_id: GPIO消费者内部的功能名称
* @lflags: gpio_lookup_flags GPIO_* 值的位掩码 - 从
* of_find_gpio() 或 of_get_gpio_hog() 返回
* @dflags: gpiod_flags - 可选的GPIO初始化标志
*
* 返回:
* 成功时返回0, 如果没有GPIO被分配给请求的功能和/或索引, 返回-ENOENT,
* 或者在尝试获取GPIO时发生其他错误, 返回另一个IS_ERR()代码.
*/
int gpiod_configure_flags(struct gpio_desc *desc, const char *con_id,
unsigned long lflags, enum gpiod_flags dflags)
{
/* 获取一个用于调试打印的名称. */
const char *name = function_name_or_default(con_id);
int ret;

/* --- 阶段一: 应用 lflags (来自设备树/板级文件的静态配置) --- */

/* 如果lflags中包含GPIO_ACTIVE_LOW, 则在描述符的内部标志中设置FLAG_ACTIVE_LOW. */
if (lflags & GPIO_ACTIVE_LOW)
set_bit(FLAG_ACTIVE_LOW, &desc->flags);

/* 如果lflags中包含GPIO_OPEN_DRAIN, 则设置FLAG_OPEN_DRAIN. */
if (lflags & GPIO_OPEN_DRAIN)
set_bit(FLAG_OPEN_DRAIN, &desc->flags);
/* 否则, 如果dflags (来自驱动的请求) 要求开漏模式... */
else if (dflags & GPIOD_FLAGS_BIT_OPEN_DRAIN) {
/*
* 这是从消费者端强制设置开漏模式.
* 这对于像I2C这样的总线是必需的, 但是查找过程
* *真的*应该首先在板级文件中将它们指定为开漏,
* 所以在这里打印一个小警告.
*/
set_bit(FLAG_OPEN_DRAIN, &desc->flags);
gpiod_warn(desc,
"enforced open drain please flag it properly in DT/ACPI DSDT/board file\n");
}

/* 如果lflags中包含GPIO_OPEN_SOURCE, 则设置FLAG_OPEN_SOURCE. */
if (lflags & GPIO_OPEN_SOURCE)
set_bit(FLAG_OPEN_SOURCE, &desc->flags);

/* 完整性检查: 确保没有定义相互冲突的上下拉/偏置配置. */
if (((lflags & GPIO_PULL_UP) && (lflags & GPIO_PULL_DOWN)) ||
((lflags & GPIO_PULL_UP) && (lflags & GPIO_PULL_DISABLE)) ||
((lflags & GPIO_PULL_DOWN) && (lflags & GPIO_PULL_DISABLE))) {
gpiod_err(desc,
"multiple pull-up, pull-down or pull-disable enabled, invalid configuration\n");
return -EINVAL;
}

/* 根据lflags应用唯一的拉电阻/偏置配置. */
if (lflags & GPIO_PULL_UP)
set_bit(FLAG_PULL_UP, &desc->flags);
else if (lflags & GPIO_PULL_DOWN)
set_bit(FLAG_PULL_DOWN, &desc->flags);
else if (lflags & GPIO_PULL_DISABLE)
set_bit(FLAG_BIAS_DISABLE, &desc->flags);

/* 设置瞬态标志, 表明引脚状态在系统睡眠时无需保持. */
ret = gpiod_set_transitory(desc, (lflags & GPIO_TRANSITORY));
if (ret < 0)
return ret;

/* --- 阶段二: 应用 dflags (来自驱动程序的动态请求) --- */

/* 如果dflags中没有请求设置方向的标志, 那么工作已经完成, 在此返回... */
if (!(dflags & GPIOD_FLAGS_BIT_DIR_SET)) {
gpiod_dbg(desc, "no flags found for GPIO %s\n", name);
return 0;
}

/* 处理方向标志 */
if (dflags & GPIOD_FLAGS_BIT_DIR_OUT)
/* 如果请求输出, 则调用内部的gpiod_direction_output_nonotify函数.
* 第二个参数 !!(dflags & GPIOD_FLAGS_BIT_DIR_VAL) 是一个C语言技巧,
* 它将 GPIOD_FLAGS_BIT_DIR_VAL 标志的存在与否转换为一个干净的 1 或 0,
* 用于设置引脚的初始输出值 (高或低).
*/
ret = gpiod_direction_output_nonotify(desc,
!!(dflags & GPIOD_FLAGS_BIT_DIR_VAL));
else
/* 否则, 将引脚设置为输入. */
ret = gpiod_direction_input_nonotify(desc);

return ret;
}

gpiod_find_and_request: GPIO 获取、请求与配置的核心引擎

此函数是Linux内核现代gpiod接口的底层核心工作函数。所有上层的gpiod_get_*便利封装函数最终都会调用它来完成实际的工作。它的核心原理是执行一个完整且健壮的”查找->请求->配置”三步流程, 将一个来自消费者驱动的、基于功能的抽象GPIO请求, 转化为一个已声明所有权并正确初始化的、可供驱动程序直接使用的硬件句柄(struct gpio_desc)

这是一个高度复杂的函数, 其内部原理融合了多种内核机制:

  1. 分层查找策略 (Find): 函数首先采用现代的、基于固件(Firmware)的查找方法。

    • 首选: 设备树/ACPI (gpiod_find_by_fwnode): 它优先使用fwnode(通常是设备树节点)来查找GPIO。它会在设备树节点中寻找匹配的<con_id>-gpios属性(例如, enable-gpios), 并解析出GPIO信息。这是首选的、与硬件描述绑定的方式。
    • 备用: 平台查找 (gpiod_find): 如果基于固件的查找没有找到结果, 并且调用者允许, 它会回退到旧式的、基于平台查找表(board file)的机制。这确保了对没有使用设备树的旧平台的向后兼容性。
  2. 所有权与资源管理 (Request): 在成功找到GPIO描述符后, 最关键的一步是调用gpiod_request

    • 此调用向gpiolib核心声明:”这个GPIO引脚现在归我(由label标识的消费者)所有”。
    • 内核会将该引脚标记为”已使用”, 防止其他驱动程序无意中请求同一个引脚而导致硬件冲突。这是一个至关重要的资源管理和互斥机制。
  3. 并发安全: 整个查找和请求过程被一个scoped_guard(srcu, &gpio_devices_srcu)块包裹。

    • SRCU (Sleepable Read-Copy-Update) 是一种高级的同步机制, 用于保护被频繁读取但很少写入的数据结构, 比如系统中的GPIO控制器列表。
    • 这个锁确保了在函数查找GPIO控制器的过程中, 该控制器不会被另一个CPU核心或因抢占而运行的任务并发地从系统中注销, 从而防止了悬空指针等竞态条件的发生。即使在STM32H750这样的单核系统中, 这也能防止任务抢占和中断上下文访问带来的并发问题。
  4. 灵活的共享机制 (Non-Exclusive Access): 函数包含了对”非独占”访问的特殊处理。

    • 正常情况下, 如果一个已经被请求的GPIO再次被请求, gpiod_request会返回-EBUSY错误。
    • 但如果第二次请求时设置了GPIOD_FLAGS_BIT_NONEXCLUSIVE标志, 此函数会抑制这个-EBUSY错误。它会直接返回已存在的描述符, 但跳过后续的配置步骤。
    • 这解决了多个设备(例如两个电源调节器)共享同一个物理使能引脚的硬件设计问题, 允许它们共享同一个GPIO句柄, 并假定第一个请求者已经完成了必要的初始化配置。
  5. 最终配置与错误恢复 (Configure): 在成功声明所有权后, 函数调用gpiod_configure_flags来应用调用者请求的初始状态, 例如将引脚设置为输出低电平、配置为开漏模式等。

    • 关键的错误处理: 如果配置步骤失败, 函数会立即调用gpiod_put(与gpiod_request配对的释放函数)来撤销刚刚成功的请求操作, 将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

void gpiod_line_state_notify(struct gpio_desc *desc, unsigned long action)
{
guard(read_lock_irqsave)(&desc->gdev->line_state_lock);

raw_notifier_call_chain(&desc->gdev->line_state_notifier, action, desc);
}

struct gpio_desc *gpiod_find_and_request(struct device *consumer,
struct fwnode_handle *fwnode,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags,
const char *label,
bool platform_lookup_allowed)
{
unsigned long lookupflags = GPIO_LOOKUP_FLAGS_DEFAULT;
const char *name = function_name_or_default(con_id);
struct gpio_desc *desc = NULL;
int ret = 0;

/*
* 使用 SRCU 读侧锁保护整个查找和请求过程.
* 这确保了在遍历和查找 GPIO 控制器列表时, 列表不会被并发修改.
*/
scoped_guard(srcu, &gpio_devices_srcu) {
/* --- 阶段 1: 查找 (Find) --- */
/* 首选方法: 通过固件节点(Device Tree/ACPI)查找. */
desc = gpiod_find_by_fwnode(fwnode, consumer, con_id, idx,
&flags, &lookupflags);

/* 如果固件查找未找到, 并且允许平台回退 */
if (gpiod_not_found(desc) && platform_lookup_allowed) {
/* 备用方法: 使用旧式的平台查找表. */
dev_dbg(consumer,
"using lookup tables for GPIO lookup\n");
desc = gpiod_find(consumer, con_id, idx, &lookupflags);
}

/* 如果查找阶段返回任何错误 (除了"未找到", 因为它被回退处理了) */
if (IS_ERR(desc)) {
dev_dbg(consumer, "No GPIO consumer %s found\n", name);
return desc; // 直接返回错误.
}

/* --- 阶段 2: 请求 (Request) --- */
/* 声明对此 GPIO 的所有权. */
ret = gpiod_request(desc, label);
} /* SRCU 锁在此处自动释放 */

/* --- 处理请求结果 --- */
if (ret) {
/*
* 这是一个关键的特殊情况处理. 如果请求失败, 检查:
* 1. 失败原因是否是 -EBUSY (已被占用)?
* 2. 调用者是否请求了 GPIOD_FLAGS_BIT_NONEXCLUSIVE (非独占访问)?
* 如果以上两个条件 *不* 同时满足, 那么这是一个真正的错误.
*/
if (!(ret == -EBUSY && flags & GPIOD_FLAGS_BIT_NONEXCLUSIVE))
return ERR_PTR(ret);

/*
* 如果满足了上述两个条件, 说明这是一个合法的共享场景.
* 打印一条信息, 然后直接返回已存在的描述符, 但跳过后续的配置.
*/
dev_info(consumer, "nonexclusive access to GPIO for %s\n", name);
return desc;
}

/* --- 阶段 3: 配置 (Configure) --- */
/* 对成功请求的 GPIO 应用初始化标志 (方向, 初始值等). */
ret = gpiod_configure_flags(desc, con_id, lookupflags, flags);
if (ret < 0) {
/* 如果配置失败, 必须撤销请求! */
gpiod_put(desc);
dev_err(consumer, "setup of GPIO %s failed: %d\n", name, ret);
return ERR_PTR(ret);
}

/* 通知用户空间等监听者, 这个 GPIO line 的状态已改变 (已被请求). */
gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_REQUESTED);

/* --- 成功 --- */
/* 返回一个完全就绪的 GPIO 描述符. */
return desc;
}

gpiod_get API: 获取GPIO描述符的 layered Convenience Wrappers

此代码片段展示了Linux内核现代GPIO接口(gpiod)中三个关键的、相互关联的API函数。它们共同构成了一个层次化的便利封装体系, 用于从消费者驱动程序(consumer)的角度, 根据功能名称(如 “enable”, “reset”)来安全地请求和获取GPIO引脚的句柄(struct gpio_desc)。其核心原理是将硬件布线(在设备树中描述)与驱动程序逻辑分离, 并为处理可选和多路GPIO提供了清晰、安全的抽象


gpiod_get_index: 获取多索引GPIO的基础函数

这是三者中最基础的函数。它的核心作用是获取与某个特定功能(con_id)关联的第N个(idx)GPIO。这对于一个功能需要多个GPIO引脚的场景(例如, 一个4位的并行数据总线)是必不可少的。

原理:

  1. 它首先获取设备的固件节点(fwnode), 这在现代Linux中通常指向设备树节点。
  2. 然后, 它将所有参数(包括设备、功能名、索引、初始化标志)传递给底层的核心函数gpiod_find_and_request
  3. gpiod_find_and_request会执行以下关键操作:
    • 在设备的设备树节点中, 查找名为<con_id>-gpios的属性(例如, data-gpios)。
    • 定位到该属性列表中的第idx个条目。
    • 解析该条目, 找到它所指向的GPIO控制器和引脚号。
    • gpiolib核心请求(request)该GPIO, 这会将其标记为”已使用”, 防止其他驱动程序产生冲突。
    • 根据传入的flags, 对GPIO进行初始配置(例如, 设置为输出低电平)。
  4. 返回值至关重要:
    • 成功: 返回一个有效的gpio_desc指针。
    • 未找到: 如果设备树中没有定义对应的GPIO, 它会返回特定的错误码-ENOENT
    • 其他错误: 如果GPIO已被占用(-EBUSY)或发生其他问题, 它会返回相应的错误码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct gpio_desc *__must_check gpiod_get_index(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags)
{
// 准备 fwnode, devname, label 等参数...
struct fwnode_handle *fwnode = dev ? dev_fwnode(dev) : NULL;
const char *devname = dev ? dev_name(dev) : "?";
const char *label = con_id ?: devname;

// 将实际工作委托给底层的 gpiod_find_and_request 函数
return gpiod_find_and_request(dev, fwnode, con_id, idx, flags, label, true);
}
EXPORT_SYMBOL_GPL(gpiod_get_index);

gpiod_get_index_optional: 获取可选的多索引GPIO

这是一个基于gpiod_get_index的便利封装。它的核心作用是改变”未找到”这种情况下的返回值, 使其对驱动开发者更友好。

原理:

  1. 它直接调用gpiod_get_index来执行获取操作。
  2. 然后, 它检查返回值。它使用gpiod_not_found(desc)(内部等价于PTR_ERR(desc) == -ENOENT)来专门判断失败的原因是否是”未找到”。
  3. 如果是因为”未找到”而失败, 它会抑制这个错误, 并返回NULL
  4. 如果是因为其他原因(如-EBUSY)而失败, 它会保留并返回原始的错误指针

这个函数对于处理硬件设计上可选的GPIO引脚极为有用。驱动程序可以通过检查返回值是否为NULL来知道该可选功能是否存在, 从而调整自身行为, 而无需处理-ENOENT这个特定的错误码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct gpio_desc *__must_check gpiod_get_index_optional(struct device *dev,
const char *con_id,
unsigned int index,
enum gpiod_flags flags)
{
struct gpio_desc *desc;

// 调用基础函数
desc = gpiod_get_index(dev, con_id, index, flags);

// 如果失败的原因是"未找到"
if (gpiod_not_found(desc))
return NULL; // 则返回 NULL, 而不是错误指针

// 否则, 返回原始的描述符或错误指针
return desc;
}
EXPORT_SYMBOL_GPL(gpiod_get_index_optional);

gpiod_get_optional: 获取单个可选GPIO (最常用)

这是最顶层的、也是最常用的便利封装。它的核心作用是获取与某个功能关联的**第一个(也是唯一一个)**可选GPIO。

原理:
它是一个极简的封装, 直接调用gpiod_get_index_optional, 并将索引idx硬编码为0

在STM32H750这样的嵌入式系统中, 大多数功能(如复位、中断、使能)都只由单个GPIO引脚控制。因此, 这个函数是驱动程序中最常见的选择。例如, reg_fixed_voltage_probe中获取使能引脚时, 使用的就是这个函数。它允许设备树中可以完全不定义enable-gpios属性, 驱动程序也能正常工作(只是没有使能控制功能), 从而大大增强了硬件描述的灵活性。

1
2
3
4
5
6
7
8
struct gpio_desc *__must_check gpiod_get_optional(struct device *dev,
const char *con_id,
enum gpiod_flags flags)
{
// 直接调用索引版本, 并将索引硬编码为 0
return gpiod_get_index_optional(dev, con_id, 0, flags);
}
EXPORT_SYMBOL_GPL(gpiod_get_optional);

drivers/gpio/gpiolib-cdev.c

lineinfo_watch_poll: 等待GPIO事件

此函数实现了poll()select()epoll()系统调用的后端逻辑。它的核心原理是提供一个无阻塞的机制来查询事件是否已发生, 并在没有事件时将当前进程注册到一个等待队列上, 以便在未来事件发生时被内核唤醒

工作流程详解:

  1. 获取会话上下文: 从file->private_data中获取在open()时创建的私有会话数据cdev
  2. 设备存在性检查: 使用SRCU机制安全地检查底层的GPIO芯片是否仍然存在。如果设备已被移除, 它会返回EPOLLHUP | EPOLLERR, 通知用户空间此文件句柄已失效。
  3. 注册等待队列: 这是poll的核心。poll_wait(file, &cdev->wait, pollt)不会阻塞。它只是将当前进程的等待信息添加到cdev->wait这个等待队列头中。这相当于对内核说: “如果未来有谁唤醒了cdev->wait这个队列, 请务必唤醒我(这个正在执行poll的进程)”。
  4. 检查当前状态: 在注册完等待后, 它会立即检查事件FIFO缓冲区(cdev->events)是否已经有数据了。这个检查是在持有自旋锁的情况下进行的, 以确保与生产者(中断处理程序)的并发安全。
  5. 返回结果:
    • 如果FIFO不为空, 意味着有事件可以立即读取。函数返回EPOLLIN | EPOLLRDNORM, poll()系统调用会立即返回, 告知应用程序可以进行read()操作了。
    • 如果FIFO为空, 函数返回0。此时, poll()系统调用不会立即返回, 而是会使应用程序进入睡眠, 等待被步骤3中注册的唤醒机制唤醒。
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
/*
* lineinfo_watch_poll: poll方法实现, 用于等待线路信息变化事件.
* @file: 文件结构体指针.
* @pollt: poll表指针, 用于注册等待队列.
* @return: 一个表示事件状态的位掩码 (如 EPOLLIN).
*/
static __poll_t lineinfo_watch_poll(struct file *file,
struct poll_table_struct *pollt)
{
/* 获取本次文件打开的私有会话数据. */
struct gpio_chardev_data *cdev = file->private_data;
__poll_t events = 0;

/* 进入SRCU临界区, 安全检查gpiochip是否存在. */
guard(srcu)(&cdev->gdev->srcu);

if (!rcu_access_pointer(cdev->gdev->chip))
return EPOLLHUP | EPOLLERR; /* 设备已消失, 返回挂起和错误. */

/* 关键一步: 将当前进程注册到cdev->wait这个等待队列上. 此函数不阻塞. */
poll_wait(file, &cdev->wait, pollt);

/*
* 在持有锁的情况下检查事件FIFO是否为空.
* 这个锁与生产者(中断处理程序)使用的锁是同一个, 保证了检查和唤醒的原子性.
*/
if (!kfifo_is_empty_spinlocked_noirqsave(&cdev->events,
&cdev->wait.lock))
/* 如果FIFO不为空, 表示有数据可读. */
events = EPOLLIN | EPOLLRDNORM;

/* 返回事件掩码. 如果没有事件, 返回0, poll()系统调用会使进程睡眠. */
return events;
}

lineinfo_watch_read: 读取GPIO事件

此函数实现了read()系统调用的后端逻辑。它的核心原理是从客户端私有的FIFO缓冲区中取出一个或多个事件, 并将它们安全地复制到用户空间提供的缓冲区中。如果缓冲区为空, 它会使调用进程进入睡眠, 直到有事件被推入缓冲区并被唤醒。

工作流程详解:

  1. 获取会话上下文与安全检查: 与poll函数类似。
  2. 阻塞/非阻塞逻辑 (主循环内):
    • 加锁: 使用scoped_guard获取保护FIFO和等待队列的自旋锁。
    • 检查FIFO: 如果FIFO为空:
      • 如果之前已经读取过数据(bytes_read > 0), 则立即返回已读取的数据, 避免不必要的阻塞。
      • 如果文件是以非阻塞模式(O_NONBLOCK)打开的, 则立即返回-EAGAIN错误, 这是标准的非阻塞I/O行为。
      • 如果以上都不是, 则进入阻塞状态。wait_event_interruptible_locked是一个强大的宏, 它会自动释放锁, 将进程置于可中断的睡眠状态并加入等待队列。当被唤醒时, 它会自动重新获取锁并继续执行。
    • 出队操作: 一旦确认FIFO非空(无论是最初就不空, 还是被唤醒后), 就调用kfifo_out从FIFO中取出一个事件到内核的event变量中。
    • 解锁: scoped_guard在代码块结束时自动释放锁。
  3. API版本兼容性: #ifdef CONFIG_GPIO_CDEV_V1部分处理了新旧两套API的兼容性问题。它会检查客户端请求的ABI版本, 如果是旧版本, 它会将从FIFO中取出的新版v2事件结构体转换为旧版v1结构体, 然后再复制给用户。
  4. 复制到用户空间: copy_to_user是一个关键的、安全的内存复制函数, 它将内核空间中的event数据复制到用户空间程序提供的buf缓冲区中, 并处理可能发生的地址错误。
  5. 循环读取: do-while循环允许在用户缓冲区足够大的情况下, 一次read()调用读取多个待处理的事件, 提高了效率。

在STM32H750上的意义:
这两个函数构成了在STM32上进行高性能、事件驱动式GPIO编程的基础。一个监控按键输入的程序无需在循环中不断地轮询GPIO电平(这会浪费大量CPU周期)。取而代之的是, 它可以调用poll()让自己的进程进入睡眠。当STM32的EXTI中断被触发时, 内核中断处理程序(生产者)会将一个事件推入FIFO并唤醒该进程。进程被唤醒后, poll()返回, 进程接着调用read()来获取事件的详细信息(例如, 哪个引脚发生了什么类型的事件)。这种机制在任何现代操作系统中, 对于处理异步硬件事件都是至关重要的, 即使是在单核系统上, 它也能极大地提高系统的响应能力和能效。

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
/*
* lineinfo_watch_read: read方法实现, 用于读取线路信息变化事件.
* @file: 文件结构体指针.
* @buf: 指向用户空间缓冲区的指针.
* @count: 用户空间缓冲区的大小.
* @off: 文件偏移量指针 (此处未使用).
* @return: 成功时返回读取的字节数, 失败时返回负的errno.
*/
static ssize_t lineinfo_watch_read(struct file *file, char __user *buf,
size_t count, loff_t *off)
{
struct gpio_chardev_data *cdev = file->private_data;
struct gpio_v2_line_info_changed event; // 内核空间的事件缓冲区
ssize_t bytes_read = 0;
int ret;
size_t event_size;

guard(srcu)(&cdev->gdev->srcu);

if (!rcu_access_pointer(cdev->gdev->chip))
return -ENODEV;

#ifndef CONFIG_GPIO_CDEV_V1
/* 如果只支持v2 API, 检查用户缓冲区大小是否足够. */
event_size = sizeof(struct gpio_v2_line_info_changed);
if (count < event_size)
return -EINVAL;
#endif

/* 循环读取, 直到用户缓冲区满或FIFO为空. */
do {
/* 使用自旋锁保护对FIFO和等待队列的访问. */
scoped_guard(spinlock, &cdev->wait.lock) {
if (kfifo_is_empty(&cdev->events)) {
/* 如果已经读到一些数据, 先返回, 避免阻塞. */
if (bytes_read)
return bytes_read;

/* 如果是非阻塞模式, 立即返回EAGAIN. */
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;

/*
* 阻塞等待, 直到FIFO不再为空.
* wait_event_interruptible_locked会自动处理加锁/解锁和睡眠.
*/
ret = wait_event_interruptible_locked(cdev->wait,
!kfifo_is_empty(&cdev->events));
if (ret)
return ret;
}
#ifdef CONFIG_GPIO_CDEV_V1
/* 兼容性处理: 根据客户端请求的ABI版本确定事件大小. */
if (atomic_read(&cdev->watch_abi_version) == 2)
event_size = sizeof(struct gpio_v2_line_info_changed);
else
event_size = sizeof(struct gpioline_info_changed);
if (count < event_size)
return -EINVAL;
#endif
/* 从FIFO中取出一个事件. */
if (kfifo_out(&cdev->events, &event, 1) != 1) {
WARN(1, "failed to read from non-empty kfifo");
return -EIO;
}
}

#ifdef CONFIG_GPIO_CDEV_V1
/* 兼容性处理: 如果需要v1格式, 进行转换. */
if (event_size == sizeof(struct gpio_v2_line_info_changed)) {
if (copy_to_user(buf + bytes_read, &event, event_size))
return -EFAULT;
} else {
struct gpioline_info_changed event_v1;
gpio_v2_line_info_changed_to_v1(&event, &event_v1);
if (copy_to_user(buf + bytes_read, &event_v1, event_size))
return -EFAULT;
}
#else
/* 将内核空间的事件数据安全地复制到用户空间缓冲区. */
if (copy_to_user(buf + bytes_read, &event, event_size))
return -EFAULT;
endif
bytes_read += event_size;
} while (count >= bytes_read + sizeof(event)); /* 检查是否还有空间读取下一个事件. */

return bytes_read;
}

gpio_chrdev_open: 打开GPIO字符设备

此函数是Linux GPIO子系统字符设备接口的open方法实现。当一个用户空间程序调用open()系统调用来打开一个GPIO控制器设备文件(例如/dev/gpiochip0)时, 内核就会执行此函数。

它的核心原理是为一个新的客户端(即一个打开的文件描述符)创建一个独立的、私有的会话上下文。这个上下文(struct gpio_chardev_data)会存储该特定客户端的所有状态, 例如它正在监视哪些GPIO线、它有哪些待处理的事件等。通过这种方式, 多个不同的用户空间程序可以同时打开并操作同一个GPIO控制器设备文件, 而它们各自的会话状态互不干扰。

工作流程详解:

  1. 获取设备上下文: 函数首先通过container_of宏从VFS层传入的通用inode结构体, 反向找到代表整个GPIO控制器设备的struct gpio_device (gdev)。
  2. 并发安全检查: 它使用SRCU(一种专门用于可睡眠上下文的读-拷贝-更新同步机制)来安全地检查底层的gpio_chip是否仍然存在。这是一个至关重要的步骤, 用于防止在用户尝试打开设备的同时, 驱动程序恰好被卸载(即”热拔插”场景), 从而避免了使用无效指针导致的系统崩溃。
  3. 分配私有会话数据: 它调用kzalloc为这次open操作分配一个全新的struct gpio_chardev_data实例(cdev)。这个结构体将作为此文件句柄的私有数据存储。
  4. 初始化会话资源:
    • bitmap_zalloc: 为cdev->watched_lines分配一个位图。这个位图的大小等于该GPIO控制器拥有的引脚总数, 用于标记该客户端正在监视哪些引脚的状态变化。
    • init_waitqueue_head: 初始化一个等待队列头(cdev->wait)。当用户空间程序对此文件句柄调用poll()select()来等待事件时, 它的进程会在此等待队列上睡眠。
    • INIT_KFIFO: 初始化一个内核FIFO缓冲区(cdev->events)。当被监视的GPIO引脚上发生事件时, 内核会将事件的详细信息推入此缓冲区, 等待用户空间程序通过read()来取走。
  5. 引用计数管理: 它调用gpio_device_get(gdev)来增加gpio_device的引用计数。这是一个关键的生命周期管理操作, 它确保了只要还有任何一个用户空间程序打开着这个设备文件, gpio_device结构体就不会被内核释放, 即使底层的硬件驱动模块已经被卸载。
  6. 注册通知回调 (订阅事件): 这是实现事件驱动监控的核心。
    • 它初始化两个”通知块”(notifier_block), lineinfo_changed_nbdevice_unregistered_nb
    • 它将这两个通知块分别注册到gdevline_state_notifierdevice_notifier通知链中。这相当于为此客户端订阅了两类事件: “某个GPIO线的配置发生了变化”和”整个GPIO设备即将被注销”。当这些事件发生时, 内核会调用这里注册的回调函数(如lineinfo_changed_notify), 这些回调函数会将事件信息放入该客户端私有的FIFO缓冲区并唤醒在等待队列上睡眠的进程。
  7. 关联与完成:
    • file->private_data = cdev: 这是将内核VFS与驱动逻辑连接起来的最后一步。它将新创建的私有会话数据cdev的指针存入struct fileprivate_data字段中。之后所有对此文件句柄的操作(如ioctl, read, release)都可以通过file->private_data轻松取回这个会话上下文。
    • nonseekable_open: 调用一个辅助函数, 将此文件标记为不可寻址(seek), 这对于流式设备是标准做法。

错误处理:
该函数使用了内核中非常标准的goto标签错误处理模式。如果在初始化过程中的任何一步失败, 代码会跳转到相应的标签, 然后像瀑布一样执行所有必要的逆向清理操作(例如, 注销通知、释放位图、减少引用计数、释放内存), 从而保证在函数出错退出时不会留下任何悬挂的资源。

在STM32H750上的意义:
在STM32H750上, 当一个用户空间应用(如通过libgpiod编写的程序)执行open("/dev/gpiochip0", ...)时, 内核就会执行此函数来为该应用准备好一个与GPIOA控制器交互的通道。此后, 该应用就可以通过ioctl来配置引脚, 或通过pollread来实时监控引脚电平或边沿触发事件。write_lock_irqsave等锁机制在单核抢占式系统上依然是必需的, 它通过禁用本地中断和抢占来保护对通知链表等共享资源的访问, 防止数据结构被并发修改所破坏。

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
/*
* gpio_chrdev_open() - 为ioctl操作打开字符设备
* @inode: 此字符设备的inode
* @file: 用于存储私有数据的文件结构体
*
* 返回: 成功时返回0, 失败时返回负的errno.
*/
static int gpio_chrdev_open(struct inode *inode, struct file *file)
{
/* 从inode中内嵌的cdev成员, 反向找到其容器gpio_device结构体的地址. */
struct gpio_device *gdev = container_of(inode->i_cdev,
struct gpio_device, chrdev);
/* cdev是为本次open调用分配的私有数据. */
struct gpio_chardev_data *cdev;
int ret = -ENOMEM;

/*
* 进入一个SRCU读端临界区, 保护对gdev->chip指针的访问.
* 确保在我们检查它的时候, 它不会被并发地移除.
*/
guard(srcu)(&gdev->srcu);

/* 如果底层的gpiochip已经消失了, 则打开失败. */
if (!rcu_access_pointer(gdev->chip))
return -ENODEV;

/* 为本次打开分配私有数据结构. */
cdev = kzalloc(sizeof(*cdev), GFP_KERNEL);
if (!cdev)
return -ENODEV;

/* 分配一个位图, 用于记录此客户端监视了哪些线路. */
cdev->watched_lines = bitmap_zalloc(gdev->ngpio, GFP_KERNEL);
if (!cdev->watched_lines)
goto out_free_cdev;

/* 初始化用于poll()的等待队列和用于存储事件的FIFO缓冲区. */
init_waitqueue_head(&cdev->wait);
/* DECLARE_KFIFO(events, struct gpioevent_data, 16); */
INIT_KFIFO(cdev->events);
/* 增加gpio_device的引用计数, 防止其在我们使用期间被释放. */
cdev->gdev = gpio_device_get(gdev);

/* 准备一个通知块, 用于接收线路配置变化的通知. */
cdev->lineinfo_changed_nb.notifier_call = lineinfo_changed_notify;
/* 加锁并注册这个通知块到gdev的线路状态通知链中. */
scoped_guard(write_lock_irqsave, &gdev->line_state_lock)
ret = raw_notifier_chain_register(&gdev->line_state_notifier,
&cdev->lineinfo_changed_nb);
if (ret)
goto out_free_bitmap;

/* 准备另一个通知块, 用于接收整个设备被注销的通知. */
cdev->device_unregistered_nb.notifier_call =
gpio_device_unregistered_notify;
/* 注册这个通知块到gdev的设备通知链中. */
ret = blocking_notifier_chain_register(&gdev->device_notifier,
&cdev->device_unregistered_nb);
if (ret)
goto out_unregister_line_notifier;

/* 关键一步: 将我们新创建的私有数据cdev与文件句柄file关联起来. */
file->private_data = cdev;
cdev->fp = file;

/* 将此文件标记为不可寻址. */
ret = nonseekable_open(inode, file);
if (ret)
goto out_unregister_device_notifier;

return ret;

/* -- 错误处理回滚路径 -- */
out_unregister_device_notifier:
blocking_notifier_chain_unregister(&gdev->device_notifier,
&cdev->device_unregistered_nb);
out_unregister_line_notifier:
scoped_guard(write_lock_irqsave, &gdev->line_state_lock)
raw_notifier_chain_unregister(&gdev->line_state_notifier,
&cdev->lineinfo_changed_nb);
out_free_bitmap:
gpio_device_put(gdev);
bitmap_free(cdev->watched_lines);
out_free_cdev:
kfree(cdev);
return ret;
}

GPIO字符设备接口的注册与注销

gpio_fileops: 文件操作函数集

这是一个静态常量结构体, 它像一张”功能表”, 定义了当用户空间对本驱动创建的字符设备文件进行操作时, 内核应该调用哪些具体的函数来响应该操作。这是连接VFS和GPIO驱动功能的桥梁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 定义一个静态的、常量类型的 file_operations 结构体.
* 它将标准的文件操作映射到我们驱动中特定的处理函数.
*/
static const struct file_operations gpio_fileops = {
/* .release: 当最后一个打开此文件的进程关闭文件描述符时, 调用 gpio_chrdev_release 函数. */
.release = gpio_chrdev_release,
/* .open: 当用户空间调用 open() 系统调用打开此设备文件时, 调用 gpio_chrdev_open 函数. */
.open = gpio_chrdev_open,
/* .poll: 当用户空间对此文件描述符使用 poll() 或 select() 等待事件时, 调用 lineinfo_watch_poll. 用于监控GPIO线状态变化. */
.poll = lineinfo_watch_poll,
/* .read: 当用户空间从此文件描述符读取数据时, 调用 lineinfo_watch_read. 用于读取GPIO线状态变化事件. */
.read = lineinfo_watch_read,
/* .owner: 将这个文件操作集的所有者设置为当前模块. 这可以防止在设备仍被使用时卸载模块. */
.owner = THIS_MODULE,
/* .unlocked_ioctl: 当用户空间对此文件描述符使用 ioctl() 系统调用时, 调用 gpio_ioctl 函数. 这是主要的控制接口. */
.unlocked_ioctl = gpio_ioctl,
/* 如果内核配置支持32位程序在64位内核上运行的兼容模式. */
#ifdef CONFIG_COMPAT
/* .compat_ioctl: 为兼容模式下的ioctl调用指定一个特殊的处理函数. */
.compat_ioctl = gpio_ioctl_compat,
#endif
};

gpiolib_cdev_register: 注册GPIO字符设备

此函数负责为一个GPIO设备(gdev)执行所有必要的步骤, 来创建一个功能齐全、可供用户空间访问的字符设备。

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
/*
* gpiolib_cdev_register: 为一个gpio_device注册一个字符设备.
* @gdev: 指向要注册的gpio_device的指针.
* @devt: 包含此字符设备主设备号的dev_t类型变量.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
int gpiolib_cdev_register(struct gpio_device *gdev, dev_t devt)
{
struct gpio_chip *gc;
int ret;

/*
* 步骤1: 初始化字符设备结构体.
* 调用 cdev_init, 将 gdev 内嵌的 chrdev 结构体与我们上面定义的 gpio_fileops 功能表关联起来.
*/
cdev_init(&gdev->chrdev, &gpio_fileops);
/* 设置字符设备的所有者为当前模块, 用于引用计数管理. */
gdev->chrdev.owner = THIS_MODULE;
/*
* 步骤2: 创建最终的设备号.
* 使用 MKDEV 宏, 结合传入的主设备号(MAJOR(devt))和gdev自身的唯一ID(gdev->id)作为次设备号,
* 来生成一个完整的、唯一的设备号. 例如, (主设备号 254, 次设备号 0) -> /dev/gpiochip0.
*/
gdev->dev.devt = MKDEV(MAJOR(devt), gdev->id);

/*
* 步骤3: 分配一个工作队列.
* alloc_ordered_workqueue 创建一个保证工作项按提交顺序串行执行的工作队列.
* WQ_HIGHPRI 表示队列中的工作项具有高调度优先级.
* 这个队列用于异步处理GPIO线的状态变化事件, 避免在中断上下文中执行耗时操作.
*/
gdev->line_state_wq = alloc_ordered_workqueue("%s", WQ_HIGHPRI,
dev_name(&gdev->dev));
if (!gdev->line_state_wq)
return -ENOMEM;

/*
* 步骤4: 添加字符设备到系统, 并创建设备节点.
* cdev_device_add 是一个复合操作, 它:
* 1. 调用 cdev_add(), 使字符设备对内核VFS层"生效".
* 2. 调用 device_create(), 触发udev/mdev在/dev目录下创建对应的设备文件.
*/
ret = cdev_device_add(&gdev->chrdev, &gdev->dev);
if (ret)
return ret;

/*
* 步骤5: 安全地获取底层的 gpio_chip 指针.
* gdev->chip 指针可能被并发地修改. SRCU(Sleepable Read-Copy Update)是一种轻量级同步机制.
* guard(srcu) 宏定义了一个读端临界区.
* 在单核抢占式系统上, 它能防止在解引用 gdev->chip 时, 恰好被一个正在修改该指针的更高优先级任务抢占.
* srcu_dereference 安全地获取一个在该临界区内保证有效的 gpio_chip 指针.
*/
guard(srcu)(&gdev->srcu);
gc = srcu_dereference(gdev->chip, &gdev->srcu);
if (!gc)
return -ENODEV;

/* 打印一条调试日志, 宣告字符设备添加成功. */
chip_dbg(gc, "added GPIO chardev (%d:%d)\n", MAJOR(devt), gdev->id);

return 0;
}

gpiolib_cdev_unregister: 注销GPIO字符设备

此函数是注册函数的逆过程, 负责清理和释放所有相关资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* gpiolib_cdev_unregister: 注销一个gpio_device的字符设备.
* @gdev: 指向要注销的gpio_device的指针.
*/
void gpiolib_cdev_unregister(struct gpio_device *gdev)
{
/* 销毁之前为处理线路状态变化而分配的工作队列. */
destroy_workqueue(gdev->line_state_wq);
/*
* 从系统中移除字符设备. cdev_device_del 是一个复合操作, 它:
* 1. 调用 device_destroy(), 触发udev/mdev从/dev目录删除设备文件.
* 2. 调用 cdev_del(), 使字符设备从VFS层失效.
*/
cdev_device_del(&gdev->chrdev, &gdev->dev);
/*
* 调用阻塞通知链, 告知系统中其他可能关心此设备的模块, 该设备正在被移除.
* 这允许其他模块执行相应的清理工作.
*/
blocking_notifier_call_chain(&gdev->device_notifier, 0, NULL);
}

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);