title: gpiolib
categories:
- linux
- drivers
- gpio
tags: - linux
- drivers
- gpio
abbrlink: 7976b063
date: 2025-10-18 16:18:39

文章目录
- drivers/gpio/gpiolib.c
-
- [gpiochip_setup_dev: 为单个GPIO控制器完成设备和接口的注册](#gpiochip_setup_dev: 为单个GPIO控制器完成设备和接口的注册)
- [gpiochip_setup_devs: 为已注册的GPIO控制器设置设备节点](#gpiochip_setup_devs: 为已注册的GPIO控制器设置设备节点)
- [gpiolib_dev_init: 初始化GPIO设备库](#gpiolib_dev_init: 初始化GPIO设备库)
- [`gpiolib` 总线与设备类型定义](#
gpiolib总线与设备类型定义) -
- [`gpio_dev_type`: GPIO芯片的设备类型](#
gpio_dev_type: GPIO芯片的设备类型) - [`gpio_bus_match`: GPIO总线的匹配规则](#
gpio_bus_match: GPIO总线的匹配规则) - [`gpio_bus_type`: "gpio" 逻辑总线](#
gpio_bus_type: "gpio" 逻辑总线)
- [`gpio_dev_type`: GPIO芯片的设备类型](#
- [gpiochip_find_base_unlocked: 动态查找可用的GPIO编号基地址](#gpiochip_find_base_unlocked: 动态查找可用的GPIO编号基地址)
- [gpiodev_add_to_list_unlocked: 将GPIO设备插入全局排序列表](#gpiodev_add_to_list_unlocked: 将GPIO设备插入全局排序列表)
- [GPIO有效引脚掩码(Valid Mask)与引脚范围管理系列函数](#GPIO有效引脚掩码(Valid Mask)与引脚范围管理系列函数)
-
- [1. 有效引脚掩码的生命周期管理](#1. 有效引脚掩码的生命周期管理)
-
- [`gpiochip_allocate_mask` & `gpiochip_free_mask` / `gpiochip_free_valid_mask`](#
gpiochip_allocate_mask&gpiochip_free_mask/gpiochip_free_valid_mask)
- [`gpiochip_allocate_mask` & `gpiochip_free_mask` / `gpiochip_free_valid_mask`](#
- [2. 通过设备树声明无效引脚 (静态方式)](#2. 通过设备树声明无效引脚 (静态方式))
-
- [`gpiochip_count_reserved_ranges` & `gpiochip_apply_reserved_ranges`](#
gpiochip_count_reserved_ranges&gpiochip_apply_reserved_ranges)
- [`gpiochip_count_reserved_ranges` & `gpiochip_apply_reserved_ranges`](#
- [3. 有效掩码的构建与查询](#3. 有效掩码的构建与查询)
-
- `gpiochip_init_valid_mask`
- [`gpiochip_query_valid_mask` & `gpiochip_line_is_valid`](#
gpiochip_query_valid_mask&gpiochip_line_is_valid)
- [4. GPIO范围与Pinctrl的关联](#4. GPIO范围与Pinctrl的关联)
- [GPIO层次化中断域(Hierarchical IRQ Domain)实现](#GPIO层次化中断域(Hierarchical IRQ Domain)实现)
-
- [1. 域的创建与配置](#1. 域的创建与配置)
- [2. 中断域操作回调 (`irq_domain_ops`) 的实现](#2. 中断域操作回调 (
irq_domain_ops) 的实现) -
- `gpiochip_hierarchy_irq_domain_translate`
- `gpiochip_hierarchy_irq_domain_alloc`
- [`gpiochip_irq_domain_activate` / `deactivate`](#
gpiochip_irq_domain_activate/deactivate)
- [3. 其他辅助与遗留(Legacy)支持函数](#3. 其他辅助与遗留(Legacy)支持函数)
- [GPIO简单中断域(Simple IRQ Domain)创建函数](#GPIO简单中断域(Simple IRQ Domain)创建函数)
-
- [gpiochip_irq_map: 映射一个Linux IRQ到GPIO引脚](#gpiochip_irq_map: 映射一个Linux IRQ到GPIO引脚)
- [gpiochip_irq_unmap: 解除一个Linux IRQ与GPIO引脚的映射](#gpiochip_irq_unmap: 解除一个Linux IRQ与GPIO引脚的映射)
- [gpiochip_irq_select: 选择一个中断控制器](#gpiochip_irq_select: 选择一个中断控制器)
- [gpiochip_domain_ops: 简单域的操作函数集](#gpiochip_domain_ops: 简单域的操作函数集)
- [gpiochip_simple_create_domain: 创建一个简单中断域](#gpiochip_simple_create_domain: 创建一个简单中断域)
- `gpiolib`中断资源管理与使能/禁用函数
-
- [gpiochip_lock_as_irq: 将GPIO引脚锁定为中断模式](#gpiochip_lock_as_irq: 将GPIO引脚锁定为中断模式)
- [gpiochip_unlock_as_irq: 解锁一个用作中断的GPIO引脚](#gpiochip_unlock_as_irq: 解锁一个用作中断的GPIO引脚)
- [gpiochip_reqres_irq / gpiochip_relres_irq: 请求/释放中断资源](#gpiochip_reqres_irq / gpiochip_relres_irq: 请求/释放中断资源)
- [gpiochip_enable_irq / gpiochip_disable_irq: 使能/禁用中断(软件状态)](#gpiochip_enable_irq / gpiochip_disable_irq: 使能/禁用中断(软件状态))
- `gpiolib`中断控制器(irqchip)的实现
-
- [1. `gpio_to_irq`的核心实现](#1.
gpio_to_irq的核心实现) - [2. `irq_chip`回调函数的包装器](#2.
irq_chip回调函数的包装器) - [3. `irqchip`的安装与初始化](#3.
irqchip的安装与初始化)
- [1. `gpio_to_irq`的核心实现](#1.
- [gpiochip_add_irqchip: 为GPIO控制器添加中断控制器功能](#gpiochip_add_irqchip: 为GPIO控制器添加中断控制器功能)
- [gpiochip_setup_dev: 创建GPIO控制器的用户空间接口](#gpiochip_setup_dev: 创建GPIO控制器的用户空间接口)
- [gpiochip_add_data: 将一个GPIO控制器注册到内核](#gpiochip_add_data: 将一个GPIO控制器注册到内核)
- [`gpiochip_add_data_with_key`: 核心注册函数](#
gpiochip_add_data_with_key: 核心注册函数) - [gpiod_find_by_fwnode: 与固件无关的 GPIO 查找调度程序](#gpiod_find_by_fwnode: 与固件无关的 GPIO 查找调度程序)
- [gpiod_add_lookup_tables: 注册GPIO查找表](#gpiod_add_lookup_tables: 注册GPIO查找表)
- [gpiod_find: 传统平台 GPIO 查找引擎](#gpiod_find: 传统平台 GPIO 查找引擎)
- [gpiod_request 和 gpiod_request_commit: 安全地请求并独占一个GPIO](#gpiod_request 和 gpiod_request_commit: 安全地请求并独占一个GPIO)
-
- [`gpiod_request_commit`: 执行请求的核心逻辑](#
gpiod_request_commit: 执行请求的核心逻辑) - [`gpiod_request`: 安全的公共API封装](#
gpiod_request: 安全的公共API封装)
- [`gpiod_request_commit`: 执行请求的核心逻辑](#
- [`gpiochip_*` 静态函数: `gpiolib` 核心到硬件驱动的安全调度层](#
gpiochip_*静态函数:gpiolib核心到硬件驱动的安全调度层) -
- [gpiochip_set: 设置GPIO输出电平的硬件调度函数](#gpiochip_set: 设置GPIO输出电平的硬件调度函数)
- [gpiochip_get_direction: 获取GPIO方向的硬件调度函数](#gpiochip_get_direction: 获取GPIO方向的硬件调度函数)
- [gpiochip_direction_input: 设置GPIO为输入的硬件调度函数](#gpiochip_direction_input: 设置GPIO为输入的硬件调度函数)
- [gpiochip_direction_output: 设置GPIO为输出的硬件调度函数](#gpiochip_direction_output: 设置GPIO为输出的硬件调度函数)
- [`gpiod_direction_input`及相关函数: 设置GPIO为输入模式](#
gpiod_direction_input及相关函数: 设置GPIO为输入模式) -
- [`gpiod_direction_input_nonotify`: 核心逻辑与硬件适配](#
gpiod_direction_input_nonotify: 核心逻辑与硬件适配) - [`gpiod_direction_input`: 公共API封装](#
gpiod_direction_input: 公共API封装)
- [`gpiod_direction_input_nonotify`: 核心逻辑与硬件适配](#
- [`gpiod_direction_output`及相关函数: 设置GPIO为输出模式的层次化实现](#
gpiod_direction_output及相关函数: 设置GPIO为输出模式的层次化实现) -
- [`gpiod_direction_output_raw_commit`: 执行硬件配置的底层核心](#
gpiod_direction_output_raw_commit: 执行硬件配置的底层核心) - [`gpiod_direction_output_nonotify`: 逻辑层核心 (处理特殊模式和安全检查)](#
gpiod_direction_output_nonotify: 逻辑层核心 (处理特殊模式和安全检查)) - [`gpiod_direction_output` 和 `gpiod_direction_output_raw`: 公共API](#
gpiod_direction_output和gpiod_direction_output_raw: 公共API)
- [`gpiod_direction_output_raw_commit`: 执行硬件配置的底层核心](#
- [`gpiod_set_transitory`及相关函数: 配置GPIO状态的持久性](#
gpiod_set_transitory及相关函数: 配置GPIO状态的持久性) -
- [`gpiod_set_transitory`: 设置引脚状态是否为瞬态的公共API](#
gpiod_set_transitory: 设置引脚状态是否为瞬态的公共API) - [`gpio_set_config_with_argument_optional`: "可选地"应用配置](#
gpio_set_config_with_argument_optional: "可选地"应用配置) - [`gpio_set_config_with_argument` 和 `gpio_do_set_config`: 打包并分发配置](#
gpio_set_config_with_argument和gpio_do_set_config: 打包并分发配置)
- [`gpiod_set_transitory`: 设置引脚状态是否为瞬态的公共API](#
- [gpiod_configure_flags: 集中式GPIO配置核心辅助函数](#gpiod_configure_flags: 集中式GPIO配置核心辅助函数)
- [gpiod_find_and_request: GPIO 获取、请求与配置的核心引擎](#gpiod_find_and_request: GPIO 获取、请求与配置的核心引擎)
- [gpiod_get API: 获取GPIO描述符的 layered Convenience Wrappers](#gpiod_get API: 获取GPIO描述符的 layered Convenience Wrappers)
-
- [`gpiod_get_index`: 获取多索引GPIO的基础函数](#
gpiod_get_index: 获取多索引GPIO的基础函数) - [`gpiod_get_index_optional`: 获取可选的多索引GPIO](#
gpiod_get_index_optional: 获取可选的多索引GPIO) -
- [`gpiod_get_optional`: 获取单个可选GPIO (最常用)](#
gpiod_get_optional: 获取单个可选GPIO (最常用))
- [`gpiod_get_optional`: 获取单个可选GPIO (最常用)](#
- [`gpiod_get_index`: 获取多索引GPIO的基础函数](#
- [GPIO 描述符消费者名称设置:gpiod_set_consumer_name](#GPIO 描述符消费者名称设置:gpiod_set_consumer_name)
- [`gpiod_set_value_cansleep` & `gpiod_set_value_nocheck`: GPIO描述符的值设定与电气特性处理](#
gpiod_set_value_cansleep&gpiod_set_value_nocheck: GPIO描述符的值设定与电气特性处理) - [`gpiod_set_raw_value_commit` & `gpio_set_open_drain/source_value_commit`: GPIO值设定的底层硬件提交](#
gpiod_set_raw_value_commit&gpio_set_open_drain/source_value_commit: GPIO值设定的底层硬件提交) - [`gpiod_set_array_value_complex`: GPIO批量设定的快速与慢速路径](#
gpiod_set_array_value_complex: GPIO批量设定的快速与慢速路径) - [`gpiod_get_value` & `gpiod_get_array_value`: GPIO值的读取与批量优化](#
gpiod_get_value&gpiod_get_array_value: GPIO值的读取与批量优化)
- [drivers/gpio/gpiolib-cdev.c GPIO字符设备接口(GPIO Character Device Interface) 现代用户空间GPIO访问的标准](#drivers/gpio/gpiolib-cdev.c GPIO字符设备接口(GPIO Character Device Interface) 现代用户空间GPIO访问的标准)
-
- [`lineinfo_watch_poll`: 等待GPIO事件](#
lineinfo_watch_poll: 等待GPIO事件) - [`lineinfo_watch_read`: 读取GPIO事件](#
lineinfo_watch_read: 读取GPIO事件) - [gpio_chrdev_open: 打开GPIO字符设备](#gpio_chrdev_open: 打开GPIO字符设备)
- GPIO字符设备接口的注册与注销
-
- [`gpio_fileops`: 文件操作函数集](#
gpio_fileops: 文件操作函数集) - [`gpiolib_cdev_register`: 注册GPIO字符设备](#
gpiolib_cdev_register: 注册GPIO字符设备) - [`gpiolib_cdev_unregister`: 注销GPIO字符设备](#
gpiolib_cdev_unregister: 注销GPIO字符设备)
- [`gpio_fileops`: 文件操作函数集](#
- [drivers/gpio/gpiolib-devres.c gpio 安全、自动管理的IO内存](#drivers/gpio/gpiolib-devres.c gpio 安全、自动管理的IO内存)
的内核对象gdev,并完成以下三件关键事情:
- 初始化设备对象 : 确保
gdev中的struct device成员被正确初始化。 - 创建字符设备 : 在
/dev目录下创建对应的gpiochipN字符设备节点,使用户空间程序可以通过文件I/O操作来访问GPIO。 - 创建
sysfs接口 : 在/sys/class/gpio目录下创建对应的gpiochipN目录和属性文件,提供一种基于文件的、用于管理和调试GPIO的接口。
c
/*
* 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函数来完成其设备节点的最终设置。
c
/*
* 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驱动)能够在这个舞台上注册自己,并以一种标准化的方式向用户空间暴露接口。
c
/*
* 定义一个静态的 '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这样内存资源宝贵的嵌入式系统中尤其重要。
c
/*
* 定义一个静态的、常量类型的 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_device和gpiolib创建的gpio_device)。fwnode->dev通常指向最主要的那个设备对象(即platform_device)。这个判断的意图是: 只有当这个gpio_device是其固件节点所代表的主要设备时, 才允许匹配。它防止了通用的"gpio总线驱动"错误地绑定到一个已经被更具体的平台驱动所拥有的硬件上, 是一种避免潜在逻辑冲突的保护机制。return 1;表示匹配成功。return 0;表示匹配失败。
c
/*
* 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指定为本总线的官方匹配规则。当任何驱动或设备尝试在此总线上进行绑定时, 内核都会调用这个函数来做决定。
c
/*
* 定义一个静态的、常量类型的 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设备链表, 以寻找第一个足够大的、未被占用的连续编号"空隙"。
工作流程详解:
- 初始化 : 函数从一个预定义的动态分配起始点
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端口的平台上, 是至关重要的。
c
/*
* 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宏在函数开头就强制检查了这一前提条件。
工作流程与算法详解:
该函数的算法是一个为链表插入优化的"寻找间隙"过程:
-
空列表处理 (最快路径) : 如果全局链表
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的注册过程非常高效。
c
/*
* 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则负责释放。
c
/* 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中"打孔"。
c
/* 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调用的主协调函数。
c
/* 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的查询能力。
c
/* 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属性不存在时, 它才会调用那个遗留的回调函数, 以提供向后兼容性。
c
/* 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
一个简单的谓词函数, 用于判断是否应使用层次化模型。
c
/* 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结构体填充一组标准的、预设的回调函数。
c
/* 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
创建层次化中断域的主函数。
c
/* 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
负责解析来自设备树的中断请求。
c
/* 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, 并递归地向父域申请资源。
c
/* 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。
c
/* 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)支持函数
c
/* 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, 即引脚偏移量)的完整映射关系。它负责完成所有必要的软件配置, 为即将到来的中断做好准备。
原理与工作流程:
- 获取上下文 : 从中断域的私有数据
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标志(用于支持嵌套的线程化中断处理器)。
c
/**
* 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阶段建立的软件关联。
c
/**
* 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, 表示"这个中断请求是给我的"。
c
/**
* 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中断域提供了一套标准的、通用的操作函数集。它就像是中断域的"行为手册", 告诉内核在对该域进行各种操作时应该调用哪些函数。
c
/*
* 定义一个静态的、常量类型的 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控制器提供了一个便捷的创建接口。
c
/*
* 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引脚被用作中断源的整个生命周期中, 对其进行状态跟踪和访问控制。这包括:
- 锁定/解锁: 当引脚被请求为中断时, 将其"锁定", 防止它同时被用作普通的输入/输出引脚, 从而避免了功能冲突。
- 模块引用计数: 确保提供GPIO控制器功能的内核模块在使用期间不会被意外卸载。
- 使能/禁用状态跟踪: 在引脚的描述符中维护一个软件状态位, 记录该中断是处于使能还是禁用状态。
gpiochip_lock_as_irq: 将GPIO引脚锁定为中断模式
当一个驱动程序请求一个GPIO引脚作为中断源时(通常在request_irq的调用链深处), irqchip子系统的request_resources回调函数(即gpiolib的gpiochip_reqres_irq)最终会调用此函数。
c
/**
* 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回调函数(即gpiolib的gpiochip_relres_irq)会调用此函数。
c
/**
* 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_resources和release_resources回调函数的标准实现。它们负责一个GPIO引脚作为中断源的"获取"和"释放"操作。
c
/**
* 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_chip的mask/unmask回调函数的包装器所调用。它们只负责在gpiolib的软件层面更新中断的使能状态 , 而不直接操作硬件 。硬件的屏蔽/解屏蔽操作由irq_chip回调本身负责。
c
/**
* 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。
c
/**
* 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的通用操作。
c
/* 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的安装与初始化
这组函数负责将上述所有部分组装起来, 完成irqchip在gpiolib中的注册。
c
/* 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内核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号。
c
/*
* 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控制器只存在于内核的内存中; 在此函数执行之后, 它就成为了一个用户空间工具(如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依次重复。
c
/*
* 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系统、设备树)一一链接起来。
c
/* 这两个宏为调用者提供了更简洁的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: 核心注册函数
工作流程概览:
- 内部对象创建 : 函数首先为内核创建一个内部的
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。
c
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。此函数正是负责处理这种差异的中间人。
其工作流程非常直接, 作为一个多路分发器:
- 接收通用句柄 : 它接收一个
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来完成实际的查找工作。
c
/*
* 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脚)。
该函数的原理非常直接, 并且以线程安全为核心:
- 获取全局锁 : 它首先获取一个全局互斥锁
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。
c
/**
* 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语言查找表之间的桥梁。
工作流程详解:
-
同步与安全 : 函数的第一步是获取一个全局互斥锁
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的执行通常意味着设备树配置存在问题或不完整。
c
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引脚标记为"已使用"的底层操作。它的原理是通过一个原子操作来确保排他性, 并调用底层硬件驱动提供的可选回调函数, 同时在任何失败的情况下都能安全地回滚所有状态变更。
c
/*
* 这些"可选的"分配调用有助于防止驱动程序之间互相干扰,
* 并在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的基础上, 增加了对内核模块生命周期的管理。
c
/*
* 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内核驱动框架的几个核心思想:
- 抽象与封装 : 上层驱动(如
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输出电平的硬件调度函数
c
/*
* 静态函数: 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方向的硬件调度函数
c
/*
* 静态函数: 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为输入的硬件调度函数
c
/*
* 静态函数: 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为输出的硬件调度函数
c
/*
* 静态函数: 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驱动交互, 并处理了各种可能的硬件驱动实现方式。
c
/*
* 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。它的作用很简单: 调用核心逻辑函数, 并在成功后向用户空间发送状态变更通知。
c
/**
* 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驱动程序交互, 发出将引脚设置为输出模式并赋予初始值的硬件命令 。它的原理是适配不同的硬件驱动能力, 并原子性地更新软件状态。
c
/*
* 静态函数声明: 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: 逻辑层核心 (处理特殊模式和安全检查)
这个函数是整个逻辑的核心。它的作用是处理所有与软件相关的复杂性, 包括逻辑电平转换、开漏/开源模式的硬件支持或软件仿真, 以及关键的安全检查。
c
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_output 和 gpiod_direction_output_raw: 公共API
这两个函数是暴露给驱动程序使用的顶层API。它们非常相似, 都是简单地调用它们各自的_nonotify或_commit版本, 然后在成功后发送一个通知, 通常用于更新用户空间的状态。
c
/**
* 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)地尝试将这个配置应用到底层的物理硬件上。
c
/**
* 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的直接辅助函数。它的核心作用是尝试应用一个配置, 但如果底层硬件明确表示"不支持"该功能, 则将其视为成功, 而不是一个错误。
c
/*
* 静态函数声明: 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_argument 和 gpio_do_set_config: 打包并分发配置
gpio_set_config_with_argument是一个简单的转换器, 而gpio_do_set_config是最终与硬件驱动程序交互的网关。
c
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)上。它负责处理引脚的所有关键电气特性(如逻辑电平、开漏/开源、上下拉)以及其工作方向(输入/输出)。
该函数的原理可以分解为两个主要阶段:
-
应用静态电气特性 (
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等寄存器的精确配置。
c
/**
* 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)。
这是一个高度复杂的函数, 其内部原理融合了多种内核机制:
-
分层查找策略 (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引脚释放回池中, 从而确保系统状态的一致性。
- 关键的错误处理 : 如果配置步骤失败, 函数会立即调用
c
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位的并行数据总线)是必不可少的。
原理:
- 它首先获取设备的固件节点(
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)或发生其他问题, 它会返回相应的错误码。
- 成功 : 返回一个有效的
c
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的便利封装。它的核心作用是改变"未找到"这种情况下的返回值, 使其对驱动开发者更友好。
原理:
- 它直接调用
gpiod_get_index来执行获取操作。 - 然后, 它检查返回值。它使用
gpiod_not_found(desc)(内部等价于PTR_ERR(desc) == -ENOENT)来专门判断失败的原因是否是"未找到"。 - 如果是因为"未找到"而失败, 它会抑制这个错误, 并返回
NULL。 - 如果是因为其他原因(如
-EBUSY)而失败, 它会保留并返回原始的错误指针。
这个函数对于处理硬件设计上可选的GPIO引脚极为有用。驱动程序可以通过检查返回值是否为NULL来知道该可选功能是否存在, 从而调整自身行为, 而无需处理-ENOENT这个特定的错误码。
c
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属性, 驱动程序也能正常工作(只是没有使能控制功能), 从而大大增强了硬件描述的灵活性。
c
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);
GPIO 描述符消费者名称设置:gpiod_set_consumer_name
本代码片段展示了 Linux 内核 GPIO 子系统中用于为一个 GPIO 描述符(gpio_desc)设置其"消费者"(consumer)名称 的核心函数。其主要功能是:允许驱动程序在获取到一个 GPIO 后,为其动态地关联一个描述性的字符串标签 。这个标签在调试(例如,通过 debugfs 查看 GPIO 状态)和系统 introspection 中非常有用,可以清晰地表明哪个驱动正在使用哪个 GPIO。该函数使用 RCU 和 SRCU (Sleepable RCU) 机制来保证标签更新的并发安全性。
代码分析
c
/**
* @brief desc_free_label - RCU 回调函数,用于安全地释放旧的 GPIO 标签。
* @param rh: 指向包含在 gpio_desc_label 结构中的 rcu_head。
*/
static void desc_free_label(struct rcu_head *rh)
{
// 从 rcu_head 指针反向推导出其所属的 gpio_desc_label 结构,并释放它。
kfree(container_of(rh, struct gpio_desc_label, rh));
}
/**
* @brief desc_set_label - 安全地更新一个 GPIO 描述符的标签。
* @param desc: 要更新的 GPIO 描述符。
* @param label: 新的标签字符串,如果为 NULL,则删除标签。
* @return int: 成功返回0,失败返回 -ENOMEM。
*/
static int desc_set_label(struct gpio_desc *desc, const char *label)
{
struct gpio_desc_label *new = NULL, *old;
// 如果提供了新的标签字符串...
if (label) {
// ...则分配一个新的 gpio_desc_label 结构,其大小足以容纳该字符串。
new = kzalloc(struct_size(new, str, strlen(label) + 1),
GFP_KERNEL);
if (!new)
return -ENOMEM;
// 复制新的标签字符串到新分配的结构中。
strcpy(new->str, label);
}
// 原子地将 desc->label 指针替换为 new,并获取旧指针 old。
old = rcu_replace_pointer(desc->label, new, 1);
// 如果存在一个旧的标签...
if (old)
// ...则使用 call_srcu 安排一个在所有读者都退出后才执行的安全释放操作。
call_srcu(&desc->gdev->desc_srcu, &old->rh, desc_free_label);
return 0;
}
/**
* @brief gpiod_line_state_notify - 广播一个 GPIO 线路状态变更通知。
* @param desc: 发生变化的 GPIO 描述符。
* @param action: 变更的类型 (一个 GPIO_V2_LINE_CHANGED_* 常量)。
*/
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);
}
/**
* @brief gpiod_set_consumer_name - 为一个 GPIO 描述符设置消费者名称。
* @param desc: 要设置消费者名称的 GPIO 描述符。
* @param name: 新的消费者名称。
* @return int: 成功返回0,否则返回负数错误码。
*/
int gpiod_set_consumer_name(struct gpio_desc *desc, const char *name)
{
int ret;
// 验证描述符是否有效 (一个宏,未在此展示)。
VALIDATE_DESC(desc);
// 调用核心函数来安全地更新标签。
ret = desc_set_label(desc, name);
if (ret == 0)
// 如果成功,则广播一个配置变更通知。
gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG);
return ret;
}
EXPORT_SYMBOL_GPL(gpiod_set_consumer_name);
gpiod_set_value_cansleep & gpiod_set_value_nocheck: GPIO描述符的值设定与电气特性处理
本代码片段展示了Linux内核中现代、基于描述符的GPIO接口(gpiod)的核心功能:设定一个GPIO引脚的逻辑值。其主要功能是通过gpiod_set_value_cansleep这个公共API,将一个抽象的逻辑值(1/0,代表开/关)转化为符合具体引脚电气特性(如高电平有效/低电平有效、推挽输出/开漏输出)的物理电平操作。
实现原理分析
该机制的核心是抽象。它将驱动开发者从关心具体GPIO引脚的电气细节中解放出来,仅需操作逻辑值即可。所有的电气特性转换都在GPIO核心层内部完成。
-
公共API (
gpiod_set_value_cansleep):- 这是提供给普通设备驱动程序使用的标准接口,其名称明确表示它只能在可以睡眠的上下文中使用(例如,内核线程或系统调用处理路径)。
might_sleep(): 这是一个用于调试和内核锁验证(lockdep)的宏。它静态地声明了当前代码路径可能会发生阻塞,如果它被错误地用在原子上下文中(如中断处理程序),内核在调试模式下会发出警告。VALIDATE_DESC(desc): 这是一个安全检查宏,用于确保传入的gpio_desc指针是有效的,防止因无效指针导致的内核崩溃。- 它本身不包含逻辑,只是在执行安全检查后,调用内部的核心实现函数
gpiod_set_value_nocheck。
-
核心逻辑 (
gpiod_set_value_nocheck):- 此函数是实现"逻辑值"到"物理值"转换的核心。
- 处理低电平有效 (Active-Low) :
- 首先,它检查GPIO描述符的
flags中是否设置了GPIOD_FLAG_ACTIVE_LOW标志。 - 如果设置了该标志,意味着此GPIO是低电平有效的(例如,一个LED在引脚输出低电平时点亮)。此时,它会将输入的逻辑值取反(
value = !value)。例如,逻辑上的"开"(value=1)会被转换为物理上的"低电平"(value=0)。
- 首先,它检查GPIO描述符的
- 处理开漏/开源 (Open-Drain/Open-Source) :
- 接着,它检查是否设置了
GPIOD_FLAG_OPEN_DRAIN或GPIOD_FLAG_OPEN_SOURCE标志。这两种是特殊的输出模式,与标准的推挽(Push-Pull)模式不同。 - 开漏 (Open-Drain) : 引脚只能主动将电平拉低到地,不能主动拉高。要输出高电平,它会将引脚置于高阻态(Hi-Z),依赖外部的上拉电阻将电平拉高。
gpio_set_open_drain_value_commit函数会处理这种逻辑(例如,逻辑0-> 物理拉低,逻辑1-> 高阻态)。 - 开源 (Open-Source): 与开漏相反,只能主动拉高,依赖外部下拉电阻拉低。
- 接着,它检查是否设置了
- 处理标准推挽 (Push-Pull) :
- 如果上述特殊模式均未设置,则GPIO为标准的推挽输出模式。此时,
gpiod_set_raw_value_commit会被调用,它会直接将经过低电平有效逻辑转换后的物理值写入硬件寄存器。
- 如果上述特殊模式均未设置,则GPIO为标准的推挽输出模式。此时,
代码分析
c
/**
* @brief 在不进行检查的情况下,设置一个GPIO线路的值。
* @param desc 要设置其值的描述符。
* @param value 要设置的值(逻辑值,0或1)。
*
* @details 该函数设置一个GPIO描述符对应的线路值,并会自动处理
* 诸如低电平有效、开漏/开源等电气特性。
*
* @return int 成功返回0,失败返回负数错误码。
*/
static int gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
/* 检查描述符的flags成员中是否设置了"低电平有效"标志。 */
if (test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
/* 如果是,则将逻辑值取反,以计算出物理电平值。*/
value = !value;
/* 检查是否为"开漏"输出模式。 */
if (test_bit(GPIOD_FLAG_OPEN_DRAIN, &desc->flags))
/* 如果是,则调用专门的函数来处理开漏的值设定。*/
return gpio_set_open_drain_value_commit(desc, value);
/* 检查是否为"开源"输出模式。 */
else if (test_bit(GPIOD_FLAG_OPEN_SOURCE, &desc->flags))
/* 如果是,则调用专门的函数来处理开源的值设定。*/
return gpio_set_open_source_value_commit(desc, value);
/* 如果是标准的推挽模式,则直接提交原始(经过低电平有效转换后)的物理值。 */
return gpiod_set_raw_value_commit(desc, value);
}
/**
* @brief 设定一个gpio的值(可在会睡眠的上下文中使用)。
* @param desc 要设定其值的gpio描述符。
* @param value 要设定的值。
*
* @details 该函数设定GPIO的逻辑值,即它会自动考虑ACTIVE_LOW(低电平有效)
* 状态。此函数应在可以睡眠的上下文(非原子上下文)中调用。
*
* @return int 成功返回0,失败返回负数错误码。
*/
int gpiod_set_value_cansleep(struct gpio_desc *desc, int value)
{
/* 标记此函数上下文可以睡眠,用于调试和静态分析。 */
might_sleep();
/* 校验传入的描述符指针是否有效,防止空指针解引用。 */
VALIDATE_DESC(desc);
/* 调用内部核心函数来执行实际的设值操作。 */
return gpiod_set_value_nocheck(desc, value);
}
/* 将该函数导出,以便内核模块可以使用。 */
EXPORT_SYMBOL_GPL(gpiod_set_value_cansleep);
gpiod_set_raw_value_commit & gpio_set_open_drain/source_value_commit: GPIO值设定的底层硬件提交
本代码片段展示了gpiod子系统在处理完逻辑值转换后,最终与硬件交互的三个核心"提交"函数。它们分别对应推挽(Push-Pull)、开漏(Open-Drain)和开源(Open-Source)三种不同的GPIO输出模式。gpiod_set_raw_value_commit是标准推挽模式的直接设值,而gpio_set_open_drain_value_commit和gpio_set_open_source_value_commit则通过一种精巧的技术------动态改变引脚方向------来模拟开漏/开源的电气行为。
实现原理分析
此机制的核心在于将高级的GPIO设值请求,转化为对底层gpio_chip驱动提供的、最基础的回调函数(set、direction_input、direction_output)的调用序列。
-
推挽模式 (
gpiod_set_raw_value_commit):- 这是最直接的情况。它首先进行一个安全检查,确保该GPIO引脚已经被配置为输出模式(
GPIOD_FLAG_IS_OUT)。如果不是,则返回权限错误(-EPERM),因为它只负责设值,不负责改变方向。 - 它通过
gpio_chip_guard安全地获取到底层的gpio_chip控制器。 - 最终,它调用
gpiochip_set(),这会直接映射到gpio_chip操作集中的.set()回调函数,将物理值写入硬件寄存器。
- 这是最直接的情况。它首先进行一个安全检查,确保该GPIO引脚已经被配置为输出模式(
-
开漏模式 (
gpio_set_open_drain_value_commit):- 这是最精巧的部分。开漏输出的物理特性是:只能主动拉低电平(输出0),不能主动拉高。要实现逻辑上的"高电平",它必须将引脚置于高阻态(Hi-Z),让外部的上拉电阻将线路电平拉高。
- 实现技巧 : 该函数通过改变引脚的方向 来模拟这一行为:
- 设置逻辑高 (
value = 1) : 调用gpiochip_direction_input()。将引脚配置为输入模式,使其进入高阻态,从而释放总线,让外部上拉电阻生效。 - 设置逻辑低 (
value = 0) : 调用gpiochip_direction_output(..., 0)。将引脚配置为输出模式,并立即将其值设为低电平,主动将总线拉低。
- 设置逻辑高 (
- 这种软件层面的模拟使得任何一个支持基本的输入/输出方向切换的GPIO控制器,都能支持开漏模式,即使硬件本身没有原生的开漏输出功能。
-
开源模式 (
gpio_set_open_source_value_commit):- 这与开漏模式完全相反。开源输出只能主动拉高电平,依赖外部下拉电阻来拉低。
- 实现技巧 :
- 设置逻辑高 (
value = 1) : 调用gpiochip_direction_output(..., 1)。将引脚配置为输出模式,并设为高电平。 - 设置逻辑低 (
value = 0) : 调用gpiochip_direction_input()。将引脚配置为输入模式,进入高阻态,让外部下拉电阻生效。
- 设置逻辑高 (
代码分析
c
/*
* Return the GPIO number of the passed descriptor relative to its chip
*/
static inline int gpio_chip_hwgpio(const struct gpio_desc *desc)
{
return desc - &desc->gdev->descs[0];
}
/**
* @brief 提交开漏GPIO的值。
* @param desc 需要设置状态的GPIO描述符。
* @param value 非零表示设置高电平,零表示设置低电平。
* @return int 成功返回0,失败返回错误码。
*/
static int gpio_set_open_drain_value_commit(struct gpio_desc *desc, bool value)
{
int ret = 0, offset = gpio_chip_hwgpio(desc);
/* 使用RAII风格的守卫,安全地获取gpio_chip控制器。 */
CLASS(gpio_chip_guard, guard)(desc);
/* 如果没有找到对应的控制器,返回设备未找到错误。 */
if (!guard.gc)
return -ENODEV;
if (value) {
/* 设置逻辑高电平:将引脚方向设为输入。*/
/* 这会使引脚进入高阻态,从而由外部的上拉电阻将线路拉高。*/
ret = gpiochip_direction_input(guard.gc, offset);
} else {
/* 设置逻辑低电平:将引脚方向设为输出,并输出低电平。*/
ret = gpiochip_direction_output(guard.gc, offset, 0);
if (!ret)
/* 成功后,更新描述符中的状态标志,记为输出。*/
set_bit(GPIOD_FLAG_IS_OUT, &desc->flags);
}
/* 记录一次GPIO方向改变的追踪事件。 */
trace_gpio_direction(desc_to_gpio(desc), value, ret);
if (ret < 0)
gpiod_err(desc,
"%s: 设置开漏值时出错,错误码 %d\n",
__func__, ret);
return ret;
}
/**
* @brief 提交开源GPIO的值。
* @param desc 需要设置状态的GPIO描述符。
* @param value 非零表示设置高电平,零表示设置低电平。
* @return int 成功返回0,失败返回错误码。
*/
static int gpio_set_open_source_value_commit(struct gpio_desc *desc, bool value)
{
int ret = 0, offset = gpio_chip_hwgpio(desc);
/* 安全地获取gpio_chip控制器。 */
CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV;
if (value) {
/* 设置逻辑高电平:将引脚方向设为输出,并输出高电平。*/
ret = gpiochip_direction_output(guard.gc, offset, 1);
if (!ret)
/* 成功后,更新描述符中的状态标志,记为输出。*/
set_bit(GPIOD_FLAG_IS_OUT, &desc->flags);
} else {
/* 设置逻辑低电平:将引脚方向设为输入。*/
/* 这会使引脚进入高阻态,从而由外部的下拉电阻将线路拉低。*/
ret = gpiochip_direction_input(guard.gc, offset);
}
/* 记录一次GPIO方向改变的追踪事件。 */
trace_gpio_direction(desc_to_gpio(desc), !value, ret);
if (ret < 0)
gpiod_err(desc,
"%s: 设置开源值时出错,错误码 %d\n",
__func__, ret);
return ret;
}
/**
* @brief 提交原始(推挽)GPIO的值。
* @param desc GPIO描述符。
* @param value 要设置的物理值(0或1)。
* @return int 成功返回0,失败返回错误码。
*/
static int gpiod_set_raw_value_commit(struct gpio_desc *desc, bool value)
{
/* 这是一个快速路径检查:如果描述符未标记为输出,则返回权限错误。*/
/* 这个函数只负责设值,不负责改变方向。*/
if (unlikely(!test_bit(GPIOD_FLAG_IS_OUT, &desc->flags)))
return -EPERM;
/* 安全地获取gpio_chip控制器。 */
CLASS(gpio_chip_guard, guard)(desc);
if (!guard.gc)
return -ENODEV;
/* 记录一次GPIO值设定的追踪事件。 */
trace_gpio_value(desc_to_gpio(desc), 0, value);
/* 调用底层gpio_chip的.set回调函数,直接操作硬件。 */
return gpiochip_set(guard.gc, gpio_chip_hwgpio(desc), value);
}
gpiod_set_array_value_complex: GPIO批量设定的快速与慢速路径
本代码片段是gpiod子系统中所有批量GPIO输出功能的最终核心实现 ------gpiod_set_array_value_complex。其核心功能是接收一个GPIO描述符数组和一个值位图,然后尽可能高效地 将这些值应用到物理硬件上。为了实现高效,它设计了一个复杂的双路径机制:一个针对"理想情况"的快速路径(fast path) ,以及一个更通用但稍慢的慢速路径(slow path)。
实现原理分析
此机制的目标是在保证正确处理各种GPIO电气特性(如低电平有效、开漏等)的前提下,最大限度地减少函数调用开销和寄存器访问次数。
-
快速路径 (Fast Path):
- 触发条件 :
if (array_info && ...)。这个路径只有在调用者提供了一个有效的array_info结构体时才会被激活。这个array_info通常由gpiod_get_array()函数在获取GPIO数组时预先计算和填充好。 - 理想情况 : 快速路径假设所有要操作的GPIO都属于同一个
gpio_chip,并且它们都是标准的推挽输出。 - 预计算的优势 :
array_info中包含了预先计算好的信息,如set_mask(一个位图,标记了数组中哪些GPIO是标准推挽输出)和invert_mask(标记了哪些GPIO是低电平有效的)。 - 实现 :
bitmap_xor(value_bitmap, ...): 一次性 处理所有低电平有效的引脚。它将输入的逻辑值位图与invert_mask进行异或(XOR)操作,从而将所有需要反转的位一次性全部翻转,直接得到最终的物理值位图。gpiochip_set_multiple(gc, array_info->set_mask, value_bitmap): 单次调用 底层gpio_chip的.set_multiple回调。set_mask告诉回调函数需要操作哪些引脚,value_bitmap则提供了这些引脚的目标电平。
- 效率: 整个过程只需要一次位图运算和一次对底层驱动的调用,这是所能达到的最高效率。
- 触发条件 :
-
慢速路径 (Slow Path):
- 触发条件 : 如果没有提供
array_info,或者快速路径处理完后仍然有剩余的GPIO(例如,数组中包含来自不同gpio_chip的引脚,或者包含了开漏/开源引脚),就会进入慢速路径。 - 职责 : 处理混合了不同
gpio_chip、不同输出类型的复杂GPIO数组。 - 实现 (分组循环) :
while (i < array_size): 这是一个外层循环,用于处理数组中所有尚未处理的GPIO。do { ... } while (... && gpio_device_chip_cmp(...)): 这是一个内层循环,它的作用是按gpio_chip对GPIO进行分组 。它会一直向后遍历,直到遇到一个属于不同gpio_chip的GPIO为止。- 在分组内处理 :
- 个别处理 : 对于开漏(Open-Drain)和 开源(Open-Source)的引脚,它会退化为调用
gpio_set_open_drain/source_value_commit进行逐个设置 。因为这两种模式可能需要改变引脚方向,无法简单地通过一次set_multiple来完成。 - 批量聚合 : 对于组内所有标准的推挽输出 引脚,它会将它们的硬件引脚号(
hwgpio)和目标值聚合到临时的mask和bits位图中。
- 个别处理 : 对于开漏(Open-Drain)和 开源(Open-Source)的引脚,它会退化为调用
- 提交分组 : 内层循环结束后,如果
count不为0(表示聚合到了至少一个推挽输出),它就会调用一次gpiochip_set_multiple(guard.gc, mask, bits)来批量提交这个分组的所有引脚。
- 效率 : 慢速路径的效率低于快速路径,因为它可能需要多次调用
gpiochip_set_multiple(每个gpio_chip分组一次),并且还需要对特殊类型的引脚进行单独处理。但它保证了功能的通用性 和正确性。
- 触发条件 : 如果没有提供
代码分析
c
static int gpiochip_set(struct gpio_chip *gc, unsigned int offset, int value)
{
int ret;
lockdep_assert_held(&gc->gpiodev->srcu);
if (WARN_ON(unlikely(!gc->set)))
return -EOPNOTSUPP;
ret = gc->set(gc, offset, value);
if (ret > 0)
ret = -EBADE;
return ret;
}
/*
* set multiple outputs on the same chip;
* use the chip's set_multiple function if available;
* otherwise set the outputs sequentially;
* @chip: the GPIO chip we operate on
* @mask: bit mask array; one bit per output; BITS_PER_LONG bits per word
* defines which outputs are to be changed
* @bits: bit value array; one bit per output; BITS_PER_LONG bits per word
* defines the values the outputs specified by mask are to be set to
*
* Returns: 0 on success, negative error number on failure.
*/
static int gpiochip_set_multiple(struct gpio_chip *gc,
unsigned long *mask, unsigned long *bits)
{
unsigned int i;
int ret;
lockdep_assert_held(&gc->gpiodev->srcu);
if (gc->set_multiple) {
ret = gc->set_multiple(gc, mask, bits);
if (ret > 0)
ret = -EBADE;
return ret;
}
/* set outputs if the corresponding mask bit is set */
for_each_set_bit(i, mask, gc->ngpio) {
ret = gpiochip_set(gc, i, test_bit(i, bits));
if (ret)
break;
}
return ret;
}
/**
* @brief 为一个GPIO数组赋值(非睡眠版本)。
* @param array_size 数组大小。
* @param desc_array GPIO描述符数组。
* @param array_info (可选) 预计算的数组信息,用于快速路径。
* @param value_bitmap 要赋的值的位图。
* @return int 成功返回0,失败返回错误码。
*/
int gpiod_set_array_value(unsigned int array_size,
struct gpio_desc **desc_array,
struct gpio_array *array_info,
unsigned long *value_bitmap)
{
if (!desc_array)
return -EINVAL;
/* 调用核心实现函数,can_sleep=false, raw=false。 */
return gpiod_set_array_value_complex(false, false, array_size,
desc_array, array_info,
value_bitmap);
}
EXPORT_SYMBOL_GPL(gpiod_set_array_value);
/**
* @brief 批量设置GPIO值的核心实现函数。
* @param raw 是否为原始值(忽略ACTIVE_LOW等)。
* @param can_sleep 当前上下文是否可以睡眠。
* @param array_size 数组大小。
* @param desc_array GPIO描述符数组。
* @param array_info (可选) 预计算的数组信息。
* @param value_bitmap 值的位图。
* @return int 成功返回0,失败返回错误码。
*/
int gpiod_set_array_value_complex(bool raw, bool can_sleep,
unsigned int array_size,
struct gpio_desc **desc_array,
struct gpio_array *array_info,
unsigned long *value_bitmap)
{
struct gpio_chip *gc;
int i = 0, ret;
/* --- 快速路径 (Fast Path) --- */
/* 检查是否满足快速路径的条件(提供了有效的array_info)。 */
if (array_info && array_info->desc == desc_array &&
array_size <= array_info->size &&
(void *)array_info == desc_array + array_info->size) {
/* ... 安全检查 ... */
guard(srcu)(&array_info->gdev->srcu); /* 使用SRCU锁保护对chip的访问 */
gc = srcu_dereference(array_info->gdev->chip,
&array_info->gdev->srcu);
if (!gc)
return -ENODEV;
/* 如果不是原始值,并且存在需要反转的引脚... */
if (!raw && !bitmap_empty(array_info->invert_mask, array_size))
/* ...通过一次XOR操作,将所有低电平有效的引脚的值全部翻转。*/
bitmap_xor(value_bitmap, value_bitmap,
array_info->invert_mask, array_size);
/* 通过一次调用,批量设置所有标准推挽输出引脚的值。 */
ret = gpiochip_set_multiple(gc, array_info->set_mask,
value_bitmap);
if (ret)
return ret;
/* 查找第一个未被快速路径处理的引脚。 */
i = find_first_zero_bit(array_info->set_mask, array_size);
/* 如果所有引脚都被处理了,则成功返回。 */
if (i == array_size)
return 0;
} else {
array_info = NULL;
}
/* --- 慢速路径 (Slow Path) --- */
/* 从第一个未处理的引脚开始循环。 */
while (i < array_size) {
/* ... 声明临时位图变量 ... */
int count = 0;
/* 获取当前引脚所属的gpio_chip。 */
CLASS(gpio_chip_guard, guard)(desc_array[i]);
if (!guard.gc)
return -ENODEV;
/* ... 为位图分配内存(动态或静态)... */
/*
* 内层循环:聚合所有属于同一个gpio_chip的引脚。
*/
do {
/* ... 获取引脚描述符、硬件号和目标值 ... */
/* ... 检查是否为输出方向 ... */
/* 处理低电平有效(ACTIVE_LOW)的翻转。 */
if (!raw && /* ... */
test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
trace_gpio_value(desc_to_gpio(desc), 0, value);
/*
* 根据引脚类型进行分派:
* 开漏和开源引脚被单独处理。
*/
if (test_bit(GPIOD_FLAG_OPEN_DRAIN, &desc->flags) && !raw) {
gpio_set_open_drain_value_commit(desc, value);
} else if (test_bit(GPIOD_FLAG_OPEN_SOURCE, &desc->flags) && !raw) {
gpio_set_open_source_value_commit(desc, value);
} else {
/* 标准推挽输出:将其聚合到临时位图中。 */
__set_bit(hwgpio, mask);
__assign_bit(hwgpio, bits, value);
count++;
}
i++;
/* ... (处理array_info以跳过已处理的引脚) ... */
/* 只要还在数组范围内,并且下一个引脚属于同一个chip,就继续循环。*/
} while ((i < array_size) &&
gpio_device_chip_cmp(desc_array[i]->gdev, guard.gc));
/* 提交聚合的位图:批量设置这个分组的所有推挽输出。 */
if (count != 0) {
ret = gpiochip_set_multiple(guard.gc, mask, bits);
if (ret)
return ret;
}
/* ... 释放动态分配的位图内存 ... */
}
return 0;
}
gpiod_get_value & gpiod_get_array_value: GPIO值的读取与批量优化
本代码片段展示了Linux内核中现代GPIO接口(gpiod)用于读取 一个或多个GPIO引脚值的核心实现。它提供了一套分层的API,从读取单个逻辑值的gpiod_get_value,到底层硬件交互的gpiod_get_raw_value_commit,再到高效的批量读取函数gpiod_get_array_value_complex。其核心设计思想是抽象化 (隐藏低电平有效等细节)和性能优化(通过批量读取和快速路径)。
实现原理分析
此机制与gpiod_set_value系列函数在设计上是对称的。它将硬件的物理电平(高/低)翻译成软件层面的逻辑值(1/0)。
-
单值读取 (
gpiod_get_value):- 职责 : 获取单个GPIO的逻辑值。
- 实现 : 这是一个两步过程的封装:
- 读取物理值 : 调用
gpiod_get_raw_value_commit(desc)来获取引脚的原始物理电平。 - 翻译为逻辑值 :
if (test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags)) value = !value;如果该GPIO被标记为"低电平有效",则将读取到的物理值取反。例如,物理低电平(0)会被翻译成逻辑高(1)。
- 读取物理值 : 调用
gpiod_get_raw_value是gpiod_get_raw_value_commit的一个简单封装,增加了安全检查。
-
底层硬件交互 (
gpiod_get_raw_value_commit):- 职责: 直接从硬件读取单个引脚的物理电平。
- 实现 :
- 它通过
gpio_desc找到对应的gpio_device和gpio_chip。 guard(srcu): 使用SRCU读端锁来安全地解引用gpio_chip指针,防止在读取过程中gpio_chip被卸载。gpio_chip_get_value(gc, desc): 这是最终的硬件操作 。它调用底层gpio_chip驱动提供的.get或.get_value_from_reg回调函数,该函数会读取GPIO控制器的输入数据寄存器,并返回特定引脚的电平值。value = value < 0 ? value : !!value;: 这是一个规范化步骤。!!value可以将任何非零值转换为1,确保返回值是标准的0或1。
- 它通过
-
批量读取 (
gpiod_get_array_value_complex):- 职责: 高效地读取一个GPIO描述符数组中所有引脚的值,并将结果存入一个位图。
- 实现 : 与
gpiod_set_array_value_complex类似,它也采用了快速路径 和慢速路径的双路径设计。 - 快速路径 :
- 条件 : 调用者提供了有效的
array_info,且所有GPIO属于同一个gpio_chip。 gpio_chip_get_multiple(gc, array_info->get_mask, value_bitmap): 单次调用 底层驱动的.get_multiple回调。get_mask告诉驱动需要读取哪些引脚。驱动可以通过单次读取 整个GPIO端口的输入数据寄存器,然后用掩码提取出所有需要的值,并将结果直接填入value_bitmap。这是最高效的方式。bitmap_xor(...): 在从硬件获取到所有物理值后,通过一次位图异或操作,将所有"低电平有效"的引脚的值进行翻转,完成逻辑值的转换。
- 条件 : 调用者提供了有效的
- 慢速路径 :
- 条件 : GPIO来自不同的
gpio_chip。 - 分组聚合 :
do { ... } while (... && gpio_device_chip_cmp(...)): 同样按gpio_chip对数组中的GPIO进行分组。 - 批量读取分组 : 对于每个分组,它构建一个
mask位图,然后调用一次gpio_chip_get_multiple来读取该分组内所有引脚的物理值。 - 逐个翻译 : 在获取到一个分组的物理值后,它会遍历该分组的GPIO,逐个 检查
GPIOD_FLAG_ACTIVE_LOW标志,并进行逻辑值翻转,然后将最终结果设置到输出的value_bitmap中。
- 条件 : GPIO来自不同的
代码分析
c
/**
* @brief 从硬件提交并获取一个GPIO的原始值。
* @param desc GPIO描述符。
* @return int 物理电平值(0或1),或负数错误码。
*/
static int gpiod_get_raw_value_commit(const struct gpio_desc *desc)
{
/* ... */
/* 使用SRCU读端锁,安全地获取gpio_chip指针。 */
guard(srcu)(&gdev->srcu);
gc = srcu_dereference(gdev->chip, &gdev->srcu);
if (!gc)
return -ENODEV;
/* 调用底层驱动的.get回调函数来读取硬件。 */
value = gpio_chip_get_value(gc, desc);
/* 规范化返回值,确保是0或1。 */
value = value < 0 ? value : !!value;
trace_gpio_value(desc_to_gpio(desc), 1, value);
return value;
}
/**
* @brief 从一个gpio_chip中读取多个引脚的值。
* @param gc gpio_chip。
* @param mask 描述需要读取哪些引脚的位图(输入)。
* @param bits 用于存储读取结果的位图(输出)。
* @return int 成功返回0,失败返回错误码。
*/
static int gpio_chip_get_multiple(struct gpio_chip *gc,
unsigned long *mask, unsigned long *bits)
{
/* ... */
/* 如果驱动提供了高效的.get_multiple回调,则优先使用它。 */
if (gc->get_multiple) {
/* ... */
return gc->get_multiple(gc, mask, bits);
}
/* 如果没有.get_multiple,则回退到在循环中逐个调用.get。 */
if (gc->get) {
int i, value;
for_each_set_bit(i, mask, gc->ngpio) {
value = gpiochip_get(gc, i);
if (value < 0)
return value;
__assign_bit(i, bits, value);
}
return 0;
}
return -EIO;
}
/* ... */
/**
* @brief 批量读取GPIO值的核心实现。
* @param raw 是否读取原始值(忽略ACTIVE_LOW)。
* @param can_sleep 当前上下文是否可以睡眠。
* @param array_size 数组大小。
* @param desc_array GPIO描述符数组。
* @param array_info (可选) 用于快速路径的预计算信息。
* @param value_bitmap 用于存储结果的位图。
* @return int 成功返回0,失败返回错误码。
*/
int gpiod_get_array_value_complex(bool raw, bool can_sleep,
unsigned int array_size,
struct gpio_desc **desc_array,
struct gpio_array *array_info,
unsigned long *value_bitmap)
{
/* ... */
/* --- 快速路径 --- */
if (array_info && /* ... 检查是否满足快速路径条件 ... */) {
/* ... 安全检查 ... */
/* ... 安全地获取gpio_chip ... */
/* 通过一次调用,批量读取所有相关引脚的物理值。 */
ret = gpio_chip_get_multiple(gc, array_info->get_mask,
value_bitmap);
if (ret)
return ret;
/* 如果不是原始值,则通过一次位图XOR操作,处理所有低电平有效的引脚。 */
if (!raw && !bitmap_empty(array_info->invert_mask, array_size))
bitmap_xor(value_bitmap, value_bitmap,
array_info->invert_mask, array_size);
/* ... 检查是否所有引脚都已处理 ... */
}
/* ... */
/* --- 慢速路径 --- */
while (i < array_size) {
/* ... */
/* 按gpio_chip对引脚进行分组,并聚合到mask位图中。 */
do {
/* ... */
__set_bit(hwgpio, mask);
/* ... */
} while ((i < array_size) &&
gpio_device_chip_cmp(desc_array[i]->gdev, guard.gc));
/* 批量读取当前分组的物理值到bits位图中。 */
ret = gpio_chip_get_multiple(guard.gc, mask, bits);
/* ... 错误处理 ... */
/* 遍历刚刚读取的分组,逐个进行低电平有效转换,并存入最终的value_bitmap。 */
for (j = first; j < i; ) {
/* ... */
int value = test_bit(hwgpio, bits);
if (!raw && test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
__assign_bit(j, value_bitmap, value);
/* ... */
}
/* ... */
}
return 0;
}
/**
* @brief 获取一个gpio的原始物理值。
* @param desc GPIO描述符。
* @return int 物理电平(0或1),或负数错误码。
*/
int gpiod_get_raw_value(const struct gpio_desc *desc)
{
VALIDATE_DESC(desc);
/* ... 警告检查 ... */
return gpiod_get_raw_value_commit(desc);
}
EXPORT_SYMBOL_GPL(gpiod_get_raw_value);
/**
* @brief 获取一个gpio的逻辑值。
* @param desc GPIO描述符。
* @return int 逻辑值(0或1),或负数错误码。
*/
int gpiod_get_value(const struct gpio_desc *desc)
{
int value;
/* ... */
/* 首先获取原始物理值。 */
value = gpiod_get_raw_value_commit(desc);
if (value < 0)
return value;
/* 如果是低电平有效,则将物理值取反得到逻辑值。 */
if (test_bit(GPIOD_FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
return value;
}
EXPORT_SYMBOL_GPL(gpiod_get_value);
/* ... (批量读取API的简单封装) ... */
drivers/gpio/gpiolib-cdev.c GPIO字符设备接口(GPIO Character Device Interface) 现代用户空间GPIO访问的标准
历史与背景
这项技术是为了解决什么特定问题而诞生的?
gpiolib-cdev.c 实现的字符设备接口是为了解决一个长期存在的问题:如何为用户空间提供一个稳定、安全、功能丰富的GPIO(通用输入/输出)访问标准。
在gpiolib-cdev出现之前,用户空间访问GPIO的主要方式是通过sysfs接口 (/sys/class/gpio/)。这种老旧的方式存在诸多严重缺陷:
- 不稳定:GPIO的编号在不同内核版本或硬件平台上可能会改变。
- 功能有限:只支持基本的方向设置(输入/输出)和值读写,不支持开漏(open-drain)、开源(open-source)、中断等待等高级功能。
- 存在竞态条件:导出(export)GPIO、设置方向、读取值的操作不是原子性的,多个进程同时操作同一个GPIO很容易出错。
- 生命周期管理混乱:一个进程导出了一个GPIO,但如果它崩溃了,这个GPIO会一直保持导出状态,可能导致资源泄漏。
- 即将废弃 :由于上述缺陷,sysfs GPIO接口已被官方明确标记为废弃(deprecated)。
gpiolib-cdev接口的诞生就是为了彻底取代sysfs ,提供一个基于标准字符设备和ioctl系统调用的、健壮的现代API,来解决所有这些问题。
它的发展经历了哪些重要的里程碑或版本迭代?
- 内核社区的共识:在废弃sysfs GPIO的呼声中,内核社区经过长时间的讨论,最终确定了基于字符设备的模型是最佳的替代方案。
- API设计与合入 :
gpiolib-cdev接口在Linux内核4.8版本中被正式引入。它的API设计吸收了过去使用GPIO的经验教训,重点关注原子性 、生命周期管理 和功能完整性。 - 配套用户空间库的开发 :为了方便开发者使用这个新的
ioctl接口,社区开发了配套的C库libgpiod。这个库封装了所有底层的ioctl调用,提供了易于使用的API(如gpiod_line_request_output(),gpiod_line_set_value()),并成为用户空间访问GPIO的官方推荐方式。 - 工具集的完善 :与
libgpiod一起,还提供了一组命令行工具(gpiodetect,gpioinfo,gpioset,gpioget,gpiomon),使得在shell脚本中或命令行上直接与GPIO交互变得简单可靠。
目前该技术的社区活跃度和主流应用情况如何?
gpiolib-cdev接口目前是Linux中唯一被推荐和积极支持的用户空间GPIO访问方式。
- 应用情况 :所有新的嵌入式Linux项目、物联网设备、创客项目(如Raspberry Pi、BeagleBone)都应该使用
libgpiod和字符设备接口。老的项目也被强烈建议从sysfs迁移过来。 - 社区状态 :该接口非常稳定。
libgpiod库仍在积极维护和更新,以适应新的内核功能和提供更好的开发者体验。
核心原理与设计
它的核心工作原理是什么?
gpiolib-cdev.c 的核心是在/dev目录下为每个GPIO控制器(GPIO Chip)创建一个对应的字符设备文件(如/dev/gpiochip0, /dev/gpiochip1等)。用户空间通过对这些设备文件进行open()和ioctl()操作来与GPIO交互。
- 设备创建 :当一个GPIO控制器驱动(如树莓派的BCM2835 GPIO驱动)在内核中注册时,
gpiolib核心会自动调用gpiolib-cdev.c中的函数,为这个控制器创建一个字符设备节点。 - 信息查询 (Querying) :用户空间程序(或
gpioinfo工具)可以open("/dev/gpiochipX"),然后使用GPIO_GET_CHIPINFO_IOCTL等ioctl命令来查询该控制器的信息,包括其名称、标签以及它管理的所有GPIO线路(Lines)的名称、方向、状态等。 - 线路请求 (Requesting Lines) :
- 这是最关键的一步。一个程序想要使用一个或多个GPIO线路,必须通过
GPIO_GET_LINEHANDLE_IOCTL这个ioctl命令来请求它们。 - 在请求时,程序可以原子性地指定所有配置:是输入还是输出、是高电平有效还是低电平有效、是开漏还是推挽、默认输出值是什么等。
- 如果请求成功,内核会返回一个新的文件描述符(line handle FD) 。这个FD唯一地代表了对这一个或多个GPIO线路的使用权。
- 这是最关键的一步。一个程序想要使用一个或多个GPIO线路,必须通过
- 操作 (Operating) :
- 程序对这个新的line handle FD执行
ioctl操作,如GPIOHANDLE_SET_VALUES_IOCTL或GPIOHANDLE_GET_VALUES_IOCTL,来设置或读取GPIO的值。
- 程序对这个新的line handle FD执行
- 生命周期管理 (Lifecycle Management) :
- 当程序
close()这个line handle FD时(或者程序崩溃,内核会自动关闭其所有FD),对这些GPIO线路的"占用"就会被自动释放。内核会将这些GPIO恢复到默认状态。这从根本上解决了sysfs的资源泄漏问题。
- 当程序
- 事件监控 (Event Monitoring) :
- 如果在请求线路时指定了需要监控事件(如边沿触发),那么对line handle FD执行
read()操作将会阻塞,直到指定的GPIO事件(如上升沿)发生。这提供了一个高效、可靠的等待GPIO中断的方式。
- 如果在请求线路时指定了需要监控事件(如边沿触发),那么对line handle FD执行
它的主要优势体现在哪些方面?
- 稳定性与可移植性 :通过名称(如
"power-led")而不是易变的编号来识别GPIO,大大提高了代码的可移植性。 - 原子性操作 :配置和请求是单一
ioctl调用,避免了竞态条件。 - 功能完整:支持所有GPIO硬件功能,如开漏/开源、偏置(bias)、边沿触发等。
- 可靠的生命周期管理:基于文件描述符的生命周期绑定,杜绝了资源泄漏。
- 安全性:一次只能有一个用户"持有"一个GPIO线路的句柄(除非明确请求为共享访问),提供了基本的访问控制。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 性能 :对于需要以极高频率(MHz级别)翻转GPIO的场景(bit-banging),
ioctl系统调用的上下文切换开销可能会成为瓶颈。在这种超高性能场景下,内核驱动或用户空间直接内存映射(UIO)可能是更好的选择。 - 学习曲线 :相比于简单的
echo "1" > /sys/class/gpio/...,基于ioctl的编程模型对初学者来说更复杂。但libgpiod库极大地降低了这个门槛。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是几乎所有用户空间需要与GPIO交互场景的唯一首选解决方案。
- 嵌入式设备控制:控制LED、读取按键状态、驱动继电器、与简单的传感器(如温湿度传感器)进行通信。
- 物联网(IoT):在IoT网关或设备上,通过GPIO监控外部事件或控制外部设备。
- 硬件测试与自动化:编写测试脚本,自动控制被测设备(DUT)的电源、复位引脚,或监控其状态指示灯。
- 创客与DIY项目:在树莓派、BeagleBone等单板计算机上进行硬件原型设计和开发。
代码示例(使用libgpiod):
c
#include <gpiod.h>
// 控制一个名为 "power-led" 的LED灯闪烁
struct gpiod_chip *chip;
struct gpiod_line *line;
int i;
chip = gpiod_chip_open_by_name("gpiochip0");
line = gpiod_chip_get_line(chip, gpiod_chip_get_line_offset_from_name(chip, "power-led"));
gpiod_line_request_output(line, "my-app", 0);
for (i = 0; i < 5; i++) {
gpiod_line_set_value(line, 1);
sleep(1);
gpiod_line_set_value(line, 0);
sleep(1);
}
gpiod_line_release(line);
gpiod_chip_close(chip);
是否有不推荐使用该技术的场景?为什么?
- 内核驱动内部 :
gpiolib-cdev是用户空间 接口。内核驱动程序内部应该使用gpiolib提供的内核API(如gpiod_get,gpiod_set_value等)。 - 超高频信号生成:如上所述,对于需要生成MHz级别波形的场景,系统调用开销过大,应考虑PRU(可编程实时单元,如BeagleBone)、SPI/I2S硬件外设或UIO。
对比分析
请将其 与 其他相似技术 进行详细对比。
gpiolib-cdev (字符设备接口) vs. sysfs (遗留接口)
| 特性 | gpiolib-cdev |
sysfs (/sys/class/gpio) |
|---|---|---|
| API模型 | 字符设备 + ioctl |
文件系统 (export, direction, value文件) |
| 状态 | 现代、推荐 | 废弃 (Deprecated)、不推荐 |
| 生命周期 | 基于文件描述符,自动清理,无泄漏。 | 手动export/unexport,进程崩溃易导致泄漏。 |
| 操作原子性 | 高。配置和请求是原子性的。 | 低。多个文件操作之间存在竞态条件。 |
| 功能支持 | 完整。支持开漏、偏置、事件等所有硬件功能。 | 基础。只支持方向和值。 |
| 线路标识 | 名称和偏移量。 | 只有全局编号。 |
| 多线路操作 | 支持。可以原子性地请求和操作多条线路。 | 不支持。只能逐个操作。 |
| 事件监控 | 高效 。通过对FD执行poll/read。 |
低效 。通过poll在value文件上实现,但功能有限。 |
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不为空 , 意味着有事件可以立即读取。函数返回
c
/*
* 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缓冲区中取出一个或多个事件, 并将它们安全地复制到用户空间提供的缓冲区中。如果缓冲区为空, 它会使调用进程进入睡眠, 直到有事件被推入缓冲区并被唤醒。
工作流程详解:
- 获取会话上下文与安全检查 : 与
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()来获取事件的详细信息(例如, 哪个引脚发生了什么类型的事件)。这种机制在任何现代操作系统中, 对于处理异步硬件事件都是至关重要的, 即使是在单核系统上, 它也能极大地提高系统的响应能力和能效。
c
/*
* 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控制器设备文件, 而它们各自的会话状态互不干扰。
工作流程详解:
- 获取设备上下文 : 函数首先通过
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等锁机制在单核抢占式系统上依然是必需的, 它通过禁用本地中断和抢占来保护对通知链表等共享资源的访问, 防止数据结构被并发修改所破坏。
c
/*
* 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驱动功能的桥梁。
c
/*
* 定义一个静态的、常量类型的 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)执行所有必要的步骤, 来创建一个功能齐全、可供用户空间访问的字符设备。
c
/*
* 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字符设备
此函数是注册函数的逆过程, 负责清理和释放所有相关资源。
c
/*
* 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/gpiolib-devres.c gpio 安全、自动管理的IO内存
devres 管理的 GPIO 描述符获取
本代码片段展示了 Linux 内核中一组资源管理版本 的 GPIO 描述符获取函数:devm_gpiod_get 和 devm_gpiod_get_index。其核心功能是:在标准的 gpiod_get 功能(即根据设备和功能名称从设备树等固件接口中查找并获取一个 GPIO)的基础上,自动地将对应的释放操作(gpiod_put)注册到 devres 框架中 。这使得驱动开发者无需再手动编写 probe 失败路径的错误处理和 remove 函数中的释放代码,极大地简化了驱动的编写并提高了其健壮性。
实现原理分析
此代码是 devres 框架与 GPIO 子系统相结合的典型应用,它利用了上一节分析的 devm_add_action 机制,将一个非 devres 管理的资源(GPIO 描述符)无缝地集成到 devres 的自动管理体系中。
-
动作的定义 (
devm_gpiod_release):devm_gpiod_release是一个简单的静态包装函数。它的函数签名void (*)(void *)完美匹配devm_add_action所需的action参数类型。- 它的唯一作用就是调用
gpiod_put(desc)。gpiod_put是 GPIO 子系统的标准函数,用于释放对一个 GPIO 描述符的"使用权"(减少其引用计数)。
-
核心封装逻辑 (
devm_gpiod_get_index):- 获取资源 : 函数首先调用非
devm版本 的gpiod_get_index来执行实际的 GPIO 查找和获取工作。如果失败,它直接返回错误码。 - 注册释放动作 : 如果成功获取到
desc,接下来的关键步骤是devm_add_action_or_reset(dev, devm_gpiod_release, desc)。
a. 此函数将devm_gpiod_release(释放函数) 和desc(要释放的对象) 打包成一个"自定义动作"。
b. 然后,它将这个动作添加到与dev关联的devres资源栈中。 - 自动清理 : 一旦
devm_add_action_or_reset成功返回,devres框架就接管了desc的生命周期。如果probe函数在后续步骤中失败,或者当驱动最终被卸载时,devres的资源栈回溯机制会自动调用devm_gpiod_release,并传入正确的desc指针,从而确保gpiod_put被正确调用。 devm_add_action_or_reset: 这个函数名暗示了它比devm_add_action更健壮。如果添加动作失败,它会自动调用一次devm_gpiod_release(desc)来立即释放刚刚获取的资源,然后再返回错误码,防止资源泄漏。
- 获取资源 : 函数首先调用非
-
处理非独占请求 (
GPIOD_FLAGS_BIT_NONEXCLUSIVE):- 这是一个重要的细节,用于处理多个使用者共享同一个 GPIO 的情况。
- 如果请求是非独占的,意味着同一个设备驱动可能在不同代码路径下多次调用
devm_gpiod_get来获取同一个 GPIO。在这种情况下,gpiod_put也必须被调用相同的次数。 - 代码通过
devm_is_action_added来检查(devm_gpiod_release, desc)这个动作是否已经被添加过。 - 如果是,说明这个
desc已经被devres管理了,就不需要再次调用devm_add_action_or_reset,直接返回desc即可。这可以防止同一个释放动作被重复注册到资源栈中。
特定场景分析:单核、无MMU的STM32H750平台
功能相关性
devm_ 系列函数是现代 Linux 平台驱动开发的标准和推荐实践 。对于在 STM32H750 平台上开发驱动程序,devm_gpiod_get 是极其重要和有用的。
-
简化驱动开发 : STM32H750 的外设驱动(例如,一个控制 SPI 设备片选的驱动,或一个读取按键状态的驱动)几乎总会需要获取 GPIO。使用
devm_gpiod_get而不是手动的gpiod_get/gpiod_put组合,可以让probe函数的代码变得极其简洁,并且完全无需担心在remove函数中忘记释放 GPIO。c// 使用 devm_gpiod_get 的 probe 函数 gpiod = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(gpiod)) return PTR_ERR(gpiod); // ... probe继续 ... // 无需在任何失败路径或 remove 函数中调用 gpiod_put() -
提高健壮性: 它从根本上消除了因复杂的错误处理路径而导致的 GPIO 资源泄漏风险。这对于需要长期稳定运行的嵌入式系统至关重要。
-
结论 :
devm_gpiod_get是在 STM32H750 平台上编写简洁、安全、可靠的 GPIO 消费者驱动的首选 API。
单核环境影响
devres框架及其内部的锁机制在单核处理器上是安全有效的(通过禁用中断来保证原子性)。- GPIO 子系统的
gpiod_get/gpiod_put内部也包含了必要的同步机制。 - 此代码的逻辑与 CPU 核心数无关。
无MMU影响
devres框架和 GPIO 子系统都是内核的核心基础设施,它们不依赖于内存管理单元(MMU)。- 它们操作的是内核数据结构(
device,gpio_desc)和通过kmalloc分配的内核内存。 - 因此,缺少 MMU 对这些函数的逻辑和正确性没有任何影响。
代码分析
c
/**
* @brief devm_gpiod_release - devres 的释放回调,用于释放单个 GPIO 描述符。
* @param desc: 指向要释放的 GPIO 描述符 (void * 类型)。
*/
static void devm_gpiod_release(void *desc)
{
// 调用 GPIO 子系统的标准释放函数。
gpiod_put(desc);
}
/**
* @brief devm_gpiod_release_array - devres 的释放回调,用于释放 GPIO 描述符数组。
* @param descs: 指向要释放的 GPIO 描述符数组 (void * 类型)。
*/
static void devm_gpiod_release_array(void *descs)
{
gpiod_put_array(descs);
}
/**
* @brief devm_gpiod_get - 资源管理版的 gpiod_get()。
* @param dev: GPIO 消费者设备。
* @param con_id: GPIO 在消费者内部的功能名称。
* @param flags: 可选的 GPIO 初始化标志。
* @return struct gpio_desc*: 成功则返回 GPIO 描述符,失败返回 ERR_PTR。
* @note 返回的描述符会在驱动卸载时自动释放。
*/
struct gpio_desc *__must_check devm_gpiod_get(struct device *dev,
const char *con_id,
enum gpiod_flags flags)
{
// 调用带有索引的版本,索引默认为0。
return devm_gpiod_get_index(dev, con_id, 0, flags);
}
EXPORT_SYMBOL_GPL(devm_gpiod_get);
/**
* @brief devm_gpiod_get_index - 资源管理版的 gpiod_get_index()。
* @param dev: GPIO 消费者设备。
* @param con_id: GPIO 在消费者内部的功能名称。
* @param idx: 要获取的 GPIO 在消费者中的索引。
* @param flags: 可选的 GPIO 初始化标志。
* @return struct gpio_desc*: 成功则返回 GPIO 描述符,失败返回 ERR_PTR。
*/
struct gpio_desc *__must_check devm_gpiod_get_index(struct device *dev,
const char *con_id,
unsigned int idx,
enum gpiod_flags flags)
{
struct gpio_desc *desc;
int ret;
// 步骤1: 调用非 devm 版本的函数来实际获取 GPIO 描述符。
desc = gpiod_get_index(dev, con_id, idx, flags);
if (IS_ERR(desc))
return desc;
/*
* 对于非独占的 GPIO 描述符,检查此描述符是否已由此设备进行资源管理。
*/
if (flags & GPIOD_FLAGS_BIT_NONEXCLUSIVE) {
bool dres;
// 检查是否已经为这个 desc 添加了释放动作。
dres = devm_is_action_added(dev, devm_gpiod_release, desc);
if (dres)
// 如果已添加,则直接返回,避免重复注册。
return desc;
}
// 步骤2: 将释放操作 devm_gpiod_release(desc) 注册到 devres 框架。
ret = devm_add_action_or_reset(dev, devm_gpiod_release, desc);
if (ret)
// 如果注册失败,devm_add_action_or_reset 内部会负责释放 desc,
// 此处只需返回错误指针。
return ERR_PTR(ret);
// 步骤3: 返回成功获取并已置于 devres 管理之下的描述符。
return desc;
}
EXPORT_SYMBOL_GPL(devm_gpiod_get_index);