Linux的GPIO驱动程序使用详解
作者:大聪明-PLUS
让我们看看 Linux 中 GPIO 驱动程序的具体结构,以及为什么要这样做。这样我们就能理解为什么在这个操作系统中,仅仅为了让 LED 闪烁就需要经过 N 层抽象。
上述内容大部分也适用于其他驱动程序。GPIO 被选为最简单的外围设备示例。
我们的文章结构如下:
- 我们先来看看硬件层面的GPIO控制;
- 我们来研究一下操作系统给我们增加了哪些要求;
- 让我们考虑一下这些要求通常是如何实现的;
- 让我们看看 Linux 中如何为不同的 SoC 实现真正的 GPIO 驱动程序;
- 让我们再次简单总结一下研究结果。
硬件级别
在硬件层面,一切都非常简单。处理器有一个 GPIO 引脚控制器,从程序员的角度来看,它就是映射到通用地址空间的十几个寄存器。
要点亮 LED,我们首先需要初始化所需的引脚(将其配置为输出),然后设置所需的值。
基本上,寄存器中有两个条目。一个条目位于确定输出方向的寄存器中。第二个条目位于确定输出值的寄存器中。这可以用十几条 CPU 指令完成。
操作系统限制
当我们开始在操作系统中实现相同的操作时,我们会产生额外的需求。
这些需求会导致额外的代码,但与此同时,这些需求的实现正是确保操作系统便捷运行的关键因素。
从内核模式和用户模式访问外设
在用户空间运行的应用程序无法访问外设寄存器和其他服务内存区域。
这是像 Linux 这样的操作系统的基本要求之一——用户进程不应该破坏其他进程的内存,尤其是内核内存。
这意味着需要一个在内核空间运行并为用户模式应用程序提供 API 的驱动程序。
GPIO 引脚必须由内核控制(例如,来自邻近的驱动程序),也就是说,内核模式也需要一个 API。
访问共享
多个用户空间进程可能想要管理同一个输出。必须提供一种机制来防止多个进程同时访问该输出时出现错误。
多个内核空间进程可能想要使用相同的输出。这些情况也需要处理,但需要排除错误。
标准 API
许多 SoC 中集成了多种 GPIO 端口实现。无论具体实现如何,驱动程序都必须提供相同的一组“旋钮”(换句话说,相同的 API)。原因如下:
- 用户空间应用程序获得了额外的平台 独立性——在任何 Linux 平台上,GPIO 引脚都以相同的方式控制;
- 对于在工作中使用 GPIO 引脚的驱动程序,也赋予了类似的自由度(例如,触摸控制器或 Wi-Fi 模块的驱动程序可以使用 GPIO 引脚进行电源管理)。
此外,大多数 SoC 可以使用 GPIO 引脚接收外部中断。Linux 也解决了这个问题,但 Linux 中的中断有其自身的抽象,值得单独研究。因此,虽然我们在评测过程中会遇到处理中断的代码,但本文不会讨论它。
实现通用 GPIO 驱动程序要求
从内核和用户空间访问外设
一切都很简单。驱动程序是一个常规的 Linux 内核模块,因此它在内核空间中执行,并且可以访问 CPU 的整个地址空间。
对于用户级应用程序,sysfs中提供了特殊文件。
自定义应用程序的 API
这是 Linux 系统的标准机制 - 通过文件/sys/class/gpio。
当我们找到系统中所需的输出编号(它通常与物理输出的编号不一致)时,我们将该编号写入文件/sys/class/gpio/export:
num=416 echo $num > /sys/class/gpio/export
此后,文件系统中会出现一个目录/sys/class/gpio/gpio$num。该目录下有文件direction和value。
控制输出方向:
echo out > /sys/class/gpio/gpio$num/direction echo in > /sys/class/gpio/gpio$num/direction
控制输出状态:
echo 1 > /sys/class/gpio/gpio$num/value echo 0 > /sys/class/gpio/gpio$num/value
读取输入状态:
cat /sys/class/gpio/gpio$num/value
如何实现这一点,我们稍后会考虑。
用户应用程序的访问分离
如果多个应用程序访问同一个“句柄”,系统将自行解决冲突。就像多个应用程序同时访问同一个文件时一样。这些冲突不需要在驱动程序层面进行处理。
内核 API 和内核访问共享
本节主要介绍 Linux 内核中与平台无关的代码。代码以存储库https://github.com/torvalds/linux的主分支为例,即本文发布时对应的内核版本 6.x。具体的驱动程序实现将以内核 4.x 和 5.x 为例进行进一步讨论。内核版本之间存在差异,但总体概念没有变化。
Linux 内核有一个独立于平台的抽象结构体 gpio_chip,它是一个包含一组特定 GPIO 引脚描述的结构体。没有单独的结构体来描述单个引脚并包含管理该引脚的函数。
题外话,这与硬件层面的关系如何。
- 在硬件层面,芯片(SoC - 片上系统)通常具有多个 GPIO 块,每个块包含数十个引脚。通常使用诸如 GPIOA、GPIOB 等名称。因此,引脚的指定格式为 GPIOA_31..GPIOA_0 等。
- 根据芯片和驱动程序的具体实现,可以实现不同的方法。可以为每个块启动一个单独的 gpio_chip 结构。但也可以实现另一种方法。
- 例如,可以按电源域划分 GPIO 块。通常,GPIOA..GPIOC 位于一个域中,而 GPIOD..GPIOF 位于另一个域中。
- 在这种情况下,供应商在提供的 SDK 中为一个域的块形成一个 gpio_chip,为另一个域的块形成第二个 gpio_chip。
总的来说,从技术角度来看,gpio_chip 的分区并不重要。基本上,这里的一切都取决于特定驱动程序作者的审美。
无论如何,在 gpio_chip 结构中,除其他内容外,还有指向特定 GPIO 块的驱动程序中实现的平台相关函数的指针:
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); … void (*set)(struct gpio_chip *chip, unsigned offset, int value);
除了 gpio_chip 结构体之外,内核还提供了与平台无关的 gpio 函数。它们分别位于 drivers/gpio/gliolib.c 和 drivers/gpio/gpiolib_sysfs.c 文件中。我们来看一下这两个文件。
第一个包含内核函数。有很多函数,我们重点介绍两个:
int gpiod_request(struct gpio_desc *desc, const char *label); void gpiod_set_value(struct gpio_desc *desc, int value);
这是允许我们占用所需 GPIO 并设置其值(如果将其配置为输出)的最小集合。其余函数基于相同的原则构建,对它们的额外考虑不会给我们带来任何根本性的新东西。
访问共享机制是在 gpiod_request() 函数内部实现的。当然,它不是直接在函数内部实现的,而是按照惯例,通过函数“下方”的几个层级来实现的:
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;
}static int gpiod_request_commit(struct gpio_desc *desc, const char *label)
{
struct gpio_chip *gc = desc->gdev->chip;
unsigned long flags;
unsigned int offset;
int ret;
if (label) {
label = kstrdup_const(label, GFP_KERNEL);
if (!label)
return -ENOMEM;
}
spin_lock_irqsave(&gpio_lock, flags);
/* NOTE: gpio_request() can be called in early boot,
* before IRQs are enabled, for non-sleeping (SOC) GPIOs.
*/
if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
desc_set_label(desc, label ? : "?");
} else {
ret = -EBUSY;
goto out_free_unlock;
}
if (gc->request) {
/* gc->request may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
offset = gpio_chip_hwgpio(desc);
if (gpiochip_line_is_valid(gc, offset))
ret = gc->request(gc, offset);
else
ret = -EINVAL;
spin_lock_irqsave(&gpio_lock, flags);
if (ret) {
desc_set_label(desc, NULL);
clear_bit(FLAG_REQUESTED, &desc->flags);
goto out_free_unlock;
}
}
if (gc->get_direction) {
/* gc->get_direction may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
gpiod_get_direction(desc);
spin_lock_irqsave(&gpio_lock, flags);
}
spin_unlock_irqrestore(&gpio_lock, flags);
return 0;
out_free_unlock:
spin_unlock_irqrestore(&gpio_lock, flags);
kfree_const(label);
return ret;
}要点如下:
1)将其包装在自旋锁中并查看此输出是否已处于请求的状态:
spin_lock_irqsave(&gpio_lock, flags);
/* NOTE: gpio_request() can be called in early boot,
* before IRQs are enabled, for non-sleeping (SOC) GPIOs.
*/
if (test_and_set_bit(FLAG_REQUESTED, &desc->flags) == 0) {
desc_set_label(desc, label ? : "?");
} else {
ret = -EBUSY;
goto out_free_unlock;
}2)如果输出不忙,那么我们检查它是否有自己的平台相关函数 request(),并调用它:
if (gc->request) {
/* gc->request may sleep */
spin_unlock_irqrestore(&gpio_lock, flags);
offset = gpio_chip_hwgpio(desc);
if (gpiochip_line_is_valid(gc, offset))
ret = gc->request(gc, offset);
else
ret = -EINVAL;
spin_lock_irqsave(&gpio_lock, flags);显然,我们在自旋锁解锁的情况下调用此函数。虽然很难保证 100%,但看起来我们已经解锁了自旋锁,因为这里我们已经排除了对特定 GPIO 的同时访问,并且建议尽可能缩短自旋锁的锁定时间,以最大程度地减少其他进程/线程的主动等待。
现在让我们看看设置给定值是什么样子的:
void gpiod_set_value(struct gpio_desc *desc, int value)
{
VALIDATE_DESC_VOID(desc);
/* Should be using gpiod_set_value_cansleep() */
WARN_ON(desc->gdev->chip->can_sleep);
gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value);static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
gpio_set_open_drain_value_commit(desc, value);
else (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
gpio_set_open_source_value_commit(desc, value);
}static void gpiod_set_raw_value_commit(struct gpio_desc *desc, bool value)
{
struct gpio_chip *gc;
gc = desc->gdev->chip;
trace_gpio_value(desc_to_gpio(desc), 0, value);
gc->set(gc, gpio_chip_hwgpio(desc), value);
}可以看出,最终调用的是平台特定函数set()。set 函数不再使用任何访问共享机制。如果某个内核代码想要使用某个 GPIO 引脚,但在调用 时出错gpiod_request(),则不应再尝试使用该 GPIO。
现在让我们看看通过 sysfs 从用户空间调用的实现。处理写入文件的函数如下所示/sys/class/gpio/export:
static ssize_t export_store(const struct class *class,
const struct class_attribute *attr,
const char *buf, size_t len)
{
struct gpio_desc *desc;
struct gpio_chip *gc;
int status, offset;
long gpio;
status = kstrtol(buf, 0, &gpio);
if (status < 0)
goto done;
desc = gpio_to_desc(gpio);
/* reject invalid GPIOs */
if (!desc) {
pr_warn("%s: invalid GPIO %ld\n", __func__, gpio);
return -EINVAL;
}
gc = desc->gdev->chip;
offset = gpio_chip_hwgpio(desc);
if (!gpiochip_line_is_valid(gc, offset)) {
pr_warn("%s: GPIO %ld masked\n", __func__, gpio);
return -EINVAL;
}
/* No extra locking here; FLAG_SYSFS just signifies that the
* request and export were done by on behalf of userspace, so
* they may be undone on its behalf too.
*/
status = gpiod_request_user(desc, "sysfs");
if (status)
goto done;
status = gpiod_set_transitory(desc, false);
if (status) {
gpiod_free(desc);
goto done;
}
status = gpiod_export(desc, true);
if (status < 0)
gpiod_free(desc);
else
set_bit(FLAG_SYSFS, &desc->flags);
done:
if (status)
pr_debug("%s: status %d\n", __func__, status);
return status ? : len;
}
static CLASS_ATTR_WO(export);这里最重要的是挑战gpiod_request_user()。如果你搜索 master 分支,你会发现
static inline int gpiod_request_user(struct gpio_desc *desc, const char *label)
{
int ret;
ret = gpiod_request(desc, label);
if (ret == -EPROBE_DEFER)
ret = -ENODEV;
return ret;
}在内核 4.x,5.x 中没有gpiod_request_user(),直接使用常规 . gpiod_request()。
static ssize_t value_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct gpiod_data *data = dev_get_drvdata(dev);
struct gpio_desc *desc = data->desc;
ssize_t status;
long value;
status = kstrtol(buf, 0, &value);
mutex_lock(&data->mutex);
if (!test_bit(FLAG_IS_OUT, &desc->flags)) {
status = -EPERM;
} else if (status == 0) {
gpiod_set_value_cansleep(desc, value);
status = size;
}
mutex_unlock(&data->mutex);
return status;
}
static DEVICE_ATTR_PREALLOC(value, S_IWUSR | S_IRUGO, value_show, value_store);void gpiod_set_value_cansleep(struct gpio_desc *desc, int value)
{
might_sleep();
VALIDATE_DESC_VOID(desc);
gpiod_set_value_nocheck(desc, value);
}
EXPORT_SYMBOL_GPL(gpiod_set_value_cansleep);static void gpiod_set_value_nocheck(struct gpio_desc *desc, int value)
{
if (test_bit(FLAG_ACTIVE_LOW, &desc->flags))
value = !value;
if (test_bit(FLAG_OPEN_DRAIN, &desc->flags))
gpio_set_open_drain_value_commit(desc, value);
else if (test_bit(FLAG_OPEN_SOURCE, &desc->flags))
gpio_set_open_source_value_commit(desc, value);
else
gpiod_set_raw_value_commit(desc, value);
}我们知道,这将导致已经熟悉的依赖于平台的 set() 函数。
具体驱动程序的实现
让我们看看以上所有内容在真实硬件上是如何实现的。
研究设置
我们使用了 BeagleBone Black(SoC TI AM335)和 Orange Pi Zero(SoC Allwinner H2)开发板。Buildroot 2021.02 被用作内核的“包装器”。这个设置没有什么特别之处,只是手头上的东西,以及作者或多或少习惯使用的东西。我们使用了两块开发板来测试 TI 和 Allwinner 两家供应商的 GPIO 驱动程序的实现。我们本来想考虑第三块开发板,但手头上没有其他可用的。
BeagleBone 的 Buildroot 使用内核版本 4.19.79。OrangePi Zero 的 Buildroot 使用内核版本 5.10.10。如上所述,现代内核版本与 4.19 和 5.10 在细节上有所不同,但总体方法没有改变。
BeagleBone 黑色 (TI AM335)
我们启动组装好的 Buildroot 镜像并保存 UART 输出。我们以以下行作为搜索的起点:
[ 0.268291] OMAP GPIO hardware version 0.1
我们搜索产生此消息的代码(该路径是相对于内核源的根目录给出的):
驱动程序/gpio/gpio-omap.c
static void omap_gpio_show_rev(struct gpio_bank *bank)
{
static bool called;
u32 rev;
if (called || bank->regs->revision == USHRT_MAX)
return;
rev = readw_relaxed(bank->base + bank->regs->revision);
pr_info("OMAP GPIO hardware version %d.%d\n",
(rev >> 4) & 0x0f, rev & 0x0f);
called = true;
}太好了,我们知道了源代码在哪里。让我们开始研究它。首先,让我们了解一下这个驱动程序的 DTS 中应该包含哪些内容。记住,内核在获悉系统存在相应的设备后,会运行驱动程序的probe()函数。
GPIO控制器是芯片内部的一个模块,通过 AXI 类型的总线连接。那里没有即插即用机制。这意味着内核根据硬件的静态描述来确定这一点。设备树(俗称“DTS”或“DTB”——尽管从技术上讲这并不完全正确。
DTS 和 DTB 是设备树的两种呈现形式。
DTS 指的是“设备树源代码”,DTB 指的是“设备树二进制文件”)。
驱动程序包含必须在 DTS 中描述的兼容字段:
static const struct of_device_id omap_gpio_match[] = {
{
.compatible = "ti,omap4-gpio",
.data = &omap4_pdata,
},
{
.compatible = "ti,omap3-gpio",
.data = &omap3_pdata,
},
{
.compatible = "ti,omap2-gpio",
.data = &omap2_pdata,
},
{ },
};
MODULE_DEVICE_TABLE(of, omap_gpio_match);驱动程序本身注册为平台驱动程序:
static struct platform_driver omap_gpio_driver = {
.probe = omap_gpio_probe,
.remove = omap_gpio_remove,
.driver = {
.name = "omap_gpio",
.pm = &gpio_pm_ops,
.of_match_table = of_match_ptr(omap_gpio_match),
},
};
/*
* gpio driver register needs to be done before
* machine_init functions access gpio APIs.
* Hence omap_gpio_drv_reg() is a postcore_initcall.
*/
static int __init omap_gpio_drv_reg(void)
{
return platform_driver_register(&omap_gpio_driver);
}
postcore_initcall(omap_gpio_drv_reg);这里我们重新回顾一下 注释和说明——GPIO 驱动程序必须在使用这些 GPIO 的驱动程序初始化之前注册。为了实现这一点,驱动程序在 postcore_initcall 阶段初始化,这个阶段比 device_initcall 阶段要早得多。
同时,从兼容列表中我们了解到该驱动程序支持三个版本的GPIO模块。要了解使用的是哪一个,我们仍然需要转到DTS:
架构/arm/boot/dts/am33xx.dts
gpio0: gpio@44e07000 {
compatible = "ti,omap4-gpio";
ti,hwmods = "gpio1";
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
reg = <0x44e07000 0x1000>;
interrupts = <96>;
};还有类似的块,名称分别为 gpio1、gpio2、gpio3。
结论:
- gpio 块至少有三个受支持的版本(omap2-gpio、omap3-gpio、omap4-gpio)。
- 在DTS中我们可以看到这些块在芯片内部的基地址。
- 这些 GPIO 模块可以产生中断。通常,这些信息可以从数据手册中获取,但是:
- 原则上,Linux 程序员打开数据表的频率比微控制器程序员要低得多。
- 对于许多处理器,您可能仍然可以在公共领域找到数据表。因此,一般来说,这些知识并非毫无用处。不过,在这个特定情况下,它确实毫无用处。
以下是 omap_gpio_probe() 中发生的情况:
驱动程序/gpio/gpio-omap.c
static int omap_gpio_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *node = dev->of_node;
const struct of_device_id *match;
const struct omap_gpio_platform_data *pdata;
struct resource *res;
struct gpio_bank *bank;
struct irq_chip *irqc;
...
if (bank->is_mpuio)
omap_mpuio_init(bank);
omap_gpio_mod_init(bank);
ret = omap_gpio_chip_init(bank, irqc);
if (ret) {
pm_runtime_put_sync(dev);
pm_runtime_disable(dev);
if (bank->dbck_flag)
clk_unprepare(bank->dbck);
return ret;
}
omap_gpio_show_rev(bank);让我们仔细看看:
if (bank->is_mpuio) omap_mpuio_init(bank);
从代码上看不清楚它是什么。如果你在搜索引擎中输入“mpuio”,你可以在TI数据表中找到它:

互联网上的TI处理器数据表
也许这足以得出以下暂时的结论:
- 这是来自 TI 的一个具体事物。
- 这是 GPIO 端口的一些特殊操作模式,为此有一个驱动程序。
- 并非所有 TI GPIO 端口都支持此模式。
- 在这种情况下,这种模式的存在与否并不会对文章主题的考虑产生很大的影响。
接下来我们有这个函数:
static void omap_gpio_mod_init(struct gpio_bank *bank)
{
void __iomem *base = bank->base;
u32 l = 0xffffffff;
if (bank->width == 16)
l = 0xffff;
if (bank->is_mpuio) {
writel_relaxed(l, bank->base + bank->regs->irqenable);
return;
}
omap_gpio_rmw(base, bank->regs->irqenable, l,
bank->regs->irqenable_inv);
omap_gpio_rmw(base, bank->regs->irqstatus, l,
!bank->regs->irqenable_inv);
if (bank->regs->debounce_en)
writel_relaxed(0, base + bank->regs->debounce_en);
/* Save OE default value (0xffffffff) in the context */
bank->context.oe = readl_relaxed(bank->base + bank->regs->direction);
/* Initialize interface clk ungated, module enabled */
if (bank->regs->ctrl)
writel_relaxed(0, base + bank->regs->ctrl);
}这些是对 GPIO 块寄存器的可靠写入。这里没有特定于 Linux 内核的操作。总的来说,对于我们讨论的主题来说,这也不是一个非常重要的功能。
但下一个函数正好符合目标:
static int omap_gpio_chip_init(struct gpio_bank *bank, struct irq_chip *irqc)
{
struct gpio_irq_chip *irq;
static int gpio;
const char *label;
int irq_base = 0;
int ret;
/*
* REVISIT eventually switch from OMAP-specific gpio structs
* over to the generic ones
*/
bank->chip.request = omap_gpio_request;
bank->chip.free = omap_gpio_free;
bank->chip.get_direction = omap_gpio_get_direction;
bank->chip.direction_input = omap_gpio_input;
bank->chip.get = omap_gpio_get;
bank->chip.direction_output = omap_gpio_output;
bank->chip.set_config = omap_gpio_set_config;
bank->chip.set = omap_gpio_set;
if (bank->is_mpuio) {
bank->chip.label = "mpuio";
if (bank->regs->wkup_en)
bank->chip.parent = &omap_mpuio_device.dev;
bank->chip.base = OMAP_MPUIO(0);
} else {
label = devm_kasprintf(bank->chip.parent, GFP_KERNEL, "gpio-%d-%d",
gpio, gpio + bank->width - 1);
if (!label)
return -ENOMEM;
bank->chip.label = label;
bank->chip.base = gpio;
}
bank->chip.ngpio = bank->width;
#ifdef CONFIG_ARCH_OMAP1
/*
* REVISIT: Once we have OMAP1 supporting SPARSE_IRQ, we can drop
* irq_alloc_descs() since a base IRQ offset will no longer be needed.
*/
irq_base = devm_irq_alloc_descs(bank->chip.parent,
-1, 0, bank->width, 0);
if (irq_base < 0) {
dev_err(bank->chip.parent, "Couldn't allocate IRQ numbers\n");
return -ENODEV;
}
#endif
/* MPUIO is a bit different, reading IRQ status clears it */
if (bank->is_mpuio) {
irqc->irq_ack = dummy_irq_chip.irq_ack;
if (!bank->regs->wkup_en)
irqc->irq_set_wake = NULL;
}
irq = &bank->chip.irq;
irq->chip = irqc;
irq->handler = handle_bad_irq;
irq->default_type = IRQ_TYPE_NONE;
irq->num_parents = 1;
irq->parents = &bank->irq;
irq->first = irq_base;
ret = gpiochip_add_data(&bank->chip, bank);
if (ret) {
dev_err(bank->chip.parent,
"Could not register gpio chip %d\n", ret);
return ret;
}
ret = devm_request_irq(bank->chip.parent, bank->irq,
omap_gpio_irq_handler,
0, dev_name(bank->chip.parent), bank);
if (ret)
gpiochip_remove(&bank->chip);
if (!bank->is_mpuio)
gpio += bank->width;原则上,一条注释就足以理解——这里我们从“OMAP 专用”结构体(struct gpio_bank)过渡到“系统级”结构体(struct gpiochip)。实际上,我们将系统级结构体作为 OMAP 专用结构体中的一个实例。这里有一些与中断相关的操作,但我们暂时不讨论这个话题。
对于我们来说现在最重要的部分是:
bank->chip.request = omap_gpio_request; bank->chip.free = omap_gpio_free; bank->chip.get_direction = omap_gpio_get_direction; bank->chip.direction_input = omap_gpio_input; bank->chip.get = omap_gpio_get; bank->chip.direction_output = omap_gpio_output; bank->chip.set_config = omap_gpio_set_config; bank->chip.set = omap_gpio_set;
这里,我们实现了从通用 API 到其平台相关代码的转换。当某些代码调用平台无关函数(例如 gpiod_request())时,系统会调用 gpio_chip.request()。如果这种情况发生在 AM335x 处理器上,我们就会调用 omap_gpio_request()。
好了,填充 gpio_chip 结构体之后,最重要的事情就是在系统中注册这个结构体。从这一刻起,系统就知道了这些 GPIO 线的存在。
让我们来看看两个函数的实现——GPIO 请求和 GPIO 值设置:
static int omap_gpio_request(struct gpio_chip *chip, unsigned offset)
{
struct gpio_bank *bank = gpiochip_get_data(chip);
unsigned long flags;
/*
* If this is the first gpio_request for the bank,
* enable the bank module.
*/
if (!BANK_USED(bank))
pm_runtime_get_sync(chip->parent);
raw_spin_lock_irqsave(&bank->lock, flags);
omap_enable_gpio_module(bank, offset);
bank->mod_usage |= BIT(offset);
raw_spin_unlock_irqrestore(&bank->lock, flags);
return 0;
}这里最重要的是自旋锁 (spinlock) 封装的内容:启用 GPIO 模块并写入内部 bank.mod_usage 结构体,这实际上复制了关于引脚“忙”状态的条目。以下是其中最重要的部分:
- 事实上,使用自旋锁可以消除碰撞。
- 虽然这里存在一个函数 omap_enable_gpio_module(),但通常不需要对硬件进行任何特殊操作。GPIO 请求只是在系统中记录“此 GPIO 已被占用”的一种方式。严格来说,平台相关的 request() 函数根本没有必要存在。
设置GPIO输出值的函数:
static void omap_gpio_set(struct gpio_chip *chip, unsigned offset, int value)
{
struct gpio_bank *bank;
unsigned long flags;
bank = gpiochip_get_data(chip);
raw_spin_lock_irqsave(&bank->lock, flags);
bank->set_dataout(bank, offset, value);
raw_spin_unlock_irqrestore(&bank->lock, flags);
}set_dataout() 函数是在驱动程序初始化期间在probe()函数中指定的:
if (bank->regs->set_dataout && bank->regs->clr_dataout) {
bank->set_dataout = omap_set_gpio_dataout_reg;
bank->set_dataout_multiple = omap_set_gpio_dataout_reg_multiple;
} else {
bank->set_dataout = omap_set_gpio_dataout_mask;
bank->set_dataout_multiple =
omap_set_gpio_dataout_mask_multiple;
}显然,这为驱动程序开发人员提供了额外的自由度。无论如何,如果我们查看 omap_set_gpio_dataout_reg(),我们会发现它已经直接与 GPIO 块寄存器进行了交互:
/* set data out value using dedicate set/clear register */
static void omap_set_gpio_dataout_reg(struct gpio_bank *bank, unsigned offset,
int enable)
{
void __iomem *reg = bank->base;
u32 l = BIT(offset);
if (enable) {
reg += bank->regs->set_dataout;
bank->context.dataout |= l;
} else {
reg += bank->regs->clr_dataout;
bank->context.dataout &= ~l;
}
writel_relaxed(l, reg);
}本质上,我们在这里看到的是一个简单的注册表条目,与我们在文章开头讨论的相同。
现在,让我们尝试验证之前通过阅读源代码获得的理论计算。确保在通过内核空间和用户空间访问时,我们确实会经历这一系列调用。让我们用最原始的方式做到这一点——在
printk(“%s: entered\n”, __func__);
我们讨论的函数中添加一行代码。然后,我们将删除日志。
首先,让我们看一下UART启动日志,我们会立即看到我们干预的痕迹:
[ 0.571306] pinctrl-single 44e10800.pinmux: 142 pins, size 568 [ 0.576824] gpiod_request_commit: entered [ 0.576852] omap_gpio_request: entered [ 0.576992] gpiod_direction_output_raw_commit: entered [ 0.577051] omap_set_gpio_direction: entered [ 0.580063] Serial: 8250/16550 driver, 6 ports, IRQ sharing enabled ... [ 1.535627] sdhci: Secure Digital Host Controller Interface driver [ 1.541871] sdhci: Copyright(c) Pierre Ossman [ 1.547696] gpiod_request_commit: entered [ 1.551745] omap_gpio_request: entered [ 1.555787] omap_set_gpio_direction: entered [ 1.560134] omap_gpio 44e07000.gpio: Could not set line 6 debounce to 200000) [ 1.568968] omap_hsmmc 48060000.mmc: Got CD GPIO
很明显,驱动程序在加载过程中使用了两个 GPIO。而且,它们的执行路径与之前提到的完全一致——首先调用与平台无关的 gpiod_request() 函数,然后调用与平台相关的 omap_gpio_request() 函数。
现在让我们看看如果我们从内核空间控制 GPIO 会发生什么:
# cd /sys/class/gpio # ls export gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport # echo 15 > export [ 803.092438] gpiod_request_commit: entered [ 803.097022] omap_gpio_request: entered # ls export gpiochip0 gpiochip64 unexport gpio15 gpiochip32 gpiochip96 # cd gpio15 # echo out > direction [ 815.830522] gpiod_direction_output_raw_commit: entered [ 815.836210] omap_set_gpio_direction: entered # echo 1 > value [ 820.621108] value_store: entered [ 820.624881] gpiod_set_value_cansleep: entered [ 820.629404] gpiod_set_raw_value_commit: entered [ 820.634088] omap_gpio_set: entered
显然,一切确实都朝着我们所讨论的相同功能发展。
OrangePi Zero(全志 H2)
这里的启动日志并没有用GPIO相关的字眼来破坏我们的印象。这些行看起来像是我们要找的内容:
[ 0.089512] sun8i-h3-pinctrl 1c20800.pinctrl: initialized sunXi PIO driver [ 0.091141] sun8i-h3-r-pinctrl 1f02c00.pinctrl: initialized sunXi PIO driver
“sun8i-h3”这个名字很容易让人混淆,因为我们主板上的处理器上有一个大大的“H2”字样。搜索可用的DTS,找不到任何类似“sun8i-h2”的版本。不过,汇编代码可以运行,这意味着H2和H3之间的区别并不大。
这意味着一切都有效,我们可以进一步研究代码。
让我们看一下显示这些行的代码。它位于此处:
驱动程序/pinctrl/sunxi/pinctrl-sunxi.c
int sunxi_pinctrl_init_with_variant(struct platform_device *pdev,
const struct sunxi_pinctrl_desc *desc,
unsigned long variant)
{
...
pctl->chip->owner = THIS_MODULE;
pctl->chip->request = gpiochip_generic_request;
pctl->chip->free = gpiochip_generic_free;
pctl->chip->set_config = gpiochip_generic_config;
pctl->chip->direction_input = sunxi_pinctrl_gpio_direction_input;
pctl->chip->direction_output = sunxi_pinctrl_gpio_direction_output;
pctl->chip->get = sunxi_pinctrl_gpio_get;
pctl->chip->set = sunxi_pinctrl_gpio_set;
pctl->chip->of_xlate = sunxi_pinctrl_gpio_of_xlate;
pctl->chip->to_irq = sunxi_pinctrl_gpio_to_irq;
pctl->chip->of_gpio_n_cells = 3;
pctl->chip->can_sleep = false;
pctl->chip->ngpio = round_up(last_pin, PINS_PER_BANK) -
pctl->desc->pin_base;
pctl->chip->label = dev_name(&pdev->dev);
pctl->chip->parent = &pdev->dev;
pctl->chip->base = pctl->desc->pin_base;
...他们这样称呼它:
驱动程序/pinctrl/sunxi/pinctrl-sun8i-v3s.c
static int sun8i_v3s_pinctrl_probe(struct platform_device *pdev)
{
unsigned long variant = (unsigned long)of_device_get_match_data(&pdev->dev);
return sunxi_pinctrl_init_with_variant(pdev, &sun8i_v3s_pinctrl_data,
variant);
}
static const struct of_device_id sun8i_v3s_pinctrl_match[] = {
{
.compatible = "allwinner,sun8i-v3-pinctrl",
.data = (void *)PINCTRL_SUN8I_V3
},
{
.compatible = "allwinner,sun8i-v3s-pinctrl",
.data = (void *)PINCTRL_SUN8I_V3S
},
{ },
};
static struct platform_driver sun8i_v3s_pinctrl_driver = {
.probe = sun8i_v3s_pinctrl_probe,
.driver = {
.name = "sun8i-v3s-pinctrl",
.of_match_table = sun8i_v3s_pinctrl_match,
},
};
builtin_platform_driver(sun8i_v3s_pinctrl_driver);与 TI 类似,这是一个平台驱动程序。实际上,很难用其他方式实现。
但明显的区别是,驱动程序不在 GPIO 目录中,而是在 pinctrl 目录中。
pinctrl 驱动程序还控制每个特定引脚的分配。通常,现代 SoC 上的 GPIO 引脚与其他 I2C/SPI/UART 级接口复用。pinctrl 驱动程序包含控制这些复用器的功能。例如,TI AM335x 上也存在 pinctrl 驱动程序,但该实体与 GPIO 驱动程序是分开的。在这里,他们决定这样做。
让我们回到 GPIO 上。无论如何,此驱动程序中的关键操作已经完成——创建、填充并在系统中注册 gpio_chip 结构的实例。让我们仔细看看这一点。
首先,你可以看到函数 request() 用于实现gpiochip_generic_request():
驱动程序/gpio/gpiolib.c
int gpiochip_generic_request(struct gpio_chip *gc, unsigned offset)
{
#ifdef CONFIG_PINCTRL
if (list_empty(&gc->gpiodev->pin_ranges))
return 0;
#endif
return pinctrl_gpio_request(gc->gpiodev->base + offset);
}
EXPORT_SYMBOL_GPL(gpiochip_generic_request);她在引擎盖下喊道:
驱动程序/pinctrl/core.c
/**
* pinctrl_gpio_request() - request a single pin to be used as GPIO
* @gpio: the GPIO pin number from the GPIO subsystem number space
*
* This function should *ONLY* be used from gpiolib-based GPIO drivers,
* as part of their gpio_request() semantics, platforms and individual drivers
* shall *NOT* request GPIO pins to be muxed in.
*/
int pinctrl_gpio_request(unsigned gpio)
{
struct pinctrl_dev *pctldev;
struct pinctrl_gpio_range *range;
int ret;
int pin;
ret = pinctrl_get_device_gpio_range(gpio, &pctldev, &range);
if (ret) {
if (pinctrl_ready_for_gpio_range(gpio))
ret = 0;
return ret;
}
mutex_lock(&pctldev->mutex);
/* Convert to the pin controllers number space */
pin = gpio_to_pin(range, gpio);
ret = pinmux_request_gpio(pctldev, range, pin, gpio);
mutex_unlock(&pctldev->mutex);
return ret;
}
EXPORT_SYMBOL_GPL(pinctrl_gpio_request);无需详细了解 pinctrl 和 pinmux 的实现,我们尝试说明:此功能不仅保留了 GPIO,而且将其固定为 GPIO,而不是 I2C/SPI/UART/等输出。
我们来看看设置GPIO输出值的函数是什么样的:
驱动程序/sunxi/pinctrl-sunxi.c
static void sunxi_pinctrl_gpio_set(struct gpio_chip *chip,
unsigned offset, int value)
{
struct sunxi_pinctrl *pctl = gpiochip_get_data(chip);
u32 reg = sunxi_data_reg(offset);
u8 index = sunxi_data_offset(offset);
unsigned long flags;
u32 regval;
raw_spin_lock_irqsave(&pctl->lock, flags);
regval = readl(pctl->membase + reg);
if (value)
regval |= BIT(index);
else
regval &= ~(BIT(index));
writel(regval, pctl->membase + reg);
raw_spin_unlock_irqrestore(&pctl->lock, flags);正如预期的那样,这里所有操作也都归结为写入外设寄存器。有趣的是,这里对寄存器的操作被封装在自旋锁中。这可能是因为寄存器操作没有原子操作,操作分为三个阶段:读取寄存器、更改指定位以及将更改后的值写入寄存器。引入自旋锁可以消除潜在的冲突。
为了确保我们理解正确,让我们通过添加调试输出来实现类似的路线。
确实,当添加此输出并使用此固件运行时,我们会陷入无限循环的消息中:
[ 20.884895] gpiod_set_value_nocheck: entered [ 20.889190] gpiod_set_raw_value_commit: entered [ 20.893720] sunxi_pinctrl_gpio_set: entered
但在日志的开头我们看到:
[ 1.623498] gpiod_request: entered [ 1.630993] gpiod_request_commit: entered [ 1.635007] gpiochip_generic_request: entered [ 1.639378] pinctrl_gpio_request: entered [ 1.643401] pin_request: entered
这基本上证实了上面的结论,但在某种程度上干扰了检查通过 sysfs 访问的过程。基本上,这是一个非常典型的 Linux 调试故事——你只需要稍微深入挖掘一下,就会发现一些完全无法理解的行为,虽然这并不那么严重(因为一直以来都是这样),但忽略它似乎也不妥。我们将把此行为的调试放在稍后的剧透部分,在这里我们将完成确保 sysfs 正常工作的工作。让我们删除 gpiod_set_...() 的额外输出,重建、重启,然后查看:
# cd /sys/class/gpio # ls export gpiochip0 gpiochip352 unexport # echo 15 > export [ 1035.505141] export_store: entered [ 1035.508561] gpiod_request: entered [ 1035.511968] gpiod_request_commit: entered [ 1035.515979] gpiochip_generic_request: entered [ 1035.520397] pinctrl_gpio_request: entered [ 1035.524426] pin_request: entered # cd gpio15 # echo out > direction # echo 1 > value [ 1057.037344] value_store: entered
显然,这正是我们所期望的。
不定期调试
谁在拉动 GPIO?调试过程大大缩短了,搜索和尝试次数也显著增加。最终,结果如下:
1) 值设置功能增加调试输出:
printk("%s: gpiochip->base=%d, offset=%d\n", __func__, chip->base, offset);我们收到了以下日志:
[ 59.314332] gpiod_set_value_cansleep: entered [ 59.318718] gpiod_set_value_nocheck: entered [ 59.322989] sunxi_pinctrl_gpio_set: entered [ 59.327174] sunxi_pinctrl_gpio_set: gpiochip->base=352, offset=6
2)关闭日志并查看:
# cat /sys/class/gpio/gpiochip352/label 1f02c00.pinctrl
3) 我们查看了处理器的数据手册。我们发现 R_PIO 块位于地址 0x1F0_2C00。实际上,我们当然可以通过 DTS 找到这一点。第二个引脚块称为 PIO。目前还不清楚它们之间有什么区别。
4) 好的,让我们进入开发板上的DTS。搜索“r_pio”并找到:
架构/arm/boot/dts/sun8i-h2-plus-orangepi-zero.dts
reg_vdd_cpux: vdd-cpux-regulator {
compatible = "regulator-gpio";
regulator-name = "vdd-cpux";
regulator-type = "voltage";
regulator-boot-on;
regulator-always-on;
regulator-min-microvolt = <1100000>;
regulator-max-microvolt = <1300000>;
regulator-ramp-delay = <50>; /* 4ms */
gpios = <&r_pio 0 6 GPIO_ACTIVE_HIGH>; /* PL6 */
enable-active-high;
gpios-states = <1>;
states = <1100000 0>, <1300000 1>;
};注释里写着PL6。看起来很像物理引脚标识。
5)打开电路板图
我们看到 PL6 引脚确实连接到名为 CPUX-VSET 的链:

这个链实际上控制着某种电源:

这对我们来说并没有什么用,但至少让我们清楚了它到底是什么。看起来 Linux 在运行时正在积极地管理 CPU 功耗。
6)对DTS节点进行注释:
&cpu0 {
cpu-supply = <®_vdd_cpux>;
};7) 重建镜像,刷入——确保垃圾信息消失。案件告破。这种引脚抖动现象的原因很有趣,但这超出了本文的讨论范围。
总结
总而言之,我们可以将所说的一切简化为一个简单的结构图:

千言万语值得,但是,借助流程图并研究源代码,您可以实现比仅借助流程图更多的东西,所以我们不要急于抛弃前面的所有文字。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
