Linux的GPIO驅(qū)動程序使用詳解
讓我們看看 Linux 中 GPIO 驅(qū)動程序的具體結(jié)構(gòu),以及為什么要這樣做。這樣我們就能理解為什么在這個操作系統(tǒng)中,僅僅為了讓 LED 閃爍就需要經(jīng)過 N 層抽象。
上述內(nèi)容大部分也適用于其他驅(qū)動程序。GPIO 被選為最簡單的外圍設(shè)備示例。
我們的文章結(jié)構(gòu)如下:
- 我們先來看看硬件層面的GPIO控制;
- 我們來研究一下操作系統(tǒng)給我們增加了哪些要求;
- 讓我們考慮一下這些要求通常是如何實現(xiàn)的;
- 讓我們看看 Linux 中如何為不同的 SoC 實現(xiàn)真正的 GPIO 驅(qū)動程序;
- 讓我們再次簡單總結(jié)一下研究結(jié)果。
硬件級別
在硬件層面,一切都非常簡單。處理器有一個 GPIO 引腳控制器,從程序員的角度來看,它就是映射到通用地址空間的十幾個寄存器。
要點亮 LED,我們首先需要初始化所需的引腳(將其配置為輸出),然后設(shè)置所需的值。
基本上,寄存器中有兩個條目。一個條目位于確定輸出方向的寄存器中。第二個條目位于確定輸出值的寄存器中。這可以用十幾條 CPU 指令完成。
操作系統(tǒng)限制
當(dāng)我們開始在操作系統(tǒng)中實現(xiàn)相同的操作時,我們會產(chǎn)生額外的需求。
這些需求會導(dǎo)致額外的代碼,但與此同時,這些需求的實現(xiàn)正是確保操作系統(tǒng)便捷運行的關(guān)鍵因素。
從內(nèi)核模式和用戶模式訪問外設(shè)
在用戶空間運行的應(yīng)用程序無法訪問外設(shè)寄存器和其他服務(wù)內(nèi)存區(qū)域。
這是像 Linux 這樣的操作系統(tǒng)的基本要求之一——用戶進程不應(yīng)該破壞其他進程的內(nèi)存,尤其是內(nèi)核內(nèi)存。
這意味著需要一個在內(nèi)核空間運行并為用戶模式應(yīng)用程序提供 API 的驅(qū)動程序。
GPIO 引腳必須由內(nèi)核控制(例如,來自鄰近的驅(qū)動程序),也就是說,內(nèi)核模式也需要一個 API。
訪問共享
多個用戶空間進程可能想要管理同一個輸出。必須提供一種機制來防止多個進程同時訪問該輸出時出現(xiàn)錯誤。
多個內(nèi)核空間進程可能想要使用相同的輸出。這些情況也需要處理,但需要排除錯誤。
標(biāo)準(zhǔn) API
許多 SoC 中集成了多種 GPIO 端口實現(xiàn)。無論具體實現(xiàn)如何,驅(qū)動程序都必須提供相同的一組“旋鈕”(換句話說,相同的 API)。原因如下:
- 用戶空間應(yīng)用程序獲得了額外的平臺 獨立性——在任何 Linux 平臺上,GPIO 引腳都以相同的方式控制;
- 對于在工作中使用 GPIO 引腳的驅(qū)動程序,也賦予了類似的自由度(例如,觸摸控制器或 Wi-Fi 模塊的驅(qū)動程序可以使用 GPIO 引腳進行電源管理)。
此外,大多數(shù) SoC 可以使用 GPIO 引腳接收外部中斷。Linux 也解決了這個問題,但 Linux 中的中斷有其自身的抽象,值得單獨研究。因此,雖然我們在評測過程中會遇到處理中斷的代碼,但本文不會討論它。
實現(xiàn)通用 GPIO 驅(qū)動程序要求
從內(nèi)核和用戶空間訪問外設(shè)
一切都很簡單。驅(qū)動程序是一個常規(guī)的 Linux 內(nèi)核模塊,因此它在內(nèi)核空間中執(zhí)行,并且可以訪問 CPU 的整個地址空間。
對于用戶級應(yīng)用程序,sysfs中提供了特殊文件。
自定義應(yīng)用程序的 API
這是 Linux 系統(tǒng)的標(biāo)準(zhǔn)機制 - 通過文件/sys/class/gpio。
當(dāng)我們找到系統(tǒng)中所需的輸出編號(它通常與物理輸出的編號不一致)時,我們將該編號寫入文件/sys/class/gpio/export:
num=416 echo $num > /sys/class/gpio/export
此后,文件系統(tǒng)中會出現(xiàn)一個目錄/sys/class/gpio/gpio$num。該目錄下有文件direction和value。
控制輸出方向:
echo out > /sys/class/gpio/gpio$num/direction echo in > /sys/class/gpio/gpio$num/direction
控制輸出狀態(tài):
echo 1 > /sys/class/gpio/gpio$num/value echo 0 > /sys/class/gpio/gpio$num/value
讀取輸入狀態(tài):
cat /sys/class/gpio/gpio$num/value
如何實現(xiàn)這一點,我們稍后會考慮。
用戶應(yīng)用程序的訪問分離
如果多個應(yīng)用程序訪問同一個“句柄”,系統(tǒng)將自行解決沖突。就像多個應(yīng)用程序同時訪問同一個文件時一樣。這些沖突不需要在驅(qū)動程序?qū)用孢M行處理。
內(nèi)核 API 和內(nèi)核訪問共享
本節(jié)主要介紹 Linux 內(nèi)核中與平臺無關(guān)的代碼。代碼以存儲庫https://github.com/torvalds/linux的主分支為例,即本文發(fā)布時對應(yīng)的內(nèi)核版本 6.x。具體的驅(qū)動程序?qū)崿F(xiàn)將以內(nèi)核 4.x 和 5.x 為例進行進一步討論。內(nèi)核版本之間存在差異,但總體概念沒有變化。
Linux 內(nèi)核有一個獨立于平臺的抽象結(jié)構(gòu)體 gpio_chip,它是一個包含一組特定 GPIO 引腳描述的結(jié)構(gòu)體。沒有單獨的結(jié)構(gòu)體來描述單個引腳并包含管理該引腳的函數(shù)。
題外話,這與硬件層面的關(guān)系如何。
- 在硬件層面,芯片(SoC - 片上系統(tǒng))通常具有多個 GPIO 塊,每個塊包含數(shù)十個引腳。通常使用諸如 GPIOA、GPIOB 等名稱。因此,引腳的指定格式為 GPIOA_31..GPIOA_0 等。
- 根據(jù)芯片和驅(qū)動程序的具體實現(xiàn),可以實現(xiàn)不同的方法??梢詾槊總€塊啟動一個單獨的 gpio_chip 結(jié)構(gòu)。但也可以實現(xiàn)另一種方法。
- 例如,可以按電源域劃分 GPIO 塊。通常,GPIOA..GPIOC 位于一個域中,而 GPIOD..GPIOF 位于另一個域中。
- 在這種情況下,供應(yīng)商在提供的 SDK 中為一個域的塊形成一個 gpio_chip,為另一個域的塊形成第二個 gpio_chip。
總的來說,從技術(shù)角度來看,gpio_chip 的分區(qū)并不重要?;旧?,這里的一切都取決于特定驅(qū)動程序作者的審美。
無論如何,在 gpio_chip 結(jié)構(gòu)中,除其他內(nèi)容外,還有指向特定 GPIO 塊的驅(qū)動程序中實現(xiàn)的平臺相關(guān)函數(shù)的指針:
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 結(jié)構(gòu)體之外,內(nèi)核還提供了與平臺無關(guān)的 gpio 函數(shù)。它們分別位于 drivers/gpio/gliolib.c 和 drivers/gpio/gpiolib_sysfs.c 文件中。我們來看一下這兩個文件。
第一個包含內(nèi)核函數(shù)。有很多函數(shù),我們重點介紹兩個:
int gpiod_request(struct gpio_desc *desc, const char *label); void gpiod_set_value(struct gpio_desc *desc, int value);
這是允許我們占用所需 GPIO 并設(shè)置其值(如果將其配置為輸出)的最小集合。其余函數(shù)基于相同的原則構(gòu)建,對它們的額外考慮不會給我們帶來任何根本性的新東西。
訪問共享機制是在 gpiod_request() 函數(shù)內(nèi)部實現(xiàn)的。當(dāng)然,它不是直接在函數(shù)內(nèi)部實現(xiàn)的,而是按照慣例,通過函數(shù)“下方”的幾個層級來實現(xiàn)的:
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)將其包裝在自旋鎖中并查看此輸出是否已處于請求的狀態(tài):
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)如果輸出不忙,那么我們檢查它是否有自己的平臺相關(guān)函數(shù) request(),并調(diào)用它:
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);顯然,我們在自旋鎖解鎖的情況下調(diào)用此函數(shù)。雖然很難保證 100%,但看起來我們已經(jīng)解鎖了自旋鎖,因為這里我們已經(jīng)排除了對特定 GPIO 的同時訪問,并且建議盡可能縮短自旋鎖的鎖定時間,以最大程度地減少其他進程/線程的主動等待。
現(xiàn)在讓我們看看設(shè)置給定值是什么樣子的:
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);
}可以看出,最終調(diào)用的是平臺特定函數(shù)set()。set 函數(shù)不再使用任何訪問共享機制。如果某個內(nèi)核代碼想要使用某個 GPIO 引腳,但在調(diào)用 時出錯gpiod_request(),則不應(yīng)再嘗試使用該 GPIO。
現(xiàn)在讓我們看看通過 sysfs 從用戶空間調(diào)用的實現(xiàn)。處理寫入文件的函數(shù)如下所示/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);這里最重要的是挑戰(zhàn)gpiod_request_user()。如果你搜索 master 分支,你會發(fā)現(xiàn)
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;
}在內(nèi)核 4.x,5.x 中沒有gpiod_request_user(),直接使用常規(guī) . 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);
}我們知道,這將導(dǎo)致已經(jīng)熟悉的依賴于平臺的 set() 函數(shù)。
具體驅(qū)動程序的實現(xiàn)
讓我們看看以上所有內(nèi)容在真實硬件上是如何實現(xiàn)的。
研究設(shè)置
我們使用了 BeagleBone Black(SoC TI AM335)和 Orange Pi Zero(SoC Allwinner H2)開發(fā)板。Buildroot 2021.02 被用作內(nèi)核的“包裝器”。這個設(shè)置沒有什么特別之處,只是手頭上的東西,以及作者或多或少習(xí)慣使用的東西。我們使用了兩塊開發(fā)板來測試 TI 和 Allwinner 兩家供應(yīng)商的 GPIO 驅(qū)動程序的實現(xiàn)。我們本來想考慮第三塊開發(fā)板,但手頭上沒有其他可用的。
BeagleBone 的 Buildroot 使用內(nèi)核版本 4.19.79。OrangePi Zero 的 Buildroot 使用內(nèi)核版本 5.10.10。如上所述,現(xiàn)代內(nèi)核版本與 4.19 和 5.10 在細(xì)節(jié)上有所不同,但總體方法沒有改變。
BeagleBone 黑色 (TI AM335)
我們啟動組裝好的 Buildroot 鏡像并保存 UART 輸出。我們以以下行作為搜索的起點:
[ 0.268291] OMAP GPIO hardware version 0.1
我們搜索產(chǎn)生此消息的代碼(該路徑是相對于內(nèi)核源的根目錄給出的):
驅(qū)動程序/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;
}太好了,我們知道了源代碼在哪里。讓我們開始研究它。首先,讓我們了解一下這個驅(qū)動程序的 DTS 中應(yīng)該包含哪些內(nèi)容。記住,內(nèi)核在獲悉系統(tǒng)存在相應(yīng)的設(shè)備后,會運行驅(qū)動程序的probe()函數(shù)。
GPIO控制器是芯片內(nèi)部的一個模塊,通過 AXI 類型的總線連接。那里沒有即插即用機制。這意味著內(nèi)核根據(jù)硬件的靜態(tài)描述來確定這一點。設(shè)備樹(俗稱“DTS”或“DTB”——盡管從技術(shù)上講這并不完全正確。
DTS 和 DTB 是設(shè)備樹的兩種呈現(xiàn)形式。
DTS 指的是“設(shè)備樹源代碼”,DTB 指的是“設(shè)備樹二進制文件”)。
驅(qū)動程序包含必須在 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);驅(qū)動程序本身注冊為平臺驅(qū)動程序:
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 驅(qū)動程序必須在使用這些 GPIO 的驅(qū)動程序初始化之前注冊。為了實現(xiàn)這一點,驅(qū)動程序在 postcore_initcall 階段初始化,這個階段比 device_initcall 階段要早得多。
同時,從兼容列表中我們了解到該驅(qū)動程序支持三個版本的GPIO模塊。要了解使用的是哪一個,我們?nèi)匀恍枰D(zhuǎn)到DTS:
架構(gòu)/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。
結(jié)論:
- gpio 塊至少有三個受支持的版本(omap2-gpio、omap3-gpio、omap4-gpio)。
- 在DTS中我們可以看到這些塊在芯片內(nèi)部的基地址。
- 這些 GPIO 模塊可以產(chǎn)生中斷。通常,這些信息可以從數(shù)據(jù)手冊中獲取,但是:
- 原則上,Linux 程序員打開數(shù)據(jù)表的頻率比微控制器程序員要低得多。
- 對于許多處理器,您可能仍然可以在公共領(lǐng)域找到數(shù)據(jù)表。因此,一般來說,這些知識并非毫無用處。不過,在這個特定情況下,它確實毫無用處。
以下是 omap_gpio_probe() 中發(fā)生的情況:
驅(qū)動程序/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);讓我們仔細(xì)看看:
if (bank->is_mpuio) omap_mpuio_init(bank);
從代碼上看不清楚它是什么。如果你在搜索引擎中輸入“mpuio”,你可以在TI數(shù)據(jù)表中找到它:

互聯(lián)網(wǎng)上的TI處理器數(shù)據(jù)表
也許這足以得出以下暫時的結(jié)論:
- 這是來自 TI 的一個具體事物。
- 這是 GPIO 端口的一些特殊操作模式,為此有一個驅(qū)動程序。
- 并非所有 TI GPIO 端口都支持此模式。
- 在這種情況下,這種模式的存在與否并不會對文章主題的考慮產(chǎn)生很大的影響。
接下來我們有這個函數(shù):
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 內(nèi)核的操作??偟膩碚f,對于我們討論的主題來說,這也不是一個非常重要的功能。
但下一個函數(shù)正好符合目標(biāo):
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 專用”結(jié)構(gòu)體(struct gpio_bank)過渡到“系統(tǒng)級”結(jié)構(gòu)體(struct gpiochip)。實際上,我們將系統(tǒng)級結(jié)構(gòu)體作為 OMAP 專用結(jié)構(gòu)體中的一個實例。這里有一些與中斷相關(guān)的操作,但我們暫時不討論這個話題。
對于我們來說現(xiàn)在最重要的部分是:
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;
這里,我們實現(xiàn)了從通用 API 到其平臺相關(guān)代碼的轉(zhuǎn)換。當(dāng)某些代碼調(diào)用平臺無關(guān)函數(shù)(例如 gpiod_request())時,系統(tǒng)會調(diào)用 gpio_chip.request()。如果這種情況發(fā)生在 AM335x 處理器上,我們就會調(diào)用 omap_gpio_request()。
好了,填充 gpio_chip 結(jié)構(gòu)體之后,最重要的事情就是在系統(tǒng)中注冊這個結(jié)構(gòu)體。從這一刻起,系統(tǒng)就知道了這些 GPIO 線的存在。
讓我們來看看兩個函數(shù)的實現(xiàn)——GPIO 請求和 GPIO 值設(shè)置:
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) 封裝的內(nèi)容:啟用 GPIO 模塊并寫入內(nèi)部 bank.mod_usage 結(jié)構(gòu)體,這實際上復(fù)制了關(guān)于引腳“忙”狀態(tài)的條目。以下是其中最重要的部分:
- 事實上,使用自旋鎖可以消除碰撞。
- 雖然這里存在一個函數(shù) omap_enable_gpio_module(),但通常不需要對硬件進行任何特殊操作。GPIO 請求只是在系統(tǒng)中記錄“此 GPIO 已被占用”的一種方式。嚴(yán)格來說,平臺相關(guān)的 request() 函數(shù)根本沒有必要存在。
設(shè)置GPIO輸出值的函數(shù):
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() 函數(shù)是在驅(qū)動程序初始化期間在probe()函數(shù)中指定的:
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;
}顯然,這為驅(qū)動程序開發(fā)人員提供了額外的自由度。無論如何,如果我們查看 omap_set_gpio_dataout_reg(),我們會發(fā)現(xiàn)它已經(jīng)直接與 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);
}本質(zhì)上,我們在這里看到的是一個簡單的注冊表條目,與我們在文章開頭討論的相同。
現(xiàn)在,讓我們嘗試驗證之前通過閱讀源代碼獲得的理論計算。確保在通過內(nèi)核空間和用戶空間訪問時,我們確實會經(jīng)歷這一系列調(diào)用。讓我們用最原始的方式做到這一點——在
printk(“%s: entered\n”, __func__);
我們討論的函數(shù)中添加一行代碼。然后,我們將刪除日志。
首先,讓我們看一下UART啟動日志,我們會立即看到我們干預(yù)的痕跡:
[ 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
很明顯,驅(qū)動程序在加載過程中使用了兩個 GPIO。而且,它們的執(zhí)行路徑與之前提到的完全一致——首先調(diào)用與平臺無關(guān)的 gpiod_request() 函數(shù),然后調(diào)用與平臺相關(guān)的 omap_gpio_request() 函數(shù)。
現(xiàn)在讓我們看看如果我們從內(nèi)核空間控制 GPIO 會發(fā)生什么:
# 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
顯然,一切確實都朝著我們所討論的相同功能發(fā)展。
OrangePi Zero(全志 H2)
這里的啟動日志并沒有用GPIO相關(guān)的字眼來破壞我們的印象。這些行看起來像是我們要找的內(nèi)容:
[ 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之間的區(qū)別并不大。
這意味著一切都有效,我們可以進一步研究代碼。
讓我們看一下顯示這些行的代碼。它位于此處:
驅(qū)動程序/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;
...他們這樣稱呼它:
驅(qū)動程序/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 類似,這是一個平臺驅(qū)動程序。實際上,很難用其他方式實現(xiàn)。
但明顯的區(qū)別是,驅(qū)動程序不在 GPIO 目錄中,而是在 pinctrl 目錄中。
pinctrl 驅(qū)動程序還控制每個特定引腳的分配。通常,現(xiàn)代 SoC 上的 GPIO 引腳與其他 I2C/SPI/UART 級接口復(fù)用。pinctrl 驅(qū)動程序包含控制這些復(fù)用器的功能。例如,TI AM335x 上也存在 pinctrl 驅(qū)動程序,但該實體與 GPIO 驅(qū)動程序是分開的。在這里,他們決定這樣做。
讓我們回到 GPIO 上。無論如何,此驅(qū)動程序中的關(guān)鍵操作已經(jīng)完成——創(chuàng)建、填充并在系統(tǒng)中注冊 gpio_chip 結(jié)構(gòu)的實例。讓我們仔細(xì)看看這一點。
首先,你可以看到函數(shù) request() 用于實現(xiàn)gpiochip_generic_request():
驅(qū)動程序/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);她在引擎蓋下喊道:
驅(qū)動程序/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);無需詳細(xì)了解 pinctrl 和 pinmux 的實現(xiàn),我們嘗試說明:此功能不僅保留了 GPIO,而且將其固定為 GPIO,而不是 I2C/SPI/UART/等輸出。
我們來看看設(shè)置GPIO輸出值的函數(shù)是什么樣的:
驅(qū)動程序/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);正如預(yù)期的那樣,這里所有操作也都?xì)w結(jié)為寫入外設(shè)寄存器。有趣的是,這里對寄存器的操作被封裝在自旋鎖中。這可能是因為寄存器操作沒有原子操作,操作分為三個階段:讀取寄存器、更改指定位以及將更改后的值寫入寄存器。引入自旋鎖可以消除潛在的沖突。
為了確保我們理解正確,讓我們通過添加調(diào)試輸出來實現(xiàn)類似的路線。
確實,當(dāng)添加此輸出并使用此固件運行時,我們會陷入無限循環(huán)的消息中:
[ 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
這基本上證實了上面的結(jié)論,但在某種程度上干擾了檢查通過 sysfs 訪問的過程?;旧?,這是一個非常典型的 Linux 調(diào)試故事——你只需要稍微深入挖掘一下,就會發(fā)現(xiàn)一些完全無法理解的行為,雖然這并不那么嚴(yán)重(因為一直以來都是這樣),但忽略它似乎也不妥。我們將把此行為的調(diào)試放在稍后的劇透部分,在這里我們將完成確保 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
顯然,這正是我們所期望的。
不定期調(diào)試
誰在拉動 GPIO?調(diào)試過程大大縮短了,搜索和嘗試次數(shù)也顯著增加。最終,結(jié)果如下:
1) 值設(shè)置功能增加調(diào)試輸出:
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)關(guān)閉日志并查看:
# cat /sys/class/gpio/gpiochip352/label 1f02c00.pinctrl
3) 我們查看了處理器的數(shù)據(jù)手冊。我們發(fā)現(xiàn) R_PIO 塊位于地址 0x1F0_2C00。實際上,我們當(dāng)然可以通過 DTS 找到這一點。第二個引腳塊稱為 PIO。目前還不清楚它們之間有什么區(qū)別。
4) 好的,讓我們進入開發(fā)板上的DTS。搜索“r_pio”并找到:
架構(gòu)/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??雌饋砗芟裎锢硪_標(biāo)識。
5)打開電路板圖
我們看到 PL6 引腳確實連接到名為 CPUX-VSET 的鏈:

這個鏈實際上控制著某種電源:

這對我們來說并沒有什么用,但至少讓我們清楚了它到底是什么。看起來 Linux 在運行時正在積極地管理 CPU 功耗。
6)對DTS節(jié)點進行注釋:
&cpu0 {
cpu-supply = <®_vdd_cpux>;
};7) 重建鏡像,刷入——確保垃圾信息消失。案件告破。這種引腳抖動現(xiàn)象的原因很有趣,但這超出了本文的討論范圍。
總結(jié)
總而言之,我們可以將所說的一切簡化為一個簡單的結(jié)構(gòu)圖:

千言萬語值得,但是,借助流程圖并研究源代碼,您可以實現(xiàn)比僅借助流程圖更多的東西,所以我們不要急于拋棄前面的所有文字。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Linux磁盤空間耗盡導(dǎo)致系統(tǒng)故障問題及說明
文章主要內(nèi)容是Linux系統(tǒng)磁盤耗盡導(dǎo)致報錯的排障和維護思路,包括確認(rèn)故障、查找占用空間的文件或目錄、清理文件以及預(yù)防措施,還介紹了ncdu工具的使用方法和日志管理、臨時文件管理、軟鏈接和硬鏈接、磁盤配額等高級維護技巧2026-01-01
linux mount報錯:you must specify the filesystem type的解決方法
這篇文章主要介紹了linux mount報錯:you must specify the filesystem type的解決方法,文中給出了詳細(xì)的解決方法示例,對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-03-03
CentOS7將Nginx添加系統(tǒng)服務(wù)的方法步驟
這篇文章主要介紹了CentOS7將Nginx添加系統(tǒng)服務(wù)的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03
CentOS7 Grub引導(dǎo)故障恢復(fù)全過程
文章介紹了當(dāng)CentOS7的/boot/grub2/grub.cfg文件丟失時的恢復(fù)步驟,包括手動引導(dǎo)進入系統(tǒng)、永久修復(fù)GRUB配置以及使用救援模式進行修復(fù)2026-01-01
Ubuntu Server 10.04修改Apache的默認(rèn)目錄的方法
這篇文章主要為大家分享下Ubuntu Server 10.04修改Apache的默認(rèn)目錄的方法,需要的朋友可以參考下2013-12-12
Linux學(xué)習(xí)第一天——ssh登錄和軟件安裝詳解
這篇文章主要介紹了Linux學(xué)習(xí)第一天——ssh登錄和軟件安裝詳解 ,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2016-12-12
linux vps服務(wù)器進程kswapd0與events/0消耗大量CPU的問題
使用了阿里云的vps服務(wù)器網(wǎng)站宕了兩次機,發(fā)工單給阿里云,發(fā)現(xiàn)原因是服務(wù)器的CPU 100%了,這也是vps的弊端,內(nèi)容給的相對小2014-03-03

