Linux GPIO子系统和PinCtrl子系统

基本概念

PinCtrl:Pin Controller,是一个虚拟概念,用于设置IOMUX,让某个引脚连接到指定模块,从而实现某个功能。不同于GPIO子系统,可用于GPIO功能、I2C功能等。
GPIO子系统:配置引脚输入、输出功能,设置方向等GPIO模块内功能。

不过,大多数的芯片并没有单独的IOMUX模块,引脚的复用、配置等,而是在GPIO模块内部实现的。


PinCtrl子系统

涉及2个对象:pin controller、client device。

  • pin controller 用它来复用引脚、配置引脚。

pin controller不存在于芯片手册,是一个软件上的概念,可认为它对应于IOMUX,用于复用引脚、配置引脚(如上下拉电阻等)。pin controller不同于GPIO Controller,可以先用pin controller将引脚配置为GPIO,再用GPIO Controller把引脚配置为输入或输出。

  • client device 声明要用哪些引脚的哪些功能,怎么配置。所谓“客户设备”,客户是指Pinctrl系统的客户,即使用Pinctrl系统的设备,使用引脚的设备。在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。

下面这张图把几个重要概念综合到一起:

注意:并不是说dts中一定会存在pincontroller、device这2个node,这里只是作为例子表明概念,其名字可能是其他。

左边pin controller节点,右边client device节点。
1)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节点中。

当UART设备处于default状态时,pinctrl子系统会自动根据上述信息将所用引脚复用为uart0功能。
当UART设备处于sleep状态时,pinctrl子系统会自动根据上述信息将所用引脚配置为高电平。

2)groups和function
一个设备会用到一个或多个引脚,这些引脚可以归纳为一组(group);
这些引脚可以复用为某个功能:function,如I2C功能,SPI功能,GPIO功能等。

一个设备可以用的多组引脚,如A1、A2两组引脚,A1组复用为F1功能,A2组复用为F2功能。

3)Generic pin multiplexing node和Generic pin configuration node
上图左边pin controller节点中,有子节点或孙节点,它们是给client device使用的。
可用来描述复用信息:哪组(group)引脚复用为哪个功能(function);
配置信息:哪组(group)引脚配置为哪个设置功能(setting),如上拉、下拉等;

注意:pin controller节点格式,没有统一格式,每家芯片都不一样。可能group、function关键字也不一样,但都有这样的概念。
client device节点格式都是类似的。

IMX6ULL pin controller

pinctrl_uart1: uart1grp {                /*!< Function assigned for  the core: Cortex-A7[ca7] */
    fsl,pins = <
        MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX      0x000010B0
        MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX      0x000010B0
    >;
};

对应client device

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

代码中引用pinctrl

这部分是透明的,驱动通常不用管。当设备切换状态时,独有的pinctrl就会被调用。比如,在platform_device和platform_driver的枚举过程中,流程如下:

really_probe:
/* If using pinctrl, bind pins now before probing */
ret = pinctrl_bind_pins(dev); // 1 引脚被设置为某个状态, 不用我们手动调用代码
    dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT); /* 获得"default"状态的pinctrl */
    dev->pins->init_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_INIT); /* 获得"init"状态的pinctrl */
ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state); /* 优先设置"init"状态的引脚 */
ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state); /* 如果没有init状态, 则设置"default"状态的引脚 */

...
ret = drv->probe(dev); // 2 调用我们的代码

也就是说,当系统休眠时,会自动其设置该设备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);   // 不再使用, 退出时调用

GPIO子系统

要操作GPIO引脚,得先把所用到的引脚配置为GPIO功能,这需要通过Pinctrl子系统实现。然后,就可以根据设置引脚方向(输入 or 输出)、读值(获取引脚电平状态)、写值(输出高or第电平)。

在裸机编程中,通过寄存器来操作GPIO引脚,不同的板子驱动代码不一样。
而BSP工程师实现了GPIO子系统后,我们可以:
在设备树里指定GPIO引脚;
在驱动代码中:
使用GPIO子系统的标准函数获得GPIO、设置GPIO方向、读取/设置GPIO值。
这样的驱动代码,跟板子无关。

在设备树中指定引脚

GPIO组号、组内编号 => 引脚
几乎所有ARM芯片中,GPIO都分为几组,每组都有若干引脚。所以在使用GPIO子系统前,先确定它是哪组的,然后是组内哪一个引脚。

在设备树中,“GPIO组”就是一个GPIO Controller,通常由芯片厂家设置好。驱动程序员要做的是找到它的名字,如"gpio1",然后指定用它里面那个引脚,比如<&gpio1 0>,表示使用GPIO1_0(GPIO第1组0号引脚)。

从imx6ull设备树文件(100ask_imx6ull-14x14.dts)的头文件imx6ul.dtsi(NXP原厂提供),截取下面关于gpio1和gpio2(2组gpio)的设备树描述。

// imx6ul.dtsi, 芯片级dts文件, 位于Linux 4.9.88 源码目录 arch/arm/boot/dts
...
gpio1: gpio@0209c000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x0209c000 0x4000>;
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
             <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    gpio-ranges = <&iomuxc    0 23 10>, <&iomuxc 10 17 6>,
              <&iomuxc 16 33 16>;
};

gpio2: gpio@020a0000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x020a0000 0x4000>;
    interrupts = <GIC_SPI 68 IRQ_TYPE_LEVEL_HIGH>,
             <GIC_SPI 69 IRQ_TYPE_LEVEL_HIGH>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    gpio-ranges = <&iomuxc 0 49 16>, <&iomuxc 16 111 6>;
};
...

我们暂时只需要关心这2个属性:

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

"gpio-controller" 表示这个节点是一个GPIO Controller,它的下面有很多引脚。
"#gpio-cells = <2>" 表示这个控制器下每个引脚都要用2个32位的数(cell)来描述。
具体用几个数,是GPIO Controller自己决定的。通常是2个,当然,可以用更多cell表示其他特性。

通常用法:用2个数(cell),第一个cell表示哪个引脚,第二个cell表示是高电平还是低电平有效。
第2个cell含义:

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

芯片厂家提供的dts文件定义了GPIO Controller,那我们的驱动程序如何引用某个引脚呢?
可以在自己的设备节点中使用属性"[-]gpios",示例如下:

// 100ask_imx6ull-14x14.dts, 板级dts文件, 位于Linux 4.9.88 源码目录 arch/arm/boot/dts

led0: cpu {
    label = "cpu";
    gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; // 属性gpios值为gpio5第3号引脚, 低电平有效
    default-state = "on";
    linux,default-trigger = "heartbeat";
};
gt9xx@5d {
        compatible = "goodix,gt9xx";
        reg = <0x5d>;
        status = "okay";
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_tsc_reset &pinctrl_touchscreen_int>;
        /*pinctrl-1 = <&pinctrl_tsc_irq>;*/
        /*pinctrl-names = "default", "int-output-low", "int-output-high", "int-input";
        pinctrl-0 = <&ts_int_default>;
        pinctrl-1 = <&ts_int_output_low>;
        pinctrl-2 = <&ts_int_output_high>;
        pinctrl-3 = <&ts_int_input>;
        */
        reset-gpios = <&gpio5 2 GPIO_ACTIVE_LOW>;
        irq-gpios = <&gpio1 5 IRQ_TYPE_EDGE_FALLING>;
        irq-flags = <2>;                /*1:rising 2: falling*/
...
};

通过gpios属性,name-gpios属性(如reset-gpios,irq-gpios)的值设为gpio-controller对应的node。

驱动代码中调用GPIO子系统

设备树中可以通过GPIO子系统指定GPIO的配置,那驱动代码中如何使用呢?

GPIO子系统有两套接口:
1)基于描述符(descriptor-based)的,函数前缀"gpiod_",使用gpio_desc结构体来表示一个引脚;

2)基于老(legacy)的,函数前缀"gpio_",使用一个整数来表示一个引脚。

操作一个GPIO引脚,要先get(获取)引脚,然后设置方向、读值、写值。

下面是2套接口简要说明:

  • descriptor-based
#include <linux/gpio/consumer.h> //descriptor-based

// 获得GPIO
gpiod_get
gpiod_get_index
gpiod_get_array
devm_gpiod_get
devm_gpiod_get_index
devm_gpiod_get_array

// 设置方向
gpiod_direction_input
gpiod_direction_output

// 读值、写值
gpiod_get_value
gpiod_set_value

// 释放GPIO
gpio_free
gpiod_put
gpiod_put_array
devm_gpiod_put
devm_gpiod_put_array
  • legacy
#include <linux/gpio.h>

// 获得GPIO
gpio_request
gpio_request_array

// 设置方向
gpio_direction_input
gpio_direction_output

// 读值、写值
gpio_get_value
gpio_set_value

// 释放GPIO
gpio_free
gpio_free_array

前缀"devm_" 含义是“设备资源管理(Managed Device Resource)”,这是一种自动释放资源的机制。其思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。

比如,在Linux开发过程中,先申请了GPIO,再申请内存;如果内存申请失败,那么在返回前就需要先释放GPIO资源。如果内存申请失败时,可以直接返回,因为设备的销毁函数会自动地释放已经申请了的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 *read, *green, *blue, *power;

red = gpiod_get_index(dev, "led", 0 GPIOD_OUT_HIGH); /* "led"是对应设备树文件中name-gpios中的name */
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_ACTIVE_LOW, 那么gpiod_set_value 的逻辑值跟引脚的物理值相反
gpiod_set_value(dec, 0); // 输出高电平
gpiod_set_value(dec, 1); // 输出低电平

// 如果设备树里引脚没有指定GPIO_ACTIVE_LOW, 或者指定为GPIO_ACTIVE_HIGH
gpiod_set_value(dec, 0); // 输出低电平
gpiod_set_value(dec, 1); // 输出高电平

旧的"gpio_" 函数没办法根据设备树信息获得引脚,它需要先知道引脚号。

引脚号如何确定?
GPIO子系统中,每注册一个GPIO Controller时,会确定它的"base number",那么控制器里的第n号引脚的号码就是:base number + n。
但如果硬件有变化、设备树有变化,该base number并不能保证是固定的,应该查看sysfs来确定base number。

sysfs的访问方法

值sysfs中访问GPIO,实际上用到就是引脚号,老的方法。

a)先确定某个GPIO Controller的基准引脚号(base number),再计算出某个引脚的号码。

方法如下:
(1)先在开发板的/sys/class/gpio目录下,找到各个gpiochipXXX目录:

# cd /sys/class/gpio
# ls
export       gpiochip0    gpiochip128  gpiochip32   gpiochip64   gpiochip96   unexport

(2)然后进入某个gpiochip目录,查看文件label的内容

(3)根据label的内容对比设备树
label内容来自设备树,比如它的寄存器基地址。用来跟设备树(dtsi文件)比较,就可以指定这对应哪个GPIO Controller。

下面是100ask_imx6ull 运行结果,对比设备树可知gpiochip96对应gpio4:

# cd gpiochip96
# ls
base       device     label      ngpio      power      subsystem  uevent
# cat label
20a8000.gpio
// imx6ull.dtsi
gpio4: gpio@020a8000 { // gpio4地址就是20a8000
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x020a8000 0x4000>;

因此,gpio4这组引脚的基准引脚号就是96,这也可以用"cat base"命令来确认:

# pwd
/sys/class/gpio/gpiochip96
# cat base
96

b)基于sysfs操作引脚

以100ask_imx6ull为例,它有一个按键,原理图:

那么GPIO4_14号码96+14=110,可以如下操作读取按键值(输入引脚):

# echo 110 > /sys/class/gpio/export          // 将gpio110配置为gpio
# echo in > /sys/class/gpio/gpio110/direction // 设置方向
# cat /sys/class/gpio/gpio110/value           // 读取gpio110输入电平
# echo 110 > /sys/class/gpio/unexport        // 取消gpio110配置

注:如果驱动程序已经使用了该引脚,那么将会export失败。

对于输出引脚,假设引脚号为N,可以用下面方法设置它的值为1:

# echo  N > /sys/class/gpio/export             // 将gpioN配置为gpio
# echo out > /sys/class/gpio/gpio110/direction // 设置方向
# echo 1 > /sys/class/gpio/gpioN/value         // 设置引脚输出值
# echo N > /sys/class/gpio/unexport            // 取消gpio110配置

基于GPIO子系统的LED驱动程序

思路:
1)通过Pinctrl子系统,在设备树中将引脚配置为GPIO功能;
2)设备树节点被内核自动转换为platform_device,需要node的compatibe属性与驱动程序的platform_driver的driver列表的某一项of_match_table的compatible相同;
3)也就是说,需要注册一个platform_driver;
4)在platform_driver的probe函数中,利用GPIO子系统的接口函数获取引脚(gpio_desc),注册文件操作接口file_operations;
5)在file_operations中,设置方向、读/写值;


小结

  1. GPIO子系统:设备树里指定GPIO引脚;提供一套C代码,用于驱动程序中设置GPIO模块内功能。
    PinCtrl子系统:通过设备树,用于引脚功能的选择,类似于IOMUX。PinCtrl子系统是系统根据设备树文件(配置)自动完成的,通常无需C驱动程序员参与(引用pinctrl)。
    另外,芯片厂家通常也会提供类似于NXP i.MX Pins Tool v6这样的工具,用于配置引脚功能,生成dts文件内容。

  2. PinCtrl可以为node定义多个state,每个state对应若干引脚。状态切换时,引脚的配置也是自动完成的。

  3. sysfs可用于在线调试GPIO功能,无需编写代码。


参考

http://www.100ask.org/

posted @ 2022-07-14 08:45  明明1109  阅读(1831)  评论(0编辑  收藏  举报