【深度】韋東山:GPIO和Pinctrl子系統的使用

作者:韋東山

參考文檔:
a. 內核 Documentation\devicetree\bindings\Pinctrl\ 目錄下:
Pinctrl-bindings.txt

b. 內核 Documentation\gpio 目錄下:
Pinctrl-bindings.txt

c. 內核 Documentation\devicetree\bindings\gpio 目錄下:
gpio.txt

注意:本章的重點在於“使用”,深入講解放在“驅動大全”的視頻裏。
前面的視頻,我們使用直接操作寄存器的方法編寫驅動。這只是爲了讓大家掌握驅動程序的本質,在實際開發過程中我們可不這樣做,太低效了!如果驅動開發都是這樣去查找寄存器,那我們就變成“寄存器工程師”了,即使是做單片機的都不執着於裸寫寄存器了。
Linux下針對引腳有2個重要的子系統:GPIO、Pinctrl。

1.Pinctrl子系統重要概念

1.1 引入

無論是哪種芯片,都有類似下圖的結構:
在這裏插入圖片描述

要想讓pinA、B用於GPIO,需要設置IOMUX讓它們連接到GPIO模塊;
要想讓pinA、B用於I2C,需要設置IOMUX讓它們連接到I2C模塊。
所以GPIO、I2C應該是並列的關係,它們能夠使用之前,需要設置IOMUX。有時候並不僅僅是設置IOMUX,還要配置引腳,比如上拉、下拉、開漏等等。

現在的芯片動輒幾百個引腳,在使用到GPIO功能時,讓你一個引腳一個引腳去找對應的寄存器,這要瘋掉。術業有專攻,這些累活就讓芯片廠家做吧──他們是BSP工程師。我們在他們的基礎上開發,我們是驅動工程師。開玩笑的,BSP工程師是更懂他自家的芯片,但是如果驅動工程師看不懂他們的代碼,那你的進步也有限啊。

所以,要把引腳的複用、配置抽出來,做成Pinctrl子系統,給GPIO、I2C等模塊使用。
BSP工程師要做什麼?看下圖:
在這裏插入圖片描述
等BSP工程師在GPIO子系統、Pinctrl子系統中把自家芯片的支持加進去後,我們就可以非常方便地使用這些引腳了:點燈簡直太簡單了。

等等,GPIO模塊在圖中跟I2C不是並列的嗎?幹嘛在講Pinctrl時還把GPIO子系統拉進來?
大多數的芯片,沒有單獨的IOMUX模塊,引腳的複用、配置等等,就是在GPIO模塊內部實現的。
在硬件上GPIO和Pinctrl是如此密切相關,在軟件上它們的關係也非常密切。
所以這2個子系統我們一起講解。

1.2 重要概念

從設備樹開始學習Pintrl會比較容易。
主要參考文檔是:內核Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt

這會涉及2個對象:pin controller、client device。
前者提供服務:可以用它來複用引腳、配置引腳。
後者使用服務:聲明自己要使用哪些引腳的哪些功能,怎麼配置它們。

a. pin controller:
在芯片手冊裏你找不到pin controller,它是一個軟件上的概念,你可以認爲它對應IOMUX──用來複用引腳,還可以配置引腳(比如上下拉電阻等)。
注意,pin controller和GPIO Controller不是一回事,前者控制的引腳可用於GPIO功能、I2C功能;後者只是把引腳配置爲輸入、輸出等簡單的功能。

b. client device
“客戶設備”,誰的客戶?Pinctrl系統的客戶,那就是使用Pinctrl系統的設備,使用引腳的設備。它在設備樹裏會被定義爲一個節點,在節點裏聲明要用哪些引腳。
下面這個圖就可以把幾個重要概念理清楚:
在這裏插入圖片描述
上圖中,左邊是pincontroller節點,右邊是client device節點:
a. pin state:
對於一個“client device”來說,比如對於一個UART設備,它有多個“狀態”:default、sleep等,那對應的引腳也有這些狀態。

怎麼理解?
比如默認狀態下,UART設備是工作的,那麼所用的引腳就要複用爲UART功能。
在休眠狀態下,爲了省電,可以把這些引腳複用爲GPIO功能;或者直接把它們配置輸出高電平。
上圖中,pinctrl-names裏定義了2種狀態:default、sleep。
第0種狀態用到的引腳在pinctrl-0中定義,它是state_0_node_a,位於pincontroller節點中。
第1種狀態用到的引腳在pinctrl-1中定義,它是state_1_node_a,位於pincontroller節點中。
當這個設備處於default狀態時,pinctrl子系統會自動根據上述信息把所用引腳複用爲uart0功能。
當這這個設備處於sleep狀態時,pinctrl子系統會自動根據上述信息把所用引腳配置爲高電平。

b. groups和function:
一個設備會用到一個或多個引腳,這些引腳就可以歸爲一組(group);
這些引腳可以複用爲某個功能:function。
當然:一個設備可以用到多能引腳,比如A1、A2兩組引腳,A1組複用爲F1功能,A2組複用爲F2功能。

c. Generic pin multiplexing node和Generic pin configuration node
在上圖左邊的pin controller節點中,有子節點或孫節點,它們是給client device使用的。
可以用來描述複用信息:哪組(group)引腳複用爲哪個功能(function);
可以用來描述配置信息:哪組(group)引腳配置爲哪個設置功能(setting),比如上拉、下拉等。

注意:pin controller節點的格式,沒有統一的標準!!!!每家芯片都不一樣。
甚至上面的group、function關鍵字也不一定有,但是概念是有的。

1.3 示例

在這裏插入圖片描述

1.4 代碼中怎麼引用pinctrl

這是透明的,我們的驅動基本不用管。當設備切換狀態時,對應的pinctrl就會被調用。
比如在platform_device和platform_driver的枚舉過程中,流程如下:
在這裏插入圖片描述
當系統休眠時,也會去設置該設備sleep狀態對應的引腳,不需要我們自己去調用代碼。

非要自己調用,也有函數:

devm_pinctrl_get_select_default(struct device *dev);      // 使用"default"狀態的引腳
pinctrl_get_select(struct device *dev, const char *name); // 根據name選擇某種狀態的引腳
pinctrl_put(struct pinctrl *p);   // 不再使用, 退出時調用

2.GPIO子系統重要概念

2.1 引入

要操作GPIO引腳,先把所用引腳配置爲GPIO功能,這通過Pinctrl子系統來實現。
然後就可以根據設置引腳方向(輸入還是輸出)、讀值──獲得電平狀態,寫值──輸出高低電平。
以前我們通過寄存器來操作GPIO引腳,即使LED驅動程序,對於不同的板子它的代碼也完全不同。
當BSP工程師實現了GPIO子系統後,我們就可以:
a. 在設備樹裏指定GPIO引腳
b. 在驅動代碼中:
使用GPIO子系統的標準函數獲得GPIO、設置GPIO方向、讀取/設置GPIO值。
這樣的驅動代碼,將是單板無關的。

2.2 在設備樹中指定引腳

在幾乎所有ARM芯片中,GPIO都分爲幾組,每組中有若干個引腳。所以在使用GPIO子系統之前,就要先確定:它是哪組的?組裏的哪一個?
在設備樹中,“GPIO組”就是一個GPIO Controller,這通常都由芯片廠家設置好。我們要做的是找到它名字,比如“gpio1”,然後指定要用它裏面的哪個引腳,比如<&gpio1 0>。
有代碼更直觀,下圖是一些芯片的GPIO控制器節點,它們一般都是廠家定義好,在xxx.dtsi文件中:
在這裏插入圖片描述

我們暫時只需要關心裏面的這2個屬性:

gpio-controller;
#gpio-cells = <2>;

“gpio-controller”表示這個節點是一個GPIO Controller,它下面有很多引腳。
“#gpio-cells = <2>”表示這個控制器下每一個引腳要用2個32位的數(cell)來描述。
爲什麼要用2個數?其實使用多個cell來描述一個引腳,這是GPIO Controller自己決定的。比如可以用其中一個cell來表示那是哪一個引腳,用另一個cell來表示它是高電平有效還是低電平有效,甚至還可以用更多的cell來示其他特性。
普遍的用法是,用第1個cell來表示哪一個引腳,用第2個cell來表示有效電平:

GPIO_ACTIVE_HIGH : 高電平有效
GPIO_ACTIVE_LOW  :  低電平有效

定義GPIO Controller是芯片廠家的事,我們怎麼引用某個引腳呢?在自己的設備節點中使用屬性"[-]gpios",示例如下:
在這裏插入圖片描述

上圖中,可以使用gpios屬性,也可以使用name-gpios屬性。

2.3 在驅動代碼中調用GPIO子系統

在設備樹中指定了GPIO引腳,在驅動代碼中如何使用?
也就是GPIO子系統的接口函數是什麼?
GPIO子系統有兩套接口:基於描述符的(descriptor-based)、老的(legacy)。前者的函數都有前綴“gpiod_”,它使用gpio_desc結構體來表示一個引腳;後者的函數都有前綴“gpio_”,它使用一個整數來表示一個引腳。

要操作一個引腳,首先要get引腳,然後設置方向,讀值、寫值。

驅動程序中要包含頭文件,
#include <linux/gpio/consumer.h> // descriptor-based

#include <linux/gpio.h> // legacy

下表列出常用的函數:
在這裏插入圖片描述

有前綴“devm_”的含義是“設備資源管理”(Managed Device Resource),這是一種自動釋放資源的機制。它的思想是“資源是屬於設備的,設備不存在時資源就可以自動釋放”。
比如在Linux開發過程中,先申請了GPIO,再申請內存;如果內存申請失敗,那麼在返回之前就需要先釋放GPIO資源。如果使用devm的相關函數,在內存申請失敗時可以直接返回:設備的銷燬函數會自動地釋放已經申請了的GPIO資源。
建議使用“devm_”版本的相關函數。

舉例,假設備在設備樹中有如下節點:

	foo_device {
		compatible = "acme,foo";
		...
		led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */
			    <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */
			    <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */

		power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>;
	};

那麼可以使用下面的函數獲得引腳:

struct gpio_desc *red, *green, *blue, *power;
red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH);
green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH);
blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH);
power = gpiod_get(dev, "power", GPIOD_OUT_HIGH);

要注意的是,gpiod_set_value設置的值是“邏輯值”,不一定等於物理值。
什麼意思?
在這裏插入圖片描述

舊的“gpio_”函數沒辦法根據設備樹信息獲得引腳,它需要先知道引腳號。

引腳號怎麼確定?
在GPIO子系統中,每註冊一個GPIO Controller時會確定它的“base number”,那麼這個控制器裏的第n號引腳的號碼就是:base number + n。
但是如果硬件有變化、設備樹有變化,這個base number並不能保證是固定的,應該查看sysfs來確定base number。

2.4 sysfs中的訪問方法

在sysfs中訪問GPIO,實際上用的就是引腳號,老的方法。
a. 先確定某個GPIO Controller的基準引腳號(base number),再計算出某個引腳的號碼。
方法如下:
① 先在開發板的/sys/class/gpio目錄下,找到各個gpiochipXXX目錄:
在這裏插入圖片描述

② 然後進入某個gpiochip目錄,查看文件label的內容
③ 根據label的內容對比設備樹
label內容來自設備樹,比如它的寄存器基地址。用來跟設備樹(dtsi文件)比較,就可以知道這對應哪一個GPIO Controller。
下圖是在100asK_imx6ull上運行的結果,通過對比設備樹可知gpiochip96對應gpio4:
在這裏插入圖片描述
所以gpio4這組引腳的基準引腳號就是96,這也可以“cat base”來再次確認。

b. 基於sysfs操作引腳:
以100ask_imx6ull爲例,它有一個按鍵,原理圖如下:
在這裏插入圖片描述
那麼GPIO4_14的號碼是96+14=110,可以如下操作讀取按鍵值:

echo  110 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio110/direction
cat /sys/class/gpio/gpio110/value
echo  110 > /sys/class/gpio/unexport

注意:如果驅動程序已經使用了該引腳,那麼將會export失敗,會提示下面的錯誤:
在這裏插入圖片描述

對於輸出引腳,假設引腳號爲N,可以用下面的方法設置它的值爲1:

echo  N > /sys/class/gpio/export
echo out > /sys/class/gpio/gpioN/direction
echo 1 > /sys/class/gpio/gpioN/value
echo  N > /sys/class/gpio/unexport

3. 基於GPIO子系統的LED驅動程序

3.1 編寫思路

GPIO的地位跟其他模塊,比如I2C、UART的地方是一樣的,要使用某個引腳,需要先把引腳配置爲GPIO功能,這要使用Pinctrl子系統,只需要在設備樹裏指定就可以。在驅動代碼上不需要我們做任何事情。
GPIO本身需要確定引腳,這也需要在設備樹裏指定。
設備樹節點會被內核轉換爲platform_device。
對應的,驅動代碼中要註冊一個platform_driver,在probe函數中:獲得引腳、註冊file_operations。
在file_operations中:設置方向、讀值/寫值。
在這裏插入圖片描述

下圖就是一個設備樹的例子:
在這裏插入圖片描述

3.2 在設備樹中添加Pinctrl信息

有些芯片提供了設備樹生成工具,在GUI界面中選擇引腳功能和配置信息,就可以自動生成Pinctrl子結點。把它複製到你的設備樹文件中,再在client device結點中引用就可以。
有些芯片只提供文檔,那就去閱讀文檔,一般在內核源碼目錄Documentation\devicetree\bindings\pinctrl下面,保存有該廠家的文檔。
如果連文檔都沒有,那隻能參考內核源碼中的設備樹文件,在內核源碼目錄arch/arm/boot/dts目錄下。
最後一步,網絡搜索。
Pinctrl子節點的樣式如下:
在這裏插入圖片描述

3.3 在設備樹中添加GPIO信息

先查看電路原理圖確定所用引腳,再在設備樹中指定:添加”[name]-gpios”屬性,指定使用的是哪一個GPIO Controller裏的哪一個引腳,還有其他Flag信息,比如GPIO_ACTIVE_LOW等。具體需要多少個cell來描述一個引腳,需要查看設備樹中這個GPIO Controller節點裏的“#gpio-cells”屬性值,也可以查看內核文檔。
示例如下:
在這裏插入圖片描述

3.4編程示例

在實際操作過程中也許會碰到意外的問題,現場演示如何解決。
a. 定義、註冊一個platform_driver
b. 在它的probe函數裏:
b.1 根據platform_device的設備樹信息確定GPIO:gpiod_get
b.2 定義、註冊一個file_operations結構體
b.3 在file_operarions中使用GPIO子系統的函數操作GPIO:
gpiod_direction_output、gpiod_set_value

好處:這些代碼對所有的代碼都是完全一樣的!
使用GIT命令載後,源碼leddrv.c位於這個目錄下:

01_all_series_quickstart\
04_快速入門_正式開始\
02_嵌入式Linux驅動開發基礎知識\source\
05_gpio_and_pinctrl\
    01_led

摘錄重點內容:
a. 註冊platform_driver
注意下面第122行的"100ask,leddrv",它會跟設備樹中節點的compatible對應:

121 static const struct of_device_id ask100_leds[] = {
122     { .compatible = "100ask,leddrv" },
123     { },
124 };
125
126 /* 1. 定義platform_driver */
127 static struct platform_driver chip_demo_gpio_driver = {
128     .probe      = chip_demo_gpio_probe,
129     .remove     = chip_demo_gpio_remove,
130     .driver     = {
131         .name   = "100ask_led",
132         .of_match_table = ask100_leds,
133     },
134 };
135

136 /* 2. 在入口函數註冊platform_driver */

137 static int __init led_init(void)
138 {
139     int err;
140
141     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
142
143     err = platform_driver_register(&chip_demo_gpio_driver);
144
145     return err;
146 }

b. 在probe函數中獲得GPIO
核心代碼是第87行,它從該設備(對應設備樹中的設備節點)獲取名爲“led”的引腳。在設備樹中,必定有一屬性名爲“led-gpios”或“led-gpio”。

77 /* 4. 從platform_device獲得GPIO
78  *    把file_operations結構體告訴內核:註冊驅動程序
79  */
80 static int chip_demo_gpio_probe(struct platform_device *pdev)
81 {
82      //int err;
83
84      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
85
86      /* 4.1 設備樹中定義有: led-gpios=<...>; */
87     led_gpio = gpiod_get(&pdev->dev, "led", 0);
88      if (IS_ERR(led_gpio)) {
89              dev_err(&pdev->dev, "Failed to get GPIO for led\n");
90              return PTR_ERR(led_gpio);
91      }
92

c. 註冊file_operations結構體:
這是老套路了:

93      /* 4.2 註冊file_operations      */
94      major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */
95
96      led_class = class_create(THIS_MODULE, "100ask_led_class");
97      if (IS_ERR(led_class)) {
98              printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
99              unregister_chrdev(major, "led");
100             gpiod_put(led_gpio);
101             return PTR_ERR(led_class);
102     }
103
104     device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */
105

d. 在open函數中調用GPIO函數設置引腳方向:

51 static int led_drv_open (struct inode *node, struct file *file)
52 {
53      //int minor = iminor(node);
54
55      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
56      /* 根據次設備號初始化LED */
57      gpiod_direction_output(led_gpio, 0);
58
59      return 0;
60 }

e. 在write函數中調用GPIO函數設置引腳值:

34 /* write(fd, &val, 1); */
35 static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
36 {
37      int err;
38      char status;
39      //struct inode *inode = file_inode(file);
40      //int minor = iminor(inode);
41
42      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
43      err = copy_from_user(&status, buf, 1);
44
45      /* 根據次設備號和status控制LED */
46      gpiod_set_value(led_gpio, status);
47
48      return 1;
49 }

f. 釋放GPIO:

gpiod_put(led_gpio);

4.在100ASK_IMX6ULL上機實驗

4.1 確定引腳並生成設備樹節點

NXP公司對於IMX6ULL芯片,有設備樹生成工具。我們也把它上傳到GIT去了,使用GIT命令載後,在這個目錄下:

01_all_series_quickstart\
04_快速入門_正式開始\
02_嵌入式Linux驅動開發基礎知識\source\
05_gpio_and_pinctrl\
tools\
imx\

安裝“Pins_Tool_for_i.MX_Processors_v6_x64.exe”後運行,打開IMX6ULL的配置文件“MCIMX6Y2xxx08.mex”,就可以在GUI界面中選擇引腳,配置它的功能,這就可以自動生成Pinctrl的子節點信息。
100ASK_IMX6ULL使用的LED原理圖如下,可知引腳是GPIO5_3:
在這裏插入圖片描述
在設備樹工具中,如下圖操作:
在這裏插入圖片描述

把自動生成的設備樹信息,放到內核源碼arch/arm/boot/dts/100ask_imx6ull-14x14.dts中,代碼如下:
a. Pinctrl信息:

&iomuxc_snvs {
……
        myled_for_gpio_subsys: myled_for_gpio_subsys{ 
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x000110A0
            >;
        };

b. 設備節點信息(放在根節點下):

        myled {
            compatible = "100ask,leddrv";
            pinctrl-names = "default";
            pinctrl-0 = <&myled_for_gpio_subsys>;
            led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
        };

4.2 編譯程序

編譯設備樹後,要更新設備樹。
編譯驅動程序時,“leddrv_未測試的原始版本.c”是有錯誤信息的,“leddrv.c”是修改過的。
測試方法,在板子上執行命令:

#insmod  leddrv.ko
#ls /dev/100ask_led0
#./ledtest /dev/100ask_led0 on
#./ledtest /dev/100ask_led0 off
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章