[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
库提供了易于使用的封装。
- Sysfs接口(已废弃):早期提供了一个通过
目前该技术的社区活跃度和主流应用情况如何?
GPIO子系统是所有嵌入式Linux系统和许多服务器硬件平台的核心基础组件。它非常稳定,但仍在积极维护以支持新的硬件特性。
- 主流应用:几乎所有的嵌入式设备驱动都会用到GPIO。内核中已经包含了大量使用GPIO的子系统级驱动,例如
gpio-keys
(按键输入)、leds-gpio
(LED控制)、gpio-fan
(风扇控制)等,这些驱动为常见的硬件模式提供了标准化的内核接口。 - 社区规范:社区强烈推荐所有新代码使用基于描述符的
gpiod_*
内核API,并使用libgpiod
库与字符设备进行用户空间交互。
核心原理与设计
它的核心工作原理是什么?
GPIO子系统的核心是生产者/消费者模型,由一个通用的中间层(称为gpiolib
)进行协调。
生产者(Provider /
gpio_chip
):- 硬件GPIO控制器的驱动程序(例如,某个SoC的GPIO模块驱动)会实现一个
struct gpio_chip
。 - 这个结构体包含了一组函数指针,用于实现具体的操作,如设置方向(
.direction_input
/.direction_output
)、读值(.get
)、写值(.set
)以及请求中断等。 - 驱动通过
gpiochip_add_data()
将其实例注册到gpiolib
中。
- 硬件GPIO控制器的驱动程序(例如,某个SoC的GPIO模块驱动)会实现一个
消费者(Consumer):
- 需要使用GPIO的设备驱动程序(例如,一个Wi-Fi模块驱动需要控制电源使能引脚)被称为消费者。
- 消费者通过调用
gpiod_get()
或devm_gpiod_get()
,并提供设备指针和在设备树中定义的名字(如"enable-gpios"
),来向gpiolib
请求一个GPIO描述符。 gpiolib
根据设备树的连接关系,找到对应的gpio_chip
,并返回一个有效的描述符。
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 |
|
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
句柄。
工作流程详解:
解析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
)中。
- 它从属性中提取出
查找提供者驱动 (
of_find_gpio_device_by_xlate
): 有了指向GPIO控制器节点的phandle
, 此函数会在内核中查找是否已经有驱动程序为该节点注册了一个gpio_device
。- 这是处理驱动依赖关系的核心点: 如果GPIO控制器驱动(例如STM32的pinctrl/gpio驱动)尚未初始化, 查找就会失败。在这种情况下, 此函数会正确地返回
-EPROBE_DEFER
, 从而安全地推迟当前消费者驱动的探测, 等待依赖就绪。
- 这是处理驱动依赖关系的核心点: 如果GPIO控制器驱动(例如STM32的pinctrl/gpio驱动)尚未初始化, 查找就会失败。在这种情况下, 此函数会正确地返回
翻译与获取 (
of_xlate_and_get_gpiod_flags
): 找到GPIO控制器驱动后, 此函数会调用该驱动的of_xlate
回调函数。xlate
(translate) 的作用是将设备树中特定于硬件的参数 (如5 GPIO_ACTIVE_LOW
) “翻译”成驱动内部可以理解的本地硬件引脚号, 并解析出标准的内核GPIO标志。- 翻译完成后, 它就从该GPIO控制器驱动管理的
gpio_chip
中获取代表该特定引脚的gpio_desc
句柄。
应用”怪癖” (
of_gpio_flags_quirks
): 在基本标志解析完成后, 它会调用of_gpio_flags_quirks
函数, 对标志进行可能的修正, 以处理各种非标准的历史遗留绑定。资源管理: 在函数结束前, 它必须调用
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 | /* |
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)。
工作流程详解:
阶段一: 标准化查找 (The Standard Path)
- 此阶段遵循Linux设备树绑定的官方规范。它使用
for_each_gpio_property_name
宏, 这是一个巧妙的工具, 它会根据输入的功能名con_id
自动生成两个标准的属性名进行尝试。例如, 如果con_id
是"enable"
, 这个宏会依次生成:"enable-gpios"
(复数形式, 用于可能包含多个GPIO的功能)"enable-gpio"
(单数形式, 向后兼容)
- 对于每一个生成的属性名, 它会调用
of_get_named_gpiod_flags
。这个底层函数负责执行实际的设备树解析工作: 它在设备树节点中查找该属性, 读取其值(通常是一个指向GPIO控制器节点的phandle
和一些参数), 并返回一个初始的GPIO描述符。 - 如果
of_get_named_gpiod_flags
成功找到了GPIO(或者返回了除-ENOENT
之外的任何”真实”错误, 如-EBUSY
), 查找过程就会立即停止并进入最后阶段。
- 此阶段遵循Linux设备树绑定的官方规范。它使用
阶段二: “怪癖”查找 (The Quirk Path)
- 只有当第一阶段完全没有找到任何匹配的属性时, 才会进入此阶段。这体现了”标准优先”的原则。
- 它会遍历一个名为
of_find_gpio_quirks
的全局函数指针数组。数组中的每一个函数都是一个专门的”怪癖处理器”, 用于解决某个特定硬件平台或旧版设备树绑定不遵循标准规范的问题。 - 例如:
of_find_gpio_rename
: 可能用于处理那些使用了非标准属性名的旧绑定。of_find_mt2701_gpio
: 这是一个非常具体的例子, 专门用于处理联发科(MediaTek) MT2701 SoC上的一种特殊GPIO绑定。
- 函数会依次调用数组中的每一个怪癖处理器, 让它们尝试用自己的特殊逻辑去查找GPIO。只要其中任何一个怪癖处理器成功找到, 查找就会停止。
最后阶段: 标志转换与返回
- 在通过标准或怪癖路径成功找到GPIO描述符后, 它会调用
of_convert_gpio_flags
。这是一个重要的翻译步骤, 它将设备树中定义的标志(如OF_GPIO_ACTIVE_LOW
)转换为gpiod
子系统内部使用的通用标志(如GPIO_ACTIVE_LOW
)。 - 最终, 它返回一个包含了正确硬件信息和标志的、可供上层函数使用的
gpio_desc
指针。
- 在通过标准或怪癖路径成功找到GPIO描述符后, 它会调用
在STM32H750这样的现代嵌入式平台上, 其设备树绑定通常遵循官方标准。因此, 在绝大多数情况下, of_find_gpio
函数会在第一阶段就成功找到所需的GPIO, 而不会进入第二阶段的怪癖处理流程。然而, 这个怪癖处理机制是Linux内核保持对大量不同硬件(包括那些有历史遗留问题的硬件)的广泛兼容性的关键所在, 体现了内核设计的灵活性和向后兼容性。
1 | /* |
drivers/gpio/gpiolib.c
gpiochip_setup_dev: 为单个GPIO控制器完成设备和接口的注册
此函数的核心职责是接收一个代表GPIO控制器(如STM32的GPIOA)的内核对象gdev
,并完成以下三件关键事情:
- 初始化设备对象: 确保
gdev
中的struct device
成员被正确初始化。 - 创建字符设备: 在
/dev
目录下创建对应的gpiochipN
字符设备节点,使用户空间程序可以通过文件I/O操作来访问GPIO。 - 创建
sysfs
接口: 在/sys/class/gpio
目录下创建对应的gpiochipN
目录和属性文件,提供一种基于文件的、用于管理和调试GPIO的接口。
1 | /* |
gpiochip_setup_devs: 为已注册的GPIO控制器设置设备节点
此函数的核心作用是遍历当前系统中所有已经注册的GPIO控制器(gpio_device
),并为每一个控制器调用gpiochip_setup_dev
函数来完成其设备节点的最终设置。
1 | /* |
gpiolib_dev_init: 初始化GPIO设备库
此函数在内核启动的早期阶段被调用,其核心职责是建立Linux内核GPIO子系统的设备驱动模型框架。它本身不注册任何具体的GPIO硬件,而是搭建一个“舞台”,让后续具体的GPIO控制器驱动(如STM32的GPIO驱动)能够在这个舞台上注册自己,并以一种标准化的方式向用户空间暴露接口。
1 | /* |
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 | /* |
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_device
和gpiolib
创建的gpio_device
)。fwnode->dev
通常指向最主要的那个设备对象(即platform_device
)。这个判断的意图是: 只有当这个gpio_device
是其固件节点所代表的主要设备时, 才允许匹配。它防止了通用的”gpio总线驱动”错误地绑定到一个已经被更具体的平台驱动所拥有的硬件上, 是一种避免潜在逻辑冲突的保护机制。return 1;
表示匹配成功。return 0;
表示匹配失败。
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 | /* |
gpiochip_find_base_unlocked: 动态查找可用的GPIO编号基地址
此函数是Linux内核gpiolib
子系统中实现GPIO控制器编号动态分配的核心算法。当一个新的GPIO控制器驱动请求动态分配其GPIO编号基地址时(gpio_chip->base = -1
), gpiochip_add_data
函数会在持有锁的情况下调用此函数。它的核心原理是以一种”贪心算法”(Greedy Algorithm)的思路, 线性扫描一个全局的、已注册的GPIO设备链表, 以寻找第一个足够大的、未被占用的连续编号”空隙”。
工作流程详解:
- 初始化: 函数从一个预定义的动态分配起始点
GPIO_DYNAMIC_BASE
开始搜索。这个值通常足够大, 以避开为特殊硬件静态预留的低地址编号。 - 遍历已注册设备链表: 它使用
list_for_each_entry_srcu
宏来安全地遍历gpio_devices
这个全局链表。这个链表按照GPIO基地址从小到大的顺序维护了所有已注册的GPIO控制器。_srcu
版本的宏使用了”读-拷贝-更新”(Read-Copy-Update)的变体, 即使在遍历过程中有其他CPU在并发地修改链表(虽然此函数被调用时已持有锁, 但这个宏是通用的), 也能保证遍历的安全性。 - 寻找空隙 (核心算法): 在循环的每一步, 它会比较当前搜索的起始点
base
和正在检查的已注册设备gdev
的范围:- 找到空隙 (成功): 如果当前
gdev
的基地址 (gdev->base
) 大于或等于base + ngpio
(当前搜索点 + 需要的引脚数量), 这意味着在base
和gdev
之间有一个足够大的空隙。循环立即break
。 - 未找到空隙 (继续搜索): 如果没有找到空隙, 函数会将搜索的起始点
base
更新为当前gdev
范围的末尾之后 (base = gdev->base + gdev->ngpio;
)。然后继续下一次循环, 检查这个新的base
与下一个已注册设备之间的关系。
- 找到空隙 (成功): 如果当前
- 边界检查: 在每次更新
base
后, 都会检查新的base
是否超出了预定义的动态分配的最大范围GPIO_DYNAMIC_MAX
。如果超出, 说明不可能再找到空隙了, 循环也会break
。 - 返回结果:
- 如果循环结束后,
base
仍然在有效的动态分配范围内, 说明找到了一个可用的基地址, 函数将其返回。 - 如果超出了范围, 说明GPIO编号空间已满, 函数返回
-ENOSPC
(“设备上没有空间”)错误。
- 如果循环结束后,
为什么这个机制很重要?
在早期的Linux内核中, GPIO控制器的基地址通常由驱动程序或板级文件静态硬编码。这种方式非常容易导致冲突, 特别是在一个平台上集成了来自不同供应商的、可热插拔的模块化硬件时。动态分配机制彻底解决了这个问题。它使得内核可以像DHCP服务器分配IP地址一样, 自动地为新加入的GPIO控制器找到一个不与任何现有设备冲突的、唯一的编号范围。这对于构建可扩展、可维护的嵌入式Linux系统, 尤其是在STM32这样拥有众多GPIO端口的平台上, 是至关重要的。
1 | /* |
gpiodev_add_to_list_unlocked: 将GPIO设备插入全局排序列表
此函数是Linux内核gpiolib
子系统内部一个至关重要的列表管理函数。它的核心原理是以一种原子性的、保证排序的方式, 将一个新初始化的GPIO设备(gdev
)插入到一个全局的、按GPIO编号基地址排序的设备链表(gpio_devices
)中。在插入的同时, 它还必须严格执行冲突检测, 确保新设备的GPIO编号范围不会与任何已存在的设备发生重叠。
这个函数是GPIO控制器能够被内核动态、安全地添加和管理的基础。它的命名后缀_unlocked
是一个明确的约定, 意味着调用此函数的代码必须已经持有了保护该链表的gpio_devices_lock
锁, lockdep_assert_held
宏在函数开头就强制检查了这一前提条件。
工作流程与算法详解:
该函数的算法是一个为链表插入优化的”寻找间隙”过程:
空列表处理 (最快路径): 如果全局链表
gpio_devices
是空的, 说明这是第一个被注册的GPIO控制器。函数直接将其添加到链表尾部并成功返回。头部插入优化 (次快路径): 函数检查新设备
gdev
是否可以被完整地插入到链表的最前端。它比较gdev
的结束地址与链表中第一个设备next
的起始地址。如果gdev
的范围在next
之前且无重叠, 就将其插入到链表头部。尾部插入优化 (次快路径): 类似地, 函数检查
gdev
是否可以被完整地插入到链表的最后端。它比较gdev
的起始地址与链表中最后一个设备prev
的结束地址。如果gdev
的范围在prev
之后且无重叠, 就将其插入到链表尾部。这两种优化(头部和尾部)覆盖了系统启动时设备按顺序注册的绝大多数情况, 避免了昂贵的完整链表遍历。中间插入 (通用路径): 如果以上优化都不适用, 函数就必须遍历整个链表来寻找一个可以容纳
gdev
的”间隙”。它使用list_for_each_entry_safe
同时追踪前一个节点prev
和下一个节点next
, 并检查是否存在一个位置, 使得gdev
的范围恰好在prev
之后且在next
之前。如果找到这样的间隙, 就执行插入并成功返回。冲突检测与失败: 如果函数遍历完整个链表都没有找到任何可以插入的间隙(即不满足上述任何一个插入条件), 这就确定地意味着
gdev
的GPIO编号范围与一个或多个已存在的设备发生了重叠。在这种情况下, 函数不会执行任何插入操作, 而是直接返回-EBUSY
错误码, 明确地告知上层调用者发生了冲突。
RCU (读-拷贝-更新) 的使用:
此函数使用了list_add_rcu
和list_add_tail_rcu
。_rcu
后缀表明这个链表是受RCU机制保护的。这是一种高级的同步技术, 它允许其他代码在不获取任何锁的情况下安全地并发读取gpio_devices
链表, 极大地提高了系统的并发性能。只有在写入(添加或删除节点)时才需要获取锁。
在STM32H750上的应用:
当STM32驱动程序一个接一个地注册其GPIO Bank (GPIOA, GPIOB, GPIOC…)时, gpiochip_add_data
内部就会调用此函数。
- 注册GPIOA时, 会命中”空列表”路径。
- 注册GPIOB时, 会命中”尾部插入”路径。
- 注册GPIOC时, 同样会命中”尾部插入”路径。
…以此类推。这使得STM32众多GPIO Bank的注册过程非常高效。
1 | /* |
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 | /* gpiochip_allocate_mask: 分配一个有效掩码位图. */ |
2. 通过设备树声明无效引脚 (静态方式)
这组函数实现了通过设备树中的gpio-reserved-ranges
属性来声明无效引脚范围。
gpiochip_count_reserved_ranges
& gpiochip_apply_reserved_ranges
count
函数检查属性是否存在且格式正确, apply
函数则读取属性内容并在valid_mask
中”打孔”。
1 | /* gpiochip_count_reserved_ranges: 计算 "gpio-reserved-ranges" 属性中的条目数量. */ |
3. 有效掩码的构建与查询
这组函数是上层API, 用于协调掩码的构建过程并提供查询接口。
gpiochip_init_valid_mask
这是gpiochip_add_data
调用的主协调函数。
1 | /* gpiochip_init_valid_mask: 为一个gpio_chip初始化其有效引脚掩码. */ |
gpiochip_query_valid_mask
& gpiochip_line_is_valid
这两个函数提供了对已构建好的valid_mask
的查询能力。
1 | /* gpiochip_query_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 | /* gpiochip_add_pin_ranges: 为gpio_chip添加引脚范围. */ |
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 | /* gpiochip_hierarchy_is_hierarchical: 判断一个gpio_chip是否被配置为层次化中断. */ |
gpiochip_hierarchy_setup_domain_ops
为子域的irq_domain_ops
结构体填充一组标准的、预设的回调函数。
1 | /* gpiochip_hierarchy_setup_domain_ops: 为层次化域的操作函数集(ops)进行设置. */ |
gpiochip_hierarchy_create_domain
创建层次化中断域的主函数。
1 | /* gpiochip_hierarchy_create_domain: 为一个gpio_chip创建一个层次化的中断域(子域). */ |
2. 中断域操作回调 (irq_domain_ops
) 的实现
这组函数是gpiochip_hierarchy_setup_domain_ops
所设置的回调, 它们定义了子域的具体行为。
gpiochip_hierarchy_irq_domain_translate
负责解析来自设备树的中断请求。
1 | /* gpiochip_hierarchy_irq_domain_translate: 层次化域的translate回调. */ |
gpiochip_hierarchy_irq_domain_alloc
这是最核心的函数, 负责将一个子域的硬件中断(hwirq)映射到一个Linux IRQ, 并递归地向父域申请资源。
1 | /* gpiochip_hierarchy_irq_domain_alloc: 层次化域的alloc回调. */ |
gpiochip_irq_domain_activate
/ deactivate
在请求/释放中断时, 锁定/解锁对应的GPIO引脚, 防止其被用作普通IO。
1 | /* gpiochip_irq_domain_activate: activate回调, 锁定GPIO引脚作为IRQ. */ |
3. 其他辅助与遗留(Legacy)支持函数
1 | /* gpiochip_set_hierarchical_irqchip: (主要用于遗留系统) 设置层次化irqchip. */ |
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
, 即引脚偏移量)的完整映射关系。它负责完成所有必要的软件配置, 为即将到来的中断做好准备。
原理与工作流程:
- 获取上下文: 从中断域的私有数据
d->host_data
中获取到对应的gpio_chip
结构体, 这是所有操作的基础。 - 有效性检查: 调用
gpiochip_irqchip_irq_valid
检查hwirq
(引脚偏移)是否在该gpio_chip
的中断有效掩码内。如果一个引脚不支持中断, 则映射失败。 - 关联核心数据:
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触发时, 内核就知道应该调用哪个函数来处理它。
- 设置父IRQ (可选): 如果该GPIO控制器的中断是级联在一个父IRQ之下的(即使是在简单模型中也可能出现这种情况), 此函数会调用
irq_set_parent
来建立这种级联关系。 - 设置默认触发类型: 如果驱动程序在
gpio_chip
中指定了default_type
(如边沿触发、电平触发), 此函数会调用irq_set_irq_type
来将这个默认配置应用到硬件上。 - 配置特殊属性: 它还会设置一些额外的属性, 如
lockdep
锁分类(用于调试死锁)和nested_thread
标志(用于支持嵌套的线程化中断处理器)。
1 | /** |
gpiochip_irq_unmap: 解除一个Linux IRQ与GPIO引脚的映射
此函数是gpiochip_irq_map
的逆操作。当一个IRQ被释放时, 内核会调用此函数来清除所有在map
阶段建立的软件关联。
1 | /** |
gpiochip_irq_select: 选择一个中断控制器
此回调函数用于一个高级场景: 当一个设备在设备树中描述的中断可以由多个不同的中断控制器来提供服务时, 内核会调用.select
回调来让驱动程序判断自己是否是那个”正确”的控制器。
原理与作用:
它的主要作用是进行匹配。内核将从设备树中解析出的中断请求fwspec
传递给它, 它需要将fwspec
中的信息与自身irq_domain
的信息进行比较。
- 对于使用设备树的现代系统, 它可能会调用
of_gpiochip_instance_match
来进行更复杂的匹配。 - 对于简单的、非层次化的域, 它主要比较
fwspec
中的固件节点(fwnode
)是否与自身域的固件节点(d->fwnode
)相同, 并可能比较总线令牌(bus_token
)。如果匹配, 就返回true
, 表示”这个中断请求是给我的”。
1 | /** |
gpiochip_domain_ops: 简单域的操作函数集
这是一个静态的irq_domain_ops
结构体实例, 它为所有通过”简单模型”创建的GPIO中断域提供了一套标准的、通用的操作函数集。它就像是中断域的”行为手册”, 告诉内核在对该域进行各种操作时应该调用哪些函数。
1 | /* |
gpiochip_simple_create_domain: 创建一个简单中断域
这是一个内部函数, 它封装了内核通用的irq_domain_create_simple
函数, 为GPIO控制器提供了一个便捷的创建接口。
1 | /* |
gpiolib
中断资源管理与使能/禁用函数
此代码片段展示了Linux内核gpiolib
子系统中用于管理GPIO引脚作为中断资源的一组核心函数。这组函数与irqchip
子系统紧密协作, 其根本原理是在一个GPIO引脚被用作中断源的整个生命周期中, 对其进行状态跟踪和访问控制。这包括:
- 锁定/解锁: 当引脚被请求为中断时, 将其”锁定”, 防止它同时被用作普通的输入/输出引脚, 从而避免了功能冲突。
- 模块引用计数: 确保提供GPIO控制器功能的内核模块在使用期间不会被意外卸载。
- 使能/禁用状态跟踪: 在引脚的描述符中维护一个软件状态位, 记录该中断是处于使能还是禁用状态。
gpiochip_lock_as_irq: 将GPIO引脚锁定为中断模式
当一个驱动程序请求一个GPIO引脚作为中断源时(通常在request_irq
的调用链深处), irqchip
子系统的request_resources
回调函数(即gpiolib
的gpiochip_reqres_irq
)最终会调用此函数。
1 | /** |
gpiochip_unlock_as_irq: 解锁一个用作中断的GPIO引脚
当一个驱动程序释放一个中断时(在free_irq
的调用链中), irqchip
子系统的release_resources
回调函数(即gpiolib
的gpiochip_relres_irq
)会调用此函数。
1 | /** |
gpiochip_reqres_irq / gpiochip_relres_irq: 请求/释放中断资源
这两个函数是irqchip
子系统的request_resources
和release_resources
回调函数的标准实现。它们负责一个GPIO引脚作为中断源的”获取”和”释放”操作。
1 | /** |
gpiochip_enable_irq / gpiochip_disable_irq: 使能/禁用中断(软件状态)
这两个函数通常被irq_chip
的mask
/unmask
回调函数的包装器所调用。它们只负责在gpiolib
的软件层面更新中断的使能状态, 而不直接操作硬件。硬件的屏蔽/解屏蔽操作由irq_chip
回调本身负责。
1 | /** |
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. irq_chip
回调函数的包装器
gpiochip_set_irq_hooks
函数通过巧妙的指针交换, 将gpiolib
自己的通用逻辑”注入”到驱动提供的irq_chip
回调中。下面是被注入的包装器函数, 它们在调用驱动原始的回调函数的同时, 执行了gpiolib
的通用操作。
1 | /* gpiochip_irq_reqres: "request_resources"回调的包装器. */ |
3. irqchip
的安装与初始化
这组函数负责将上述所有部分组装起来, 完成irqchip
在gpiolib
中的注册。
1 | /* gpiochip_set_irq_hooks: 为gpio_chip的irqchip安装包装器钩子. */ |
gpiochip_add_irqchip: 为GPIO控制器添加中断控制器功能
此函数是Linux内核gpiolib
和irqchip
两大子系统之间的核心桥梁。它的根本原理是将一个已经注册的GPIO控制器(gpio_chip
)进一步封装和注册, 使其在内核中也扮演一个标准的中断控制器(irqchip
)的角色。完成此函数的调用后, gpiolib
就具备了将一个GPIO引脚号翻译成一个全局Linux IRQ号的能力(即gpio_to_irq()
功能得以实现), 并且能够处理来自该引脚的中断请求。
这是一个复杂但设计精巧的注册过程, 其工作流程如下:
前提检查与配置: 函数首先进行一系列健全性检查。例如, 如果驱动使用了”链式中断处理器”(
parent_handler
), 那么该GPIO控制器的操作函数就绝不能休眠, 因为链式处理器通常在原子上下文中被调用。它还会警告并修正一个不推荐的做法: 在设备树系统中使用驱动硬编码的默认中断触发类型, 因为这应该由设备树来描述。创建中断域 (
irq_domain
) - 核心逻辑: 这是函数最关键的一步。它会根据GPIO控制器的特性, 选择两种方式之一来创建其中断域:- 层次化域 (Hierarchical Domain): 这是为像STM32这样复杂的SoC设计的。在这种模型中, GPIO控制器本身并不是顶级中断控制器, 而是作为一个次级(或三级)控制器, 级联(cascaded)在另一个主中断控制器之下(例如, STM32的EXTI)。函数会调用
gpiochip_hierarchy_create_domain
来创建一个子域(child domain), 并将其与驱动指定的**父域(parent domain)**关联起来。 - 简单域 (Simple Domain): 对于一些简单的硬件, GPIO控制器可能就是主中断源, 或者其级联关系非常简单。在这种情况下, 函数会创建一个独立的、非层次化的中断域。
- 层次化域 (Hierarchical Domain): 这是为像STM32这样复杂的SoC设计的。在这种模型中, GPIO控制器本身并不是顶级中断控制器, 而是作为一个次级(或三级)控制器, 级联(cascaded)在另一个主中断控制器之下(例如, STM32的EXTI)。函数会调用
设置链式中断处理器 (可选): 如果GPIO控制器是一个”中断解复用器”(即它自己有一条中断线连接到父中断控制器, 当其任何一个引脚中断时, 这条线都会触发), 此函数会调用
irq_set_chained_handler_and_data
。这个调用会将父中断控制器上的那个IRQ配置为: 当它触发时, 不去执行一个普通的中断服务程序, 而是直接调用本GPIO控制器驱动提供的parent_handler
函数。这个parent_handler
的职责就是去查询自己内部的寄存器, 找出到底是哪个GPIO引脚真正触发了中断。最终注册与激活: 在创建好
irq_domain
并设置好所有链接后, 函数会调用gpiochip_irqchip_add_allocated_domain
将这个域与gpio_chip
正式绑定。从此,gpiolib
和irqchip
两大子系统就完全关联起来了。
在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 | /* |
gpiochip_setup_dev: 创建GPIO控制器的用户空间接口
此函数是gpiolib
注册流程的最后一步, 也是至关重要的一步。它的核心原理是将一个已经在内核内部完全初始化好的gpio_device
对象”发布”(publish)给系统的更高层和用户空间, 主要通过两种机制来完成: 注册一个字符设备(character device) 和 创建其sysfs接口。
这个函数是连接内核内部的gpiolib
世界和外部的用户空间世界的桥梁。在此函数成功执行之前, GPIO控制器只存在于内核的内存中; 在此函数执行之后, 它就成为了一个用户空间工具(如libgpiod
的gpiodetect
, gpioinfo
命令)和udev
/mdev
系统可以看见并与之交互的实体。
工作流程详解:
- 设备对象初始化: 它首先调用
device_initialize
, 对内核的gpio_device
内部嵌入的struct device
对象进行标准化的最后准备。此时, 设备对象已准备就绪, 但尚未对系统可见。 - 字符设备注册: 这是最关键的一步。它调用
gcdev_register
(GPIO Character Device Register), 将该GPIO控制器注册为一个字符设备。- 内核会从
gpiolib
的动态主设备号gpio_devt
中为这个新设备分配一个唯一的设备号(major:minor)。 - 这个注册操作会触发
udev
或mdev
守护进程, 在/dev/
目录下自动创建一个对应的设备节点, 例如/dev/gpiochip0
。 - 现代的Linux GPIO用户空间工具(基于
libgpiod
)就是通过open()
这个字符设备节点来与内核中的GPIO控制器进行交互的, 这种方式取代了旧的、已被废弃的通过sysfs
的/sys/class/gpio/export
接口来控制引脚的方法。
- 内核会从
- Sysfs接口注册: 它接着调用
gpiochip_sysfs_register
, 在/sys/class/gpio/
目录下创建一个名为gpiochipN
(N是该控制器的ID号)的符号链接, 指向其在/sys/devices/
下的真实设备目录。同时, 它还会创建一些用于描述该控制器属性的只读文件, 例如:label
: 包含该控制器的名称(例如 “GPIOA”)。base
: 该控制器在全局GPIO编号空间中的起始编号。ngpio
: 该控制器管理的引脚数量。
这些sysfs
文件主要用于系统状态的查看、调试和诊断。
- 错误处理: 函数包含了健壮的错误处理逻辑。如果在注册
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 | /* |
gpiochip_add_data: 将一个GPIO控制器注册到内核
此函数是Linux内核gpiolib
子系统的心脏。一个设备驱动程序(例如STM32的pinctrl驱动)在准备好一个描述其硬件能力的struct gpio_chip
结构体之后, 会调用此函数, 将其正式注册并”激活”, 使其成为一个对整个内核可用的、功能完备的GPIO控制器。
它的核心原理是一个精心设计的、分阶段的”构造”过程, 它将一个驱动提供的、半成品的gpio_chip
蓝图, 实例化为一个内核内部的、标准化的gpio_device
对象, 并将其与内核的各大关键子系统(设备模型、中断系统、pinctrl系统、设备树)一一链接起来。
1 | /* 这两个宏为调用者提供了更简洁的API, 它们会自动将用于高级锁分类的key参数设置为NULL. */ |
gpiochip_add_data_with_key
: 核心注册函数
工作流程概览:
- 内部对象创建: 函数首先为内核创建一个内部的
gpio_device
结构体, 这是gpiolib
核心用来管理控制器的标准容器。它还会为此控制器分配一个全局唯一的ID号(例如, 0, 1, 2…), 并生成对应的设备名(如 “gpiochip0”)。 - 描述符分配: 它为该控制器的每一个引脚都分配一个
struct gpio_desc
描述符。这是现代内核中代表单个GPIO引脚的标准方式。 - 全局编号空间分配: 它会为该控制器在Linux全局GPIO编号空间中分配一段连续的编号。虽然这种全局编号机制已不被推荐(现代驱动应使用描述符), 但为了兼容旧的API和
sysfs
接口, 这一步仍然是必需的。此函数支持动态分配(推荐,gc->base = -1
)和静态分配(已废弃)两种模式。 - 子系统集成: 这是最关键的部分, 它像接线员一样, 将这个新创建的
gpio_device
连接到各个相关子系统:- pinctrl: 调用
gpiochip_add_pin_ranges
来注册GPIO编号范围。 - 设备树(OF): 调用
of_gpiochip_add
来解析设备树中与GPIO相关的属性。 - 中断(IRQ): 调用
gpiochip_add_irqchip
将该GPIO控制器注册为一个irqchip
(中断控制器), 这使得gpio_to_irq()
功能得以实现。
- pinctrl: 调用
- sysfs设备创建: 最后, 它调用
gpiochip_setup_dev
在/sys/class/gpio/
目录下创建对应的gpiochipN
设备节点, 使得用户空间工具(如gpiodetect
,udev
)可以看到并与之交互。 - 错误处理: 此函数拥有一个非常健壮的错误处理机制。它使用了一系列的
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 | int gpiochip_add_data_with_key(struct gpio_chip *gc, void *data, |
gpiod_find_by_fwnode: 与固件无关的 GPIO 查找调度程序
此函数是Linux内核gpiod
子系统中一个至关重要的内部调度函数。它的核心原理是充当一个抽象层, 将一个来自上层API的、基于通用固件句柄(fwnode
)的GPIO查找请求, 路由到与该固件类型相匹配的、特定于技术的后端解析函数。
这个函数是实现驱动程序跨平台可移植性的关键。一个编写良好的驱动程序不应该关心它所运行的系统是使用设备树(Device Tree)还是ACPI来描述硬件, 它只知道设备有一个fwnode
。此函数正是负责处理这种差异的中间人。
其工作流程非常直接, 作为一个多路分发器:
- 接收通用句柄: 它接收一个
fwnode_handle
作为输入。这是一个通用的、不透明的句柄, 可以代表设备树节点、ACPI设备节点, 甚至是纯软件定义的节点。 - 识别句柄类型: 它使用一系列的类型检查函数 (
is_of_node
,is_acpi_node
,is_software_node
) 来确定fwnode
的真实 underlying 类型。 - 分派到专用后端:
- 如果
fwnode
是一个设备树节点, 它就调用of_find_gpio
。of_find_gpio
是专门为设备树设计的后端, 它知道如何去解析设备树节点中的<con_id>-gpios
属性(例如enable-gpios
), 并将设备树的phandle和specifier转换为内核的gpio_desc
。 - 如果
fwnode
是一个ACPI节点, 它就调用acpi_find_gpio
。acpi_find_gpio
则知道如何去解析ACPI表中的_CRS
(Current Resource Settings)资源, 找到匹配的GpioIo
或GpioInt
条目来获取GPIO信息。 - 如果
fwnode
是一个软件节点, 它就调用swnode_find_gpio
, 该函数用于处理在代码中定义的、用于模拟固件描述的软件节点层次结构。
- 如果
- 返回结果: 它将专用后端函数的返回值(一个
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 | /* |
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脚)。
该函数的原理非常直接, 并且以线程安全为核心:
- 获取全局锁: 它首先获取一个全局互斥锁
gpio_lookup_lock
。这个锁保护着全局的gpio_lookup_list
链表。 - 添加到全局链表: 在锁的保护下, 它遍历调用者传入的查找表数组, 并使用
list_add_tail
将每一个查找表中的list
成员(一个struct list_head
)添加到gpio_lookup_list
链表的末尾。 - 释放锁: 遍历完成后, 锁被自动释放。
当系统中的某个驱动程序(消费者)稍后调用gpiod_get()
来请求一个GPIO时, 内核的GPIO核心代码就会遍历这个gpio_lookup_list
全局链表, 查找是否有哪个已注册的表项能够匹配该消费者设备的名称和它请求的GPIO功能名称。如果找到匹配项, 内核就能够知道要分配哪个具体的物理GPIO。
1 | /** |
gpiod_find: 传统平台 GPIO 查找引擎
此函数是Linux内核gpiod
子系统中负责执行基于平台查找表(platform lookup table)的GPIO查找的底层核心函数。它的核心原理是充当一个备用/回退机制, 用于那些没有使用现代固件描述(如设备树或ACPI)的系统。在这种旧式系统中, 硬件布线信息不是在设备树中描述, 而是通过C代码中的静态查找表(通常在”board file”中定义)来提供的。
gpiod_find
是连接消费者驱动程序和这些静态C语言查找表之间的桥梁。
工作流程详解:
同步与安全: 函数的第一步是获取一个全局互斥锁
gpio_lookup_lock
。这至关重要, 因为这些静态查找表可以在系统运行时被动态地添加或移除。这个锁确保了在函数遍历查找表的过程中, 查找表本身不会被另一个任务或CPU核心并发地修改, 从而防止了竞态条件和数据损坏。查找正确的表 (
gpiod_find_lookup_table
): 系统中可能存在多个查找表, 每个表可能与特定的设备或总线相关联。此函数首先会根据传入的dev
参数, 找到与该消费者设备最匹配的那个查找表。遍历与匹配: 找到正确的表之后, 函数会遍历表中的每一个条目 (
struct gpiod_lookup
)。对于每个条目, 它会执行精确的匹配逻辑:- 索引 (
idx
): 必须与请求的索引完全匹配。 - 功能ID (
con_id
): 如果表中的条目定义了con_id
, 那么请求的con_id
也必须存在且完全相同。如果表中条目没有定义con_id
(即为NULL), 它可以匹配任何功能名称, 这通常用于只有一个GPIO的简单设备。
- 索引 (
两种查找方式: 匹配成功后, 它会根据表中条目的内容, 采用两种方式之一来定位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
。
- 方式A: 按全局名称查找 (罕见): 如果表条目中的
健壮的依赖处理 (
-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 | static struct gpio_desc *gpiod_find(struct device *dev, const char *con_id, |
gpiod_request 和 gpiod_request_commit: 安全地请求并独占一个GPIO
这两个函数协同工作, 共同构成了Linux内核gpiolib
框架中用于”请求”或”声明”一个GPIO引脚的核心API。当一个设备驱动程序需要使用某个GPIO引脚时, 它必须先调用gpiod_request
来获得对该引脚的独占访问权。这个过程确保了不会有多个驱动程序试图同时控制同一个物理引脚, 从而避免了硬件冲突。
gpiod_request_commit
是执行实际工作的内部核心函数, 而gpiod_request
则是一个安全封装, 它在调用核心函数之前处理了至关重要的模块生命周期管理。
gpiod_request_commit
: 执行请求的核心逻辑
此函数负责执行所有将GPIO引脚标记为”已使用”的底层操作。它的原理是通过一个原子操作来确保排他性, 并调用底层硬件驱动提供的可选回调函数, 同时在任何失败的情况下都能安全地回滚所有状态变更。
1 | /* |
gpiod_request
: 安全的公共API封装
此函数是在驱动程序中应该被调用的标准API。它在gpiod_request_commit
的基础上, 增加了对内核模块生命周期的管理。
1 | /* |
gpiochip_*
静态函数: gpiolib
核心到硬件驱动的安全调度层
这四个静态函数是Linux gpiolib
框架内部的核心组件。它们共同构成了一个安全调度层(Safe Dispatch Layer)或网关(Gateway), 其核心作用是将上层 gpiolib
的通用、硬件无关的请求, 安全地分发到下层具体的、硬件相关的gpio_chip
驱动程序所实现的回调函数中。
这些函数的设计原理体现了Linux内核驱动框架的几个核心思想:
- 抽象与封装: 上层驱动(如
gpiod_direction_output
)不需要知道底层硬件是STM32、NXP还是TI的芯片。它们只与通用的gpio_desc
交互。而这一组gpiochip_*
函数就是实现这种抽象的关键环节, 它们负责调用与gpio_desc
关联的那个具体硬件驱动(gpio_chip
)的实现。 - 健壮性与错误检查: 每一个函数都内置了关键的检查:
lockdep_assert_held
: 这是一个锁调试断言, 确保调用者已经持有了适当的锁(在这里是SRCU读锁)。SRCU(Sleepable Read-Copy Update)是一种高级锁机制, 即使在单核系统上, 它也能确保在一个驱动正在使用某个gpio_chip
时, 提供该gpio_chip
的内核模块不会被中途卸载, 从而防止了悬空指针等严重问题。WARN_ON
: 这是一个运行时检查, 用于确保底层的gpio_chip
驱动程序确实实现了它应该实现的回调函数。如果一个上层函数试图调用一个gpio_chip
驱动没有提供的功能(例如, 在一个只支持输入的芯片上调用.set
), 内核会打印一个警告, 这极大地帮助了驱动开发者的调试。
- API约定强制: 内核API约定错误码必须是负的
errno
值。这些函数会检查底层驱动的返回值, 如果驱动错误地返回了一个正值, 它们会将其规范化为-EBADE
(错误的交换描述符), 从而保证了整个内核API的一致性。
在STM32H750的上下文中, 当gpiolib
核心需要操作一个GPIO时(例如GPIOC的第5脚), gc
参数就会是一个指向代表STM32 GPIOC端口的gpio_chip
结构体的指针。而gc->set
、gc->direction_input
等函数指针, 则会指向在ST的pinctrl-stm32.c
驱动中实现的、真正通过读写GPIOC->MODER
, GPIOC->ODR
, GPIOC->BSRR
等寄存器来操作硬件的函数。
gpiochip_set: 设置GPIO输出电平的硬件调度函数
1 | /* |
gpiochip_get_direction: 获取GPIO方向的硬件调度函数
1 | /* |
gpiochip_direction_input: 设置GPIO为输入的硬件调度函数
1 | /* |
gpiochip_direction_output: 设置GPIO为输出的硬件调度函数
1 | /* |
gpiod_direction_input
及相关函数: 设置GPIO为输入模式
这一组函数是gpiod_direction_output
函数的逻辑对应面, 它们共同构成了将一个GPIO引脚配置为输入模式的标准实现。同样, 它们也采用了层次化设计, 从一个简单的公共API深入到一个能够智能适应不同硬件能力的内部核心函数。
其核心原理是优先使用硬件驱动提供的专用回调函数来将引脚设置为输入, 如果专用函数不存在, 则通过查询引脚当前状态来推断其是否可用作输入, 最终在成功后更新gpiolib
的内部软件状态标志并应用任何必要的偏置(如上拉/下拉电阻)。
gpiod_direction_input_nonotify
: 核心逻辑与硬件适配
这是执行所有实际工作的核心函数。它负责与底层gpio_chip
驱动交互, 并处理了各种可能的硬件驱动实现方式。
1 | /* |
gpiod_direction_input
: 公共API封装
这个函数是暴露给驱动程序使用的标准顶层API。它的作用很简单: 调用核心逻辑函数, 并在成功后向用户空间发送状态变更通知。
1 | /** |
gpiod_direction_output
及相关函数: 设置GPIO为输出模式的层次化实现
这一组函数共同构成了Linux gpiolib
框架中将一个GPIO引脚配置为输出模式的完整实现。它们采用了一种层次化的设计, 从一个易于使用的高层逻辑API, 逐层深入到底层的硬件交互, 每一层都增加了特定的功能, 如安全检查、逻辑值转换、硬件能力适配和软件仿真。
gpiod_direction_output_raw_commit
: 执行硬件配置的底层核心
这是整个功能链的最底层和最核心的函数。它的作用是直接与底层的gpio_chip
驱动程序交互, 发出将引脚设置为输出模式并赋予初始值的硬件命令。它的原理是适配不同的硬件驱动能力, 并原子性地更新软件状态。
1 | /* |
gpiod_direction_output_nonotify
: 逻辑层核心 (处理特殊模式和安全检查)
这个函数是整个逻辑的核心。它的作用是处理所有与软件相关的复杂性, 包括逻辑电平转换、开漏/开源模式的硬件支持或软件仿真, 以及关键的安全检查。
1 |
|
gpiod_direction_output
和 gpiod_direction_output_raw
: 公共API
这两个函数是暴露给驱动程序使用的顶层API。它们非常相似, 都是简单地调用它们各自的_nonotify
或_commit
版本, 然后在成功后发送一个通知, 通常用于更新用户空间的状态。
1 | /** |
gpiod_set_transitory
及相关函数: 配置GPIO状态的持久性
这一组函数共同实现了一个功能: 配置一个GPIO引脚的状态在系统低功耗(挂起/suspend)或复位事件中是否应该被保持(持久化, persistent)还是可以丢失(瞬态的, transitory)。这对于电源管理至关重要, 例如, 一个用于唤醒系统的引脚必须保持其状态, 而一个用于点亮LED的引脚则可以在系统睡眠时被关闭。
这个功能通过一个从高层API到底层硬件驱动调用的函数链来实现。我们将从最高层的gpiod_set_transitory
开始, 逐层深入。
gpiod_set_transitory
: 设置引脚状态是否为瞬态的公共API
这是驱动程序应该调用的顶层函数。它的核心原理是实现了一个两级状态管理: (1) 它无条件地更新内核gpiolib
框架中关于该引脚的软件状态标志; (2) 然后, 它”尽力而为”(best-effort)地尝试将这个配置应用到底层的物理硬件上。
1 | /** |
gpio_set_config_with_argument_optional
: “可选地”应用配置
此函数是gpiod_set_transitory
的直接辅助函数。它的核心作用是尝试应用一个配置, 但如果底层硬件明确表示”不支持”该功能, 则将其视为成功, 而不是一个错误。
1 | /* |
gpio_set_config_with_argument
和 gpio_do_set_config
: 打包并分发配置
gpio_set_config_with_argument
是一个简单的转换器, 而gpio_do_set_config
是最终与硬件驱动程序交互的网关。
1 | static int gpio_set_config(struct gpio_desc *desc, enum pin_config_param mode) |
gpiod_configure_flags: 集中式GPIO配置核心辅助函数
此函数是Linux gpiolib
框架内部的一个核心辅助函数。它的主要作用是提供一个统一的接口, 用于将从两个不同来源获取的配置标志——静态的板级描述(lflags
)和动态的驱动程序请求(dflags
)——应用到一个GPIO引脚描述符(desc
)上。它负责处理引脚的所有关键电气特性(如逻辑电平、开漏/开源、上下拉)以及其工作方向(输入/输出)。
该函数的原理可以分解为两个主要阶段:
应用静态电气特性 (
lflags
): 函数首先处理从设备树、ACPI或板级文件中解析出的标志(lflags
)。这些标志定义了引脚在特定硬件设计中的固有电气属性。- 逻辑电平 (Active-Low): 如果
GPIO_ACTIVE_LOW
被设置, 它会在此引脚的内部标志中设置FLAG_ACTIVE_LOW
。这会反转该引脚的逻辑含义, 即物理低电平被软件视为”1”或”激活”, 反之亦然。 - 输出模式 (Open-Drain/Source): 它会设置
FLAG_OPEN_DRAIN
或FLAG_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
标志, 表明此引脚的状态在系统睡眠/挂起期间无需被保持, 这是一种电源管理优化。
- 逻辑电平 (Active-Low): 如果
应用动态方向和初始值 (
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_flags
。lflags
参数会携带从STM32H750设备树中解析出的标志(如GPIO_ACTIVE_LOW
), 而dflags
则携带驱动自身指定的标志(如GPIOD_OUT_HIGH
)。此函数随后会将这些抽象的标志转换为对底层STM32 GPIO驱动gpio_chip
的回调函数(如.direction_output
)的调用, 最终实现对STM32 GPIO端口的MODER
, PUPDR
, OTYPER
, ODR
等寄存器的精确配置。
1 | /** |
gpiod_find_and_request: GPIO 获取、请求与配置的核心引擎
此函数是Linux内核现代gpiod
接口的底层核心工作函数。所有上层的gpiod_get_*
便利封装函数最终都会调用它来完成实际的工作。它的核心原理是执行一个完整且健壮的”查找->请求->配置”三步流程, 将一个来自消费者驱动的、基于功能的抽象GPIO请求, 转化为一个已声明所有权并正确初始化的、可供驱动程序直接使用的硬件句柄(struct gpio_desc
)。
这是一个高度复杂的函数, 其内部原理融合了多种内核机制:
分层查找策略 (Find): 函数首先采用现代的、基于固件(Firmware)的查找方法。
- 首选: 设备树/ACPI (
gpiod_find_by_fwnode
): 它优先使用fwnode
(通常是设备树节点)来查找GPIO。它会在设备树节点中寻找匹配的<con_id>-gpios
属性(例如,enable-gpios
), 并解析出GPIO信息。这是首选的、与硬件描述绑定的方式。 - 备用: 平台查找 (
gpiod_find
): 如果基于固件的查找没有找到结果, 并且调用者允许, 它会回退到旧式的、基于平台查找表(board file)的机制。这确保了对没有使用设备树的旧平台的向后兼容性。
- 首选: 设备树/ACPI (
所有权与资源管理 (Request): 在成功找到GPIO描述符后, 最关键的一步是调用
gpiod_request
。- 此调用向
gpiolib
核心声明:”这个GPIO引脚现在归我(由label
标识的消费者)所有”。 - 内核会将该引脚标记为”已使用”, 防止其他驱动程序无意中请求同一个引脚而导致硬件冲突。这是一个至关重要的资源管理和互斥机制。
- 此调用向
并发安全: 整个查找和请求过程被一个
scoped_guard(srcu, &gpio_devices_srcu)
块包裹。- SRCU (Sleepable Read-Copy-Update) 是一种高级的同步机制, 用于保护被频繁读取但很少写入的数据结构, 比如系统中的GPIO控制器列表。
- 这个锁确保了在函数查找GPIO控制器的过程中, 该控制器不会被另一个CPU核心或因抢占而运行的任务并发地从系统中注销, 从而防止了悬空指针等竞态条件的发生。即使在STM32H750这样的单核系统中, 这也能防止任务抢占和中断上下文访问带来的并发问题。
灵活的共享机制 (Non-Exclusive Access): 函数包含了对”非独占”访问的特殊处理。
- 正常情况下, 如果一个已经被请求的GPIO再次被请求,
gpiod_request
会返回-EBUSY
错误。 - 但如果第二次请求时设置了
GPIOD_FLAGS_BIT_NONEXCLUSIVE
标志, 此函数会抑制这个-EBUSY
错误。它会直接返回已存在的描述符, 但跳过后续的配置步骤。 - 这解决了多个设备(例如两个电源调节器)共享同一个物理使能引脚的硬件设计问题, 允许它们共享同一个GPIO句柄, 并假定第一个请求者已经完成了必要的初始化配置。
- 正常情况下, 如果一个已经被请求的GPIO再次被请求,
最终配置与错误恢复 (Configure): 在成功声明所有权后, 函数调用
gpiod_configure_flags
来应用调用者请求的初始状态, 例如将引脚设置为输出低电平、配置为开漏模式等。- 关键的错误处理: 如果配置步骤失败, 函数会立即调用
gpiod_put
(与gpiod_request
配对的释放函数)来撤销刚刚成功的请求操作, 将GPIO引脚释放回池中, 从而确保系统状态的一致性。
- 关键的错误处理: 如果配置步骤失败, 函数会立即调用
1 |
|
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位的并行数据总线)是必不可少的。
原理:
- 它首先获取设备的固件节点(
fwnode
), 这在现代Linux中通常指向设备树节点。 - 然后, 它将所有参数(包括设备、功能名、索引、初始化标志)传递给底层的核心函数
gpiod_find_and_request
。 gpiod_find_and_request
会执行以下关键操作:- 在设备的设备树节点中, 查找名为
<con_id>-gpios
的属性(例如,data-gpios
)。 - 定位到该属性列表中的第
idx
个条目。 - 解析该条目, 找到它所指向的GPIO控制器和引脚号。
- 向
gpiolib
核心请求(request)该GPIO, 这会将其标记为”已使用”, 防止其他驱动程序产生冲突。 - 根据传入的
flags
, 对GPIO进行初始配置(例如, 设置为输出低电平)。
- 在设备的设备树节点中, 查找名为
- 返回值至关重要:
- 成功: 返回一个有效的
gpio_desc
指针。 - 未找到: 如果设备树中没有定义对应的GPIO, 它会返回特定的错误码
-ENOENT
。 - 其他错误: 如果GPIO已被占用(
-EBUSY
)或发生其他问题, 它会返回相应的错误码。
- 成功: 返回一个有效的
1 | struct gpio_desc *__must_check gpiod_get_index(struct device *dev, |
gpiod_get_index_optional
: 获取可选的多索引GPIO
这是一个基于gpiod_get_index
的便利封装。它的核心作用是改变”未找到”这种情况下的返回值, 使其对驱动开发者更友好。
原理:
- 它直接调用
gpiod_get_index
来执行获取操作。 - 然后, 它检查返回值。它使用
gpiod_not_found(desc)
(内部等价于PTR_ERR(desc) == -ENOENT
)来专门判断失败的原因是否是”未找到”。 - 如果是因为”未找到”而失败, 它会抑制这个错误, 并返回
NULL
。 - 如果是因为其他原因(如
-EBUSY
)而失败, 它会保留并返回原始的错误指针。
这个函数对于处理硬件设计上可选的GPIO引脚极为有用。驱动程序可以通过检查返回值是否为NULL
来知道该可选功能是否存在, 从而调整自身行为, 而无需处理-ENOENT
这个特定的错误码。
1 | struct gpio_desc *__must_check gpiod_get_index_optional(struct device *dev, |
gpiod_get_optional
: 获取单个可选GPIO (最常用)
这是最顶层的、也是最常用的便利封装。它的核心作用是获取与某个功能关联的**第一个(也是唯一一个)**可选GPIO。
原理:
它是一个极简的封装, 直接调用gpiod_get_index_optional
, 并将索引idx
硬编码为0
。
在STM32H750这样的嵌入式系统中, 大多数功能(如复位、中断、使能)都只由单个GPIO引脚控制。因此, 这个函数是驱动程序中最常见的选择。例如, reg_fixed_voltage_probe
中获取使能引脚时, 使用的就是这个函数。它允许设备树中可以完全不定义enable-gpios
属性, 驱动程序也能正常工作(只是没有使能控制功能), 从而大大增强了硬件描述的灵活性。
1 | struct gpio_desc *__must_check gpiod_get_optional(struct device *dev, |
drivers/gpio/gpiolib-cdev.c
lineinfo_watch_poll
: 等待GPIO事件
此函数实现了poll()
、select()
和epoll()
系统调用的后端逻辑。它的核心原理是提供一个无阻塞的机制来查询事件是否已发生, 并在没有事件时将当前进程注册到一个等待队列上, 以便在未来事件发生时被内核唤醒。
工作流程详解:
- 获取会话上下文: 从
file->private_data
中获取在open()
时创建的私有会话数据cdev
。 - 设备存在性检查: 使用
SRCU
机制安全地检查底层的GPIO芯片是否仍然存在。如果设备已被移除, 它会返回EPOLLHUP | EPOLLERR
, 通知用户空间此文件句柄已失效。 - 注册等待队列: 这是
poll
的核心。poll_wait(file, &cdev->wait, pollt)
不会阻塞。它只是将当前进程的等待信息添加到cdev->wait
这个等待队列头中。这相当于对内核说: “如果未来有谁唤醒了cdev->wait
这个队列, 请务必唤醒我(这个正在执行poll
的进程)”。 - 检查当前状态: 在注册完等待后, 它会立即检查事件FIFO缓冲区(
cdev->events
)是否已经有数据了。这个检查是在持有自旋锁的情况下进行的, 以确保与生产者(中断处理程序)的并发安全。 - 返回结果:
- 如果FIFO不为空, 意味着有事件可以立即读取。函数返回
EPOLLIN | EPOLLRDNORM
,poll()
系统调用会立即返回, 告知应用程序可以进行read()
操作了。 - 如果FIFO为空, 函数返回
0
。此时,poll()
系统调用不会立即返回, 而是会使应用程序进入睡眠, 等待被步骤3中注册的唤醒机制唤醒。
- 如果FIFO不为空, 意味着有事件可以立即读取。函数返回
1 | /* |
lineinfo_watch_read
: 读取GPIO事件
此函数实现了read()
系统调用的后端逻辑。它的核心原理是从客户端私有的FIFO缓冲区中取出一个或多个事件, 并将它们安全地复制到用户空间提供的缓冲区中。如果缓冲区为空, 它会使调用进程进入睡眠, 直到有事件被推入缓冲区并被唤醒。
工作流程详解:
- 获取会话上下文与安全检查: 与
poll
函数类似。 - 阻塞/非阻塞逻辑 (主循环内):
- 加锁: 使用
scoped_guard
获取保护FIFO和等待队列的自旋锁。 - 检查FIFO: 如果FIFO为空:
- 如果之前已经读取过数据(
bytes_read > 0
), 则立即返回已读取的数据, 避免不必要的阻塞。 - 如果文件是以非阻塞模式(
O_NONBLOCK
)打开的, 则立即返回-EAGAIN
错误, 这是标准的非阻塞I/O行为。 - 如果以上都不是, 则进入阻塞状态。
wait_event_interruptible_locked
是一个强大的宏, 它会自动释放锁, 将进程置于可中断的睡眠状态并加入等待队列。当被唤醒时, 它会自动重新获取锁并继续执行。
- 如果之前已经读取过数据(
- 出队操作: 一旦确认FIFO非空(无论是最初就不空, 还是被唤醒后), 就调用
kfifo_out
从FIFO中取出一个事件到内核的event
变量中。 - 解锁:
scoped_guard
在代码块结束时自动释放锁。
- 加锁: 使用
- API版本兼容性:
#ifdef CONFIG_GPIO_CDEV_V1
部分处理了新旧两套API的兼容性问题。它会检查客户端请求的ABI版本, 如果是旧版本, 它会将从FIFO中取出的新版v2事件结构体转换为旧版v1结构体, 然后再复制给用户。 - 复制到用户空间:
copy_to_user
是一个关键的、安全的内存复制函数, 它将内核空间中的event
数据复制到用户空间程序提供的buf
缓冲区中, 并处理可能发生的地址错误。 - 循环读取:
do-while
循环允许在用户缓冲区足够大的情况下, 一次read()
调用读取多个待处理的事件, 提高了效率。
在STM32H750上的意义:
这两个函数构成了在STM32上进行高性能、事件驱动式GPIO编程的基础。一个监控按键输入的程序无需在循环中不断地轮询GPIO电平(这会浪费大量CPU周期)。取而代之的是, 它可以调用poll()
让自己的进程进入睡眠。当STM32的EXTI中断被触发时, 内核中断处理程序(生产者)会将一个事件推入FIFO并唤醒该进程。进程被唤醒后, poll()
返回, 进程接着调用read()
来获取事件的详细信息(例如, 哪个引脚发生了什么类型的事件)。这种机制在任何现代操作系统中, 对于处理异步硬件事件都是至关重要的, 即使是在单核系统上, 它也能极大地提高系统的响应能力和能效。
1 | /* |
gpio_chrdev_open: 打开GPIO字符设备
此函数是Linux GPIO子系统字符设备接口的open
方法实现。当一个用户空间程序调用open()
系统调用来打开一个GPIO控制器设备文件(例如/dev/gpiochip0
)时, 内核就会执行此函数。
它的核心原理是为一个新的客户端(即一个打开的文件描述符)创建一个独立的、私有的会话上下文。这个上下文(struct gpio_chardev_data
)会存储该特定客户端的所有状态, 例如它正在监视哪些GPIO线、它有哪些待处理的事件等。通过这种方式, 多个不同的用户空间程序可以同时打开并操作同一个GPIO控制器设备文件, 而它们各自的会话状态互不干扰。
工作流程详解:
- 获取设备上下文: 函数首先通过
container_of
宏从VFS层传入的通用inode
结构体, 反向找到代表整个GPIO控制器设备的struct gpio_device
(gdev
)。 - 并发安全检查: 它使用
SRCU
(一种专门用于可睡眠上下文的读-拷贝-更新同步机制)来安全地检查底层的gpio_chip
是否仍然存在。这是一个至关重要的步骤, 用于防止在用户尝试打开设备的同时, 驱动程序恰好被卸载(即”热拔插”场景), 从而避免了使用无效指针导致的系统崩溃。 - 分配私有会话数据: 它调用
kzalloc
为这次open
操作分配一个全新的struct gpio_chardev_data
实例(cdev
)。这个结构体将作为此文件句柄的私有数据存储。 - 初始化会话资源:
bitmap_zalloc
: 为cdev->watched_lines
分配一个位图。这个位图的大小等于该GPIO控制器拥有的引脚总数, 用于标记该客户端正在监视哪些引脚的状态变化。init_waitqueue_head
: 初始化一个等待队列头(cdev->wait
)。当用户空间程序对此文件句柄调用poll()
或select()
来等待事件时, 它的进程会在此等待队列上睡眠。INIT_KFIFO
: 初始化一个内核FIFO缓冲区(cdev->events
)。当被监视的GPIO引脚上发生事件时, 内核会将事件的详细信息推入此缓冲区, 等待用户空间程序通过read()
来取走。
- 引用计数管理: 它调用
gpio_device_get(gdev)
来增加gpio_device
的引用计数。这是一个关键的生命周期管理操作, 它确保了只要还有任何一个用户空间程序打开着这个设备文件,gpio_device
结构体就不会被内核释放, 即使底层的硬件驱动模块已经被卸载。 - 注册通知回调 (订阅事件): 这是实现事件驱动监控的核心。
- 它初始化两个”通知块”(
notifier_block
),lineinfo_changed_nb
和device_unregistered_nb
。 - 它将这两个通知块分别注册到
gdev
的line_state_notifier
和device_notifier
通知链中。这相当于为此客户端订阅了两类事件: “某个GPIO线的配置发生了变化”和”整个GPIO设备即将被注销”。当这些事件发生时, 内核会调用这里注册的回调函数(如lineinfo_changed_notify
), 这些回调函数会将事件信息放入该客户端私有的FIFO缓冲区并唤醒在等待队列上睡眠的进程。
- 它初始化两个”通知块”(
- 关联与完成:
file->private_data = cdev
: 这是将内核VFS与驱动逻辑连接起来的最后一步。它将新创建的私有会话数据cdev
的指针存入struct file
的private_data
字段中。之后所有对此文件句柄的操作(如ioctl
,read
,release
)都可以通过file->private_data
轻松取回这个会话上下文。nonseekable_open
: 调用一个辅助函数, 将此文件标记为不可寻址(seek), 这对于流式设备是标准做法。
错误处理:
该函数使用了内核中非常标准的goto
标签错误处理模式。如果在初始化过程中的任何一步失败, 代码会跳转到相应的标签, 然后像瀑布一样执行所有必要的逆向清理操作(例如, 注销通知、释放位图、减少引用计数、释放内存), 从而保证在函数出错退出时不会留下任何悬挂的资源。
在STM32H750上的意义:
在STM32H750上, 当一个用户空间应用(如通过libgpiod
编写的程序)执行open("/dev/gpiochip0", ...)
时, 内核就会执行此函数来为该应用准备好一个与GPIOA控制器交互的通道。此后, 该应用就可以通过ioctl
来配置引脚, 或通过poll
和read
来实时监控引脚电平或边沿触发事件。write_lock_irqsave
等锁机制在单核抢占式系统上依然是必需的, 它通过禁用本地中断和抢占来保护对通知链表等共享资源的访问, 防止数据结构被并发修改所破坏。
1 | /* |
GPIO字符设备接口的注册与注销
gpio_fileops
: 文件操作函数集
这是一个静态常量结构体, 它像一张”功能表”, 定义了当用户空间对本驱动创建的字符设备文件进行操作时, 内核应该调用哪些具体的函数来响应该操作。这是连接VFS和GPIO驱动功能的桥梁。
1 | /* |
gpiolib_cdev_register
: 注册GPIO字符设备
此函数负责为一个GPIO设备(gdev
)执行所有必要的步骤, 来创建一个功能齐全、可供用户空间访问的字符设备。
1 | /* |
gpiolib_cdev_unregister
: 注销GPIO字符设备
此函数是注册函数的逆过程, 负责清理和释放所有相关资源。
1 | /* |
drivers/gpio/gpio-stmpe.c
STMPE GPIO 操作回调函数集
stmpe_gpio_get
: 获取GPIO引脚的输入电平
- 作用: 当内核或用户空间需要读取一个GPIO引脚的当前逻辑状态(高电平或低电平)时,
gpiolib
框架会调用此函数。 - 原理:
- 获取上下文:
gpiochip_get_data(chip)
获取到指向本驱动私有数据stmpe_gpio
的指针, 从而可以访问到与父设备通信的stmpe
句柄。 - 定位寄存器:
stmpe->regs
是一个数组, 存储了不同功能寄存器的基地址。STMPE_IDX_GPMR_LSB
是”GPIO Pin Monitor Register”(GPIO引脚监视寄存器)的基地址索引。offset / 8
计算出该引脚属于哪个8位的寄存器(哪个bank)。 - 定位比特位:
BIT(offset % 8)
计算出该引脚在8位寄存器中所对应的比特位掩码(例如,offset
为10, 掩码为BIT(2)
, 即0x04
)。 - 硬件交互:
stmpe_reg_read
通过I2C/SPI总线读取目标寄存器的值。 - 解析结果:
ret & mask
从读取到的8位值中分离出我们关心的那一位。!!
是一个C语言技巧, 用于将任何非零值转换为1, 0值保持为0, 确保返回值是标准的逻辑值。
- 获取上下文:
1 | static int stmpe_gpio_get(struct gpio_chip *chip, unsigned offset) |
stmpe_gpio_set
: 设置GPIO引脚的输出电平
- 作用: 当需要将一个配置为输出的GPIO引脚设置为高电平或低电平时,
gpiolib
会调用此函数。 - 原理:
- 选择操作: STMPE芯片通常有独立的”GPIO Pin Set Register”(GPSR, 写1置位)和”GPIO Pin Clear Register”(GPCR, 写1清零)。函数根据传入的
val
值(1或0)来选择which
寄存器基址。 - 定位: 与
get
函数一样, 计算出确切的寄存器地址和比特位掩码。 - 特殊处理: 某些STMPE芯片型号可能只有一个寄存器用于置位/清零。代码通过比较GPSR和GPCR的地址是否相同来检测这种情况。如果是, 它会调用
stmpe_set_bits
(一个读-修改-写操作)来设置或清除相应的位。 - 标准操作: 对于有独立置位/清零寄存器的型号, 只需向选定的寄存器写入掩码即可触发硬件操作。
stmpe_reg_write
完成这个总线写操作。
- 选择操作: STMPE芯片通常有独立的”GPIO Pin Set Register”(GPSR, 写1置位)和”GPIO Pin Clear Register”(GPCR, 写1清零)。函数根据传入的
1 | static int stmpe_gpio_set(struct gpio_chip *chip, unsigned int offset, int val) |
stmpe_gpio_get_direction
: 获取GPIO引脚的方向
- 作用: 查询一个GPIO引脚当前是被配置为输入还是输出。
- 原理: 此操作围绕”GPIO Pin Direction Register”(GPDR)进行。
- 定位: 计算GPDR寄存器的地址和比特位掩码。注意这里有一个
- (offset / 8)
的笔误, 应该是+
, 假设在父驱动的寄存器地址表中已做了修正。 - 硬件交互: 读取GPDR寄存器的值。
- 解析: 检查对应的比特位。根据STMPE的数据手册, 如果该位是1, 表示输出; 如果是0, 表示输入。函数返回
gpiolib
定义的标准方向常量。
- 定位: 计算GPDR寄存器的地址和比特位掩码。注意这里有一个
1 | static int stmpe_gpio_get_direction(struct gpio_chip *chip, |
stmpe_gpio_direction_output
/ stmpe_gpio_direction_input
: 设置GPIO引脚的方向
- 作用: 将一个GPIO引脚配置为输入或输出模式。
- 原理:
direction_output
: 这是一个两步操作, 以避免引脚在方向切换瞬间产生不确定的电平(毛刺)。- 首先调用
stmpe_gpio_set
预先设置好期望的输出电平val
。此时方向尚未改变, 但硬件内部状态已准备好。 - 然后调用
stmpe_set_bits
(一个安全的读-修改-写函数), 将GPDR寄存器中对应的比特位置为1, 正式将引脚切换为输出模式。
- 首先调用
direction_input
: 这是一个单步操作。只需调用stmpe_set_bits
将GPDR寄存器中对应的比特位清零即可。
1 | static int stmpe_gpio_direction_output(struct gpio_chip *chip, |
stmpe_gpio_request
: 请求使用一个GPIO
- 作用: 当一个驱动程序首次通过
gpio_request()
或gpiod_get()
请求使用某个GPIO时,gpiolib
会调用此函数。这是执行一次性初始化配置的理想位置。 - 原理:
- 检查保留位: 首先检查该引脚
offset
是否在norequest_mask
(从设备树读取)中被标记为保留。如果是, 则返回-EINVAL
拒绝请求。 - 设置复用功能: STMPE芯片的引脚通常是多功能的(例如, GPIO, ADC, PWM等)。
stmpe_set_altfunc
是一个父驱动提供的函数, 它会通过总线向芯片发送命令, 确保该引脚的复用功能被正确地设置为GPIO模式。这是确保引脚能作为通用IO使用的关键一步。
- 检查保留位: 首先检查该引脚
1 | static int stmpe_gpio_request(struct gpio_chip *chip, unsigned offset) |
STMPE GPIO驱动的核心实现
此代码片段展示了stmpe-gpio
驱动程序的核心部分: 它定义了驱动的私有数据结构, 并提供了所有必要的回调函数, 以将STMPE IO扩展器的硬件操作与Linux内核通用的gpiolib
框架连接起来。其核心原理是将gpiolib
的抽象请求(如”设置引脚5为高电平”)转换为对STMPE芯片的具体、面向寄存器的I2C/SPI总线操作。
struct stmpe_gpio
: 驱动私有数据结构
这个结构体是stmpe-gpio
驱动实例的”大脑”, 它包含了驱动运行时所需的所有状态和信息。
1 | /* |
template_chip
: gpiolib接口的”蓝图”
这个静态常量结构体将上面定义的所有回调函数打包在一起, 作为注册到gpiolib
的模板。
1 | /* |
stmpe_gpio_probe: 初始化STMPE IO扩展器的GPIO功能
此函数是stmpe-gpio
平台驱动程序的探测函数, 它是驱动程序的核心入口点。当内核的设备模型发现一个与此驱动匹配的设备时(通常是通过设备树), 就会调用此函数。它的核心作用是将一个物理上的STMPE芯片(通过I2C或SPI连接的外部IO扩展器)的GPIO功能, 初始化并注册到Linux内核通用的gpiolib
子系统中, 使这些外部IO引脚能像处理器片上GPIO一样被系统标准地访问和控制。
其工作原理可以分解为以下几个关键步骤:
- 获取父设备数据与内存分配: 此驱动是一个”多功能设备”(MFD, Multi-Function Device)的客户端。它首先从其父设备(即主
stmpe
驱动, 负责I2C/SPI通信)获取一个指向stmpe
核心数据结构的句柄。然后, 它使用devm_kzalloc
为自身的状态结构stmpe_gpio
分配内存, 这种内存分配方式确保了在驱动卸载时资源能被自动释放, 极大地简化了错误处理。 - 配置
gpio_chip
结构体:gpio_chip
是gpiolib
框架的核心, 它像一个”驱动蓝图”, 包含了一系列函数指针, 用于定义如何对GPIO进行读、写、设置方向等操作。此函数使用一个预定义的template_chip
作为模板, 并根据从父设备获取的信息(如GPIO数量)对其进行定制。将base
设置为-1, 是让gpiolib
动态地为这些GPIO分配一个未被占用的编号区间。 - 硬件使能与中断处理: 它调用父驱动提供的
stmpe_enable
函数, 向物理芯片发送命令以开启GPIO功能块。这是实际的硬件交互。接着, 它处理中断:- 它获取一个由STMPE芯片产生的中断号。这个中断是”共享的”, 即芯片上任何一个GPIO产生中断, 都会触发这一根物理中断线。
- 它注册一个线程化中断处理程序(
stmpe_gpio_irq
)。当物理中断发生时, 内核会唤醒一个专门的内核线程来执行这个处理函数, 这对于通过较慢总线(如I2C)连接的设备是最佳实践, 避免在中断上下文中进行耗时操作。 - 它配置
gpio_irq_chip
结构。这是将gpiolib
和irqchip
(中断控制器)框架连接起来的”胶水”。它定义了如何为单个GPIO使能/屏蔽中断、设置触发类型等操作。当上层请求某个GPIO的中断时,gpiolib
会通过gpio_irq_chip
中的函数指针, 调用本驱动的相应函数来操作硬件。
- 注册到GPIOLIB: 最后, 它调用
devm_gpiochip_add_data
, 将完全配置好的gpio_chip
注册到gpiolib
中。一旦注册成功, 系统中其他任何部分(包括用户空间)就可以通过标准的GPIO接口(例如,gpio_request
,gpiod_get
,/sys/class/gpio
等)来使用这些来自STMPE芯片的GPIO引脚了。
1 | /* |
stmpe_gpio: STMPE GPIO驱动的定义与注册
此代码片段的核心作用是定义一个针对”STMPE”系列芯片(通常是I2C/SPI接口的IO扩展器、触摸屏控制器等)上GPIO功能的平台驱动程序, 并通过内核的初始化调用机制, 在系统启动的适当时机将其注册到内核中。
- 驱动定义 (
stmpe_gpio_driver
):struct platform_driver
是Linux平台总线模型中用来描述一个驱动程序的核心数据结构。它告诉内核这个驱动的名字、关键的回调函数(如probe
), 以及一些行为属性。一旦注册, 内核就会用它的名字("stmpe-gpio"
)去匹配设备树或板级文件中定义的设备。 - 注册函数 (
stmpe_gpio_init
): 这个函数是驱动注册的入口点。它只做一件事: 调用platform_driver_register
将stmpe_gpio_driver
这个”驱动蓝图”提交给内核的平台总线核心, 使其变为一个”活”的、可用于设备匹配的驱动程序。 - 初始化调用 (
subsys_initcall
):subsys_initcall
是一个宏, 它将stmpe_gpio_init
函数的地址放入一个特殊的内存段中。在内核启动过程中, 会有一个专门的阶段来执行这个段里的所有函数指针。这确保了该GPIO驱动在核心子系统初始化之后、依赖它的具体设备驱动(device drivers)初始化之前被注册, 从而保证了正确的初始化顺序。
1 | /* |