详解GPIO子系统
好长时间没去写博客了,今天呢,我们来讲讲GPIO子系统,我们在讲解一个新东西之前,得先去了解一下他是干啥的吧,所以下面我们来看看关于GPIO子系统的重要的概念:
引入:
要操作GPIO引脚,先把所用引脚配置为GPIO功能,这通过Pinctrl子系统来实现。
然后就可以根据设置引脚方向(输入还是输出)、读值──获得电平状态,写值──输出高低电平。
以前我们通过寄存器来操作GPIO引脚,即使LED驱动程序,对于不同的板子它的代码也完全不同。
当BSP工程师实现了GPIO子系统后,我们就可以:
a. 在设备树里指定GPIO引脚
b. 在驱动代码中:
使用GPIO子系统的标准函数获得GPIO、设置GPIO方向、读取/设置GPIO值。
这样的驱动代码,将是单板无关的。
在设备树中指定引脚:
在几乎所有ARM芯片中,GPIO都分为几组,每组中有若干个引脚。所以在使用GPIO子系统之前,就要先确定:它是哪组的?组里的哪一个?
在设备树中,“GPIO组”就是一个GPIO Controller,这通常都由芯片厂家设置好。我们要做的是找到它名字,比如“gpio1”,然后指定要用它里面的哪个引脚,比如<&gpio1 0>。
有代码更直观,下图是一些芯片的GPIO控制器节点,它们一般都是厂家定义好,在xxx.dtsi文件中:
我们暂时只需要关心里面的这2个属性:
“gpio-controller”表示这个节点是一个GPIO Controller,它下面有很多引脚。
“#gpio-cells = <2>”表示这个控制器下每一个引脚要用2个32位的数(cell)来描述。
为什么要用2个数?其实使用多个cell来描述一个引脚,这是GPIO Controller自己决定的。比如可以用其中一个cell来表示那是哪一个引脚,用另一个cell来表示它是高电平有效还是低电平有效,甚至还可以用更多的cell来示其他特性。
普遍的用法是,用第1个cell来表示哪一个引脚,用第2个cell来表示有效电平:
定义GPIO Controller是芯片厂家的事,我们怎么引用某个引脚呢?在自己的设备节点中使用属性"[<name>-]gpios",示例如下:
上图中,可以使用gpios属性,也可以使用name-gpios属性。
在驱动代码中调用GPIO子系统:
在设备树中指定了GPIO引脚,在驱动代码中如何使用?
也就是GPIO子系统的接口函数是什么?
GPIO子系统有两套接口:基于描述符的(descriptor-based)、老的(legacy)。前者的函数都有前缀“gpiod_”,它使用gpio_desc结构体来表示一个引脚;后者的函数都有前缀“gpio_”,它使用一个整数来表示一个引脚。
要操作一个引脚,首先要get引脚,然后设置方向,读值、写值。
下表列出常用的函数:
有前缀“devm_”的含义是“设备资源管理”(Managed Device Resource),这是一种自动释放资源的机制。它的思想是“资源是属于设备的,设备不存在时资源就可以自动释放”。
比如在Linux开发过程中,先申请了GPIO,再申请内存;如果内存申请失败,那么在返回之前就需要先释放GPIO资源。如果使用devm的相关函数,在内存申请失败时可以直接返回:设备的销毁函数会自动地释放已经申请了的GPIO资源。
建议使用“devm_”版本的相关函数。
下面来看一个简单的例子:
假设备在设备树中有如下节点:
那么可以使用下面的函数获得引脚:
下面我们来看看怎么去写一个具体的驱动程序:
编写思路:
GPIO的地位跟其他模块,比如I2C、UART的地方是一样的,要使用某个引脚,需要先把引脚配置为GPIO功能,这要使用Pinctrl子系统,只需要在设备树里指定就可以。在驱动代码上不需要我们做任何事情。
GPIO本身需要确定引脚,这也需要在设备树里指定。
设备树节点会被内核转换为platform_device。
对应的,驱动代码中要注册一个platform_driver,在probe函数中:获得引脚、注册file_operations。
在file_operations中:设置方向、读值/写值。
下图就是一个设备树的例子:
在设备树中添加Pinctrl信息:
有些芯片提供了设备树生成工具,在GUI界面中选择引脚功能和配置信息,就可以自动生成Pinctrl子结点。把它复制到你的设备树文件中,再在client device结点中引用就可以。
有些芯片只提供文档,那就去阅读文档,一般在内核源码目录Documentation\devicetree\bindings\pinctrl下面,保存有该厂家的文档。
如果连文档都没有,那只能参考内核源码中的设备树文件,在内核源码目录arch/arm/boot/dts目录下。
最后一步,网络搜索。
Pinctrl子节点的样式如下:
在设备树中添加GPIO信息:
先查看电路原理图确定所用引脚,再在设备树中指定:添加”[name]-gpios”属性,指定使用的是哪一个GPIO Controller里的哪一个引脚,还有其他Flag信息,比如GPIO_ACTIVE_LOW等。具体需要多少个cell来描述一个引脚,需要查看设备树中这个GPIO Controller节点里的“#gpio-cells”属性值,也可以查看内核文档。
示例如下:
总结起来的话,就是我们需要先把这个引脚复用成gpio模式,也就是先使用软件去配置出Pinctrl,然后在我们自己写的设备树里面,我们需要去写好pinctrl的clien端,然后接着我们还需要去使用gpio子系统,其实也是类似于去写好gpio子系统的clien端:
哦哦哦,在gpio子系统应该叫device
我们知道,我们编写的clien端会生成一个platform_device结构体,所以我们需要有一个compatible属性,去跟我们写的驱动程序去进行匹配:
编程示例:
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
好处:这些代码对所有的板子都是完全一样的!
跟设备树生成的platform_device结构体匹配成功后,就会去调用probe函数:
这里会去调用内核提供的gpiod_get函数去获取设备树中led-gpios=<...>的信息,然后就去获取节点中名为led的gpio子节点:
返回值是一个gpio描述结构体来着,后面我们讲gpio子系统相关的数据结构的时候会讲到
最后就是把file_operations结构体注册给内核,这里面有open close write函数
那么到这里,我们驱动程序就写完了,也算是初识gpio子系统了,下一篇我们来讲解他相关的数据结构,完结,撒花(doge.)