作者 | 赵青窕
随着Linux内核代码的逐步完善,其GPIO口的操作接口也在不断完善。内核中存在多种GPIO API接口,我们该如何使用这些API接口呢?我们又该如何在设备树中配置GPIO呢?
目前的内核中提供了三种版本的API接口供我们使用,分别是Pinctrl子系统对应的API接口和GPIO子系统对应的API接口(GPIO子系统提供了两种类型的API接口),在本文中我将会通过内核中GPIO架构的角度来说明这三类API接口;我们对GPIO的控制,除了合适的API接口外,还需要通过设备树对GPIO进行配置,虽然不同架构中设备树的配置方式不同,但本文中也会举例说明设备树如何配置,对应的代码中如何使用API接口;最后说明我们可以用到的调试手段。
1.内核GPIO架构
下图是内核中,GPIO架构中的核心部分框架图(暂时不考虑GPIO架构对应的sysfs和debugfs):
图 1:GPIO架构
从上图中的上半部分可以看出以下关键信息:
- 内核提供了两种控制引脚的方式,一种是采用Pinctrl子系统,一种是采用GPIO子系统(该系统又有两种方式,后面小节中会进行说明),用户编写驱动时可以调用这两个子系统提供的API接口来达到控制GPIO口的目的;
- GPIO子系统的功能是通过Pinctrl子系统来实现的。虽然从图中看出,Pinctrl子系统和GPIO子系统并存,但内核需要对所有的GPIO口进行管理,因此就需要一个统一的管理接口或者模块,Pinctrl就完成这种统一管理的目标,比如我们采用GPIO子系统的API接口gpio_request来申请GPIO口时,内核需要记录哪些GPIO口已经申请过了,若Pinctrl子系统和GPIO子系统各维护一套GPIO管理策略,那就可能导致Pinctrl子系统和GPIO子系统同时操作同一个GPIO口的情况,这种显然是不可行的,且从高通平台内核代码中可以看出gpio_request有如下的调用关系:
gpio_request—>gpiod_request—-> gpiod_request_commit—->chip->request(系统启动时设置为gpopchip_generic_request) —->pinctrl_gpio_request
该调用关系从GPIO子系统的API函数gpio_request最后调用了Pinctrl子系统的函数pinctrl_gpio_request,这种调用关系,也证实了Pinctrl和GPIO子系统的关系。对于其他接口,如gpio_direction_input,gpio_set_value等函数的调用,同gpio_request相似,其最后均是调用到对应的chip->direction_input,chip->set,进而调用pinctrl_gpio_direction_input,等函数,由于这类API函数比较多,在此就不展示其调用关系了;
- 内核采用结构体struct pinctrl_dev来表示一个控制器,所有的pinctrl_dev会形成链表,链表头就是pinctrldev_list,在函数pinctrl_gpio_request内部,会调用函数pinctrl_get_device_gpio_range来根据GPIO号,遍历链表pinctrldev_list来查找该GPIO口对应的pinctrl_dev,当然这部分工作均是由系统来维护,我们只需知道整个框架,并如何使用GPIO的整个子系统即可。
注意:目前从我看到的代码中发现,有些厂家在实现GPIO子系统时,并非所有的功能均通过Pinctrl子系统,但gpio_request是会通过Pinctrl子系统,因为Pinctrl子系统中会标记哪些GPIO已经request了,这样后续模块采用request继续申请该资源时就会失败;不过大家放心,即使其部分功能不通过Pinctrl子系统,但其对驱动模块提供的API接口不受其影响。
2.用户驱动中控制GPIO
在编写驱动时,可以采用以下两种方式来设置:
- Pinctrl方式,该方式最终是采用Pinctrl子系统来实现各项功能的;
- 采用GPIO子系统接口的方式,该方式其实有两种,分别是legacy和gpio description的方式。但目前的内核中,legacy内部会调用gpio description的方式,后面内容中,我会以legacy的方式来说明使用方法。
Pinctrl控制引脚方式
下面,我以高通平台Pinctrl的方式来说明其代码和设备树的配置。
我们先来看一下设备树:
&tlmm{
client1_state1: client1_state1 {
mux {
pins = "gpio0";
function = "gpio";
};
config {
pins = "gpio0";
bias-disable;
drive-strength =<2> ;
input-enable;
};
};
client1_state2: client1_state2 {
mux {
pins = "gpio0";
function = "gpio";
};
config {
pins = "gpio0";
bias-disable;
drive-strength = <2>;
output-high;
};
};
};
&soc {
client1 {
pinctrl-names = "state1", "state2";
pinctrl-0 = <&client1_state1 >;
pinctrl-1 = <&client1_state2>;
};
};
上面是典型的设备树的配置,其中包含两个节点,分别是tlmm中的引脚配置和我们的设备client1。
tlmm中定义了两种GPIO状态,分别是作为输入功能的GPIO0和作为输出功能的GPIO0。不同平台的设备树中的配置方法不同,但均需要像上面设备树那样,配置两部分:
引脚功能配置,是作为普通的GPIO口,还是复用为其他的功能;
引脚驱动配置,包括引脚内部的上拉或者下拉,驱动能力等。
接下来,一起看下代码中如何进行操作,其操作需要按照下面的顺序:
- 先调用devm_pinctrl_get或者pinctrl_get函数获取对应的struct pinctrl*;
- 紧接着调用pinctrl_lookup_state(struct pinctrl *p, const char *name)来根据name获取对应的配置;
- 最后我们采用函数pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *state)来进行状态的选择。选择某一状态,就是设置了对应的引脚,且引脚的request操作是在该函数内部完成的,该函数内部会调用pin_request来进行申请,且调用该函数后,引脚的状态就是设备树中设备的状态,如上面设备树中,client1_state1对应的引脚使用了其GPIO功能,且配置为输入(设备树中是通过input-enable进行配置的),client1_state2对应的引脚也使用了其GPIO的功能,且配置为输出高(设备树中是通过out-high进行配置的)。
采用GPIO子系统
&soc {
client1 {
qcom,gpio-client1 = <&tlmm 100 0>; //100就是GPIO号
}
这种方式在设备树的配置方式上比较简单,其代码操作如下:
- 使用函数of_get_named_gpio(node, ” qcom,gpio-client1″, 0)获取GPIO号。
- 接下来是最重要的一步,调用函数gpio_request来进行GPIO的request操作。该函数最后会通过Pinctrl接口间接调用pin_request来进行引脚的申请操作。之前有一次工作中粗心,忘记request操作,但发现该GPIO可能也会正常操作,但会有工作不稳定的情况发生;
- 接下来可以调用函数gpio_direction_input或者gpio_direction_output来进行GPIO输入或者输出模式的配置,gpio_direction_output调用的同时,可以设置输出高或者输出低;
- 如果配置位输入模式,则可以使用函数gpio_get_value来获取GPIO口的状态。
如果需要把引脚配置为中断功能,则我们需要使用函数irq = gpio_to_irq(gpio)来获得irq号,根据irq号来进行适当的中断配置。
通过上面两种方式,我们可以发现Pinctrl的方式,其设备树复杂,但API接口简单,只需要通过函数pinctrl_select_state选择设备树中某一配置即可。GPIO子系统的方式是设备树简单,但代码复杂,设备树中只配置了GPIO号,其余的如GPIO的方向及输出信号均需要通过代码来进行设置,这正是不同API接口及其设备树的优缺点。
3.GPIO调试
不同平台的调试方式可能会存在一些差异,比如MTK的不同平台间都会有差异,在此我就介绍一种常见的需要debugfs支持的方式。
我们在编译内核时,需要配置相应的debugfs宏来打开该功能,只有配置相应宏后,我们就可以进入机器的/sys/class/gpio下对GPIO口进行操作,下面是对应的操作顺序:
- cd /sys/class/gpio/
- echo 99 > export(此处99代表引脚号,确切地说echo时,应采用对应引脚gpio_request获得到的数值)
- cd gpio99
- echo in/out > direction //设置GPIO输入或输出
- cat direction //获取GPIO输入输出状态
- echo 0/1 > value //拉低或者拉高对应的GPIO口
- cat value //查看GPIO口的高低状态
4.总结
希望大家通过本文,可以了解内核中GPIO的机制并掌握其操作方法,但需要说明的是在内核之前的阶段也会进行引脚的配置,比如我们使用串口来打印bootloader阶段的日志,这种情况下串口引脚一定是在内核之前的阶段进行配置的。
作者介绍
赵青窕,51CTO社区编辑,从事多年驱动开发。研究兴趣包含安全OS和网络安全领域,发表过网络相关专利。