往期内容
本专栏往期内容:
- Pinctrl子系统和其主要结构体引入
- Pinctrl子系统pinctrl_desc结构体进一步介绍
- Pinctrl子系统中client端设备树相关数据结构介绍和解析
- inctrl子系统中Pincontroller构造过程驱动分析:imx_pinctrl_soc_info结构体
- Pinctrl子系统中client端使用pinctrl过程的驱动分析
- Pinctrl子系统中Pincontroller和client驱动程序的编写
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有往期内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有往期内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有往期内容观看顺序
前言
Linux 4.x内核文档
- Linux-4.9.88\Documentation\gpio📎drivers-on-gpio.txt📎gpio.txt📎gpio-legacy.txt📎sysfs.txt📎board.txt📎consumer.txt📎driver.txt
- Linux-4.9.88\Documentation\devicetree\bindings\gpio\gpio.txt📎gpio.txt
- Linux-4.9.88\drivers\gpio\gpio-74x164.c📎gpio-74x164.c
本文主要讲解GPIO子系统的层次结构体、提供的相关API接口以及相关数据结构,同时通过对内核的示例驱动代码添加一些注释使能更好的理解GPIO子系统。
1.GPIO子系统的层次
1.1 层次
1.2 GPIOLIB向上提供的接口
descriptor-based | legacy |
---|---|
获得GPIO | |
gpiod_get | gpio_request |
gpiod_get_index | |
gpiod_get_array | gpio_request_array |
devm_gpiod_get | |
devm_gpiod_get_index | |
devm_gpiod_get_array | |
---------------------- | --------------------- |
设置方向 | |
gpiod_direction_input | gpio_direction_input |
gpiod_direction_output | gpio_direction_output |
---------------------- | --------------------- |
读值、写值 | |
gpiod_get_value | gpio_get_value |
gpiod_set_value | gpio_set_value |
---------------------- | --------------------- |
释放GPIO | |
gpio_free | gpio_free |
gpiod_put | gpio_free_array |
gpiod_put_array | |
devm_gpiod_put | |
devm_gpiod_put_array |
GPIO子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”,它使用gpio_desc结构体来表示一个引脚;后者的函数都有前缀“gpio_”,它使用一个整数来表示一个引脚。
有前缀“devm_”的含义是“设备资源管理”(Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。
实际上会调用到gpio_chip中的相关函数:
对应这两种方法有啥区别,下面在介绍gpio_desc中会提到。
1.3 GPIOLIB向下提供的接口
如果chip无法被注册(例如chip->base无效或者已经和其它不同的chip关联),则返回负数的errno。否侧返回0。
当函数在开机早期被调用,以便GPIOs可以自由适用于时,chip->parent设备必须在gpio框架的arch_initcall()调用之前注册好。否则,GPIOs的sysfs初始化将失败。
函数只能在core_initcall()初始化之后(即在core_initcall之后)调用。
如果chip_base为负,则要求动态分配一系列有效的GPIO。
先自己构造好GPIO控制器对应的gpio_chip(里面有对GPIO控制器对应引脚控制和中断的相关操作函数),通过上面该函数来构造gpio_device来表示一个GPIO控制器
2.重要的3个核心数据机构
记住GPIO Controller的要素,这有助于理解它的驱动程序:
- 一个GPIO Controller里有多少个引脚?有哪些引脚?
- 需要提供函数,设置引脚方向、读取/设置数值
- 需要提供函数,把引脚转换为中断
以Linux面向对象编程的思想,一个GPIO Controller必定会使用一个结构体来表示,这个结构体必定含有这些信息:
- GPIO引脚信息
- 控制引脚的函数
- 中断相关的函数
2.1 gpio_device
每个GPIO Controller用一个gpio_device来表示:
- 里面每一个gpio引脚用一个gpio_desc来表示
- gpio引脚的函数(引脚控制、中断相关),都放在gpio_chip里
/*** struct gpio_device - GPIO 设备的内部状态容器* * @id: GPIO 芯片的数字 ID。* @dev: GPIO 设备对应的 `device` 结构体,表示 Linux 设备模型中的该 GPIO 设备。* @chrdev: GPIO 设备的字符设备结构,用于设备节点访问 GPIO。* @mockdev: 类设备,用于过时的 sysfs 接口;如果该接口不使用,可能为 NULL。* @owner: 模块的引用计数,当 GPIO 正在使用时,防止模块被卸载。* @chip: 指向 `gpio_chip` 的指针,保存 GPIO 芯片的静态数据,包括 GPIO 操作函数。* @descs: 描述符数组,包含该 GPIO 设备的所有 GPIO 线的描述符,数组大小为 `ngpio`。* @ngpio: GPIO 设备中的 GPIO 线数量,与 `descs` 数组大小相同。* @base: GPIO 的基础编号(已弃用),全局编号空间中的起始值,在设备创建时分配。* @label: GPIO 设备的描述性名称,例如 SoC 内的 IP 组件的部件号或名称。* @data: 驱动程序分配的每个实例的数据,用于存储驱动特定的自定义数据。* @list: 将多个 `gpio_device` 结构连接在一起,用于遍历。* * 该状态容器保存了一个 GPIO 设备的大多数运行时变量数据,* 在 GPIO 芯片被移除后,它仍可以被保留并在用户空间使用。*/
struct gpio_device {int id; // GPIO 芯片的数字 IDstruct device dev; // GPIO 设备的 device 结构struct cdev chrdev; // 字符设备结构,用于字符设备节点struct device *mockdev; // 用于过时的 sysfs 接口的类设备,可能为 NULLstruct module *owner; // 模块引用计数,防止模块卸载struct gpio_chip *chip; // 指向 gpio_chip 的指针,保存 GPIO 芯片的静态信息struct gpio_desc *descs; // GPIO 描述符数组int base; // 在全局 GPIO 空间中的基础编号(已弃用)u16 ngpio; // GPIO 设备中的 GPIO 线数量char *label; // 描述性名称void *data; // 驱动程序分配的自定义数据struct list_head list; // 链表结构,用于将多个 `gpio_device` 串联#ifdef CONFIG_PINCTRL/** 如果 CONFIG_PINCTRL 被启用,GPIO 控制器可以描述其在 SoC 中实际服务的管脚范围。* 该信息将由 pinctrl 子系统使用,以配置 GPIO 管脚使用。*/struct list_head pin_ranges; // pinctrl 的 GPIO 管脚范围
#endif
};
以1号GPIO Controller,如下图,那么其就有一个gpio_device
结构体来表示,可以看出他有四个引脚,每个引脚就是用其成员struct gpio_desc *descs
来表示,这是个数组来着,具体是什么下文会讲解。
-
id
就是1,因为这个1号GPIO控制器。 -
base
就是引脚编号基值,在这里就是0,在传统legacy中有用,需要注意的是它是全局的。- 比如第二个GPIO控制器,有32个引脚(编号范围是pin0pin31),那么加上1号控制器全部就有36个引脚是吧,此时我通过legacy的方式去使用**编号**为10的引脚(也就是第11根pin)。可以知道10号引脚不在1号GPIO控制器内(因为其只有4根pin,pin03),那就会去2号控制器找,那么注意了,在2号控制器的base就是4(全局,注意是编号值,编号是从0开始的!),10引脚对应其哪一个???10-4=6,也就是编号值为6的引脚(在全局中对应第11根pin)
- descriptor-based中,就弃用了好像,按照上面的情景,我还是去找编号为10的引脚,在1号控制器没找到,因为其大于
ngpio-1
,那么就会去第二个控制器找,有32根pin(pin0pin31),那么要找的编号为10的引脚就直接对应到pin0pin31中的pin10,从全局上来看就是第4+11 = 15根引脚,是不是就和legacy不一样了
还有疑惑的可以看一下下面的图:
📎leddrv.c
2.2 gpio_chip
并不需要自己创建gpio_device,编写驱动时要创建的是gpio_chip,里面提供了:
- 控制引脚的函数
- 中断相关的函数
- 引脚信息:支持多少个引脚?各个引脚的名字?
/*** struct gpio_chip - 抽象 GPIO 控制器* * @label: GPIO 设备的功能性名称,如部件号或 SoC 中实现该 GPIO 的 IP 模块名称。* @gpiodev: 内部状态结构的指针,用于 `gpio_device` 结构。* @parent: 可选的父设备,提供 GPIO 的来源。* @owner: 引用模块,防止在 GPIO 使用时移除模块。** @request: 可选的钩子函数,用于激活 GPIO 控制器模块(如开启模块电源和时钟);可能会导致休眠。* @free: 可选的钩子函数,用于释放 GPIO 控制器模块(如关闭电源和时钟);也可能休眠。* @get_direction: 获取 GPIO 方向(0 表示输出,1 表示输入),返回负数表示错误。* @direction_input: 将 GPIO 配置为输入;返回错误代码(如果失败)。* @direction_output: 将 GPIO 配置为输出,并设置初始输出值;返回错误代码。* @get: 获取 GPIO 信号的当前值,0 表示低电平,1 表示高电平,负值表示错误。* @set: 设置 GPIO 的输出值(高或低)。* @set_multiple: 为多个 GPIO 信号设置输出值,使用位掩码 `mask` 定义多个信号。* @set_debounce: 设置特定 GPIO 的去抖时间(仅限支持中断触发的 GPIO 芯片)。* @set_single_ended: 设置 GPIO 线为开漏、开源或非单端(例如从开漏恢复到正常模式),适用于支持这些功能的硬件。* @to_irq: 支持非静态 `gpio_to_irq()` 映射的可选钩子,不能在实现中休眠。* @dbg_show: 可选的调试显示例程,显示 GPIO 芯片的状态。* * @base: 该芯片处理的第一个 GPIO 编号;如果设置为负数,则动态分配编号。* 建议使用动态分配方式(base = -1),以避免全局静态 GPIO 编号空间。* @ngpio: 该控制器管理的 GPIO 数量。* @names: 可选的 GPIO 名称数组,为 GPIO 芯片的 GPIO 提供别名,数组长度必须为 `ngpio`。* @can_sleep: 若 `get()` 和 `set()` 函数会休眠(如通过 I2C 或 SPI 访问 GPIO 扩展芯片),则该标志应为真。* @irq_not_threaded: 若 `can_sleep` 为真但 IRQ 不需要线程化,则应设置此标志。** 通用 GPIO 控制器寄存器的接口:* * @read_reg: 通用 GPIO 的读取寄存器。* @write_reg: 通用 GPIO 的写入寄存器。* @pin2mask: GPIO 引脚到位掩码的转换器回调。* @reg_dat: 输入数据寄存器。* @reg_set: 输出设定寄存器(设为高电平)。* @reg_clr: 输出清除寄存器(设为低电平)。* @reg_dir: GPIO 方向寄存器。* @bgpio_bits: 通用 GPIO 的寄存器位数。* @bgpio_lock: 用于锁定 `bgpio_data`,保证数据写入的原子性。* @bgpio_data: 通用 GPIO 的数据寄存器,用于安全地清除/设置位。* @bgpio_dir: 方向寄存器的镜像。* * 与 GPIO IRQ 相关的字段:* * @irqchip: GPIO IRQ 芯片实现,通常由 GPIO 驱动提供。* @irqdomain: 中断翻译域,用于在硬件 IRQ 与 Linux IRQ 之间映射。* @irq_base: GPIO IRQ 芯片的第一个 Linux IRQ 编号(已弃用)。* @irq_handler: GPIO 中断的处理程序(通常为预定义的中断核心函数)。* @irq_default_type: GPIO 驱动初始化期间应用的默认中断触发类型。* @irq_parent: GPIO IRQ 芯片的父中断编号。* @irq_need_valid_mask: 如果设置为真,核心会分配 `irq_valid_mask`,且所有位均为 1。* @irq_valid_mask: 允许包含在 IRQ 域的有效 GPIO 位掩码。* @lock_key: 每个 GPIO IRQ 芯片的 lockdep 类。* * 设备树相关的字段(CONFIG_OF_GPIO):* * @of_node: 设备树中的节点指针。* @of_gpio_n_cells: GPIO 节点的单元数量。* @of_xlate: 用于 OF(设备树)的 GPIO 芯片转换回调。** 该结构体使平台能够抽象各种来源的 GPIO,提供一致的接口调用,* 并支持不同硬件架构的 GPIO 访问和控制。*/
struct gpio_chip {const char *label;struct gpio_device *gpiodev;struct device *parent;struct module *owner;int (*request)(struct gpio_chip *chip,unsigned offset);void (*free)(struct gpio_chip *chip,unsigned offset);int (*get_direction)(struct gpio_chip *chip,unsigned offset);int (*direction_input)(struct gpio_chip *chip,unsigned offset);int (*direction_output)(struct gpio_chip *chip,unsigned offset, int value);int (*get)(struct gpio_chip *chip,unsigned offset);void (*set)(struct gpio_chip *chip,unsigned offset, int value);void (*set_multiple)(struct gpio_chip *chip,unsigned long *mask,unsigned long *bits);int (*set_debounce)(struct gpio_chip *chip,unsigned offset,unsigned debounce);int (*set_single_ended)(struct gpio_chip *chip,unsigned offset,enum single_ended_mode mode);int (*to_irq)(struct gpio_chip *chip,unsigned offset);void (*dbg_show)(struct seq_file *s,struct gpio_chip *chip);int base;u16 ngpio;const char *const *names;bool can_sleep;bool irq_not_threaded;#if IS_ENABLED(CONFIG_GPIO_GENERIC)unsigned long (*read_reg)(void __iomem *reg);void (*write_reg)(void __iomem *reg, unsigned long data);unsigned long (*pin2mask)(struct gpio_chip *gc, unsigned int pin);void __iomem *reg_dat;void __iomem *reg_set;void __iomem *reg_clr;void __iomem *reg_dir;int bgpio_bits;spinlock_t bgpio_lock;unsigned long bgpio_data;unsigned long bgpio_dir;
#endif#ifdef CONFIG_GPIOLIB_IRQCHIPstruct irq_chip *irqchip;struct irq_domain *irqdomain;unsigned int irq_base;irq_flow_handler_t irq_handler;unsigned int irq_default_type;int irq_parent;bool irq_need_valid_mask;unsigned long *irq_valid_mask;struct lock_class_key *lock_key;
#endif#if defined(CONFIG_OF_GPIO)struct device_node *of_node;int of_gpio_n_cells;int (*of_xlate)(struct gpio_chip *gc,const struct of_phandle_args *gpiospec, u32 *flags);
#endif
};
内核中给了详细的英文注释,这里按照个人理解将其转译出来。
2.3 gpio_desc
使用GPIO子系统时,首先是获得某个引脚对应的gpio_desc。
gpio_device表示一个GPIO Controller,里面支持多个GPIO。
在gpio_device中有一个gpio_desc数组,每一引脚有一项gpio_desc。
struct gpio_desc {struct gpio_device *gdev; /* 所属 GPIO 设备的指针 */unsigned long flags; /* GPIO 的状态标志位 *//* 状态标志的位定义 */
#define FLAG_REQUESTED 0 /* 标记 GPIO 已被请求 */
#define FLAG_IS_OUT 1 /* 标记 GPIO 为输出方向 */
#define FLAG_EXPORT 2 /* 通过 sysfs_lock 保护的 GPIO 导出标志 */
#define FLAG_SYSFS 3 /* GPIO 是否通过 /sys/class/gpio 导出 */
#define FLAG_ACTIVE_LOW 6 /* GPIO 为低电平有效 */
#define FLAG_OPEN_DRAIN 7 /* GPIO 为开漏类型 */
#define FLAG_OPEN_SOURCE 8 /* GPIO 为开源类型 */
#define FLAG_USED_AS_IRQ 9 /* GPIO 已用作中断 */
#define FLAG_IS_HOGGED 11 /* GPIO 被占用 *//* 连接标签,用于标识 GPIO 功能或用途 */const char *label;/* GPIO 的名称 */const char *name;
};
3.怎么编写GPIO Controller驱动程序
分配、设置、注册gpioc_chip结构体,示例:drivers\gpio\gpio-74x164.c
📎gpio-74x164.c,内核提供的 对 74x164 移位寄存器的 SPI 控制功能,将寄存器的位作为 GPIO 使用。通过实现 GPIO 接口函数,它可以与 Linux 的 GPIO 子系统交互,使用户空间可以访问和控制寄存器的引脚状态。 下面是个人对代码添加的一些注释
/** 74Hx164 - 通用串入/并出 8位移位寄存器 GPIO 驱动* * 该驱动程序实现了对 74x164 类的串入/并出移位寄存器的控制,提供 GPIO 接口。*/#include <linux/init.h>
#include <linux/mutex.h>
#include <linux/spi/spi.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/slab.h>
#include <linux/module.h>#define GEN_74X164_NUMBER_GPIOS 8 // 每个移位寄存器有 8 个 GPIO 引脚// 定义 gen_74x164_chip 结构体,表示一个 GPIO 扩展器设备实例
struct gen_74x164_chip {struct gpio_chip gpio_chip; // GPIO 控制器结构struct mutex lock; // 保护移位寄存器缓冲区的互斥锁u32 registers; // 移位寄存器数量u8 buffer[0]; // 缓冲区,用于存储所有移位寄存器的状态,大小动态分配
};// 将缓冲区内容写入硬件寄存器
static int __gen_74x164_write_config(struct gen_74x164_chip *chip)
{return spi_write(to_spi_device(chip->gpio_chip.parent), chip->buffer,chip->registers);
}// 获取指定 GPIO 引脚的值
static int gen_74x164_get_value(struct gpio_chip *gc, unsigned offset)
{struct gen_74x164_chip *chip = gpiochip_get_data(gc);u8 bank = chip->registers - 1 - offset / 8; // 计算目标寄存器u8 pin = offset % 8; // 计算目标寄存器的引脚位置int ret;mutex_lock(&chip->lock); // 获取锁,防止并发访问ret = (chip->buffer[bank] >> pin) & 0x1; // 获取指定引脚的值mutex_unlock(&chip->lock); // 释放锁return ret;
}// 设置指定 GPIO 引脚的值
static void gen_74x164_set_value(struct gpio_chip *gc,unsigned offset, int val)
{struct gen_74x164_chip *chip = gpiochip_get_data(gc);u8 bank = chip->registers - 1 - offset / 8; // 计算目标寄存器u8 pin = offset % 8; // 计算目标寄存器的引脚位置mutex_lock(&chip->lock); // 获取锁if (val)chip->buffer[bank] |= (1 << pin); // 设置引脚为高电平elsechip->buffer[bank] &= ~(1 << pin); // 设置引脚为低电平__gen_74x164_write_config(chip); // 将配置写入硬件寄存器mutex_unlock(&chip->lock); // 释放锁
}// 设置多个 GPIO 引脚的值,使用掩码确定设置的目标引脚
static void gen_74x164_set_multiple(struct gpio_chip *gc, unsigned long *mask,unsigned long *bits)
{struct gen_74x164_chip *chip = gpiochip_get_data(gc);unsigned int i, idx, shift;u8 bank, bankmask;mutex_lock(&chip->lock); // 获取锁for (i = 0, bank = chip->registers - 1; i < chip->registers; i++, bank--) {idx = i / sizeof(*mask);shift = i % sizeof(*mask) * BITS_PER_BYTE;bankmask = mask[idx] >> shift;if (!bankmask)continue;chip->buffer[bank] &= ~bankmask; // 清除当前引脚chip->buffer[bank] |= bankmask & (bits[idx] >> shift); // 设置新值}__gen_74x164_write_config(chip); // 写入寄存器mutex_unlock(&chip->lock); // 释放锁
}// 配置 GPIO 引脚为输出模式,并设置初始值
static int gen_74x164_direction_output(struct gpio_chip *gc,unsigned offset, int val)
{gen_74x164_set_value(gc, offset, val); // 设置引脚值return 0;
}// SPI 驱动探测函数,初始化 74x164 设备
static int gen_74x164_probe(struct spi_device *spi)
{struct gen_74x164_chip *chip;u32 nregs; // 寄存器数量int ret;spi->bits_per_word = 8; // 配置 SPI 传输位宽ret = spi_setup(spi); // 设置 SPI 设备if (ret < 0)return ret;// 从设备树读取 "registers-number" 属性if (of_property_read_u32(spi->dev.of_node, "registers-number", &nregs)) {dev_err(&spi->dev, "设备树中缺少 registers-number 属性.\n");return -EINVAL;}// 为 chip 结构体分配内存chip = devm_kzalloc(&spi->dev, sizeof(*chip) + nregs, GFP_KERNEL);if (!chip)return -ENOMEM;spi_set_drvdata(spi, chip); // 设置驱动程序数据// 初始化 GPIO 控制器参数chip->gpio_chip.label = spi->modalias;chip->gpio_chip.direction_output = gen_74x164_direction_output;chip->gpio_chip.get = gen_74x164_get_value;chip->gpio_chip.set = gen_74x164_set_value;chip->gpio_chip.set_multiple = gen_74x164_set_multiple;chip->gpio_chip.base = -1;chip->registers = nregs; // 设置寄存器数量chip->gpio_chip.ngpio = GEN_74X164_NUMBER_GPIOS * chip->registers;// 从设备树中读取默认寄存器值并初始化缓冲区of_property_read_u8_array(spi->dev.of_node, "registers-default", chip->buffer, chip->registers);chip->gpio_chip.can_sleep = true;chip->gpio_chip.parent = &spi->dev;chip->gpio_chip.owner = THIS_MODULE;mutex_init(&chip->lock); // 初始化互斥锁// 将初始配置写入硬件寄存器ret = __gen_74x164_write_config(chip);if (ret) {dev_err(&spi->dev, "写入失败: %d\n", ret);goto exit_destroy;}// 注册 GPIO 控制器ret = gpiochip_add_data(&chip->gpio_chip, chip);if (!ret)return 0;exit_destroy:mutex_destroy(&chip->lock); // 清理互斥锁return ret;
}// SPI 驱动移除函数
static int gen_74x164_remove(struct spi_device *spi)
{struct gen_74x164_chip *chip = spi_get_drvdata(spi);gpiochip_remove(&chip->gpio_chip); // 删除 GPIO 控制器mutex_destroy(&chip->lock); // 销毁互斥锁return 0;
}// 设备树匹配表,列出支持的设备
static const struct of_device_id gen_74x164_dt_ids[] = {{ .compatible = "fairchild,74hc595" },{ .compatible = "nxp,74lvc594" },{},
};
MODULE_DEVICE_TABLE(of, gen_74x164_dt_ids);// SPI 驱动程序结构
static struct spi_driver gen_74x164_driver = {.driver = {.name = "74x164",.of_match_table = gen_74x164_dt_ids,},.probe = gen_74x164_probe,.remove = gen_74x164_remove,
};
module_spi_driver(gen_74x164_driver); // 注册 SPI 驱动程序MODULE_AUTHOR("Gabor Juhos <juhosg@openwrt.org>");
MODULE_AUTHOR("Miguel Gaio <miguel.gaio@efixo.com>");
MODULE_DESCRIPTION("74X164 8位移位寄存器的 GPIO 扩展器驱动");
MODULE_LICENSE("GPL v2");