GPIO子系统自主实现(简单版)

1. GPIO子系统的作用

芯片内部有很多引脚,这些引脚可以接到GPIO模块,也可以接到I2C等模块。

通过Pinctrl子系统来选择引脚的功能(mux function)、配置引脚:

当一个引脚被复用为GPIO功能时,我们可以去设置它的方向、设置/读取它的值。

GPIO名为"General Purpose Input/Output",通用目的输入/输出,就是常用的引脚。

GPIO可能是芯片自带的,也可能通过I2C、SPI接口扩展:

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

1.1 通用功能

  • 可以设为输出:让它输出高低电平;

  • 可以设为输入,读取引脚当前电平;

  • 可以用来触发中断

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

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

1.2 通用属性

  • Active-High and Active-Low

    以LED为例,需要设置GPIO电平。但是有些电路可能是高电平点亮LED,有些是低电平点亮LED。

    可以使用如下代码:

    复制代码
    gpiod_set_value(gpio, 1);  // 输出高电平点亮LED
    gpiod_set_value(gpio, 0);  // 输出低电平点亮LED

    对应同一个目标:点亮LED,对于不同的LED,就需要不同的代码,原因在于上面的代码中1、0表示的是"物理值"。

    如果能使用"逻辑值",同样的逻辑值在不同的配置下输出对应的物理值,就可以保持代码一致,比如:

    复制代码
    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:引脚被设置为高电平时才会驱动电路

1.3 GPIO子系统的作用

管理GPIO,既能支持芯片本身的GPIO,也能支持扩展的GPIO。

提供统一的、简便的访问接口,实现:输入、输出、中断。

2 GPIO子系统重要概念

2.1 引入

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

然后就可以根据设置引脚方向(输入还是输出)、读值──获得电平状态,写值──输出高低电平。

以前我们通过寄存器来操作GPIO引脚,即使LED驱动程序,对于不同的板子它的代码也完全不同。

当BSP工程师实现了GPIO子系统后,我们就可以:

a. 在设备树里指定GPIO引脚

b. 在驱动代码中:

使用GPIO子系统的标准函数获得GPIO、设置GPIO方向、读取/设置GPIO值。

这样的驱动代码,将是单板无关的。

2.2 在设备树中指定引脚

在几乎所有ARM芯片中,GPIO都分为几组,每组中有若干个引脚。所以在使用GPIO子系统之前,就要先确定:它是哪组的?组里的哪一个?

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

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

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

cpp 复制代码
gpio-controller;
#gpio-cells = <2>;
"gpio-controller"表示这个节点是一个GPIO Controller,它下面有很多引脚。
"#gpio-cells = <2>"表示这个控制器下每一个引脚要用2个32位的数(cell)来描述。

为什么要用2个数?其实使用多个cell来描述一个引脚,这是GPIO Controller自己决定的。比如可以用其中一个cell来表示那是哪一个引脚,用另一个cell来表示它是高电平有效还是低电平有效,甚至还可以用更多的cell来示其他特性。

普遍的用法是,用第1个cell来表示哪一个引脚,用第2个cell来表示有效电平:

cpp 复制代码
GPIO_ACTIVE_HIGH : 高电平有效
GPIO_ACTIVE_LOW  :  低电平有效

定义GPIO Controller是芯片厂家的事,我们怎么引用某个引脚呢?在自己的设备节点中使用属性"[<name>-]gpios",示例如下:

上图中,可以使用gpios属性,也可以使用name-gpios属性。

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

在设备树中指定了GPIO引脚,在驱动代码中如何使用?

也就是GPIO子系统的接口函数是什么?

GPIO子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀"gpiod_",它使用gpio_desc结构体来表示一个引脚;后者的函数都有前缀"gpio_",它使用一个整数来表示一个引脚。

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

驱动程序中要包含头文件,

cpp 复制代码
#include <linux/gpio/consumer.h>   // descriptor-based
或
#include <linux/gpio.h>            // legacy

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

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

建议使用"devm_"版本的相关函数。

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

cpp 复制代码
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>;
	};

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

cpp 复制代码
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时会确定它的"base number",那么这个控制器里的第n号引脚的号码就是:base number + n。

但是如果硬件有变化、设备树有变化,这个base number并不能保证是固定的,应该查看sysfs来确定base number。

2.4 sysfs中的访问方法_IMX6ULL

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

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

方法如下:

① 先在开发板的/sys/class/gpio目录下,找到各个gpiochipXXX目录:

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

③ 根据label的内容对比设备树

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

下图是在100asK_imx6ull上运行的结果,通过对比设备树可知gpiochip96对应gpio4:

所以gpio4这组引脚的基准引脚号就是96,这也可以"cat base"来再次确认。

b. 基于sysfs操作引脚:

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

那么GPIO4_14的号码是96+14=110,可以如下操作读取按键值:

cpp 复制代码
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

注意:如果驱动程序已经使用了该引脚,那么将会export失败,会提示下面的错误:

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

cpp 复制代码
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

2.5 sysfs中的访问方法_STM32MP157

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

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

方法如下:

① 先在开发板的/sys/class/gpio目录下,找到各个gpiochipXXX目录:

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

③ 根据label的内容就知道它是哪组引脚

下图是在100ask_stm32mp157上运行的结果,可知gpiochip96对应GPIOG:

所以GPIOG这组引脚的基准引脚号就是96,这也可以"cat base"来再次确认。

b. 基于sysfs操作引脚:

以100ask_stm32mp157为例,它有一个按键,原理图如下:

那么PG2的号码是96+2=98,可以如下操作读取按键值:

cpp 复制代码
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

注意:如果驱动程序已经使用了该引脚,那么将会export失败,会提示下面的错误:

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

cpp 复制代码
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

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

3.1 编写思路

GPIO的地位跟其他模块,比如I2C、UART的地方是一样的,要使用某个引脚,需要先把引脚配置为GPIO功能,这要使用Pinctrl子系统,只需要在设备树里指定就可以。在驱动代码上不需要我们做任何事情。

GPIO本身需要确定引脚,这也需要在设备树里指定。

设备树节点会被内核转换为platform_device。

对应的,驱动代码中要注册一个platform_driver,在probe函数中:获得引脚、注册file_operations。

在file_operations中:设置方向、读值/写值。

下图就是一个设备树的例子:

3.2 在设备树中添加Pinctrl信息

有些芯片提供了设备树生成工具,在GUI界面中选择引脚功能和配置信息,就可以自动生成Pinctrl子结点。把它复制到你的设备树文件中,再在client device结点中引用就可以。

有些芯片只提供文档,那就去阅读文档,一般在内核源码目录Documentation\devicetree\bindings\pinctrl下面,保存有该厂家的文档。

如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录arch/arm/boot/dts目录下。

最后一步,网络搜索。

Pinctrl子节点的样式如下:

3.3 在设备树中添加GPIO信息

先查看电路原理图确定所用引脚,再在设备树中指定:添加"[name]-gpios"属性,指定使用的是哪一个GPIO Controller里的哪一个引脚,还有其他Flag信息,比如GPIO_ACTIVE_LOW等。具体需要多少个cell来描述一个引脚,需要查看设备树中这个GPIO Controller节点里的"#gpio-cells"属性值,也可以查看内核文档。

示例如下:

3.4 编程示例

在实际操作过程中也许会碰到意外的问题,现场演示如何解决。

a. 定义、注册一个platform_driver

b. 在它的probe函数里:

b.1 根据platform_device的设备树信息确定GPIO:gpiod_get

b.2 定义、注册一个file_operations结构体

b.3 在file_operarions中使用GPIO子系统的函数操作GPIO:

gpiod_direction_output、gpiod_set_value

摘录重点内容:

a. 注册platform_driver

注意下面第122行的"100ask,leddrv",它会跟设备树中节点的compatible对应:

cpp 复制代码
121 static const struct of_device_id ask100_leds[] = {
122     { .compatible = "100ask,leddrv" },
123     { },
124 };
125
126 /* 1. 定义platform_driver */
127 static struct platform_driver chip_demo_gpio_driver = {
128     .probe      = chip_demo_gpio_probe,
129     .remove     = chip_demo_gpio_remove,
130     .driver     = {
131         .name   = "100ask_led",
132         .of_match_table = ask100_leds,
133     },
134 };
135
136 /* 2. 在入口函数注册platform_driver */
137 static int __init led_init(void)
138 {
139     int err;
140
141     printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
142
143     err = platform_driver_register(&chip_demo_gpio_driver);
144
145     return err;
146 }

b. 在probe函数中获得GPIO

核心代码是第87行,它从该设备(对应设备树中的设备节点)获取名为"led"的引脚。在设备树中,必定有一属性名为"led-gpios"或"led-gpio"。

cpp 复制代码
77 /* 4. 从platform_device获得GPIO
78  *    把file_operations结构体告诉内核:注册驱动程序
79  */
80 static int chip_demo_gpio_probe(struct platform_device *pdev)
81 {
82      //int err;
83
84      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
85
86      /* 4.1 设备树中定义有: led-gpios=<...>; */
87     led_gpio = gpiod_get(&pdev->dev, "led", 0);
88      if (IS_ERR(led_gpio)) {
89              dev_err(&pdev->dev, "Failed to get GPIO for led\n");
90              return PTR_ERR(led_gpio);
91      }
92

c. 注册file_operations结构体:

这是老套路了:

cpp 复制代码
93      /* 4.2 注册file_operations      */
94      major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */
95
96      led_class = class_create(THIS_MODULE, "100ask_led_class");
97      if (IS_ERR(led_class)) {
98              printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
99              unregister_chrdev(major, "led");
100             gpiod_put(led_gpio);
101             return PTR_ERR(led_class);
102     }
103
104     device_create(led_class, NULL, MKDEV(major, 0), NULL, "100ask_led%d", 0); /* /dev/100ask_led0 */
105

d. 在open函数中调用GPIO函数设置引脚方向:

cpp 复制代码
51 static int led_drv_open (struct inode *node, struct file *file)
52 {
53      //int minor = iminor(node);
54
55      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
56      /* 根据次设备号初始化LED */
57      gpiod_direction_output(led_gpio, 0);
58
59      return 0;
60 }

e. 在write函数中调用GPIO函数设置引脚值:

cpp 复制代码
34 /* write(fd, &val, 1); */
35 static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
36 {
37      int err;
38      char status;
39      //struct inode *inode = file_inode(file);
40      //int minor = iminor(inode);
41
42      printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
43      err = copy_from_user(&status, buf, 1);
44
45      /* 根据次设备号和status控制LED */
46      gpiod_set_value(led_gpio, status);
47
48      return 1;
49 }

f. 释放GPIO:

cpp 复制代码
gpiod_put(led_gpio);

4.在100ASK_IMX6ULL上机实验

a. Pinctrl信息:

cpp 复制代码
&iomuxc_snvs {
......
    imx6ul-evk {    
    myled_for_gpio_subsys: myled_for_gpio_subsys{ 
            fsl,pins = <
                MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x000110A0
            >;
        };
......
}

b. 设备节点信息(放在根节点下):

cpp 复制代码
myled {
            compatible = "100ask,leddrv";
            pinctrl-names = "default";
            pinctrl-0 = <&myled_for_gpio_subsys>;
            led-gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
        };

为避免引脚冲突,还要修改arch/arm/boot/dts/100ask_imx6ull-14x14.dts,在leds节点中如下增加status属性,禁止它:

cpp 复制代码
leds {
        compatible = "gpio-leds";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_leds>;

        status = "disabled";

        led0: cpu {
            label = "cpu";
            gpios = <&gpio5 3 GPIO_ACTIVE_LOW>;
            default-state = "on";
            linux,default-trigger = "heartbeat";
        };
    };

5.GPIO子系统层次与数据结构

1. GPIO子系统的层次

1.1 层次

1.2 GPIOLIB向上提供的接口

gpiod_direction_input、gpiod_direction_output等

1.3 GPIOLIB向下提供的接口

2. 重要的3个核心数据结构

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

  • 一个GPIO Controller里有多少个引脚?有哪些引脚?

  • 需要提供函数,设置引脚方向、读取/设置数值

  • 需要提供函数,把引脚转换为中断

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

  • GPIO引脚信息

  • 控制引脚的函数

  • 中断相关的函数

2.1 gpio_device

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

  • 里面每一个gpio引脚用一个gpio_desc来表示

  • gpio引脚的函数(引脚控制、中断相关),都放在gpio_chip里

2.2 gpio_chip

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

  • 控制引脚的函数

  • 中断相关的函数

  • 引脚信息:支持多少个引脚?各个引脚的名字?

2.3 gpio_desc

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

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

在gpio_device中有一个gpio_desc数组,每一引脚有一项gpio_desc。

3. 怎么编写GPIO Controller驱动程序

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

6.IMX6ULL的GPIO驱动源码分析

1. 设备树

Linux-4.9.88\arch\arm\boot\dts\imx6ull.dtsi:

cpp 复制代码
aliases {
		can0 = &flexcan1;
		can1 = &flexcan2;
		ethernet0 = &fec1;
		ethernet1 = &fec2;
		gpio0 = &gpio1;
};

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控制器的设备树中,有两项是必须的:

  • gpio-controller : 表明这是一个GPIO控制器

  • gpio-cells : 指定使用多少个cell(就是整数)来描述一个引脚

当解析设备节点中的GPIO信息时,需要用到上面的属性。

比如下面的led-gpios,在#gpio-cells = <2>的情况下,它表示的引脚数量是1。

cpp 复制代码
myled {
            compatible = "100ask,leddrv";
            led-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
        };

2. 驱动程序

Linux-4.9.88\drivers\gpio\gpio-mxc.c

2.1 分配gpio_chip

cpp 复制代码
static int mxc_gpio_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	struct mxc_gpio_port *port;
	struct resource *iores;
	int irq_base = 0;
	int err;

	mxc_gpio_get_hw(pdev);

	port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
	if (!port)
		return -ENOMEM;

2.2 设置gpio_chip

2.3 注册gpio_chip

cpp 复制代码
err = devm_gpiochip_add_data(&pdev->dev, &port->gc, port);
	if (err)
		goto out_bgio;

7.编写一个虚拟GPIO控制器的驱动程序

1. 硬件功能

假设这个虚拟的GPIO Controller有4个引脚:

2. 编写设备树文件

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

3. 编写驱动程序

核心:分配/设置/注册一个gpio_chip结构体。

cpp 复制代码
#include <linux/module.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/mfd/syscon.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_address.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
#include <linux/slab.h>
#include <linux/regmap.h>

static struct gpio_chip * g_virt_gpio;
static int g_gpio_val = 0;

static const struct of_device_id virtual_gpio_of_match[] = {
	{ .compatible = "100ask,virtual_gpio", },
	{ },
};

static int virt_gpio_direction_output(struct gpio_chip *gc,
		unsigned offset, int val)
{
	printk("set pin %d as output %s\n", offset, val ? "high" : "low");
	return 0;
}

static int virt_gpio_direction_input(struct gpio_chip *chip,
					unsigned offset)
{
	printk("set pin %d as input\n", offset);
	return 0;
}


static int virt_gpio_get_value(struct gpio_chip *gc, unsigned offset)
{
	int val;
	val = (g_gpio_val & (1<<offset)) ? 1 : 0;
	printk("get pin %d, it's val = %d\n", offset, val);
	return val;
}

static void virt_gpio_set_value(struct gpio_chip *gc,
		unsigned offset, int val)
{
	printk("set pin %d as %d\n", offset, val);
	if (val)
		g_gpio_val |= (1 << offset);
	else
		g_gpio_val &= ~(1 << offset);
}

static int virtual_gpio_probe(struct platform_device *pdev)
{
	int ret;
	unsigned int val;
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	/* 1. 分配gpio_chip */
	g_virt_gpio = devm_kzalloc(&pdev->dev, sizeof(*g_virt_gpio), GFP_KERNEL);
	
	/* 2. 设置gpio_chip */
	
	/* 2.1 设置函数 */
	g_virt_gpio->label = pdev->name;
	g_virt_gpio->direction_output = virt_gpio_direction_output;
	g_virt_gpio->direction_input  = virt_gpio_direction_input;
	g_virt_gpio->get = virt_gpio_get_value;
	g_virt_gpio->set = virt_gpio_set_value;
	
	g_virt_gpio->parent = &pdev->dev;
	g_virt_gpio->owner = THIS_MODULE;
	
	/* 2.2 设置base、ngpio值 */
	g_virt_gpio->base = -1;
	ret = of_property_read_u32(pdev->dev.of_node, "ngpios", &val);
	g_virt_gpio->ngpio = val;
	
	/* 3. 注册gpio_chip */
	ret = devm_gpiochip_add_data(&pdev->dev, g_virt_gpio, NULL);
	
	return 0;
}
static int virtual_gpio_remove(struct platform_device *pdev)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}


static struct platform_driver virtual_gpio_driver = {
	.probe		= virtual_gpio_probe,
	.remove		= virtual_gpio_remove,
	.driver		= {
		.name	= "100ask_virtual_gpio",
		.of_match_table = of_match_ptr(virtual_gpio_of_match),
	}
};


/* 1. 入口函数 */
static int __init virtual_gpio_init(void)
{	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1.1 注册一个platform_driver */
	return platform_driver_register(&virtual_gpio_driver);
}


/* 2. 出口函数 */
static void __exit virtual_gpio_exit(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 2.1 反注册platform_driver */
	platform_driver_unregister(&virtual_gpio_driver);
}

module_init(virtual_gpio_init);
module_exit(virtual_gpio_exit);

MODULE_LICENSE("GPL");

设备树文件

cpp 复制代码
/ {
    gpio_virt: virtual_gpiocontroller {
        compatible = "100ask,virtual_gpio";
        gpio-controller;
        #gpio-cells = <2>;
        ngpios = <4>;
    };

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

8.GPIO子系统与Pinctrl子系统的交互

1. 使用GPIO前应该设置Pinctrl

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

要使用pinA来控制LED,首先要通过Pinctrl子系统把它设置为GPIO功能,然后才能设置它为输出引脚、设置它的输出值。

所以在设备树文件里,应该添加Pinctrl的内容:

cpp 复制代码
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功能了。

2. GPIO和Pinctrl的映射关系

2.1 示例

从上图可知:

  • 左边的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功能

2.2 数据结构

3. GPIO调用Pinctrl的过程

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

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

调用关系如下:

cpp 复制代码
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驱动程序时,所设置chip->request函数,一般直接调用gpiochip_generic_request,它导致Pinctrl把引脚复用为GPIO功能。

cpp 复制代码
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功能:

cpp 复制代码
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;
}

3. 我们要做什么

如果不想在使用GPIO引脚时,在设备树中设置Pinctrl信息,

如果想让GPIO和Pinctrl之间建立联系,

我们需要做这些事情:

3.1 表明GPIO和Pinctrl间的联系

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

  • GPIO系统中有引脚号

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

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

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

cpp 复制代码
// 当前GPIO控制器的0号引脚, 对应pinctrlA中的128号引脚, 数量为12
gpio-ranges = <&pinctrlA 0 128 12>; 

3.2 解析这些联系

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

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

  • 注册gpio_chip时会自动调用

cpp 复制代码
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]);

3.3 编程

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

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

cpp 复制代码
#include <linux/module.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/io.h>
#include <linux/mfd/syscon.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_address.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio/driver.h>
#include <linux/slab.h>
#include <linux/regmap.h>

static struct gpio_chip * g_virt_gpio;
static int g_gpio_val = 0;

static const struct of_device_id virtual_gpio_of_match[] = {
	{ .compatible = "100ask,virtual_gpio", },
	{ },
};

static int virt_gpio_direction_output(struct gpio_chip *gc,
		unsigned offset, int val)
{
	printk("set pin %d as output %s\n", offset, val ? "high" : "low");
	return 0;
}

static int virt_gpio_direction_input(struct gpio_chip *chip,
					unsigned offset)
{
	printk("set pin %d as input\n", offset);
	return 0;
}


static int virt_gpio_get_value(struct gpio_chip *gc, unsigned offset)
{
	int val;
	val = (g_gpio_val & (1<<offset)) ? 1 : 0;
	printk("get pin %d, it's val = %d\n", offset, val);
	return val;
}

static void virt_gpio_set_value(struct gpio_chip *gc,
		unsigned offset, int val)
{
	printk("set pin %d as %d\n", offset, val);
	if (val)
		g_gpio_val |= (1 << offset);
	else
		g_gpio_val &= ~(1 << offset);
}

static int virtual_gpio_probe(struct platform_device *pdev)
{
	int ret;
	unsigned int val;
	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	/* 1. 分配gpio_chip */
	g_virt_gpio = devm_kzalloc(&pdev->dev, sizeof(*g_virt_gpio), GFP_KERNEL);
	
	/* 2. 设置gpio_chip */
	
	/* 2.1 设置函数 */
	g_virt_gpio->label = pdev->name;
	g_virt_gpio->direction_output = virt_gpio_direction_output;
	g_virt_gpio->direction_input  = virt_gpio_direction_input;
	g_virt_gpio->get = virt_gpio_get_value;
	g_virt_gpio->set = virt_gpio_set_value;
	g_virt_gpio->request = gpiochip_generic_request;
	
	g_virt_gpio->parent = &pdev->dev;
	g_virt_gpio->owner = THIS_MODULE;
	
	/* 2.2 设置base、ngpio值 */
	g_virt_gpio->base = -1;
	ret = of_property_read_u32(pdev->dev.of_node, "ngpios", &val);
	g_virt_gpio->ngpio = val;
	
	/* 3. 注册gpio_chip */
	ret = devm_gpiochip_add_data(&pdev->dev, g_virt_gpio, NULL);
	
	return 0;
}
static int virtual_gpio_remove(struct platform_device *pdev)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}


static struct platform_driver virtual_gpio_driver = {
	.probe		= virtual_gpio_probe,
	.remove		= virtual_gpio_remove,
	.driver		= {
		.name	= "100ask_virtual_gpio",
		.of_match_table = of_match_ptr(virtual_gpio_of_match),
	}
};


/* 1. 入口函数 */
static int __init virtual_gpio_init(void)
{	
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1.1 注册一个platform_driver */
	return platform_driver_register(&virtual_gpio_driver);
}


/* 2. 出口函数 */
static void __exit virtual_gpio_exit(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 2.1 反注册platform_driver */
	platform_driver_unregister(&virtual_gpio_driver);
}

module_init(virtual_gpio_init);
module_exit(virtual_gpio_exit);

MODULE_LICENSE("GPL");
相关推荐
李永奉1 小时前
51单片机-实现定时器模块教程
单片机·嵌入式硬件·51单片机
青草地溪水旁2 小时前
`lock()` 和 `unlock()` 线程同步函数
linux·c++·c
2401_858286112 小时前
OS26.【Linux】进程程序替换(下)
linux·运维·服务器·开发语言·算法·exec·进程
IT永勇2 小时前
STM32高级定时器-输出比较模式
stm32·单片机·嵌入式开发·pwm输出·高级定时器
lkf197112 小时前
centos安装jenkins
linux·centos·jenkins
花小璇学linux3 小时前
imx6ull-驱动开发篇41——Linux RTC 驱动实验
linux·驱动开发·嵌入式软件
秦jh_3 小时前
【MySQL】基本查询
linux·数据库·c++·mysql
刃神太酷啦3 小时前
Linux 常用指令全解析:从基础操作到系统管理(1w字精简版)----《Hello Linux!》(2)
linux·运维·服务器·c语言·c++·算法·leetcode
什么半岛铁盒4 小时前
处理端口和 IP 地址
网络·单片机·tcp/ip