gpio子系統和pinctrl子系統(上)


前言

  隨着內核的發展,linux驅動框架在不斷的變化。很早很早以前,出現了gpio子系統,后來又出現了pinctrl子系統。在網上很難看到一篇講解這類子系統的文章。就拿gpio操作來說吧,很多時候都是簡單的調用gpio子系統提供的api,然后根據sdk說明文檔寫明的gpio號傳參數,至於里面的工作過程對於驅動工程師而言就像個黑盒子。當我們自己設計的板子和demo板有很大變動時,問題就出現了。首先遇到的是怎么配置pin(是基於設備樹還是不基於設備樹,基於設備樹的話,怎么修改設備樹關於pinctrl部分的內容,里面各個字段什么意思,怎么改),然后是在哪里配置pin(內核部分有哪些需要相應修改,還是不需要一點修改呢),接着就是怎么調試等等。我想只有清楚了盡量多的gpio子系統和pinctrl子系統細節,才會更快更好的完成這些工作。有些平台的實現沒有使用內核提供的pinctrl子系統,而是繼續采用在內核提供pinctrl子系統前自己實現的那套機制來pinmux操作,如omap,有些平台則基於pinctrl子系統來實現pinmux、pinconf的控制。本文以gpio子系統為入口慢慢深入,最后分析pinctrl子系統。

如果有錯誤的地方,歡迎大家直接指出

gpio子系統

  gpio子系統幫助我們管理整個系統gpio的使用情況,同時通過sys文件系統導出了調試信息和應用層控制接口。它內部實現主要提供了兩類接口,一類給bsp工程師,用於注冊gpio chip(也就是所謂的gpio控制器驅動),另一部分給驅動工程師使用,為驅動工程師屏蔽了不同gpio chip之間的區別,驅動工程師調用的api的最終操作流程會導向gpio對應的gpio chip的控制代碼,也就是bsp的代碼。

gpio子系統核心實現分析

gpio子系統的內容在drivers/gpio文件夾下,主要文件有: 
devres.c
gpiolib.c
gpiolib-of.c
gpiolib-acpi.c
gpio-xxx.c
devres.c是針對gpio api增加的devres機制的支持,devres機制講解請參考另一篇博文,gpiolib.c是gpio子系統的核心實現,gpiolib-of.c是對設備樹的支持,gpiolib-acpi.c和acpi相關,不分析(acpi還未深入了解_),最后情景分析的時候,會找一個平台的gpio-xxx.c來分析。

從驅動工程師使用的api開始分析吧!也分兩代,legacy的api主要會用到的接口有(現在推薦采用新的,基於描述符的api):
gpio_requestgpio_free
gpio_direction_inputgpio_direction_output
gpio_to_irq
gpio_export

一般的流程:

//請求一個/一組gpio
gpio_request/devm_gpio_request、gpio_request_one/devm_gpio_request_one、gpio_request_array   ---------<1>
...
//設置gpio方向為輸入/輸出
gpio_direction_input或者gpio_direction_output        ---------<2>
...
//將該gpio通過sys文件系統導出,應用層可以通過文件操作gpio
gpio_export                                          ---------<3>
...
//如果gpio為輸入,獲取gpio值,如果gpio為輸出,可以設置gpio高低電平
gpio_get_value、gpio_set_value                       ---------<4>
...
//將gpio轉為對應的irq,然后注冊該irq的中斷handler
request_irq(gpio_to_irq(gpio_num)...)                ---------<5>
...
//釋放請求的一個或者一組gpio
gpio_free/devm_gpio_free、gpio_free_array                           ---------<6>
...

下面一個個來分析吧!
<1> 以gpio_request為例,gpio_request_onegpio_request_array是它的擴展,devm_為前綴的是gpio devres機制的實現。
int gpio_request(unsigned gpio, const char *label)
參數為gpio號和為該gpio指定的表簽名,具體gpio號是多少,可以通過sdk開發包說明文檔查看,或者查看設備樹文件,再或者基於bank數量推算(當然,這樣可能不准),實在沒辦法的話,瞄一眼gpio chip驅動的代碼吧!gpio_request主要做了以下動作:

  1. 檢查是否已經被申請,沒有的話,標記為已申請
  2. 填充label到該pin數據結構,用於debug
  3. 如果chip driver提供了request回調,調用它
  4. 如果chip driver提供了get_direction回調,調用它,通過它更新pin數據結構,標明gpio方向

gpio_request_one多一個flags參數,通過該參數,可以指定GPIOF_OPEN_DRAINGPIOF_OPEN_SOURCEGPIOF_DIR_INGPIOF_EXPORT等標志,如果指定了GPIOF_DIR_IN,那么后面就不需要自己再額外調用gpio_direction_input或者gpio_direction_output了,如果指定了GPIOF_EXPORT,后面就不需要自己調用gpio_export了。

gpio_request_array是對gpio_request_one的封裝,用於處理同時申請多個gpio的情形。

<2> gpio_direction_input或者gpio_direction_output用來設置該gpio為輸入還是輸出,它們主要是回調gpio chip driver提供的direction_input或者direction_output來設置該gpio寄存器為輸入、輸出。

<3> gpio_export主要用於調試,它會將該gpio的信息通過sys文件系統導出,這樣應用層可以直接查看狀態、設置狀態等。

<4> gpio_get_value或者gpio_set_value和input、output類似,如果為輸入,獲取該gpio的值,如果為輸出,設置該gpio的值,內部也是調用gpio chip driver提供的get、set。

<5> gpio_to_irq用於獲取該gpio對應的中斷號,這個需要設備樹里的該gpio節點描述使用哪個中斷號(並不是所有的gpio都可以觸發中斷的)。它里面的實現就是回調gpio chip driver提供的to_irq

<6> gpio_free就不用說了啦,gpio_request的逆操作。

要使用以上接口,需要#include <linux/gpio.h>,且還有一組用於允許睡眠場景的api沒有給出,更多相關的說明可以參考Documentation/gpio/gpio-legacy.txt

上面的分析沒有深入的代碼層,之所以沒有深入分析,是因為打算放到后面分析基於描述符api時啦!其實,legacy gpio 大部分api就是基於描述符api來實現的。最新的,基於描述符一般的流程:

...
//請求第一個/指定某一個gpio desc,該返回值用於后面的操作
gpiod_get/devm_gpiod_get、gpiod_get_index/devm_gpiod_get_index   ---------<1>
...
//設置gpio方向為輸入/輸出
gpiod_direction_input或者gpiod_get_direction                 ---------<2>
...
//將該gpio通過sys文件系統導出,應用層可以通過文件操作gpio
gpiod_export                                                ---------<3>
...
//如果gpio為輸入,獲取gpio值,如果gpio為輸出,可以設置gpio高低電平
gpiod_get_value或者gpiod_set_value                           ---------<4>
...
//將gpio轉為對應的irq,然后注冊該irq的中斷handler
request_irq(gpiod_to_irq(gpio_desc)...)                     ---------<5>
...
//釋放請求的一個或者一組gpio
gpiod_put/devm_gpiod_put                                    ---------<6>
...

還是下面一個個來分析吧!
<1> gpiod_get內部的處理和gpio_request一樣,不過輸入參數變為char *con_id,這個需要從設備樹文件里查看到,除此之外,我們還可以在設備樹文件里添加參數(GPIO_ACTIVE_LOWGPIO_OPEN_DRAINGPIO_OPEN_SOURCE)來觸發該接口內部設置gpio,具體的參數格式和具體的gpio chip driver有關,一般可以在/Documentation/devicetree/bindings/gpio里找到對應平台的。舉個例子:

        row-gpios = <&gpio1 25 GPIO_ACTIVE_HIGH     /* Bank1, pin25 */          
                 &gpio1 26 GPIO_ACTIVE_HIGH     /* Bank1, pin26 */              
                 &gpio1 27 GPIO_ACTIVE_HIGH>;   /* Bank1, pin27 */ 

struct gpio_desc *__must_check gpiod_get(struct device *dev, const char *con_id)
對應上面的設備樹描述,con_id就是row了,獲取的也會是第一個設置,即gpio1 25 GPIO_ACTIVE_HIGH,如果想獲取第二個設置,我們得通過gpiod_get_index,並將輸入參數idx設置為1。

<2> gpiod_direction_inputgpio_direction_input功能一樣,實際上gpio_direction_input僅僅簡單封裝了gpiod_direction_input,不再描述。

<3> gpiod_export 同上

<4> gpiod_get_value 同上

<5> gpiod_to_irq 同上

<6> gpiod_put 同上

要使用以上接口,需要#include <linux/gpio/consumer.h>,且還有一組用於允許睡眠場景的api沒有給出,更多相關的說明可以參考Documentation/gpio/consumer.txt

下面簡單分析下gpio子系統內部實現:
1> gpio chip driver的初始化
gpio子系統提供了兩層接口,一層給上層驅動工程師調用,一層給下層bsp工程師調用。上層使用前,當然先得bsp工程師完成對應的動作。先看一張網上的截圖:

gpio子系統
bsp工程師通過gpiochip_add將gpio chip添加到gpio子系統中,下面就分析它:

int gpiochip_add(struct gpio_chip *chip)
{
	unsigned long	flags;
	int		status = 0;
	unsigned	id;
	int		base = chip->base;

	//如果指定了base,也就是指定了啟示gpio號,需要校驗下chip的所有gpio是否有效
    //這里會用到ARCH_NR_GPIOS宏,它可以在配置的時候,通過CONFIG_ARCH_NR_GPIO修改,
    //否則采用默認值256
	if ((!gpio_is_valid(base) || !gpio_is_valid(base + chip->ngpio - 1))
			&& base >= 0) {
		status = -EINVAL;
		goto fail;
	}

	spin_lock_irqsave(&gpio_lock, flags);

	//如果沒有指定base,那么需要基於該chip的gpio數量在系統支持的gpio范圍里找一段區間給該chip
	if (base < 0) {
		base = gpiochip_find_base(chip->ngpio);
		if (base < 0) {
			status = base;
			goto unlock;
		}
		chip->base = base;
	}

	//到這里的時候,說明一切正常,把它加入到全局的gpiochip鏈表中去吧,注意,加入的時候會基於base排序
    //這也保證了gpiochip_find_base的實現
	status = gpiochip_add_to_list(chip);

	//如果加入成功,最后一步就是初始化該chip對應的那些gpio了
	if (status == 0) {
		chip->desc = &gpio_desc[chip->base];

		for (id = 0; id < chip->ngpio; id++) {
			struct gpio_desc *desc = &chip->desc[id];
            //將該chip對應的那些gpio對應的數據結構desc初始化,指向擁有它的chip
			desc->chip = chip;

			/* REVISIT:  most hardware initializes GPIOs as
			 * inputs (often with pullups enabled) so power
			 * usage is minimized.  Linux code should set the
			 * gpio direction first thing; but until it does,
			 * and in case chip->get_direction is not set,
			 * we may expose the wrong direction in sysfs.
			 */
            //如果chip driver沒有指定chip->direction_input,意味着不是輸入,那就設置為輸出咯
			desc->flags = !chip->direction_input
				? (1 << FLAG_IS_OUT)
				: 0;
		}
	}

	spin_unlock_irqrestore(&gpio_lock, flags);

#ifdef CONFIG_PINCTRL
	//這里在配置了pinctrl的時候,會初始化它,后面會用到
	INIT_LIST_HEAD(&chip->pin_ranges);
#endif

	//初始化設備樹相關的信息,后面會詳細講一下這部分
	of_gpiochip_add(chip);
    //acpi方式的先忽略吧
	acpi_gpiochip_add(chip);

	if (status)
		goto fail;

	//將該gpiochip導出到sys,用於調試和應用層直接操作
	status = gpiochip_export(chip);
	if (status)
		goto fail;

	pr_debug("%s: registered GPIOs %d to %d on device: %s\n", __func__,
		chip->base, chip->base + chip->ngpio - 1,
		chip->label ? : "generic");

	return 0;

unlock:
	spin_unlock_irqrestore(&gpio_lock, flags);
fail:
	/* failures here can mean systems won't boot... */
	pr_err("%s: GPIOs %d..%d (%s) failed to register\n", __func__,
		chip->base, chip->base + chip->ngpio - 1,
		chip->label ? : "generic");
	return status;
}

還是以zynq平台為例,看看輸入參數struct gpio_chip *chip的初始化過程:

chip->label = "zynq_gpio";
chip->owner = THIS_MODULE;
chip->dev = &pdev->dev;
chip->get = zynq_gpio_get_value;
chip->set = zynq_gpio_set_value;
chip->request = zynq_gpio_request;
chip->free = zynq_gpio_free;
chip->direction_input = zynq_gpio_dir_in;
chip->direction_output = zynq_gpio_dir_out;
chip->to_irq = zynq_gpio_to_irq;
chip->dbg_show = NULL;
chip->base = 0;		/* default pin base */
chip->ngpio = ZYNQ_GPIO_NR_GPIOS;
chip->can_sleep = 0;

get、set、request、free、direction_inputdirection_output、to_irq這幾個回調應該很清楚了,也知道是哪幾個接口調用的它們了吧,另外幾個關鍵的參數base和ngpio在上面的gpiochip_add里應該也已經清楚了它們的重要作用了吧!

of_gpiochip_add會處理gpio chip設備樹相關的東西:

void of_gpiochip_add(struct gpio_chip *chip)
{
	if ((!chip->of_node) && (chip->dev))
		chip->of_node = chip->dev->of_node;

	if (!chip->of_node)
		return;

	//如果沒有指定of_xlate,那給一個默認的吧!of_xlate用於解析設備樹里gpio屬性
    //不同的soc可能需要不同的解析方法,但是如果沒有什么特別,那就用默認的解析吧
    //當前設備樹的節點里支持兩種格式的屬性,一種是xxx-gpios,另一種就直接是gpios
    //如前文中用到的row-gpios就是一種
	if (!chip->of_xlate) {
		chip->of_gpio_n_cells = 2;
		chip->of_xlate = of_gpio_simple_xlate;
	}

	//這一步就是與pinctrl子系統打交道啦!后面會簡單分析下它都做了什么 
    //等到講pinctrl子系統的時候就更加清楚了
	of_gpiochip_add_pin_range(chip);
    //增加該節點引用計數
	of_node_get(chip->of_node);
}

關於of_gpiochip_add_pin_range,看起來很復雜,其實也就那樣啦_

static void of_gpiochip_add_pin_range(struct gpio_chip *chip)
{
	struct device_node *np = chip->of_node;
	struct of_phandle_args pinspec;
	struct pinctrl_dev *pctldev;
	int index = 0, ret;
	const char *name;
	static const char group_names_propname[] = "gpio-ranges-group-names";
	struct property *group_names;

	if (!np)
		return;

	//查找該gpiochip里的gpio-ranges-group-names屬性
	group_names = of_find_property(np, group_names_propname, NULL);

	for (;; index++) {
    	//提取該gpiochip設備樹信息里的gpio-ranges屬性,按3個字段為一組解析,解析后
        //放到pinspec中,這個gpio-ranges可能存在多組,因此用index來控制,一組一組來處理
        //舉個例子:
        //gpio0: gpio@ffc40000 {                                                      
        //    compatible = "renesas,gpio-r8a7778", "renesas,gpio-rcar";               
        //    reg = <0xffc40000 0x2c>;                                                
        //    interrupt-parent = <&gic>;                                              
        //    interrupts = <0 103 IRQ_TYPE_LEVEL_HIGH>;                               
        //    #gpio-cells = <2>;                                                      
        //    gpio-controller;                                                        
        //    gpio-ranges = <&pfc 0 0 32>;                                            
        //    #interrupt-cells = <2>;                                                 
        //    interrupt-controller;                                                   
        //}; 
        //gpio-ranges屬性是gpio子系統規定的屬性,更詳細的信息參考	            
        //Documentation/devicetree/bindings/gpio/gpio.txt        
		ret = of_parse_phandle_with_fixed_args(np, "gpio-ranges", 3,
				index, &pinspec);
		if (ret)
			break;

		//獲取到pinspec.np(也就是pinctrl對應的節點)對應的pctldev
        //注意,這個時候的gpiochip屬於pinctrl的一個client
		pctldev = of_pinctrl_get(pinspec.np);
		if (!pctldev)
			break;

		//這部分的解析也是Documentation/devicetree/bindings/gpio/gpio.txt里有詳細
        //說明的,如果最后一個參數為0,表示代表一個gpio group,否則,第一個參數意思是gpio號起始值
        //第二個參數意思是與之對應的pin號的起始值,最后一個參數表示連續多少各
		if (pinspec.args[2]) {
			if (group_names) {//這里僅僅是校驗,如果不是表示gpio group,那么對應的
            	//gpio-ranges-group-names屬性里對應的字段應該為""
				ret = of_property_read_string_index(np,
						group_names_propname,
						index, &name);
				if (strlen(name)) {
					pr_err("%s: Group name of numeric GPIO ranges must be the empty string.\n",
						np->full_name);
					break;
				}
			}
			/* npins != 0: linear range */
            //到這里說明確定不是gpio group啦,那就將它們加入到pinctrl子系統管理起來吧
            //,這里后面分析pinctrl子系統的時候再回過頭來分析它
            ret = gpiochip_add_pin_range(chip,
					pinctrl_dev_get_devname(pctldev),
					pinspec.args[0],
					pinspec.args[1],
					pinspec.args[2]);
			if (ret)
				break;
		} else {
			/* npins == 0: special range */
            //校驗,為pin gourp時,gpio 子系統要求第一個參數必須要0
			if (pinspec.args[1]) {
				pr_err("%s: Illegal gpio-range format.\n",
					np->full_name);
				break;
			}

			//如果是pin group,那么gpio-ranges-group-names屬性必須要有,通過它來指定
            //那個group
			if (!group_names) {
				pr_err("%s: GPIO group range requested but no %s property.\n",
					np->full_name, group_names_propname);
				break;
			}

			//獲取gpio-ranges-group-names屬性里第index對應的字串(也就是組名)
			ret = of_property_read_string_index(np,
						group_names_propname,
						index, &name);
			if (ret)
				break;

			if (!strlen(name)) {
				pr_err("%s: Group name of GPIO group range cannot be the empty string.\n",
				np->full_name);
				break;
			}

			//一切ok,按group將它們加入到pinctrl子系統管理起來吧,這里后面分析pinctrl子系統的
            //時候再回過頭來分析它
			ret = gpiochip_add_pingroup_range(chip, pctldev,
						pinspec.args[0], name);
			if (ret)
				break;
		}
	}
}

總結一下,gpiochip_add總共做了以下主要事情:

  1. 將chip添加到全局的gpio chip鏈表中,用於gpio chip沖突處理和gpio管理
  2. 將該gpio chip對應的那段gpio都初始化
  3. 初始化設備樹相關的信息,用於后面的屬性解析及向pinctrl子系統同步下
  4. 導出到sys

還有一點需要補充,從這里我們也會發現pinctrl由gpio子系統調用了,驅動工程師以及bsp工程師都不用關心,多好啊!后面會再次看到,大部分都是內核的通用部分代碼處理了pinctrl,驅動工程師以及bsp工程師仍然不用關心,只需要關心設備樹里pinctrl相關的部分,不過我們也得知其所以然啊,不然改動那些和pinctrl相關的總是心理沒底啊!!!和bsp驅動工程師相關的部分就一個函數,沒其他的了。不過准備chip里面的那些字段也夠bsp工程師忙一陣子了吧_

下面開始分析驅動工程師調用的gpio_request的過程,它的核心實現是調用gpiod_requestgpiod_getgpiod_get_index的核心實現處理也調用了gpiod_request,但還做了一些其他事情,如解析gpios或者xxx-gpios屬性獲取設備樹里指定的flags以及通過指定的gpio號獲取到對應的desc(在上面的gpiochip_add過程中,我們有看到了desc的初始化),當然解析的過程會用到gpiochip_add里說過的of_xlate

if (IS_ENABLED(CONFIG_OF) && dev && dev->of_node) {
    dev_dbg(dev, "using device tree for GPIO lookup\n");
    desc = of_find_gpio(dev, con_id, idx, &flags);
} else if (IS_ENABLED(CONFIG_ACPI) && dev && ACPI_HANDLE(dev)) {
    dev_dbg(dev, "using ACPI for GPIO lookup\n");
    desc = acpi_find_gpio(dev, con_id, idx, &flags);
}

以及根據設備樹里指定的flags設置desc,比如是否低電平有效,是否開漏輸出等

if (flags & GPIO_ACTIVE_LOW)
    set_bit(FLAG_ACTIVE_LOW, &desc->flags);
if (flags & GPIO_OPEN_DRAIN)
    set_bit(FLAG_OPEN_DRAIN, &desc->flags);
if (flags & GPIO_OPEN_SOURCE)
    set_bit(FLAG_OPEN_SOURCE, &desc->flags);

gpio_request里面做的事情前面已經說的很清楚了,下面結合代碼再回顧下:

static int gpiod_request(struct gpio_desc *desc, const char *label)
{
	int status = -EPROBE_DEFER;
	struct gpio_chip *chip;

	//檢查desc是否有效,gpio_request會根據傳入的gpio號在全局的desc里定位到desc
    //gpiod_get和gpiod_get_index則是通過解析設備樹信息,提取里面的gpio號,然后再轉換
	if (!desc) {
		pr_warn("%s: invalid GPIO\n", __func__);
		return -EINVAL;
	}

	//獲取該desc的擁有者,即gpio chip(這個初始化在gpiochip_add里已經分析過了)
	chip = desc->chip;
	if (!chip)
		goto done;

	if (try_module_get(chip->owner)) {//增加下chip的引用
		status = __gpiod_request(desc, label);//核心動作都是在__gpiod_request里完成,就不再跟進去分析了^_^
		if (status < 0)
			module_put(chip->owner);
	}

done:
	if (status)
		gpiod_dbg(desc, "%s: status %d\n", __func__, status);

	return status;
}

總結一下,驅動工程師用gpio_request等api請求指定的gpio,這些gpio其實都是由bsp工程師調用gpiochip_add添加的,gpio_request會標記它,防止被不同的模塊重復引用該gpio,當然也會告訴下gpio chip(如果chip想要被通知的話)

2> gpio_to_irq/gpiod_to_irq的過程分析
通過前面的分析,應該對gpio子系統有了一個比較完全的了解吧!其他的api應該也都能理解(猜測到會做什么),這里在對和gpio相關的中斷分析下

gpio_to_irq只是簡單的對gpiod_to_irq進行封裝,主要看gpiod_to_irq

int gpiod_to_irq(const struct gpio_desc *desc)
{
	struct gpio_chip	*chip;
	int			offset;

	if (!desc)
		return -EINVAL;
	chip = desc->chip;
    //獲取該gpio號對應於該chip的offset,由於chip的起始號不一定就開始與系統全局desc的起點
  	//而該chip的處理又都是基於0開始的,所以得轉一下啦
	offset = gpio_chip_hwgpio(desc);
    //調用芯片驅動提供的to_irq,如果chip driver不支持中斷,那么to_irq應該就是空咯,說明不支持
    //從這里應該也清楚的知道了gpio號與中斷號的對應關系是由chip driver處理的
    //驅動工程師用的gpio號都是全局的,bsp工程師用的gpio號都是局部的
	return chip->to_irq ? chip->to_irq(chip, offset) : -ENXIO;
}

還是看一下zynq的to_irq的實現吧!說到這里,會牽涉到中斷子系統(貌似我還沒寫過中斷子系統的文章),如果不太清楚,就跳過吧!

static int zynq_gpio_to_irq(struct gpio_chip *chip, unsigned offset)
{
	return irq_find_mapping(irq_domain, offset);
}

看起來貌似很簡單吧!其實這也要歸功於中斷子系統的功勞啦!一般有中斷控制器功能的設備會在該設備驅動里調用irq_domain_add_xxx接口來注冊一個irq domain,然后會調用irq_create_mapping來創建irq號與硬件號的對應關系,里面會分配期望的irq desc,分配的時候,該中斷控制器對應的中斷號就確定了,然后會綁定該irq號與傳入的硬件號。這就為后面的irq_find_mapping提供了支持,通過硬件號獲取對應的irq號。這里再多說一句,大部分gpio chip同時也是一個中斷控制器,寫bsp的苦逼們不僅要gpiochip_add還要irq_domain_add等等操作,於是乎,gpio子系統解救他們來了,提供了一個接口gpiochip_irqchip_add,這接口完成后和gpio中斷相關的所有事情,於是乎,bsp驅動工程師們也只需要調用兩個接口了_多么美好!

未完,待續!
2015年7月


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM