文章目录
- Pinctrl 子系统重要概念
- 概述
- 重要概念
- pin controller:
- client device:
- 代码中怎么引用 pinctrl
- GPIO 子系统重要概念
- 概述
- 在设备树中指定引脚
- 在驱动代码中调用 GPIO 子系统
- 头文件
- 常用函数
- 实例:
BSP工程师针对芯片的寄存器写Pinctrl子系统,驱动工程师使用Pinctrl 子系统
Pinctrl 子系统重要概念
概述
无论是哪种芯片,都有类似图的结构, IOMUX 认为是引脚功能选择器,有时候还需要配置引脚,比如上拉、下拉、开漏等等。前面提到的这些操作都可以交给Pinctrl 子系统完成。即把引脚的复用、配置抽出来,做成 Pinctrl 子系统,给 GPIO、I2C 等模块使用
ps:大多数的芯片,没有单独的 IOMUX 模块,即没有pinctl子系统去管理引脚的复用、配置等等,这些功能都在GPIO 模块内部实现的,所以在硬件上 GPIO 和 Pinctrl 是密切相关,在软件上它们的关系也非常密切
重要概念
涉及 2 个对象:pin controller、client device。
- 前者提供服务:可以用它来复用引脚、配置引脚。
- 后者使用服务:声明自己要使用哪些引脚的哪些功能,怎么配置它们。
pin controller:
在芯片手册里你找不到 pin controller,它是一个软件上的概念,你可以认为它对应 IOMUX──用来复用引脚,还可以配置引脚(比如上下拉电阻等)
注意,pin controller 和 GPIO Controller 不是一回事,前者控制的引脚可用于 GPIO 功能、I2C 功能;后者只是把引脚配置为输入、输出等简单的功能。即
先用 pin controller 把引脚配置为 GPIO,再用 GPIO Controler 把引脚配置为输入或输出
。
client device:
Pinctrl 系统的客户,那就是使用 Pinctrl 系统的设备,使用引脚的设备。它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚
关系图,右边device是设备树的一个节点,它是Pinctrl 系统的客户
首先是介绍一个概念引脚的状态:pin state
:
- 对于一个“client device”来说,比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,如默认状态下,UART 设备是工作的,那么所用的引脚就要复用为 UART功能。在休眠状态下,为了省电,可以把这些引脚复用为 GPIO 功能;或者直接把它们配置输出高电平。上图中,pinctrl-names 里定义了 2 种状态:default、sleep。
某种状态下对应的引脚配置有如下关联:
- 第 0 种状态用到的引脚在 pinctrl-0 中定义,它是 state_0_node_a,位于 pincontroller 节点中。当这个设备处于 default 状态时,pinctrl 子系统会自动根据上述信息
把所用引脚复用为 uart0 功能
。 - 第 1 种状态用到的引脚在 pinctrl-1 中定义,它是 state_1_node_a,位于 pincontroller 节点中。当这这个设备处于 sleep 状态时,pinctrl 子系统会自动根据上述信息
把所用引脚配置为高电平
。
左图 pin controller 节点中,是给client device 使用的。
- 可以用来描述复用信息:哪组(group)引脚复用为哪个功能(function);
- 可以用来描述配置信息:哪组(group)引脚配置为哪个设置功能(setting),比如上拉、下拉等。
注意:pin controller 节点的格式,没有统一的标准,每家芯片都不一样。甚至上面的 group、function关键字也不一定有,但是概念是有的。
代码中怎么引用 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); // 不再使用, 退出时调用
GPIO 子系统重要概念
概述
ps:大多数的芯片,没有单独的 IOMUX 模块,即没有pinctl子系统去管理引脚的复用、配置等等,这些功能都在GPIO 模块内部实现的,所以在硬件上 GPIO 和 Pinctrl 是密切相关,在软件上它们的关系也非常密切
通过 Pinctrl 子系统,先把所用引脚配置为 GPIO 功能,然后就可以根据设置引脚方向(输入还是输出)、读值──获得电平状态,
写值──输出高低电平。
当 BSP 工程师实现了 GPIO 子系统后,我们就可以:
在设备树里指定 GPIO 引脚
- 在驱动代码中:
使用 GPIO 子系统的标准函数
获得 GPIO、设置 GPIO 方向、读取/设置 GPIO 值。
这样的驱动代码,将是单板无关的,更换单板只需要修改设备树文件,指定GPIO引脚
在设备树中指定引脚
指定引脚就要先确定:它是哪组的?组里的哪一个?
在设备树中,“GPIO 组”就是一个 GPIO Controller,这通常都由芯片厂家设置好
。我们要做的是找到它名字,一般都是厂家定
义好,在 xxx.dtsi 文件中:该节点中有关键属性gpio-controller
和#gpio-cells
- “gpio-controller”表示这个节点是一个 GPIO Controller,它下面有很多引脚。
- “#gpio-cells = <2>”表示这个控制器下每一个引脚要用 2 个 32 位的数(cell)来描述。普遍的用法是,用第 1 个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平:
GPIO_ACTIVE_HIGH : 高电平有效
GPIO_ACTIVE_LOW : 低电平有效
在自己的设备节点中使用属性"[<name>-]gpios
",引用某个引脚,可以使用 gpios
属性,也可以使用 name-gpios
属性,示例如下:
name:描述符,在GPIO标准函数中作为参数
在驱动代码中调用 GPIO 子系统
GPIO 子系统有两套接口:
- 基于描述符的(descriptor-based)。函数都有前缀“gpiod_”,它使用 gpio_desc 结构体来表示一个引脚;
- 老的(legacy)。函数都有前缀“gpio_”,它使用一个整数来表示一个引脚。
头文件
#include <linux/gpio/consumer.h> // descriptor-based
或
#include <linux/gpio.h> // legacy
常用函数
descriptor-based | legacy |
---|---|
获得 GPIO | |
gpiod_get | gpio_request |
gpiod_get_index | |
gpiod_get_array | gpio_request_array |
devm_gpiod_get | |
devm_gpiod_get_index | |
devm_gpiod_get_array | |
设置方向 | |
gpiod_direction_input | gpio_direction_input |
gpiod_direction_output | gpio_direction_output |
读值、写值 | |
gpiod_get_value | gpio_get_value |
gpiod_set_value | gpio_set_value |
释放 GPIO | |
gpio_free | gpio_free |
gpiod_put | gpio_free_array |
gpiod_put_array | |
devm_gpiod_put | |
devm_gpiod_put_array |
建议使用“devm_”版本的相关函数
- 有前缀“devm”的含义是“设备资源管理” (Managed DeviceResource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的GPIO 资源。
实例:
假设备在设备树中有如下节点
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 时会确定它的“basenumber”,那么这个控制器里的第 n 号引脚的号码就是:base number + n。但是如果硬件有变化、设备树有变化,这个 base number 并不能保证是固定的,应该查看 sysfs 虚拟文件系统中来确定 base number。
先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipXXX 目录,每个目录对应一个GPIO控制器,然后进入某个 gpiochip 目录,查看文件 label 的内容,根据 label 的内容对比设备树,label 内容来自设备树,比如它的寄存器基地址。用来跟设备树(dtsi 文件)比较,就可以知道这对应哪一个 GPIO Controller。
例如,下图是在 stm32mp157 上运行的结果,通过对比设备树可知 gpiochip112对应 gpioH,所以 gpioH 这组引脚的基准引脚号就是 112,cat base 指令得到的也是基准引脚,cat ngpio得到的是这组引脚里面有多少个引脚
打开ubuntu,进入内核的设备树文件目录,cd /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4/arch/arm64/boot/dts,打开设备树文件:vi stm32mp15xxx-100ask.dtsi,根据label值找到对应的那组寄存器
如图,如果想要操作PG2引脚,可知该引脚属于GPIOG那一组,通过设备树和控制器的对比,找到该组的引脚的基准引脚号,然后在该基准引脚号的基础上加2得到该引脚的引脚号
那么,我们可以在终端输入如下指令操作该引脚:
将引脚暴露在/sys/class/gpio 目录下
echo 98 > /sys/class/gpio/export
修改方向位输入
echo in > /sys/class/gpio/gpio98/direction
获取引脚值
cat /sys/class/gpio/gpio98/value
销毁该引脚控制器
echo 98 > /sys/class/gpio/unexport
对于输出引脚,我们可以调用如下指令使其输入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