0%

Pinctrl和GPIO子系统

1. GPIO 和 Pinctrl 子系统的使用

在 LED 驱动程序中,我们使用寄存器操作的方式来配置硬件,这种方式是非常低效的,对于具有大量寄存器的芯片肯定需要将寄存器操作,可以由芯片厂的BSP工程师来封装成库,然后来让驱动开发人员进行调用,这就是今天引入的 GPIO 和 Pinctrl 子系统。

1.1 Pinctrl 子系统

参考文档:内核目录\Documentation\devicetree\bindings\pinctrl\pinctrl-bindings.txt

Pinctrl 子系统涉及 2 个对象:pin controller、client device。

  • pin controller 提供服务,可以用它来复用引脚、配置引脚。
  • client device 使用服务,声明自己要使用哪些引脚的哪些功能,怎么配置它们。

1.1.1 pin controller

pin controller 是一个软件上的概念,用来复用引脚,还可以配置引脚(比如上下拉电阻等)。

pin controller 和后面讲到的 GPIO Controller 不是一回事,GPIO Controller 只是把引脚配置为输入、输出等简单的功能。

1.1.2 client device

Pinctrl 系统的客户,就是使用引脚的设备,它在设备树里会被定义为一个节点,在节点里声明要用哪些引脚。例如:

image-20240102204442724

上图中,左边是 pin controller 节点,右边是 client device 节点.

(1) pin state

比如对于一个 UART 设备,它有多个“状态”:default、sleep 等,那对应的引脚在设备处于不同状态时也需要配置为不同的模式。例如UART默认状态两个引脚需要配置为输出+输入模式,在休眠模式下节约功耗则可以配置为均输出高电平。

(2) groups 和 function

一个设备会用到一个或多个引脚,这些引脚就可以归为一组(group);

这些引脚可以复用为某个功能:function。

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

(3) Generic pin multiplexing node 和 Generic pin configuration node

在上图左边的 pin controller 节点中,一些其他属性可以描述不同的引脚信息,例如,哪组 (group) 引脚配置为哪个设置功能 (setting),比如上拉、下拉等。

pin controller 节点的格式,没有统一的标准。每家芯片都不一样。 甚至上面的 group、function 关键字也不一定有,但是概念是有的。

Pinctrl 子系统举例:

image-20240102210056127

1.1.3 代码中怎么引用 pinctrl

基本上驱动程序不用管,当设备切换状态时,对应的 pinctrl 就会被自动调用来切换引脚状态。

非要调用,也有函数:

1
2
devm_pinctrl_get_select_default(struct device *dev); // 使用"default"状态的引脚
pinctrl_get_select(struct device *dev, const char *name); // 根据 name 选择某种状态的引脚

1.2 GPIO 子系统

要操作 GPIO 引脚,先把所用引脚配置为 GPIO 功能,这通过 Pinctrl 子系统来实现。

然后就可以根据设置引脚方向(输入还是输出),获取引脚电平(读),设置引脚电平(写),这通过 GPIO 子系统来实现。

1.2.1 在设备树中指定引脚

几乎所有 ARM 芯片中,GPIO 都分为几组,每组中有若干个引脚。

所以在使用 GPIO 子系统之前,就要先确定引脚的组别和组内序号

在设备树中,“GPIO组”就是一个 GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。

下图是一些芯片的 GPIO 控制器节点,它们一般都是厂家定义好,在 xxx.dtsi 文件中:

image-20240102211315432

暂时只需要关心里面的这 2 个属性:

1
2
gpio-controller; 
#gpio-cells = <2>;
  • gpio-controller 表示这个节点是一个 GPIO Controller,它下面有很多引脚。
  • #gpio-cells = <2> 表示这个控制器下每一个引脚要用 2 个 32 位的数 (cell)来描述。具体用几个数以及如何表示都可以自己决定;普遍的用法是,用第 1 个 cell 来表示哪一个引脚,用第 2 个 cell 来表示有效电平
1
2
GPIO_ACTIVE_HIGH: 高电平有效 
GPIO_ACTIVE_LOW : 低电平有效

片厂来定义 GPIO Controller ,我们如果需要修改设备树文件,只需要引用他们规定的引脚即可:

image-20240102212214670

例如上图中,在设备树文件,我们定义的设备节点中直接引用 “gpios” 或者 “xxx-gpios” 属性即可

1.2.2 在驱动代码中调用 GPIO 子系统

GPIO 子系统有两套接口:

  1. 基于描述符 - descriptor-based。函数都有前缀 gpiod_,它使用 gpio_desc 结构体来表示 一个引脚;
  2. 过时的接口 - legacy。函数都有前缀 gpio_,它使用一个整数来表示一个引脚。

要操作一个引脚,首先要 get 引脚,然后设置方向,读值、写值。

(1) 常用函数

需包含头文件:

1
2
3
#include <linux/gpio/consumer.h> // descriptor-based
or
#include <linux/gpio.h> // legacy

image-20240102213415783

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

比如在 Linux 开发过程中,先申请了 GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放 GPIO 资源。如果使用 devm 的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的 GPIO 资源。建议使用devm_版本的相关函数。

(2) 使用 descriptor-based 方式获得引脚

假设备在设备树中有如下节点:

1
2
3
4
5
6
7
8
9
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>;
};

那么可以使用下面的函数获得引脚:

1
2
3
4
5
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 设置的值是“逻辑值”,不一定等于物理值。之所以使用逻辑值,是为了保持功能代码不变。以LED不同的驱动电路为例,见下图:

image-20240103114637359

(3) 使用老的方式获得引脚

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

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

  1. 先在开发板的/sys/class/gpio 目录下,找到各个 gpiochipxxx 目录:

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

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

    image-20240103104344564

    从图中可以知道 GPIO Controller (gpio4) 对应的基准引脚号是 96,则对于引脚 “GPIO4_IO14”的引脚号为 96 + 14 = 110,可以按照如下操作读取引脚的值:

    1
    2
    3
    4
    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

    按照如下操作设置引脚的值:

    1
    2
    3
    4
    echo 110 > /sys/class/gpio/export
    echo out > /sys/class/gpio/gpio110/direction
    echo 1 > /sys/class/gpio/gpio110/value
    echo 110 > /sys/class/gpio/unexport

如果驱动程序已经使用了该引脚,那么将会 export 失败,会提示:write error: Device orr resource busy

2. 基于 Pinctrl 和 GPIO 子系统编写LED驱动

通过使用 Pinctrl 和 GPIO 子系统封装了硬件操作中大量的寄存器设备,使得驱动代码和芯片种类解绑,大大简化了驱动代码的编写。

2.1 基于 Pinctrl 子系统修改设备树

  1. 有些芯片提供了设备树生成工具,在 GUI 界面中选择引脚功能和配置信息, 就可以自动生成 Pinctrl 子结点。把它复制到你的设备树文件中,再在 client device 结点中引用就可以。
  2. 有些芯片只提供文档,那就去阅读文档,一般在内核源码目录 Documentation\devicetree\bindings\pinctrl 下面,保存有该厂家的文档。
  3. 如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录 arch/arm/boot/dts 目录下。

IMX6ULL提供了GUI工具,可以在GUI工具中配置引脚的复用和焊盘的相关参数:

image-20240103202925506

将生成的 Pinctrl 子节点添加到 dts 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
&iomuxc_snvs {
pinctrl-names = "default_snvs";
pinctrl-0 = <&pinctrl_hog_2>;

imx6ull-board {
pinctrl-0 = <&pinctrl_krocz_led>;
pinctrl_krocz_led: krocz_ledgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03 0x000110E0
>;
};
};

imx6ul-evk {
pinctrl_hog_2: hoggrp-2 {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x1b0b0 /* enet1 reset */
MX6ULL_PAD_SNVS_TAMPER6__GPIO5_IO06 0x1b0b0 /* enet2 reset */
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x000110A0 /*key 1*/
>;
};
...
};
};

PS:我尝试将 pinctrl_krocz_led 节点直接放置在 &iomuxc_snvs 节点下,但是重启开发板之后发现网络连接不上;通过串口连上开发板之后,装载内核驱动模块时报错找不着引脚,不过感觉语法上应该没有问题。之后,将 pinctrl_krocz_led 放置在原有的 imx6ul-evk 或者像GUI生成的那样放在 imx6ull-board 下是可以的,没有问题。

在 dts 中新加入一个设备节点:

1
2
3
4
5
6
krocz_led {
compatible = "krocz,leddrv"; // 和 platform_driver 进行匹配
pinctrl-names = "default"; // 该设备节点可能的状态
pinctrl-0 = <&pinctrl_krocz_led>; // 第一种状态使用的 Pinctrl 配置
myled-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; // 设备节点所使用的引脚
};

通过上述代码,我们基于 Pinctrl 子系统设置了 LED 引脚,以及引脚复用功能为 GPIO,并且配置了引脚的相关属性,如上拉电阻阻值,驱动能力,引脚速率等。

基于 GPIO 子系统设置了 LED 引脚,以及该引脚逻辑电平和物理电平的关系,并可以通过 GPIO 子系统来设置该引脚的方向,读写引脚值等。

2.2 通用LED驱动代码

代码的主要部分:

  1. 注册 file_operations
  2. 内核模块的 init 和 exit
  3. platform_driver 的 probe 和 remove

2.2.1 编写 file_operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
char status;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(&status, buf, 1);
gpiod_set_value(led_gpio, status);
return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
gpiod_direction_output(led_gpio, 0); // 默认写逻辑0,熄灭LED灯
return 0;
}

static struct file_operations led_drv = {
.owner = THIS_MODULE,
.open = led_drv_open,
.write = led_drv_write,
};

基于 Pinctrl 和 GPIO 子系统将芯片寄存器操作封装,使得我们的驱动代码和芯片类型无关,从而使得仅这一个LED驱动代码就可以在任意芯片上通用。在不同的芯片上仅需要修改对应的设备树文件,不需要修改驱动代码文件。

2.2.2 内核模块注册

1
2
3
4
5
6
7
8
9
10
11
12
13
static int __init led_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&chip_demo_gpio_driver);
return err;
}

static void __exit led_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&chip_demo_gpio_driver);
}

主要是注册 platform_driver

2.2.3 设备驱动的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

/* 4.1 设备树中定义 myled-gpios=<...>; */
/* device结构体 char*的功能名称 用以GPIO初始化的flags,0表示高电平有效 */
led_gpio = gpiod_get(&pdev->dev, "myled", 0);
if (IS_ERR(led_gpio)) {
dev_err(&pdev->dev, "Failed to get GPIO for led\n");
return PTR_ERR(led_gpio);
}

major = register_chrdev(0, "krocz_led", &led_drv); /* /dev/led */
led_class = class_create(THIS_MODULE, "krocz_led_class");
if (IS_ERR(led_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "krocz_led");
gpiod_put(led_gpio);
return PTR_ERR(led_class);
}
device_create(led_class, NULL, MKDEV(major, 0), NULL, "krocz_led%d", 0);
return 0;

}

static int chip_demo_gpio_remove(struct platform_device *pdev)
{
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
unregister_chrdev(major, "krocz_led");
gpiod_put(led_gpio);

return 0;
}

主要获取 GPIO 引脚,注册 file_operations 结构体,创建设备文件等。