一、GPIO子系统的作用

GPIO子系统既能支持芯片本身的GPIO,也能支持扩展的GPIO。提供统一、简便的访问接口,实现:输入、输出、中断。

芯片内部有很多引脚,这些引脚可以接到GPIO模块,也可以接到I2C等模块。通过Pinctrl子系统来选择引脚的功能(mux function)、配置引脚:

01_pinctrl_hardware_block

当一个引脚被复用为GPIO功能时,我们可以去设置它的方向、设置/读取它的值。GPIO名为”General Purpose Input/Output”,通用目的输入/输出,就是常用的引脚。GPIO可能是芯片自带的,也可能通过I2C、SPI接口扩展:

02_gpio_block

GPIO有一些通用功能、通用属性。

通用功能:

  • 可以设为输出:让它输出高低电平;
  • 可以设为输入,读取引脚当前电平;
  • 可以用来触发中断

对于芯片自带的GPIO,它的访问时很快的,可以在获得spinlocks的情况下操作它。

但是,对于通过I2C、SPI等接口扩展的GPIO,访问它们时可能导致休眠,所以这些”GPIO Expander”就不能在获得spinlocks的情况下使用。

通用属性:

  • Active-High and Active-Low

    以LED为例,需要设置GPIO电平。但是有些电路可能是高电平点亮LED,有些是低电平点亮LED。可以使用如下代码:

1
2
gpiod_set_value(gpio, 1);  // 输出高电平点亮LED
gpiod_set_value(gpio, 0); // 输出低电平点亮LED

对应同一个目标:点亮LED,对于不同的LED,就需要不同的代码,原因在于上面的代码中1、0表示的是”物理值”。如果能使用”逻辑值”,同样的逻辑值在不同的配置下输出对应的物理值,就可以保持代码一致,比如:

1
2
3
gpiod_set_value(gpio, 1);  // 输出逻辑1
// 在Active-High的情况下它会输出高电平
// 在Active-Low的情况下它会输出低电平
  • Open Drain and Open Source

    有多个GPIO驱动同时驱动一个电路时,就需要设置Open Drain或Open Source。

    • Open Drain:引脚被设置为低电平时才会驱动电路,典型场景是I2C接口。
    • Open Source:引脚被设置为高电平时才会驱动电路

二、GPIO子系统层次

2.1 整体层次

03_gpio_system_level

03_gpio_system_level2

2.2 GPIOLIB向上提供的接口

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

2.3 GPIOLIB向下提供的接口

04_gpiochip_add_data

三、核心数据结构

整体框架:

05_data_struct

记住GPIO Controller的要素,这有助于理解它的驱动程序:

  • 一个GPIO Controller里有多少个引脚?有哪些引脚?
  • 需要提供函数,设置引脚方向、读取/设置数值
  • 需要提供函数,把引脚转换为中断

以Linux面向对象编程的思想,一个GPIO Controller必定会使用一个结构体来表示,这个结构体必定含有这些信息:

  • GPIO引脚信息
  • 控制引脚的函数
  • 中断相关的函数

3.1 gpio_device

每个GPIO Controller用一个gpio_device来表示:

  • 里面每一个gpio引脚用一个gpio_desc来表示
  • gpio引脚的函数(引脚控制、中断相关),都放在gpio_chip里

05_gpio_device

3.2 gpio_chip

我们并不需要自己创建gpio_device,编写BSP驱动时要创建的是gpio_chip,里面提供了:

  • 控制引脚的函数
  • 中断相关的函数
  • 引脚信息:支持多少个引脚?各个引脚的名字?

06_gpio_chip

3.3 gpio_desc

我们去使用GPIO子系统时,首先是获得某个引脚对应的gpio_desc。

gpio_device表示一个GPIO Controller,里面支持多个GPIO引脚。

gpio_device中有一个gpio_desc数组,每个引脚都被抽象成gpio_desc。

07_gpio_desc

3.4 gpio_chip相关函数的实现机制

08_gpiod_set_value

偷一张图:

08_gpiod_set_value2

四、 GPIO子系统驱动程序流程

分配、设置、注册gpioc_chip结构体,示例:drivers\gpio\gpio-74x164.c

08_gpio_controller_driver_example

imx6ull的gpio子系统的整体流程:

09_imx_gpio_controller_driver_example

五、 GPIO子系统与Pinctrl子系统的交互

5.1 使用GPIO前应先设置Pinctrl

假设使用这个虚拟的GPIO Controller的pinA来控制LED:

10_virtual_gpio_led

要使用pinA来控制LED,首先要通过Pinctrl子系统把它设置为GPIO功能,然后才能使用GPIO子系统设置它为输出引脚、设置它的输出值。所以在设备树文件里,应该添加Pinctrl的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
virtual_pincontroller {
compatible = "100ask,virtual_pinctrl";
myled_pin: myled_pin {
functions = "gpio";
groups = "pin0";
configs = <0x11223344>;
};
};

gpio_virt: virtual_gpiocontroller {
compatible = "100ask,virtual_gpio";
gpio-controller;
#gpio-cells = <2>;
ngpios = <4>;
};

myled {
compatible = "100ask,leddrv";
led-gpios = <&gpio_virt 0 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&myled_pin>;
};

但是很多芯片,并不要求在设备树中把把引脚复用为GPIO功能。

比如STM32MP157,在它的设备树工具STM32CubeMX即使把引脚配置为GPIO功能,它也不会在设备树中出现。

原因在于:GPIO走了后门。

现实的芯片中,并没有Pinctrl这样的硬件,它的功能大部分是在GPIO模块中实现的。

Pinctrl是一个软件虚拟处理的概念,它的实现本来就跟GPIO密切相关。

甚至一些引脚默认就是GPIO功能。

按理说:

一个引脚可能被用作GPIO,也可能被用作I2C,GPIO和I2C这些功能时相同低位的。

要用作GPIO,需要先通过Pinctrl把引脚复用为GPIO功能。

但是Pinctrl和GPIO关系密切,当你使用gpiod_get获得GPIO引脚时,它就偷偷地通过Pinctrl把引脚复用为GPIO功能了。

5.2 GPIO和Pinctrl的映射关系

11_gpio_and_pinctrl_range

从上图可知:

  • 左边的Pinctrl支持8个引脚,在Pinctrl的内部编号为0~7

  • 图中有2个GPIO控制器

    • GPIO0内部引脚编号为0~3,假设在GPIO子系统中全局编号为100 ~ 103
    • GPIO1内部引脚编号为0~3,假设在GPIO子系统中全局编号为104 ~ 107
  • 假设我们要使用pin1_1,应该这样做:

    • 根据GPIO1的内部编号1,可以换算为Pinctrl子系统中的编号5
    • 使用Pinctrl的函数,把第5个引脚配置为GPIO功能

对应数据结构:

12_gpio_pin_range

5.3 GPIO间接调用Pinctrl的过程

GPIO子系统中的request函数,用来申请某个GPIO引脚,

它会导致Pinctrl子系统中的这2个函数之一被调用:pmxops->gpio_request_enablepmxops->request

13_gpio_request

调用关系如下:

1
2
3
4
5
6
7
8
gpiod_get
gpiod_get_index
desc = of_find_gpio(dev, con_id, idx, &lookupflags);
ret = gpiod_request(desc, con_id ? con_id : devname);
ret = gpiod_request_commit(desc, label);
if (chip->request) {
ret = chip->request(chip, offset);
}

我们编写GPIO controller驱动程序时,所设置chip->request的函数,一般直接赋值为gpiochip_generic_request,它导致Pinctrl把引脚复用为GPIO功能。

1
2
3
4
5
6
7
8
9
gpiochip_generic_request(struct gpio_chip *chip, unsigned offset)
pinctrl_request_gpio(chip->gpiodev->base + offset)
ret = pinctrl_get_device_gpio_range(gpio, &pctldev, &range); // gpio是引脚的全局编号

/* Convert to the pin controllers number space */
pin = gpio_to_pin(range, gpio);

ret = pinmux_request_gpio(pctldev, range, pin, gpio);
ret = pin_request(pctldev, pin, owner, range);

Pinctrl子系统中的pin_request函数就会把引脚配置为GPIO功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int pin_request(struct pinctrl_dev *pctldev, int pin, const char *owner, struct pinctrl_gpio_range *gpio_range)
{
const struct pinmux_ops *ops = pctldev->desc->pmxops;

/*
* If there is no kind of request function for the pin we just assume
* we got it by default and proceed.
*/
if (gpio_range && ops->gpio_request_enable)
/* This requests and enables a single GPIO pin */
status = ops->gpio_request_enable(pctldev, gpio_range, pin);
else if (ops->request)
status = ops->request(pctldev, pin);
else
status = 0;
}

5.4 如何实现GPIO间接调用Pinctrl

如果不想在使用GPIO引脚时在设备树中设置Pinctrl信息,就要让GPIO和Pinctrl之间建立联系,我们需要做这些事情:

5.4.1 表明GPIO和Pinctrl间的联系

在GPIO controller设备树中使用gpio-ranges来描述它们之间的联系:

  • GPIO系统中有引脚号

  • Pinctrl子系统中也有自己的引脚号

  • 2个号码要建立映射关系

  • 在GPIO设备树中使用如下代码建立映射关系

1
2
// 当前GPIOA控制器的0号引脚, 对应pinctrlA中的128号引脚, 数量为12
gpio-ranges = <&pinctrlA 0 128 12>;

5.4.2 解析这些联系

在GPIO驱动程序中,解析跟Pinctrl之间的联系:处理gpio-ranges:

  • 这不需要我们自己写代码

  • 注册gpio_chip时会自动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int gpiochip_add_data(struct gpio_chip *chip, void *data)
status = of_gpiochip_add(chip);
status = of_gpiochip_add_pin_range(chip);

of_gpiochip_add_pin_range
for (;; index++) {
ret = of_parse_phandle_with_fixed_args(np, "gpio-ranges", 3, index, &pinspec);

pctldev = of_pinctrl_get(pinspec.np); // 根据gpio-ranges的第1个参数找到pctldev

// 增加映射关系
/* npins != 0: linear range */
ret = gpiochip_add_pin_range(chip,
pinctrl_dev_get_devname(pctldev),
pinspec.args[0],
pinspec.args[1],
pinspec.args[2]);

5.4.2 编程

  • 在GPIO驱动程序中, 提供:gpio_chip->request

  • 在Pinctrl驱动程序中,提供:pmxops->gpio_request_enablepmxops->request

  • gpiod_get -> gpio_chip->request -> pmxops->request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pinctrl_virt: virtual_pincontroller {
compatible = "100ask,virtual_pinctrl";
};

gpio_virt: virtual_gpiocontroller {
compatible = "100ask,virtual_gpio";
gpio-controller;
#gpio-cells = <2>;
ngpios = <4>;
gpio-ranges = <&pinctrl_virt 0 0 4>;
};

myled {
compatible = "100ask,leddrv";
led-gpios = <&gpio_virt 0 GPIO_ACTIVE_LOW>;
};