大家好!我是大聪明-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内核中的平台无关代码。具体的驱动程序实现将在后续章节中使用内核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 芯片进行分区并不特别重要。这主要取决于驱动程序作者的审美偏好。
` `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`);`
很明显,我们在调用此函数时自旋锁未锁定。虽然很难百分之百确定,但看起来我们锁定自旋锁是因为我们已经禁止同时访问特定的 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` `if` (`test_bit`(`FLAG_OPEN_SOURCE`, `&desc->flags`))
`gpio_set_open_source_value_commit`(`desc`, `value`);
`else`
`https`:`//github.com/torvalds/linux/blob/master/drivers/gpio/gpiolib.c#L3221(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()。该值设置函数不再使用任何访问共享机制。如果内核代码想要使用某个 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`);`
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(TI AM335 SoC)和 Orange Pi Zero(Allwinner H2 SoC)。内核封装版本为 Buildroot 2021.02。这套配置没什么特别之处,只是我手头正好有这些设备,而且也比较熟悉而已。我们使用这两块板子来评估来自两家厂商------TI 和 Allwinner------的 GPIO 驱动程序实现。我们本来也考虑过第三家厂商,但当时手头没有其他选择。
BeagleBone 开发板的 Buildroot 使用 4.19.79 内核版本,OrangePi Zero 开发板的 Buildroot 使用 5.10.10 内核版本。如前所述,现代内核版本与 4.19 和 5.10 在细节上有所不同,但总体方法保持不变。
BeagleBone Black(TI AM335)
运行编译好的 Buildroot 镜像并保存 UART 输出。我们将使用以下输出作为搜索的起点:
` [ 0.268291] OMAP GPIO hardware version 0.1`
我们搜索生成此消息的代码(路径相对于内核源代码所在的根目录):
drivers/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:
arch/arm/boot/dts/am33xx.dtsi
` 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() 函数的运行过程:
drivers/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)处理器的数据手册
或许这足以得出以下初步结论:
-
这是德州仪器特有的产品。
-
这是 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`;
}`
这里最重要的是自旋锁中包含的内容:启用 GPIO 模块并写入内部的 bank.mod_usage 结构,这实际上复制了引脚的"busy"状态。以下是这一切的关键所在:
-
采用自旋锁进行包裹以避免碰撞这一事实本身就是一种策略。
-
尽管这里使用了 `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之间的差异并不大。
H2+ 是 H3 的一个变体,面向低端 OTT 机顶盒,它缺乏千兆 MAC 和 4K HDMI 输出支持。
经证实,H3 镜像可以在 H2+ 上运行。
这意味着一切都有效,我们可以进一步研究代码。
我们来看看输出这些代码的源代码。它位于这里:
drivers/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`;
...`
drivers/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一样,这也是一个平台驱动程序。很难做出其他选择。
不过,一个显著的区别是,该驱动程序位于pinctrl目录下,而不是GPIO目录下。
pinctrl 驱动程序还控制每个引脚的分配。通常,现代 SoC 上的 GPIO 引脚会与其他 I2C/SPI/UART 接口复用。pinctrl 驱动程序包含控制
这些复用器的函数。例如,TI AM335x 上也存在 pinctrl 驱动程序,但它与 GPIO 驱动程序是分开的。本文也采用了这种方法。
我们还是回到 GPIO 上来吧。这个驱动程序的关键步骤仍然不变:创建一个 gpio_chip 结构体的实例,填充数据,并将其注册到系统中。让我们仔细看看。
首先,可以看出 request() 函数用于实现gpiochip_generic_request():
drivers/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`);`
drivers/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 输出值的函数是什么样的:
drivers/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 `
-
我们查看了处理器的数据手册。我们发现 R_PIO 引脚位于地址 0x1F0_2C00。当然,我们也可以从 DTS 中找到这个信息。第二组引脚称为 PIO。目前还不清楚它们之间的区别。
-
好的,我们打开DTS查看开发板。搜索"r_pio"并找到:
arch/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。这看起来很像是物理引脚的标识。
我们看到引脚 PL6 确实连接到一个名为 CPUX-VSET 的电路:

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

这虽然没让事情变得更容易,但至少让我们清楚地知道问题可能出在哪里。看来 Linux 会在运行时主动管理 CPU 资源。
6)在DTS中注释该节点:
` &cpu0 {
cpu-supply = <®_vdd_cpux>;
};`
- 重建镜像,刷入系统,并验证垃圾信息是否消失。问题已解决。PIN 码出现这种波动的原因很有意思,但超出了本文的讨论范围。
总而言之
总而言之,以上所有内容都可以用一个简单的结构图来概括:

胜过千言万语
然而,结合流程图和源代码进行研究,可以取得比单独使用流程图更大的成果,所以我们不要急于抛弃前面所有的文字。