Linux内核GPIO子系统分析

概述

Linux内核的GPIO子系统通过gpiolib来实现,gpiolib始于2.6.24版本,这里是gpiolib的初始提交信息,这里是gpiolib的初始代码

下面是子系统的架构图:
在这里插入图片描述
gpiolib向上为使用gpio的设备驱动程序提供了统一的接口,向下为SoC的gpio控制器提供注册到系统的接口。

gpiolib为驱动程序提供的服务包括:

  • 系统中GPIO信息的管理,比如有多少个GPIO,每个GPIO的编号是什么等;
  • GPIO的申请、释放;
  • IO的输入、输出方向的设置;IO电平的输出或者输入设置;以及GPIO与中断号的相互转换;
  • DTS中关于GPIO相关的配置信息的解析;
  • gpio系统与sysfs文件系统的交互;
  • gpio系统与debugfs文件系统的交互等。

gpiolib为SoC芯片的GPIO控制器提供的服务包括:

  • 将GPIO控制器抽象为gpio_chip,并提供接口将gpio_chip注册到系统中;
  • gpio_chip抽象了关于GPIO执行申请、释放、方向设置、IO电平输出等接口,特定SoC芯片的GPIO控制器驱动程序需要实现这些接口,从而使设备驱动程序可以正常使用gpio;

3.12.0版本之后,社区对于GPIO子系统进行了重构,内核对于gpio的管理从基于gpio num的方式,修改为基于“opaque handlers”的方式,下面是gpiolib重构时代码提交信息

  • gpio works with integers, whereas gpiod operates on opaque handlers
    which cannot be forged or used before proper acquisition
  • gpiod get/set functions are aware of the active low state of a GPIO
  • gpio consumers should now include <linux/gpio/consumer.h> to access
    the new interface, whereas chips drivers will use
    <linux/gpio/driver.h>

这段提交信息中,说到了两种gpiolib子系统的工作原理,以及重构后,gpio消费者通过“<include/linux/gpio/consumer.h>”使用新版的gpio配置接口,gpio控制器芯片通过“<include/linux/gpio/driver.h>”使用新版的gpio控制器配置接口。

为了兼容之前的gpio相关的设备驱动,内核保留了重构之前的gpio接口,接口声明位于”<include/linux/gpio.h>“,该文件的开头的注释部分也说明了该文件中定义的接口已经被遗弃,该文件的作用是为了兼容之前的设备驱动程序, 对于新的驱动程序请使用“<incldue/linux/gpio/consumer.h>”中的接口。

This is the LEGACY GPIO bulk include file, including legacy APIs. It is
used for GPIO drivers still referencing the global GPIO numberspace,
and should not be included in new code.

If you're implementing a GPIO driver, only include <linux/gpio/driver.h>
If you're implementing a GPIO consumer, only include <linux/gpio/consumer.h>

see Documentation/driver-api/gpio/legacy.rst。

架构实现

gpiolib通过gpio_chip结构抽象了所有对于GPIO的操作,gpio chip驱动程序,实现这些抽象接口,并将gpio_chip注册到gpiolib子系统中。gpio驱动程序首先通过gpio申请接口,申请成功后,返回gpio的句柄,之后,通过这个句柄完成对于gpio的各种操作。下图表示gpio驱动程序如何通过gpiolib框架完成对于特定gpiochip的访问控制的。
在这里插入图片描述
gpiolib子系统经过多年的发展,其对于gpio的管理方式产生了两种不同的机制,一种是基于gpio num的方式,另一种是基于gpio_desc的描述符形式。前一种机制由于种种问题,现在已经被废弃,但为了向后兼容,内核对于该机制进行了保留,但对于新的gpio驱动程序,内核强烈建议使用新版的机制。

下面的章节会介绍在编写gpio相关的驱动程序时常用到的数据结构和接口,其分为两部分:旧架构和新架构。旧架构主要讲述gpiolib重构之前的使用方式;新架构主要讲述gpiolib重构之后的使用方式。每部分都会分为四个小部分:数据结构、APIs、示例。本文所介绍的内核代码版本为5.8

旧架构

需要声明的是,下面所说的gpio操作机制,已经在3.12.0版本的内核之后,被列为历史遗迹,在新的gpio相关驱动中不推荐使用。include/linux/gpio.h在4.17.0版本中,再次强调该文件中的接口已经成为遗产
对于gpio驱动,使用<linux/gpio/consumer.h>中的接口,对于gpio chip驱动,使用<linux/gpio/driver.h>中的接口。

This is the LEGACY GPIO bulk include file, including legacy APIs. It is
used for GPIO drivers still referencing the global GPIO numberspace,
and should not be included in new code.

If you're implementing a GPIO driver, only include <linux/gpio/driver.h>
If you're implementing a GPIO consumer, only include <linux/gpio/consumer.h>

内核需要配置CONFIG_GPIOLIB选项,用来启用gpiolib子系统。

数据结构

对于gpio相关的驱动来说,没有比较重要的数据结构,所有关于gpio的操作都时基于一个gpio num,这里就不涉及数据结构了。

APIs

当内核启用CONFIG_GPIOLIB之后,驱动程序就可以使用gpio相关的接口完成gpio相关的操作。对于gpio相关的操作一般分为下面几类:

获取gpio num
	int of_get_named_gpio_flags(struct device_node *np,
										const char *list_name, int index, enum of_gpio_flags *flags);

	int of_get_gpio_flags(struct device_node *np, int index,enum of_gpio_flags *flags);
	
	int of_get_named_gpio(struct device_node *np, const char *propname, int index);

	int of_get_gpio(struct device_node *np, int index);

上面是获取设备节点中gpio相关配置信息的api函数。下面举一个例子,来说明一下各个api的使用方式。

假如,有一个驱动程序gpio-drv,其dts配置信息如下:

	gpio-dmeo{
		compatible = "gpio-drv";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_gpio0>;

		gpios = <0
		         &gpio1 1 2
		         0
		         &gpio2 3 4>;
	}

上面的设备dts配置中的,gpios字段定义了驱动程序使用到的gpios,其包括四个gpio,其中,索引为0、2的gpio没有配置,下面我们分别使用上面提到的api获取一下gpios的配置信息。

比如,我们想获得第二个gpio的配置信息,我们可以使用如下方式:

	of_get_named_gpio_flags(np, "gpios", 1, &flags); //注意,gpio的索引值从0开始,gpios对应于dts中的gpios
	
	of_get_gpio_flags(np, 1, &flags); //注意,该api只适用于存在单个gpio配置信息的情况下,例如,本地中只有gpios一个gpios配置信息。

	of_get_named_gpio(np, "gpios", 1);

	of_get_gpio(np, 1);//此api与of_get_gpio_flags的注意事项一样。
获取gpio数量
	int of_gpio_named_count(struct device_node *np, const char* propname);

	int of_gpio_count(struct device_node *np);

这两个api用于获取设备驱动相关的gpios的数量,例如,上一节的dts配置,

	of_gpio_named_count(np, "gpios");//返回4
	of_gpio_count(np);//返回4
申请/释放gpio num

有了gpio num之后,在正式使用gpio之前需要申请,使用完之后还要释放。

	int gpio_request(unsigned gpio, const char *label);
	int gpio_request_one(unsigned gpio,
					unsigned long flags, const char *label);

	int gpio_request_array(const struct gpio *array, size_t num);
	void gpio_free(unsigned gpio);
	void gpio_free_array(const struct gpio *array, size_t num);
	
	int devm_gpio_request(struct device *dev, unsigned gpio,
			    const char *label);

	int devm_gpio_request_one(struct device *dev, unsigned gpio,
				unsigned long flags, const char *label);

	void devm_gpio_free(struct device *dev, unsigned int gpio);

这里需要注意就是, 如果申请成功,返回值为0,反之失败,可以通过errno分析失败的原因。

设置gpio方向

gpio可以设置为输出或者输入,具体的设置api如下:

	int gpio_direction_input(unsigned gpio);
	int gpio_direction_output(unsigned gpio, int value);
设置gpio输出值

gpio如果配置成输出模式,那么可以通过如下api控制gpio的输出值。

	void gpio_set_value(unsigned gpio, int value);
获取gpio当前值

gpio如果配置成输入模式,那么可以通过如下api读取gpio的值。

	int gpio_get_value(unsigned gpio);

注意,gpio为输出模式时,也能通过该API获取当前IO值。
获取gpio对应的irq num

一般情况下,每个gpio都可以作为单独的中断源,每个gpio num对应一个具体的irq num,通过下面的api可以将一个gpio转换为irq。

	int gpio_to_irq(unsigned gpio);

同时,使用irq_to_gpio可以将irq转换为对应的gpio num。

	int irq_to_gpio(unsigned irq);

示例

假如,有一个驱动程序gpio-drv,其dts配置信息如下:

	gpio-dmeo{
		compatible = "gpio-drv";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_gpio0>;

		gpios = <0
		         &gpio1 1 2
		         0
		         &gpio2 3 4>;
	}

下面是gpio-drv使用gpios中定义的gpio代码。

	static int gpio_drv_probe(struct platform_device *pdev)
	{
		int gpio1, gpio3;
                                                   
	    struct device *dev = &pdev->dev;
	    struct device_node *node = pdev->dev.of_node;
		
		gpio1 = of_get_named_gpio_flags(node, "gpios", 1, NULL);

		if (gpio1 < 0) {
			dev_err(dev, "get gpios failed, index = %d.", 1);
			return -EINVAL;
		}
		gpio3 = of_get_named_gpio_flags(node, "gpios", 3, NULL);
		if (gpio3 < 0) {
			dev_err(dev, "get gpios failed, index = %d.", 3);
			return -EINVAL;
		}

		if (devm_gpio_request(gpio1) < 0) {
			dev_err(dev, "get gpios failed, gpio1.");
			return -EINVAL;
		}

		if (devm_gpio_request(gpio3) < 0) {
			dev_err(dev, "get gpios failed, gpio3.");
			return -EINVAL;
		}

		... 
		
		gpio_direction_output(gpio1, 0);

		gpio_direction_input(gpio3);

		...

		gpio_set_value(gpio1, 1);
		gpio_get_value(gpio3);
	}

新架构

gpio的新架构的所有接口都是基于gpio_desc的,这避免了直接使用gpio num的诸多问题。内核文档consumer.rst详细描述了重构后的gpio子系统的使用方式。接口的前缀由gpio*改为了gpiod*。内核头文件<linux/gpio/consumer.h>描述用于编写gpio相关驱动所需要的接口和数据结构定义。

内核文档intro.rst描述了GPIO在内核中的接口定义,对于新、旧架构都做了介绍。同时,文中还提到了GPIO的基本含义,以及几个通用的gpio的相关属性,比如,Active-High、Active-Low,以及Open Drain、Open Souce。

内核文档board.rst描述了几种如何将GPIO分配给设备的方式,比如,Device Treee、ACPI、Platform Data等,本文主要涉及的是Device Tree,其他两种方式不是主流的方式。

内核文档driver.rst描述了如何基于最新的架构编写gpio chip的驱动程序。内核头文件<linux/gpio/driver.h>描述了用于gpio chip驱动相关的接口和数据结构的定义。

内核文档drivers-on-gpio.rst描述了内核中与GPIO有关的子系统,谈到了如何在内核或者用户层,关联和使用这些子系统。

内核文档using-gpio.rst描述了gpio子系统为用户空间提供的标准ABI接口,以及如何基于libgpiod库完成用户空间GPIO应用开发的方式。

上面各个文档讲述的都十分的详细,而且都会配套相应的示例,对于想深入学习GPIO子系统的小伙伴们都是十分有帮助的。

数据结构

对于编写gpio驱动来说,一般涉及到的数据结构就是struct gpio_desc,它唯一代表一个gpio line,其抽象了gpio相关的硬件特性,使得驱动开发者可以不用关心于具体的gpio chip就能很好的实现gpio相关的控制。

这是struct gpio_desc的定义:

	struct gpio_desc {
			struct gpio_device	*gdev;
			unsigned long		flags;
			/* flag symbols are bit numbers */
			#define FLAG_REQUESTED	0
			#define FLAG_IS_OUT	1
			#define FLAG_EXPORT	2	/* protected by sysfs_lock */
			#define FLAG_SYSFS	3	/* exported via /sys/class/gpio/control */
			#define FLAG_ACTIVE_LOW	6	/* value has active low */
			#define FLAG_OPEN_DRAIN	7	/* Gpio is open drain type */
			#define FLAG_OPEN_SOURCE 8	/* Gpio is open source type */
			#define FLAG_USED_AS_IRQ 9	/* GPIO is connected to an IRQ */
			#define FLAG_IRQ_IS_ENABLED 10	/* GPIO is connected to an enabled IRQ */
			#define FLAG_IS_HOGGED	11	/* GPIO is hogged */
			#define FLAG_TRANSITORY 12	/* GPIO may lose value in sleep or reset */
			#define FLAG_PULL_UP    13	/* GPIO has pull up enabled */
			#define FLAG_PULL_DOWN  14	/* GPIO has pull down enabled */
			#define FLAG_BIAS_DISABLE    15	/* GPIO has pull disabled */
			
			/* Connection label */
			const char		*label;
			/* Name of the GPIO */
			const char		*name;
	};
  • gdev :抽象了具体的gpio chip设备。
  • flags:表示gpio的属性,下面定义了具体的属性值,比如,open_drain、open_source、pull_up、pull_down、bias_disabel等。
  • label:定义了gpio的标签名,一般用作device tree相关处理接口的参数。
  • name : 定义了gpio 的名字。

gpio驱动在使用gpio之前,首先需要获得gpio_desc描述符,才能继续操作gpio。

APIs

获得/处置gpio

如果设备只需要一个gpio,那么可以通过gpiod_get获得对应的gpio_desc。

struct gpio_desc *gpiod_get(struct device *dev, const char *con_id,
		    enum gpiod_flags flags);

如果设备同时定义了多个gpio(比如用于LED数码显示的设备驱动),那么,在获取gpio时,需要额外增加一个index参数用于区分。

struct gpio_desc *gpiod_get_index(struct device *dev,
				  const char *con_id, unsigned int idx,
				  enum gpiod_flags flags);

对于flags用于初始化gpio的方向和初始值,其定义如下:

  • GPIOD_ASIS或0:表示对gpio不进行初始化,之后必须设置gpio的方向才能使用该gpio。
  • GPIOD_IN:初始化该gpio作为输入模式。
  • GPIOD_OUT_LOW :初始化该gpio作为输出模式,并且输出低电平。
  • GPIOD_OUT_HIGH :初始化该gpio作为输出模式,并且输出高电平。
  • GPIOD_OUT_LOW_OPEN_DRAIN:初始化该gpio为输出模式,默认输出低电平,并且其电气特性为开漏输出。
  • GPIOD_OUT_HIGH_OPEN_DRAIN:初始化该gpio为输出模式,默认输出高电平,并且其电气特性为开漏输出。

最后两个gpio配置一般应用在电气连接必须是开漏的场景,比如I2C的SDA和SCL。

**所有接口成功返回gpio_desc,失败返回可以通过IS_ERR检测的错误信息(注意:绝不会返回NULL)。**错误信息-ENOENT 表示没有为该设备分配请求的gpio,其他的错误表示gpio可以获得,但是,过程中存在某些错误,这对于进一步的调试非常有帮助。

struct gpio_descs *gpiod_get_array(struct device *dev,
				   const char *con_id,
				   enum gpiod_flags flags);

对于配置了多个gpios的设备,可以通过上面的函数一次获得,结构struct gpio_descs的定义如下:

struct gpio_descs {
	struct gpio_array *info;
	unsigned int ndescs;
	struct gpio_desc *desc[];
}

desc指向gpio的gpio_desc数组,这可以加速关于gpio的访问速度。

可以使用下面的函数释放gpio_desc的使用权。

void gpiod_put(struct gpio_desc *desc);
void gpiod_put_array(struct gpio_descs *descs);

注意:

  • gpio_desc在调用完gpiod_put之后,禁止再次使用该gpio_desc。

  • struct gpio_descs *descs必须要使用gpiod_put_array进行释放,不能单独使用gpiod_put释放每个gpio desc。

      struct gpio_desc *devm_gpiod_get(struct device *dev, const char *con_id,
      				 enum gpiod_flags flags);
    
      struct gpio_desc *devm_gpiod_get_index(struct device *dev,
      				       const char *con_id,
      				       unsigned int idx,
      				       enum gpiod_flags flags);
    
    
      struct gpio_descs *devm_gpiod_get_array(struct device *dev,
      					const char *con_id,
      					enum gpiod_flags flags);
    
      void devm_gpiod_put(struct device *dev, struct gpio_desc *desc);
    
      void devm_gpiod_put_array(struct device *dev, struct gpio_descs *descs);
    

上面提供了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>;
};

设备foo_device定义了两个gpio功能属性,led-gpios和power-gpios。gpio 15、16、17用于led-gpios属性,gpio 1用于power-gpios属性。获取这些gpios的方式如下:

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);

注意,index和con_id参数与foo_device属性的对应关系。

设置方向

使用gpio之前必须首先设置gpio的方向,如果在gpiod_get*()的flags参数已经指定了方向,可以不再进行方向的设置。

int gpiod_direction_input(struct gpio_desc *desc);
int gpiod_direction_output(struct gpio_desc *desc, int value);

两个函数,成功返回0,失败返回负数。必须仔细检查两个函数的返回值,因为get/set函数不会返回错误码。必须在进程上下文使用这些函数,对于spinlock-safe的GPIOs可以在系统启动的早期(没有创建相应的进程)使用。

对于配成输出的gpio,value会作为初始化进行输出。这对于系统初始化时是有帮助的,可以去除gpio上的毛刺。

驱动程序可以通过下面的函数查询gpio的方向:

int gpiod_get_direction(const struct gpio_desc *desc);

返回值,0表示输出,1表示输入,否则会返回错误码。

必须注意的是,gpio不会有默认的方向,对没有初始化方向的gpio进行操作是未定义的行为

spinlock-safe的gpio访问

所谓的spinlock-safe的gpio访问说的是,驱动程序在对这些gpio的访问时不会睡眠,因为在使用spinlock的场景中是不允许睡眠发生的,所以,对于这些gpio的访问是spinlock-sfae的,换句话说,可以在不允许发生睡眠的上下文中使用这些gpios,比如中断上下文。

对于GPIO控制器,可以通过内存访问指令进行直接的读写,所以,这些附属于GPIO控制器的GPIOs一般都是spinlock-safe的gpios。可以在原子上下文中使用如下函数访问gpios。

int gpiod_get_value(const struct gpio_desc *desc);
void gpiod_set_value(struct gpio_desc *desc, int value);

注意:

  • vlaue:value是布尔类型,0表示低电平,非零表示高电平。
  • 对于方向为输出的gpio,通过gpiod_get_value可以得到gpio当前值,即为当前gpio引脚上的真实电平状态。实际情况是,可能得到的值与输出的值不匹配,可能的原因是gpio被配置成开漏输出或者输出存在延时。比如,对于I2C总线,由于SDA配成开漏输出,所以,SDA上只要有一个GPIO输出0,其他所有GPIO当前状态应该都是0。
  • get/set不会返回错误码,因为“无效的gpio”应该提前通过gpiod_direcion_*函数进行检查出来。
  • 不是所有的平台都支持读取输出模式下的gpio值,此时,对于gpio的读取永远返回0,比如,imx6ul平台的gpio,需要配置SPIN寄存器才能读取输出模式下的gpio值。
gpios与IRQs之间的映射

gpio引脚一般都可以作为中断源,可以使用如下函数将gpio转换为对应的IRQ num。

int gpiod_to_irq(const struct gpio_desc *desc);

如果成功返回irq num,否则返回负的错误码,这可能是由于gpio不能作为IRQ中断使用。注意,gpio作为中断源使用时,必须被配置成输入模式。gpiod_to_irq不会睡眠。

使用gpiod_to_irq成功获取到irq num之后,可以做为irq_request或者free_irq的参数使用。

与旧gpio子系统通信

内核中仍然有一些子系统在使用基于gpio num的gpio访问方式,为了能够与之兼容,内核提供了两种架构相互转化的函数。

int desc_to_gpio(const struct gpio_desc *desc);
struct gpio_desc *gpio_to_desc(unsigned gpio);
  • desc_to_gpio返回的gpio在desc释放之前都是可用的。
  • 传递给gpio_desc的gpio必须是通过gpio_requst*获得的。
  • 由另一个函数,释放由其中一个函数返回gpio的行为是不允许和未定义的。

示例

这里以dht11的驱动程序为例,简要说一下,如何使用gpiod_*函数族。

dht11是比较常用的温湿度采集模块,其通信方式为单总线(one-wire bus)方式,即,CPU通过一个GPIO就可以实现与之通信。

下面是dht11的device tree配置信息。

	humidity_sensor {
		compatible = "dht11";
		gpios = <&gpio0 6 0>;
}

dht11_probe对gpio进行申请和配置。

static int dht11_probe(struct platform_device *pdev) 
{
	... ...

	dht11->gpiod = devm_gpiod_get(dev, NULL, GPIOD_IN);
	if (IS_ERR(dht11->gpiod))
		return PTR_ERR(dht11->gpiod);

	... ...

	dht11->irq = gpiod_to_irq(dht11->gpiod);
	if (dht11->irq < 0) {
		dev_err(dev, "GPIO %d has no interrupt\n", desc_to_gpio(dht11->gpiod));
		return -EINVAL;
	}
	... ... 
}

dht11_read_raw对gpio的方向和值进行配置。

static int dht11_read_raw(struct iio_dev *iio_dev,
		  const struct iio_chan_spec *chan,
		int *val, int *val2, long m) {
	... ...

	ret = gpiod_direction_output(dht11->gpiod, 0);
	if (ret)
		goto err;
	... ...

	ret = gpiod_direction_input(dht11->gpiod);
	if (ret)
		goto err;
	... ...
}

dht11_handle_irq中处理了gpio的状态。

static irqreturn_t dht11_handle_irq(int irq, void *data)
{
	... ...

	dht11->edges[dht11->num_edges++].value =
					gpiod_get_value(dht11->gpiod);

	... ...
}

总结

好了,上文主要分析了GPIO新旧架构之间的区别和联系,目前,社区明确要求在新的驱动程序中使用新的架构去进行开发。实际开发时,我们需要首先确定当前的内核版本是否支持GPIO新架构接口,然后,再选择具体的GPIO使用方式。后面的文章会继续分析GPIO子系统关于gpio chip的部分。

  • 38
    点赞
  • 113
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值