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,那我们的驱动程序如何引用某个引脚呢?
可以在自己的设备节点中使用属性"[
// 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中,设置方向、读/写值;
小结
-
GPIO子系统:设备树里指定GPIO引脚;提供一套C代码,用于驱动程序中设置GPIO模块内功能。
PinCtrl子系统:通过设备树,用于引脚功能的选择,类似于IOMUX。PinCtrl子系统是系统根据设备树文件(配置)自动完成的,通常无需C驱动程序员参与(引用pinctrl)。
另外,芯片厂家通常也会提供类似于NXP i.MX Pins Tool v6这样的工具,用于配置引脚功能,生成dts文件内容。 -
PinCtrl可以为node定义多个state,每个state对应若干引脚。状态切换时,引脚的配置也是自动完成的。
-
sysfs可用于在线调试GPIO功能,无需编写代码。