[toc]
的内核对象gdev,并完成以下三件关键事情:
初始化设备对象 : 确保gdev中的struct device成员被正确初始化。
创建字符设备 : 在/dev目录下创建对应的gpiochipN字符设备节点,使用户空间程序可以通过文件I/O操作来访问GPIO。
创建sysfs接口 : 在/sys/class/gpio目录下创建对应的gpiochipN目录和属性文件,提供一种基于文件的、用于管理和调试GPIO的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 static int gpiochip_setup_dev (struct gdevice *gdev) { struct fwnode_handle *fwnode = dev_fwnode(&gdev->dev); int ret; device_initialize(&gdev->dev); if (fwnode && !fwnode->dev) fwnode_dev_initialized(fwnode, false ); ret = gcdev_register(gdev, gpio_devt); if (ret) return ret; ret = gpiochip_sysfs_register(gdev); if (ret) goto err_remove_device; 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: gcdev_unregister(gdev); return ret; }
gpiochip_setup_devs: 为已注册的GPIO控制器设置设备节点 此函数的核心作用是遍历当前系统中所有已经注册的GPIO控制器(gpio_device),并为每一个控制器调用gpiochip_setup_dev函数来完成其设备节点的最终设置 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 static void gpiochip_setup_devs (void ) { struct gpio_device *gdev ; int ret; guard(srcu)(&gpio_devices_srcu); list_for_each_entry_srcu(gdev, &gpio_devices, list , srcu_read_lock_held(&gpio_devices_srcu)) { ret = gpiochip_setup_dev(gdev); if (ret) dev_err(&gdev->dev, "Failed to initialize gpio device (%d)\n" , ret); } }
gpiolib_dev_init: 初始化GPIO设备库 此函数在内核启动的早期阶段被调用,其核心职责是建立Linux内核GPIO子系统的设备驱动模型框架 。它本身不注册任何具体的GPIO硬件,而是搭建一个“舞台”,让后续具体的GPIO控制器驱动(如STM32的GPIO驱动)能够在这个舞台上注册自己,并以一种标准化的方式向用户空间暴露接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 static struct device_driver gpio_stub_drv = { .name = "gpio_stub_drv" , .bus = &gpio_bus_type, .probe = gpio_stub_drv_probe, }; static int __init gpiolib_dev_init (void ) { int ret; ret = bus_register(&gpio_bus_type); if (ret < 0 ) { pr_err("gpiolib: could not register GPIO bus type\n" ); return ret; } 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; } 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; } gpiolib_initialized = true ; gpiochip_setup_devs(); #if IS_ENABLED(CONFIG_OF_DYNAMIC) && IS_ENABLED(CONFIG_OF_GPIO) WARN_ON(of_reconfig_notifier_register(&gpio_of_notifier)); #endif return ret; } core_initcall(gpiolib_dev_init);
gpiolib 总线与设备类型定义此代码片段定义了Linux内核gpiolib子系统用于融入内核标准设备模型(Device Model)的两个核心数据结构。它的核心原理是 创建一个名为 “gpio” 的逻辑总线(logical bus) , 并为所有注册的GPIO控制器(gpio_chip)定义一个统一的设备类型(gpio_chip)。这使得内核可以将每一个GPIO控制器都视为一个标准化的”设备”, 并通过一个虚拟的”总线”来管理它们, 从而能够复用设备模型提供的所有成熟机制, 如驱动绑定、电源管理和sysfs接口。
gpio_dev_type: GPIO芯片的设备类型这是一个struct device_type实例, 它为所有由gpiolib创建的struct gpio_device对象提供了一组通用的属性。
原理与作用 :device_type的主要作用是为一类设备提供共享的特性, 最重要的是统一的释放(release)回调函数 。
.name = "gpio_chip": 为这类设备指定了一个内部名称 “gpio_chip”。这个名字主要用于调试和内核内部识别。
.release = gpiodev_release: 这是此结构体最关键的部分。它指定了一个回调函数gpiodev_release。当一个gpio_device对象的最后一个引用被释放(其引用计数降为0)时, 内核的设备模型会自动调用这个函数。gpiodev_release函数内部会负责清理和释放与该gpio_device对象相关的所有内存和资源。这是一种健壮的、自动化的资源管理机制, 对于防止内存泄漏至关重要, 在像STM32H750这样内存资源宝贵的嵌入式系统中尤其重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static const struct device_type gpio_dev_type = { .name = "gpio_chip" , .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; 表示匹配失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static int gpio_bus_match (struct device *dev, const struct device_driver *drv) { struct fwnode_handle *fwnode = dev_fwnode(dev); if (fwnode && fwnode->dev != dev) return 0 ; return 1 ; }
gpio_bus_type: “gpio” 逻辑总线这是一个struct bus_type实例, 它在内核中注册了一个全新的、名为”gpio”的总线。
原理与作用 : 注册这个结构体会在sysfs中创建/sys/bus/gpio/目录。所有被gpiolib注册的GPIO控制器都会作为设备出现在这个总线上。这提供了一个统一的场所来管理和查看系统中的所有GPIO控制器。
.name = "gpio": 定义了总线的名称, 这也是sysfs中目录的名称。
.match = gpio_bus_match: 将上面定义的自定义匹配函数gpio_bus_match指定为本总线的官方匹配规则。当任何驱动或设备尝试在此总线上进行绑定时, 内核都会调用这个函数来做决定。
1 2 3 4 5 6 7 8 9 10 11 12 13 static const struct bus_type gpio_bus_type = { .name = "gpio" , .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端口的平台上, 是至关重要的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static int gpiochip_find_base_unlocked (u16 ngpio) { unsigned int base = GPIO_DYNAMIC_BASE; struct gpio_device *gdev ; list_for_each_entry_srcu(gdev, &gpio_devices, list , lockdep_is_held(&gpio_devices_lock)) { if (gdev->base >= base + ngpio) break ; base = gdev->base + gdev->ngpio; if (base < GPIO_DYNAMIC_BASE) base = GPIO_DYNAMIC_BASE; if (base > GPIO_DYNAMIC_MAX - ngpio) break ; } 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的注册过程非常高效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 static int gpiodev_add_to_list_unlocked (struct gpio_device *gdev) { struct gpio_device *prev , *next ; lockdep_assert_held(&gpio_devices_lock); if (list_empty(&gpio_devices)) { list_add_tail_rcu(&gdev->list , &gpio_devices); return 0 ; } next = list_first_entry(&gpio_devices, struct gpio_device, list ); if (gdev->base + gdev->ngpio <= next->base) { list_add_rcu(&gdev->list , &gpio_devices); return 0 ; } 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 ; } list_for_each_entry_safe(prev, next, &gpio_devices, list ) { if (&next->list == &gpio_devices) break ; if (prev->base + prev->ngpio <= gdev->base && gdev->base + gdev->ngpio <= next->base) { list_add_rcu(&gdev->list , &prev->list ); return 0 ; } } synchronize_srcu(&gpio_devices_srcu); return -EBUSY; }
GPIO有效引脚掩码(Valid Mask)与引脚范围管理系列函数 此代码片段展示了Linux内核gpiolib子系统中一组用于管理GPIO控制器引脚有效性和范围映射的函数。它们共同构成了一个强大而灵活的系统, 其核心原理是通过静态的设备树声明和/或动态的驱动回调, 创建一个精确的”有效引脚掩码”(valid mask), 并将GPIO编号范围与pinctrl子系统关联起来 。这使得gpiolib能够安全地处理具有非连续、复杂引脚布局的硬件, 并确保了不同子系统间的正确协作。
1. 有效引脚掩码的生命周期管理 这组函数负责创建、填充和释放valid_mask位图。
gpiochip_allocate_mask & gpiochip_free_mask / gpiochip_free_valid_mask这是位图最基本的内存管理。allocate负责分配内存并设定一个”全部有效”的初始状态, free则负责释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static unsigned long *gpiochip_allocate_mask (struct gpio_chip *gc) { unsigned long *p; p = bitmap_alloc(gc->ngpio, GFP_KERNEL); if (!p) return NULL ; bitmap_fill(p, gc->ngpio); return p; } static void gpiochip_free_mask (unsigned long **p) { bitmap_free(*p); *p = NULL ; } 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_rangescount函数检查属性是否存在且格式正确, apply函数则读取属性内容并在valid_mask中”打孔”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 static unsigned int gpiochip_count_reserved_ranges (struct gpio_chip *gc) { struct device *dev = &gc->gpiodev->dev; int size; size = device_property_count_u32(dev, "gpio-reserved-ranges" ); if (size > 0 && size % 2 == 0 ) return size; return 0 ; } static int gpiochip_apply_reserved_ranges (struct gpio_chip *gc) { while (size) { u32 count = ranges[--size]; u32 start = ranges[--size]; if (start >= gc->ngpio || start + count > gc->ngpio) continue ; bitmap_clear(gc->gpiodev->valid_mask, start, count); } return 0 ; }
3. 有效掩码的构建与查询 这组函数是上层API, 用于协调掩码的构建过程并提供查询接口。
gpiochip_init_valid_mask这是gpiochip_add_data调用的主协调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static int gpiochip_init_valid_mask (struct gpio_chip *gc) { int ret; if (!(gpiochip_count_reserved_ranges(gc) || gc->init_valid_mask)) return 0 ; gc->gpiodev->valid_mask = gpiochip_allocate_mask(gc); if (!gc->gpiodev->valid_mask) return -ENOMEM; ret = gpiochip_apply_reserved_ranges(gc); if (ret) return ret; if (gc->init_valid_mask) return gc->init_valid_mask(gc, gc->gpiodev->valid_mask, gc->ngpio); return 0 ; }
gpiochip_query_valid_mask & gpiochip_line_is_valid这两个函数提供了对已构建好的valid_mask的查询能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const unsigned long *gpiochip_query_valid_mask (const struct gpio_chip *gc) { return gc->gpiodev->valid_mask; } bool gpiochip_line_is_valid (const struct gpio_chip *gc, unsigned int offset) { if (!gc->gpiodev) return true ; if (likely(!gc->gpiodev->valid_mask)) return true ; return test_bit(offset, gc->gpiodev->valid_mask); }
4. GPIO范围与Pinctrl的关联 gpiochip_add_pin_ranges此函数负责将GPIO控制器的编号范围与pinctrl子系统关联起来。
原理与作用 : 它的作用是调用驱动提供的add_pin_ranges回调函数, 在这个回调中驱动通常会手动调用pinctrl_add_gpio_range来注册映射关系。但是, 这个机制很大程度上是遗留(legacy)的 。
现代方法 : 现代内核强烈推荐使用设备树中的gpio-ranges属性来完成这个映射。内核的OF(Open Firmware)解析器会自动处理这个属性, 无需驱动操心。
函数行为 : 因此, 此函数首先会检查设备树中是否存在gpio-ranges属性。如果存在, 它会立即返回成功 , 有意地跳过调用驱动的add_pin_ranges回调, 以避免重复或冲突的映射。只有在gpio-ranges属性不存在时, 它才会调用那个遗留的回调函数, 以提供向后兼容性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int gpiochip_add_pin_ranges (struct gpio_chip *gc) { if (device_property_present(&gc->gpiodev->dev, "gpio-ranges" )) return 0 ; if (gc->add_pin_ranges) return gc->add_pin_ranges(gc); return 0 ; }
GPIO层次化中断域(Hierarchical IRQ Domain)实现 此代码片段是Linux内核gpiolib中用于实现和管理层次化中断域 的核心。其根本原理是为那些级联(cascaded)在另一个主中断控制器之下的GPIO控制器, 创建一个”子域”(child domain), 并提供一套完整的操作函数集来管理这个子域的生命周期, 包括它的创建、中断翻译、分配和释放 。
这个模型对于像STM32这样具有分层中断结构的复杂SoC是必不可少的。在STM32中, GPIO引脚 -> EXTI控制器 -> NVIC(CPU)构成了一个清晰的中断层次。这组函数就是在软件层面精确地建模这种硬件上的父子级联关系, 使得内核能够正确地将一个来自特定GPIO引脚的中断请求, 逐级翻译并路由到CPU。
1. 域的创建与配置 这组函数负责判断是否需要创建层次化域, 并执行创建和配置过程。
gpiochip_hierarchy_is_hierarchical一个简单的谓词函数, 用于判断是否应使用层次化模型。
1 2 3 4 5 6 7 8 9 10 static bool gpiochip_hierarchy_is_hierarchical (struct gpio_chip *gc) { return !!gc->irq.parent_domain; }
gpiochip_hierarchy_setup_domain_ops为子域的irq_domain_ops结构体填充一组标准的、预设的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void gpiochip_hierarchy_setup_domain_ops (struct irq_domain_ops *ops) { ops->activate = gpiochip_irq_domain_activate; ops->deactivate = gpiochip_irq_domain_deactivate; ops->alloc = gpiochip_hierarchy_irq_domain_alloc; if (!ops->translate) ops->translate = gpiochip_hierarchy_irq_domain_translate; if (!ops->free ) ops->free = irq_domain_free_irqs_common; }
gpiochip_hierarchy_create_domain创建层次化中断域的主函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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) 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 , gc->ngpio, gc->irq.fwnode, &gc->irq.child_irq_domain_ops, gc); if (!domain) return ERR_PTR(-ENOMEM); gpiochip_set_hierarchical_irqchip(gc, gc->irq.chip); return domain; }
2. 中断域操作回调 (irq_domain_ops) 的实现 这组函数是gpiochip_hierarchy_setup_domain_ops所设置的回调, 它们定义了子域的具体行为。
gpiochip_hierarchy_irq_domain_translate负责解析来自设备树的中断请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int gpiochip_hierarchy_irq_domain_translate (struct irq_domain *d, struct irq_fwspec *fwspec, unsigned long *hwirq, unsigned int *type) { if (is_of_node(fwspec->fwnode)) return irq_domain_translate_twothreecell(d, fwspec, hwirq, type); if (is_fwnode_irqchip(fwspec->fwnode)) { } return -EINVAL; }
gpiochip_hierarchy_irq_domain_alloc这是最核心 的函数, 负责将一个子域的硬件中断(hwirq)映射到一个Linux IRQ, 并递归地向父域申请资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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; irq_hw_number_t hwirq; unsigned int type = IRQ_TYPE_NONE; struct irq_fwspec *fwspec = data; union gpio_irq_fwspec gpio_parent_fwspec = {}; unsigned int parent_hwirq; unsigned int parent_type; struct gpio_irq_chip *girq = &gc->irq; int ret; ret = gc->irq.child_irq_domain_ops.translate(d, fwspec, &hwirq, &type); if (ret) return ret; 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; } irq_domain_set_info(d, irq, hwirq, gc->irq.chip, gc, girq->handler, NULL , NULL ); irq_set_probe(irq); ret = girq->populate_parent_alloc_arg(gc, &gpio_parent_fwspec, parent_hwirq, parent_type); if (ret) return ret; ret = irq_domain_alloc_irqs_parent(d, irq, 1 , &gpio_parent_fwspec); if (ret) chip_err(gc, "failed to allocate parent hwirq %d for hwirq %lu\n" , parent_hwirq, hwirq); return ret; }
gpiochip_irq_domain_activate / deactivate在请求/释放中断时, 锁定/解锁对应的GPIO引脚, 防止其被用作普通IO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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); return gpiochip_lock_as_irq(gc, hwirq); } 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); return gpiochip_unlock_as_irq(gc, hwirq); }
3. 其他辅助与遗留(Legacy)支持函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static void gpiochip_set_hierarchical_irqchip (struct gpio_chip *gc, struct irq_chip *irqchip) { if (is_of_node(gc->irq.fwnode)) return ; } 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 ; fwspec->param[0 ] = parent_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标志(用于支持嵌套的线程化中断处理器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static int gpiochip_irq_map (struct irq_domain *d, unsigned int irq, irq_hw_number_t hwirq) { struct gpio_chip *gc = d->host_data; int ret = 0 ; if (!gpiochip_irqchip_irq_valid(gc, hwirq)) return -ENXIO; irq_set_chip_data(irq, gc); irq_set_lockdep_class(irq, gc->irq.lock_key, gc->irq.request_key); irq_set_chip_and_handler(irq, gc->irq.chip, gc->irq.handler); if (gc->irq.threaded) irq_set_nested_thread(irq, 1 ); irq_set_noprobe(irq); if (gc->irq.num_parents == 1 ) ret = irq_set_parent(irq, gc->irq.parents[0 ]); else if (gc->irq.map ) ret = irq_set_parent(irq, gc->irq.map [hwirq]); if (ret < 0 ) return ret; if (gc->irq.default_type != IRQ_TYPE_NONE) irq_set_irq_type(irq, gc->irq.default_type); return 0 ; }
gpiochip_irq_unmap: 解除一个Linux IRQ与GPIO引脚的映射 此函数是gpiochip_irq_map的逆操作。当一个IRQ被释放时, 内核会调用此函数来清除所有在map阶段建立的软件关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void gpiochip_irq_unmap (struct irq_domain *d, unsigned int irq) { struct gpio_chip *gc = d->host_data; if (gc->irq.threaded) irq_set_nested_thread(irq, 0 ); irq_set_chip_and_handler(irq, NULL , NULL ); irq_set_chip_data(irq, NULL ); }
gpiochip_irq_select: 选择一个中断控制器 此回调函数用于一个高级场景: 当一个设备在设备树中描述的中断可以由多个 不同的中断控制器来提供服务时, 内核会调用.select回调来让驱动程序判断自己是否是那个”正确”的控制器。
原理与作用 : 它的主要作用是进行匹配。内核将从设备树中解析出的中断请求fwspec传递给它, 它需要将fwspec中的信息与自身irq_domain的信息进行比较。
对于使用设备树的现代系统, 它可能会调用of_gpiochip_instance_match来进行更复杂的匹配。
对于简单的、非层次化的域, 它主要比较fwspec中的固件节点(fwnode)是否与自身域的固件节点(d->fwnode)相同, 并可能比较总线令牌(bus_token)。如果匹配, 就返回true, 表示”这个中断请求是给我的”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static int gpiochip_irq_select (struct irq_domain *d, struct irq_fwspec *fwspec, enum irq_domain_bus_token bus_token) { struct fwnode_handle *fwnode = fwspec->fwnode; struct gpio_chip *gc = d->host_data; unsigned int index = fwspec->param[0 ]; if (fwspec->param_count == 3 && is_of_node(fwnode)) return of_gpiochip_instance_match(gc, index); return (fwnode && (d->fwnode == fwnode) && (d->bus_token == bus_token)); }
gpiochip_domain_ops: 简单域的操作函数集 这是一个静态的irq_domain_ops结构体实例, 它为所有通过”简单模型”创建的GPIO中断域提供了一套标准的、通用的操作函数集。它就像是中断域的”行为手册”, 告诉内核在对该域进行各种操作时应该调用哪些函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static const struct irq_domain_ops gpiochip_domain_ops = { .map = gpiochip_irq_map, .unmap = gpiochip_irq_unmap, .select = gpiochip_irq_select, .xlate = irq_domain_xlate_twothreecell, };
gpiochip_simple_create_domain: 创建一个简单中断域 这是一个内部函数, 它封装了内核通用的irq_domain_create_simple函数, 为GPIO控制器提供了一个便捷的创建接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static struct irq_domain *gpiochip_simple_create_domain (struct gpio_chip *gc) { struct fwnode_handle *fwnode = dev_fwnode(&gc->gpiodev->dev); struct irq_domain *domain ; domain = irq_domain_create_simple(fwnode, gc->ngpio, gc->irq.first, &gpiochip_domain_ops, gc); 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)最终会调用此函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 int gpiochip_lock_as_irq (struct gpio_chip *gc, unsigned int offset) { struct gpio_desc *desc ; desc = gpiochip_get_desc(gc, offset); if (IS_ERR(desc)) return PTR_ERR(desc); 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; } } 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; } set_bit(FLAG_USED_AS_IRQ, &desc->flags); set_bit(FLAG_IRQ_IS_ENABLED, &desc->flags); return 0 ; } EXPORT_SYMBOL_GPL(gpiochip_lock_as_irq);
gpiochip_unlock_as_irq: 解锁一个用作中断的GPIO引脚 当一个驱动程序释放一个中断时(在free_irq的调用链中), irqchip子系统的release_resources回调函数(即gpiolib的gpiochip_relres_irq)会调用此函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void gpiochip_unlock_as_irq (struct gpio_chip *gc, unsigned int offset) { struct gpio_desc *desc ; desc = gpiochip_get_desc(gc, offset); if (IS_ERR(desc)) return ; 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引脚作为中断源的”获取”和”释放”操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 int gpiochip_reqres_irq (struct gpio_chip *gc, unsigned int offset) { int ret; if (!try_module_get(gc->gpiodev->owner)) return -ENODEV; ret = gpiochip_lock_as_irq(gc, offset); if (ret) { 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); void gpiochip_relres_irq (struct gpio_chip *gc, unsigned int offset) { gpiochip_unlock_as_irq(gc, offset); module_put(gc->gpiodev->owner); } EXPORT_SYMBOL_GPL(gpiochip_relres_irq);
gpiochip_enable_irq / gpiochip_disable_irq: 使能/禁用中断(软件状态) 这两个函数通常被irq_chip的mask/unmask回调函数的包装器所调用。它们只负责在gpiolib的软件层面更新中断的使能状态 , 而不直接操作硬件 。硬件的屏蔽/解屏蔽操作由irq_chip回调本身负责。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 void gpiochip_disable_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))) clear_bit(FLAG_IRQ_IS_ENABLED, &desc->flags); } EXPORT_SYMBOL_GPL(gpiochip_disable_irq); 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))) { WARN_ON(test_bit(FLAG_IS_OUT, &desc->flags) && !test_bit(FLAG_OPEN_DRAIN, &desc->flags)); set_bit(FLAG_IRQ_IS_ENABLED, &desc->flags); } } EXPORT_SYMBOL_GPL(gpiochip_enable_irq);
gpiolib中断控制器(irqchip)的实现此代码片段是Linux内核gpiolib子系统与irqchip子系统集成的核心部分。它的根本原理是提供一套标准的适配器(Adapter)和包装器(Wrapper)函数, 将一个驱动程序提供的、特定于硬件的gpio_chip对象, 封装成一个内核可以统一识别和管理的标准irq_chip对象 。这使得任何一个GPIO控制器, 无论其硬件实现如何, 都能作为中断源无缝地融入内核的中断处理框架, 从而实现了gpio_to_irq()这个关键功能。
1. gpio_to_irq的核心实现 gpiochip_to_irq是驱动程序和内核其他部分将一个GPIO引脚坐标(控制器+偏移)翻译成一个全局Linux IRQ号所调用的核心API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static int gpiochip_to_irq (struct gpio_chip *gc, unsigned int offset) { struct irq_domain *domain = gc->irq.domain; if (!gc->irq.initialized) return -EPROBE_DEFER; if (!gpiochip_irqchip_irq_valid(gc, offset)) return -ENXIO; #ifdef CONFIG_IRQ_DOMAIN_HIERARCHY if (irq_domain_is_hierarchy(domain)) { struct irq_fwspec spec ; spec.fwnode = domain->fwnode; spec.param_count = 2 ; spec.param[0 ] = gc->irq.child_offset_to_irq(gc, offset); spec.param[1 ] = IRQ_TYPE_NONE; return irq_create_fwspec_mapping(&spec); } #endif return irq_create_mapping(domain, offset); }
2. irq_chip回调函数的包装器 gpiochip_set_irq_hooks函数通过巧妙的指针交换, 将gpiolib自己的通用逻辑”注入”到驱动提供的irq_chip回调中。下面是被注入的包装器函数, 它们在调用驱动原始的回调函数的同时, 执行了gpiolib的通用操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 int gpiochip_irq_reqres (struct irq_data *d) { struct gpio_chip *gc = irq_data_get_irq_chip_data(d); unsigned int hwirq = irqd_to_hwirq(d); return gpiochip_reqres_irq(gc, hwirq); } EXPORT_SYMBOL(gpiochip_irq_reqres); 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); gpiochip_relres_irq(gc, hwirq); } EXPORT_SYMBOL(gpiochip_irq_relres); 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); if (gc->irq.irq_mask) gc->irq.irq_mask(d); gpiochip_disable_irq(gc, hwirq); } 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); gpiochip_enable_irq(gc, hwirq); if (gc->irq.irq_unmask) gc->irq.irq_unmask(d); } 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); gc->irq.irq_enable(d); } 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); gc->irq.irq_disable(d); gpiochip_disable_irq(gc, hwirq); }
3. irqchip的安装与初始化 这组函数负责将上述所有部分组装起来, 完成irqchip在gpiolib中的注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 static void gpiochip_set_irq_hooks (struct gpio_chip *gc) { struct irq_chip *irqchip = gc->irq.chip; if (irqchip->flags & IRQCHIP_IMMUTABLE) return ; chip_warn(gc, "not an immutable chip, please consider fixing it!\n" ); 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 ; 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; } } static int gpiochip_irqchip_add_allocated_domain (struct gpio_chip *gc, struct irq_domain *domain, bool allocated_externally) { if (!domain) return -EINVAL; if (gc->to_irq) chip_warn(gc, "to_irq is redefined in %s and you shouldn't rely on it\n" , __func__); gc->to_irq = gpiochip_to_irq; gc->irq.domain = domain; gc->irq.domain_is_allocated_externally = allocated_externally; barrier(); 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控制器可能就是主中断源, 或者其级联关系非常简单。在这种情况下, 函数会创建一个独立的、非层次化的中断域。
设置链式中断处理器 (可选) : 如果GPIO控制器是一个”中断解复用器”(即它自己有一条中断线连接到父中断控制器, 当其任何一个引脚中断时, 这条线都会触发), 此函数会调用irq_set_chained_handler_and_data。这个调用会将父中断控制器上的那个IRQ配置为: 当它触发时, 不去执行一个普通的中断服务程序, 而是直接调用 本GPIO控制器驱动提供的parent_handler函数。这个parent_handler的职责就是去查询自己内部的寄存器, 找出到底是哪个GPIO引脚真正触发了中断。
最终注册与激活 : 在创建好irq_domain并设置好所有链接后, 函数会调用gpiochip_irqchip_add_allocated_domain将这个域与gpio_chip正式绑定。从此, gpiolib和irqchip两大子系统就完全关联起来了。
在STM32H750上的应用: STM32的中断系统是典型的层次化 结构:GPIO Pin -> GPIO Bank -> EXTI Controller -> NVIC (CPU Interrupt Controller)
因此, 当STM32的GPIO驱动调用gpiochip_add_irqchip时, 总是会走”层次化域”的路径 :
STM32的EXTI驱动会首先注册一个代表EXTI的父 irq_domain。
当gpiolib为GPIOA这个Bank注册irqchip时, gpiochip_add_irqchip会创建一个新的irq_domain, 并将其parent指针指向EXTI的域。
当上层驱动调用gpio_to_irq()请求PA5的中断时, 这个两级域的层次结构就会被用来进行翻译, 最终返回一个由NVIC管理的、全局唯一的Linux IRQ号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 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; 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; 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_set_chained_handler_and_data(gc->irq.parents[i], gc->irq.parent_handler, data); } } gpiochip_set_irq_hooks(gc); ret = gpiochip_irqchip_add_allocated_domain(gc, domain, false ); if (ret) return ret; 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依次重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 static int gpiochip_setup_dev (struct gpio_device *gdev) { struct fwnode_handle *fwnode = dev_fwnode(&gdev->dev); int ret; device_initialize(&gdev->dev); if (fwnode && !fwnode->dev) fwnode_dev_initialized(fwnode, false ); ret = gcdev_register(gdev, gpio_devt); if (ret) return ret; ret = gpiochip_sysfs_register(gdev); if (ret) goto err_remove_device; 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: gcdev_unregister(gdev); return ret; }
gpiochip_add_data: 将一个GPIO控制器注册到内核 此函数是Linux内核gpiolib子系统的心脏。一个设备驱动程序(例如STM32的pinctrl驱动)在准备好一个描述其硬件能力的struct gpio_chip结构体之后, 会调用此函数, 将其正式注册并”激活” , 使其成为一个对整个内核可用的、功能完备的GPIO控制器。
它的核心原理是一个精心设计的、分阶段的”构造”过程 , 它将一个驱动提供的、半成品的gpio_chip蓝图, 实例化为一个内核内部的、标准化的gpio_device对象, 并将其与内核的各大关键子系统(设备模型、中断系统、pinctrl系统、设备树)一一链接起来。
1 2 3 4 #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()功能得以实现。
sysfs设备创建 : 最后, 它调用gpiochip_setup_dev在/sys/class/gpio/目录下创建对应的gpiochipN设备节点, 使得用户空间工具(如gpiodetect, udev)可以看到并与之交互。
错误处理 : 此函数拥有一个非常健壮的错误处理机制。它使用了一系列的goto标签, 如果在上述任何一个阶段失败, 程序会跳转到对应的标签, 并以与注册相反的顺序 , 精确地撤销所有已经成功完成的步骤, 确保不会有任何资源泄漏或状态不一致, 保证了系统的稳定性。
在STM32H750上的应用: 当stm32_gpiolib_register_bank函数为STM32的某一个GPIO Bank(例如, GPIOA)准备好其gpio_chip结构体后, 它就会调用gpiochip_add_data (通过宏)。这个调用会触发上述所有流程, 最终结果是:
GPIOA被注册为gpiochipN。
它所管理的16个引脚(PA0-PA15)在内核中都有了对应的gpio_desc。
它会被分配一段GPIO编号(例如, 0-15)。
gpio_to_irq功能被激活, 可以将PA5的中断请求转换为一个全局的Linux IRQ号。
其他驱动程序从此可以通过gpio_request(5, ...)来申请并使用PA5。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 int gpiochip_add_data_with_key (struct gpio_chip *gc, void *data, struct lock_class_key *lock_key, struct lock_class_key *request_key) { struct gpio_device *gdev ; unsigned int desc_index; int base = 0 ; int ret; if ((gc->set && gc->set_rv) || (gc->set_multiple && gc->set_multiple_rv)) return -EINVAL; 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); gc->gpiodev = gdev; gpiochip_set_data(gc, data); device_set_node(&gdev->dev, gpiochip_choose_fwnode(gc)); ret = ida_alloc(&gpio_ida, GFP_KERNEL); if (ret < 0 ) goto err_free_gdev; gdev->id = ret; ret = dev_set_name(&gdev->dev, GPIOCHIP_NAME "%d" , gdev->id); if (ret) goto err_free_ida; if (gc->parent && gc->parent->driver) gdev->owner = gc->parent->driver->owner; else if (gc->owner) gdev->owner = gc->owner; else gdev->owner = THIS_MODULE; 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); scoped_guard(mutex, &gpio_devices_lock) { base = gc->base; if (base < 0 ) { base = gpiochip_find_base_unlocked(gc->ngpio); if (base < 0 ) { ret = base; goto err_free_label; } gc->base = base; } else { dev_warn(&gdev->dev, "Static allocation of GPIO base is deprecated, use dynamic allocation.\n" ); } gdev->base = base; 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 if (gc->names) gpiochip_set_desc_names(gc); 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; 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); } ret = of_gpiochip_add(gc); if (ret) goto err_free_valid_mask; ret = gpiochip_add_pin_ranges(gc); if (ret) goto err_remove_of_chip; acpi_gpiochip_add(gc); machine_gpiochip_add(gc); ret = gpiochip_irqchip_init_valid_mask(gc); if (ret) goto err_free_hogs; ret = gpiochip_irqchip_init_hw(gc); if (ret) goto err_remove_irqchip_mask; ret = gpiochip_add_irqchip(gc, lock_key, request_key); if (ret) goto err_remove_irqchip_mask; if (gpiolib_initialized) { ret = gpiochip_setup_dev(gdev); if (ret) goto err_remove_irqchip; } return 0 ; 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来完成实际的查找工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 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); struct gpio_desc *desc = ERR_PTR(-ENOENT); if (is_of_node(fwnode)) { dev_dbg(consumer, "using DT '%pfw' for '%s' GPIO lookup\n" , fwnode, name); desc = of_find_gpio(to_of_node(fwnode), con_id, idx, lookupflags); } else if (is_acpi_node(fwnode)) { dev_dbg(consumer, "using ACPI '%pfw' for '%s' GPIO lookup\n" , fwnode, name); desc = acpi_find_gpio(fwnode, con_id, idx, flags, lookupflags); } else if (is_software_node(fwnode)) { dev_dbg(consumer, "using swnode '%pfw' for '%s' GPIO lookup\n" , fwnode, name); desc = swnode_find_gpio(fwnode, con_id, idx, lookupflags); } 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void gpiod_add_lookup_tables (struct gpiod_lookup_table **tables, size_t n) { unsigned int i; guard(mutex)(&gpio_lookup_lock); for (i = 0 ; i < n; i++) list_add_tail(&tables[i]->list , &gpio_lookup_list); }
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。
健壮的依赖处理 (-EPROBE_DEFER) : 这是此函数设计中非常关键的一点。在上述查找过程中(无论是按名称还是按标签), 如果依赖的GPIO控制器驱动程序尚未被内核探测和初始化, 那么查找就会失败。此时, gpiod_find不会 返回一个硬性的”未找到”错误, 而是会返回-EPROBE_DEFER。这个特殊的返回值会通知上层调用者和内核驱动模型:”我的一个依赖项还没准备好, 请稍后重试探测我这个消费者驱动”。这是自动解决驱动加载顺序问题的核心机制。
与STM32H750的关系 对于一个使用现代设备树的STM32H750系统, gpiod_find函数通常不会被执行 。
GPIO的查找会由gpiod_find_and_request首先调用gpiod_find_by_fwnode, 然后分派到of_find_gpio来处理。
只有在of_find_gpio完全没有在设备树中找到任何匹配的GPIO属性, 并且 上层调用者(gpiod_find_and_request)的platform_lookup_allowed参数为true时, gpiod_find才会作为最后的手段被调用。
因此, 在一个配置正确的STM32设备树系统中, gpiod_find的执行通常意味着设备树配置存在问题或不完整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 static struct gpio_desc *gpiod_find (struct device *dev, const char *con_id, unsigned int idx, unsigned long *flags) { struct gpio_desc *desc = ERR_PTR(-ENOENT); struct gpiod_lookup_table *table ; struct gpiod_lookup *p ; struct gpio_chip *gc ; guard(mutex)(&gpio_lookup_lock); table = gpiod_find_lookup_table(dev); if (!table) return desc; 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))) continue ; if (p->chip_hwnum == U16_MAX) { desc = gpio_name_to_desc(p->key); if (desc) { *flags = p->flags; return desc; } dev_warn(dev, "cannot find GPIO line %s, deferring\n" , p->key); return ERR_PTR(-EPROBE_DEFER); } struct gpio_device *gdev __free (gpio_device_put ) = gpio_device_find_by_label(p->key); if (!gdev) { dev_warn(dev, "cannot find GPIO chip %s, deferring\n" , p->key); return ERR_PTR(-EPROBE_DEFER); } gc = gpio_device_get_chip(gdev); if (gc->ngpio <= p->chip_hwnum) { dev_err(dev, "requested GPIO %u (%u) is out of range [0..%u] for chip %s\n" , idx, p->chip_hwnum, gc->ngpio - 1 , gc->label); return ERR_PTR(-EINVAL); } desc = gpio_device_get_desc(gdev, p->chip_hwnum); *flags = p->flags; return desc; } return desc; }
gpiod_request 和 gpiod_request_commit: 安全地请求并独占一个GPIO 这两个函数协同工作, 共同构成了Linux内核gpiolib框架中用于”请求”或”声明”一个GPIO引脚的核心API。当一个设备驱动程序需要使用某个GPIO引脚时, 它必须先调用gpiod_request来获得对该引脚的独占访问权。这个过程确保了不会有多个驱动程序试图同时控制同一个物理引脚, 从而避免了硬件冲突。
gpiod_request_commit是执行实际工作的内部核心函数, 而gpiod_request则是一个安全封装, 它在调用核心函数之前处理了至关重要的模块生命周期管理。
gpiod_request_commit: 执行请求的核心逻辑此函数负责执行所有将GPIO引脚标记为”已使用”的底层操作。它的原理是通过一个原子操作来确保排他性, 并调用底层硬件驱动提供的可选回调函数, 同时在任何失败的情况下都能安全地回滚所有状态变更 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 static int gpiod_request_commit (struct gpio_desc *desc, const char *label) { unsigned int offset; int ret; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; if (test_and_set_bit(FLAG_REQUESTED, &desc->flags)) return -EBUSY; offset = gpio_chip_hwgpio(desc); if (!gpiochip_line_is_valid(guard.gc, offset)) return -EINVAL; if (guard.gc->request) { ret = guard.gc->request(guard.gc, offset); if (ret > 0 ) ret = -EBADE; if (ret) goto out_clear_bit; } if (guard.gc->get_direction) gpiod_get_direction(desc); ret = desc_set_label(desc, label ? : "?" ); if (ret) goto out_clear_bit; return 0 ; out_clear_bit: clear_bit(FLAG_REQUESTED, &desc->flags); return ret; }
gpiod_request: 安全的公共API封装此函数是在驱动程序中应该被调用的标准API。它在gpiod_request_commit的基础上, 增加了对内核模块生命周期的管理 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 int gpiod_request (struct gpio_desc *desc, const char *label) { int ret = -EPROBE_DEFER; VALIDATE_DESC(desc); if (try_module_get(desc->gdev->owner)) { ret = gpiod_request_commit(desc, label); if (ret) module_put(desc->gdev->owner); else 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输出电平的硬件调度函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 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; }
gpiochip_get_direction: 获取GPIO方向的硬件调度函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static int gpiochip_get_direction (struct gpio_chip *gc, unsigned int offset) { int ret; lockdep_assert_held(&gc->gpiodev->srcu); if (WARN_ON(!gc->get_direction)) return -EOPNOTSUPP; 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; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static int gpiochip_direction_input (struct gpio_chip *gc, unsigned int offset) { int ret; lockdep_assert_held(&gc->gpiodev->srcu); if (WARN_ON(!gc->direction_input)) return -EOPNOTSUPP; ret = gc->direction_input(gc, offset); if (ret > 0 ) ret = -EBADE; return ret; }
gpiochip_direction_output: 设置GPIO为输出的硬件调度函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 static int gpiochip_direction_output (struct gpio_chip *gc, unsigned int offset, int value) { int ret; lockdep_assert_held(&gc->gpiodev->srcu); if (WARN_ON(!gc->direction_output)) return -EOPNOTSUPP; ret = gc->direction_output(gc, offset, value); if (ret > 0 ) ret = -EBADE; return ret; }
这一组函数是gpiod_direction_output函数的逻辑对应面, 它们共同构成了将一个GPIO引脚配置为输入模式 的标准实现。同样, 它们也采用了层次化设计, 从一个简单的公共API深入到一个能够智能适应不同硬件能力的内部核心函数。
其核心原理是优先使用硬件驱动提供的专用回调函数来将引脚设置为输入, 如果专用函数不存在, 则通过查询引脚当前状态来推断其是否可用作输入, 最终在成功后更新gpiolib的内部软件状态标志并应用任何必要的偏置(如上拉/下拉电阻) 。
这是执行所有实际工作的核心函数。它负责与底层gpio_chip驱动交互, 并处理了各种可能的硬件驱动实现方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 int gpiod_direction_input_nonotify (struct gpio_desc *desc) { int ret = 0 , dir; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; if (!guard.gc->get && guard.gc->direction_input) { gpiod_warn(desc, "%s: missing get() but have direction_input()\n" , __func__); return -EIO; } 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; }
这个函数是暴露给驱动程序使用的标准顶层API。它的作用很简单: 调用核心逻辑函数, 并在成功后向用户空间发送状态变更通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int gpiod_direction_input (struct gpio_desc *desc) { int ret; VALIDATE_DESC(desc); ret = gpiod_direction_input_nonotify(desc); if (ret == 0 ) gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG); return ret; } EXPORT_SYMBOL_GPL(gpiod_direction_input);
gpiod_direction_output及相关函数: 设置GPIO为输出模式的层次化实现这一组函数共同构成了Linux gpiolib框架中将一个GPIO引脚配置为输出模式的完整实现。它们采用了一种层次化的设计, 从一个易于使用的高层逻辑API, 逐层深入到底层的硬件交互, 每一层都增加了特定的功能, 如安全检查、逻辑值转换、硬件能力适配和软件仿真。
gpiod_direction_output_raw_commit: 执行硬件配置的底层核心这是整个功能链的最底层和最核心的函数。它的作用是直接与底层的gpio_chip驱动程序交互, 发出将引脚设置为输出模式并赋予初始值的硬件命令 。它的原理是适配不同的硬件驱动能力, 并原子性地更新软件状态 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 static int gpiod_direction_output_raw_commit (struct gpio_desc *desc, int value) { int val = !!value, ret = 0 , dir; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; if (!guard.gc->set && !guard.gc->direction_output) { gpiod_warn(desc, "%s: missing set() and direction_output() operations\n" , __func__); return -EIO; } 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; } if (!ret) set_bit(FLAG_IS_OUT, &desc->flags); trace_gpio_value(desc_to_gpio(desc), 0 , val); trace_gpio_direction(desc_to_gpio(desc), 0 , ret); return ret; }
gpiod_direction_output_nonotify: 逻辑层核心 (处理特殊模式和安全检查)这个函数是整个逻辑的核心。它的作用是处理所有与软件相关的复杂性, 包括逻辑电平转换、开漏/开源模式的硬件支持或软件仿真, 以及关键的安全检查 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 int gpiod_direction_input_nonotify (struct gpio_desc *desc) { int ret = 0 , dir; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; if (!guard.gc->get && guard.gc->direction_input) { gpiod_warn(desc, "%s: missing get() but have direction_input()\n" , __func__); return -EIO; } 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; } int gpiod_direction_output_nonotify (struct gpio_desc *desc, int value) { unsigned long flags; int ret; flags = READ_ONCE(desc->flags); if (test_bit(FLAG_ACTIVE_LOW, &flags)) value = !value; else value = !!value; 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; } if (test_bit(FLAG_OPEN_DRAIN, &flags)) { ret = gpio_set_config(desc, PIN_CONFIG_DRIVE_OPEN_DRAIN); if (!ret) goto set_output_value; 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; if (!value) goto set_output_flag; } else { gpio_set_config(desc, PIN_CONFIG_DRIVE_PUSH_PULL); } set_output_value: ret = gpio_set_bias(desc); if (ret) return ret; return gpiod_direction_output_raw_commit(desc, value); set_output_flag: ret = gpiod_direction_input_nonotify(desc); if (ret) return ret; set_bit(FLAG_IS_OUT, &desc->flags); return 0 ; }
gpiod_direction_output 和 gpiod_direction_output_raw: 公共API这两个函数是暴露给驱动程序使用的顶层API。它们非常相似, 都是简单地调用它们各自的_nonotify或_commit版本, 然后在成功后发送一个通知 , 通常用于更新用户空间的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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); int gpiod_direction_output_raw (struct gpio_desc *desc, int value) { int ret; VALIDATE_DESC(desc); ret = gpiod_direction_output_raw_commit(desc, value); if (ret == 0 ) gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_CONFIG); return ret; } EXPORT_SYMBOL_GPL(gpiod_direction_output_raw);
gpiod_set_transitory及相关函数: 配置GPIO状态的持久性这一组函数共同实现了一个功能: 配置一个GPIO引脚的状态在系统低功耗(挂起/suspend)或复位事件中是否应该被保持(持久化, persistent)还是可以丢失(瞬态的, transitory) 。这对于电源管理至关重要, 例如, 一个用于唤醒系统的引脚必须保持其状态, 而一个用于点亮LED的引脚则可以在系统睡眠时被关闭。
这个功能通过一个从高层API到底层硬件驱动调用的函数链来实现。我们将从最高层的gpiod_set_transitory开始, 逐层深入。
gpiod_set_transitory: 设置引脚状态是否为瞬态的公共API这是驱动程序应该调用的顶层函数。它的核心原理是实现了一个两级状态管理: (1) 它无条件地更新内核gpiolib框架中关于该引脚的软件状态标志; (2) 然后, 它”尽力而为”(best-effort)地尝试将这个配置应用到底层的物理硬件上 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 int gpiod_set_transitory (struct gpio_desc *desc, bool transitory) { VALIDATE_DESC(desc); assign_bit(FLAG_TRANSITORY, &desc->flags, transitory); return gpio_set_config_with_argument_optional(desc, PIN_CONFIG_PERSIST_STATE, !transitory); }
gpio_set_config_with_argument_optional: “可选地”应用配置此函数是gpiod_set_transitory的直接辅助函数。它的核心作用是尝试应用一个配置, 但如果底层硬件明确表示”不支持”该功能, 则将其视为成功, 而不是一个错误 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 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); if (ret != -ENOTSUPP) return ret; switch (mode) { case PIN_CONFIG_PERSIST_STATE: dev_dbg(dev, "Persistence not supported for GPIO %d\n" , gpio); break ; default : break ; } return 0 ; }
gpio_set_config_with_argument 和 gpio_do_set_config: 打包并分发配置gpio_set_config_with_argument是一个简单的转换器, 而gpio_do_set_config是最终与硬件驱动程序交互的网关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 static int gpio_set_config (struct gpio_desc *desc, enum pin_config_param mode) { return gpio_set_config_with_argument(desc, mode, 0 ); } static int gpio_set_config_with_argument (struct gpio_desc *desc, enum pin_config_param mode, u32 argument) { unsigned long config; config = pinconf_to_config_packed(mode, argument); return gpio_do_set_config(desc, config); } int gpio_do_set_config (struct gpio_desc *desc, unsigned long config) { int ret; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; if (!guard.gc->set_config) return -ENOTSUPP; ret = guard.gc->set_config(guard.gc, gpio_chip_hwgpio(desc), config); if (ret > 0 ) ret = -EBADE; #ifdef CONFIG_GPIO_CDEV 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; }
此函数是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标志, 表明此引脚的状态在系统睡眠/挂起期间无需被保持, 这是一种电源管理优化。
应用动态方向和初始值 (dflags) : 在处理完静态标志后, 函数会检查调用者(通常是设备驱动)传入的dflags。
方向设置 : 如果dflags中包含GPIOD_FLAGS_BIT_DIR_SET标志, 意味着驱动程序希望明确设置引脚的方向。
如果GPIOD_FLAGS_BIT_DIR_OUT被设置, 它会调用gpiod_direction_output_nonotify将引脚配置为输出模式。同时, 它会检查GPIOD_FLAGS_BIT_DIR_VAL标志, 以此决定引脚的初始输出电平 是高还是低。
否则, 它会调用gpiod_direction_input_nonotify将引脚配置为输入模式。
如果dflags中不 包含GPIOD_FLAGS_BIT_DIR_SET, 函数在完成第一阶段后就会直接返回成功。这允许驱动程序只获取一个GPIO句柄并应用其静态电气特性, 而不立即改变其方向。
一个值得注意的细节是该函数如何处理OPEN_DRAIN的动态请求: 它允许驱动程序通过dflags来强制设置开漏模式, 但前提是lflags中没有指定。然而, 它会打印一条警告, 强调这种配置应该 在设备树等板级描述中定义, 驱动程序强制指定是一种不规范的备用手段。
在STM32H750这样的系统中, 当一个设备驱动(例如I2C驱动)调用devm_gpiod_get_optional()或类似函数时, gpiolib核心最终会调用gpiod_configure_flags。lflags参数会携带从STM32H750设备树中解析出的标志(如GPIO_ACTIVE_LOW), 而dflags则携带驱动自身指定的标志(如GPIOD_OUT_HIGH)。此函数随后会将这些抽象的标志转换为对底层STM32 GPIO驱动gpio_chip的回调函数(如.direction_output)的调用, 最终实现对STM32 GPIO端口的MODER, PUPDR, OTYPER, ODR等寄存器的精确配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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; if (lflags & GPIO_ACTIVE_LOW) set_bit(FLAG_ACTIVE_LOW, &desc->flags); if (lflags & GPIO_OPEN_DRAIN) set_bit(FLAG_OPEN_DRAIN, &desc->flags); else if (dflags & GPIOD_FLAGS_BIT_OPEN_DRAIN) { set_bit(FLAG_OPEN_DRAIN, &desc->flags); gpiod_warn(desc, "enforced open drain please flag it properly in DT/ACPI DSDT/board file\n" ); } 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; } 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; 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) 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)的机制。这确保了对没有使用设备树的旧平台的向后兼容性。
所有权与资源管理 (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句柄, 并假定第一个请求者已经完成了必要的初始化配置。
最终配置与错误恢复 (Configure) : 在成功声明所有权后, 函数调用gpiod_configure_flags来应用调用者请求的初始状态, 例如将引脚设置为输出低电平、配置为开漏模式等。
关键的错误处理 : 如果配置步骤失败, 函数会立即调用gpiod_put(与gpiod_request配对的释放函数)来撤销 刚刚成功的请求操作, 将GPIO引脚释放回池中, 从而确保系统状态的一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 void gpiod_line_state_notify (struct gpio_desc *desc, unsigned long action) { guard(read_lock_irqsave)(&desc->gdev->line_state_lock); raw_notifier_call_chain(&desc->gdev->line_state_notifier, action, desc); } struct gpio_desc *gpiod_find_and_request (struct device *consumer, struct fwnode_handle *fwnode, const char *con_id, unsigned int idx, enum gpiod_flags flags, const char *label, bool platform_lookup_allowed) { unsigned long lookupflags = GPIO_LOOKUP_FLAGS_DEFAULT; const char *name = function_name_or_default(con_id); struct gpio_desc *desc = NULL ; int ret = 0 ; scoped_guard(srcu, &gpio_devices_srcu) { 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; } ret = gpiod_request(desc, label); } if (ret) { 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; } 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); } gpiod_line_state_notify(desc, GPIO_V2_LINE_CHANGED_REQUESTED); 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)或发生其他问题, 它会返回相应的错误码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct gpio_desc *__must_check gpiod_get_index (struct device *dev, const char *con_id, unsigned int idx, enum gpiod_flags flags) { struct fwnode_handle *fwnode = dev ? dev_fwnode(dev) : NULL ; const char *devname = dev ? dev_name(dev) : "?" ; const char *label = con_id ?: devname; 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这个特定的错误码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct gpio_desc *__must_check gpiod_get_index_optional (struct device *dev, const char *con_id, unsigned int index, enum gpiod_flags flags) { struct gpio_desc *desc ; desc = gpiod_get_index(dev, con_id, index, flags); if (gpiod_not_found(desc)) return NULL ; return desc; } EXPORT_SYMBOL_GPL(gpiod_get_index_optional);
gpiod_get_optional: 获取单个可选GPIO (最常用)这是最顶层的、也是最常用的便利封装。它的核心作用是获取与某个功能关联的**第一个(也是唯一一个)**可选GPIO。
原理 : 它是一个极简的封装, 直接调用gpiod_get_index_optional, 并将索引idx硬编码为0。
在STM32H750这样的嵌入式系统中, 大多数功能(如复位、中断、使能)都只由单个GPIO引脚控制。因此, 这个函数是驱动程序中最常见的选择。例如, reg_fixed_voltage_probe中获取使能引脚时, 使用的就是这个函数。它允许设备树中可以完全不定义enable-gpios属性, 驱动程序也能正常工作(只是没有使能控制功能), 从而大大增强了硬件描述的灵活性。
1 2 3 4 5 6 7 8 struct gpio_desc *__must_check gpiod_get_optional (struct device *dev, const char *con_id, enum gpiod_flags flags) { 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) 机制来保证标签更新的并发安全性。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 static void desc_free_label (struct rcu_head *rh) { kfree(container_of(rh, struct gpio_desc_label, rh)); } static int desc_set_label (struct gpio_desc *desc, const char *label) { struct gpio_desc_label *new = NULL , *old; if (label) { new = kzalloc(struct_size(new, str, strlen (label) + 1 ), GFP_KERNEL); if (!new) return -ENOMEM; strcpy (new->str, label); } old = rcu_replace_pointer(desc->label, new, 1 ); if (old) call_srcu(&desc->gdev->desc_srcu, &old->rh, desc_free_label); return 0 ; } 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); } 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)。
处理开漏/开源 (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会被调用,它会直接将经过低电平有效逻辑转换后的物理值写入硬件寄存器。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static int gpiod_set_value_nocheck (struct gpio_desc *desc, int value) { 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); } 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_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()。将引脚配置为输入模式,进入高阻态,让外部下拉电阻生效。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 static inline int gpio_chip_hwgpio (const struct gpio_desc *desc) { return desc - &desc->gdev->descs[0 ]; } static int gpio_set_open_drain_value_commit (struct gpio_desc *desc, bool value) { int ret = 0 , offset = gpio_chip_hwgpio(desc); 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); } trace_gpio_direction(desc_to_gpio(desc), value, ret); if (ret < 0 ) gpiod_err(desc, "%s: 设置开漏值时出错,错误码 %d\n" , __func__, ret); return ret; } static int gpio_set_open_source_value_commit (struct gpio_desc *desc, bool value) { int ret = 0 , offset = gpio_chip_hwgpio(desc); 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); } trace_gpio_direction(desc_to_gpio(desc), !value, ret); if (ret < 0 ) gpiod_err(desc, "%s: 设置开源值时出错,错误码 %d\n" , __func__, ret); return ret; } 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; CLASS(gpio_chip_guard, guard)(desc); if (!guard.gc) return -ENODEV; trace_gpio_value(desc_to_gpio(desc), 0 , value); 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位图中。
提交分组 : 内层循环结束后,如果count不为0(表示聚合到了至少一个推挽输出),它就会调用一次gpiochip_set_multiple(guard.gc, mask, bits)来批量提交 这个分组的所有引脚。
效率 : 慢速路径的效率低于快速路径,因为它可能需要多次调用gpiochip_set_multiple(每个gpio_chip分组一次),并且还需要对特殊类型的引脚进行单独处理。但它保证了功能的通用性 和正确性 。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 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; } 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; } for_each_set_bit(i, mask, gc->ngpio) { ret = gpiochip_set(gc, i, test_bit(i, bits)); if (ret) break ; } return ret; } 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; return gpiod_set_array_value_complex(false , false , array_size, desc_array, array_info, value_bitmap); } EXPORT_SYMBOL_GPL(gpiod_set_array_value); 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; 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); 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)) 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 ; } while (i < array_size) { int count = 0 ; CLASS(gpio_chip_guard, guard)(desc_array[i]); if (!guard.gc) return -ENODEV; do { 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++; } 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中。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 static int gpiod_get_raw_value_commit (const struct gpio_desc *desc) { guard(srcu)(&gdev->srcu); gc = srcu_dereference(gdev->chip, &gdev->srcu); if (!gc) return -ENODEV; value = gpio_chip_get_value(gc, desc); value = value < 0 ? value : !!value; trace_gpio_value(desc_to_gpio(desc), 1 , value); return value; } static int gpio_chip_get_multiple (struct gpio_chip *gc, unsigned long *mask, unsigned long *bits) { if (gc->get_multiple) { return gc->get_multiple(gc, mask, bits); } 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; } 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 && ) { ret = gpio_chip_get_multiple(gc, array_info->get_mask, value_bitmap); if (ret) return ret; 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) { do { __set_bit(hwgpio, mask); } while ((i < array_size) && gpio_device_chip_cmp(desc_array[i]->gdev, guard.gc)); ret = gpio_chip_get_multiple(guard.gc, mask, bits); 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 ; } 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); 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);
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线路的使用权。
操作 (Operating) :
程序对这个新的line handle FD执行ioctl操作,如GPIOHANDLE_SET_VALUES_IOCTL或GPIOHANDLE_GET_VALUES_IOCTL,来设置或读取GPIO的值。
生命周期管理 (Lifecycle Management) :
当程序close()这个line handle FD时(或者程序崩溃,内核会自动关闭其所有FD),对这些GPIO线路的“占用”就会被自动释放 。内核会将这些GPIO恢复到默认状态。这从根本上解决了sysfs的资源泄漏问题。
事件监控 (Event Monitoring) :
如果在请求线路时指定了需要监控事件(如边沿触发),那么对line handle FD执行read()操作将会阻塞,直到指定的GPIO事件(如上升沿)发生。这提供了一个高效、可靠的等待GPIO中断的方式。
它的主要优势体现在哪些方面?
稳定性与可移植性 :通过名称(如"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) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <gpiod.h> 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中注册的唤醒机制唤醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 ; guard(srcu)(&cdev->gdev->srcu); if (!rcu_access_pointer(cdev->gdev->chip)) return EPOLLHUP | EPOLLERR; poll_wait(file, &cdev->wait, pollt); if (!kfifo_is_empty_spinlocked_noirqsave(&cdev->events, &cdev->wait.lock)) events = EPOLLIN | EPOLLRDNORM; 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()来获取事件的详细信息(例如, 哪个引脚发生了什么类型的事件)。这种机制在任何现代操作系统中, 对于处理异步硬件事件都是至关重要的, 即使是在单核系统上, 它也能极大地提高系统的响应能力和能效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 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 event_size = sizeof (struct gpio_v2_line_info_changed); if (count < event_size) return -EINVAL; #endif do { scoped_guard(spinlock, &cdev->wait.lock) { if (kfifo_is_empty(&cdev->events)) { if (bytes_read) return bytes_read; if (file->f_flags & O_NONBLOCK) return -EAGAIN; ret = wait_event_interruptible_locked(cdev->wait, !kfifo_is_empty(&cdev->events)); if (ret) return ret; } #ifdef CONFIG_GPIO_CDEV_V1 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 if (kfifo_out(&cdev->events, &event, 1 ) != 1 ) { WARN(1 , "failed to read from non-empty kfifo" ); return -EIO; } } #ifdef CONFIG_GPIO_CDEV_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等锁机制在单核抢占式系统上依然是必需的, 它通过禁用本地中断和抢占来保护对通知链表等共享资源的访问, 防止数据结构被并发修改所破坏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 static int gpio_chrdev_open (struct inode *inode, struct file *file) { struct gpio_device *gdev = container_of(inode->i_cdev, struct gpio_device, chrdev); struct gpio_chardev_data *cdev ; int ret = -ENOMEM; guard(srcu)(&gdev->srcu); 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; init_waitqueue_head(&cdev->wait); INIT_KFIFO(cdev->events); cdev->gdev = gpio_device_get(gdev); cdev->lineinfo_changed_nb.notifier_call = lineinfo_changed_notify; 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; ret = blocking_notifier_chain_register(&gdev->device_notifier, &cdev->device_unregistered_nb); if (ret) goto out_unregister_line_notifier; file->private_data = cdev; cdev->fp = file; ret = nonseekable_open(inode, file); if (ret) goto out_unregister_device_notifier; return ret; out_unregister_device_notifier: blocking_notifier_chain_unregister(&gdev->device_notifier, &cdev->device_unregistered_nb); out_unregister_line_notifier: scoped_guard(write_lock_irqsave, &gdev->line_state_lock) raw_notifier_chain_unregister(&gdev->line_state_notifier, &cdev->lineinfo_changed_nb); out_free_bitmap: gpio_device_put(gdev); bitmap_free(cdev->watched_lines); out_free_cdev: kfree(cdev); return ret; }
GPIO字符设备接口的注册与注销 gpio_fileops: 文件操作函数集这是一个静态常量结构体, 它像一张”功能表”, 定义了当用户空间对本驱动创建的字符设备文件进行操作时, 内核应该调用哪些具体的函数来响应该操作。这是连接VFS和GPIO驱动功能的桥梁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static const struct file_operations gpio_fileops = { .release = gpio_chrdev_release, .open = gpio_chrdev_open, .poll = lineinfo_watch_poll, .read = lineinfo_watch_read, .owner = THIS_MODULE, .unlocked_ioctl = gpio_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = gpio_ioctl_compat, #endif };
gpiolib_cdev_register: 注册GPIO字符设备此函数负责为一个GPIO设备(gdev)执行所有必要的步骤, 来创建一个功能齐全、可供用户空间访问的字符设备。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 int gpiolib_cdev_register (struct gpio_device *gdev, dev_t devt) { struct gpio_chip *gc ; int ret; cdev_init(&gdev->chrdev, &gpio_fileops); gdev->chrdev.owner = THIS_MODULE; gdev->dev.devt = MKDEV(MAJOR(devt), gdev->id); gdev->line_state_wq = alloc_ordered_workqueue("%s" , WQ_HIGHPRI, dev_name(&gdev->dev)); if (!gdev->line_state_wq) return -ENOMEM; ret = cdev_device_add(&gdev->chrdev, &gdev->dev); if (ret) return ret; guard(srcu)(&gdev->srcu); gc = srcu_dereference(gdev->chip, &gdev->srcu); if (!gc) return -ENODEV; chip_dbg(gc, "added GPIO chardev (%d:%d)\n" , MAJOR(devt), gdev->id); return 0 ; }
gpiolib_cdev_unregister: 注销GPIO字符设备此函数是注册函数的逆过程, 负责清理和释放所有相关资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void gpiolib_cdev_unregister (struct gpio_device *gdev) { destroy_workqueue(gdev->line_state_wq); 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。1 2 3 4 5 6 gpiod = devm_gpiod_get(dev, "reset" , GPIOD_OUT_LOW); if (IS_ERR(gpiod)) return PTR_ERR(gpiod);
提高健壮性 : 它从根本上消除了因复杂的错误处理路径而导致的 GPIO 资源泄漏风险。这对于需要长期稳定运行的嵌入式系统至关重要。
结论 : devm_gpiod_get 是在 STM32H750 平台上编写简洁、安全、可靠的 GPIO 消费者驱动的首选 API。
单核环境影响
devres 框架及其内部的锁机制在单核处理器上是安全有效的(通过禁用中断来保证原子性)。
GPIO 子系统的 gpiod_get/gpiod_put 内部也包含了必要的同步机制。
此代码的逻辑与 CPU 核心数无关。
无MMU影响
devres 框架和 GPIO 子系统都是内核的核心基础设施,它们不依赖于内存管理单元(MMU)。
它们操作的是内核数据结构(device, gpio_desc)和通过 kmalloc 分配的内核内存。
因此,缺少 MMU 对这些函数的逻辑和正确性没有任何影响 。
代码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 static void devm_gpiod_release (void *desc) { gpiod_put(desc); } static void devm_gpiod_release_array (void *descs) { gpiod_put_array(descs); } struct gpio_desc *__must_check devm_gpiod_get (struct device *dev, const char *con_id, enum gpiod_flags flags) { return devm_gpiod_get_index(dev, con_id, 0 , flags); } EXPORT_SYMBOL_GPL(devm_gpiod_get); 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; desc = gpiod_get_index(dev, con_id, idx, flags); if (IS_ERR(desc)) return desc; if (flags & GPIOD_FLAGS_BIT_NONEXCLUSIVE) { bool dres; dres = devm_is_action_added(dev, devm_gpiod_release, desc); if (dres) return desc; } ret = devm_add_action_or_reset(dev, devm_gpiod_release, desc); if (ret) return ERR_PTR(ret); return desc; } EXPORT_SYMBOL_GPL(devm_gpiod_get_index);