【Linux驱动】Pinctrl子系统 | GPIO子系统 | 基于子系统的LED驱动程序

🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
图

在前面的LED驱动程序中,有三种实现方式:

  • 硬件操作绑定到驱动程序中。
  • platform_device提供硬件信息,platform_driver获取硬件资源,并进行操作。
  • 使用设备树提供硬件信息,platform_driver获取硬件资源,并进行操作。

无论使用哪种方式,都需要通过ioremap函数将物理地址映射为虚拟地址,这几种方式存在两个问题:

  • 和硬件强相关,需要去查看芯片手册,更像是"寄存器"编程。
  • 驱动程序不通用,无法做到一套驱动程序适用于所有开发板。

在Linux中,针对开发板的引脚有两个子系统:GPIO子系统和Pinctrl子系统。

🛷Pinctrl子系统

图
如上图所示,开发板中几乎所有引脚都支持功能复用:

  • 要想让pinApinB作为通用GPIO口去使用,就需要设置IOMUX模块,配置这两个引脚的复用功能为GPIO功能模块。
  • 要想让pinApinB作为I2C功能去使用,就需要设置IOMUX模块,配置这两个引脚的复用功能为I2C功能模块。
  • GPIO模块和I2C模块与可复用的其他功能地位是相等的。

在将引脚复位为某种功能时,首先就需要配置IOMUX模块,有的时候还需要配置引脚的上拉,下拉,开漏等模式。

大多数芯片是没有单独的IOMUX模块的,需要配置其他寄存器模块来实现功能复用。

我们现在使用的芯片,动辄上百个引脚,而且配置复用功能的方式和步骤不尽相同,如果我们在编程的时候去对照芯片手册寻找这些寄存器和设置方式的话,无疑会把我们逼疯。

  • 所以,各个芯片厂家的BSP工程师就设计了一个Pinctrl子系统。

Pinctrl子系统是一个软件层面的子系统,它将配置引脚复用为各个功能的操作都放在这个子系统中,我们写驱动程序时,只需要直接去使用Pinctrl子系统就可以实现对不同功能的复用。

🥅设备树中的Pinctrl子系统

图
如上图所示设备树中代码所示,分为左右两部分:

  • 左半部分是pin_controller引脚控制子系统,这部分设备树的代码是用来使用Pinctrl子系统的。
  • 右半部分是client_device客户设备,是由我们写驱动程序的"客户"实现的。

client_device:

上图右半部分中client_device节点的名字是device,代称一个具体设备,如ledkey等等。

  • pinctrl-names:用来表示设备状态,此时有default默认状态和sleep状态两种。
  • pinctrl-0:表示第0种状态default时,该客户设备要设置的引脚复用功能,属性值是<&state_0_node_a>
  • pinctrl-1:表示第1种状态sleep时,该客户设备要设置的引脚复用功能,属性值是<&state_1_node_a>

如何理解呢?比如一个UART设备,它在默认状态default下是工作的,此时串口用到的引脚就要复用为UART功能。

在休眠状态下,为了省电,可以把用到的引脚复用为GPIO功能,或者直接把它输出高电平。

pin_controller:

上图左半部分中pin controller节点的名字是pincontroller,代称功能复用节点,如uartpwm等等。

  • function:表示要复用成什么功能,属性值是uart0,表示要复用成串口0。
  • groups:表示该功能要用到的所有引脚,这些引脚归为一个组groups
  • 配置信息:如上图中的output-high属性,表示配置为输出高电平。

上图客户设备中的pinctrl-names 里定义了 2 种状态:default、sleep:

  • 第 0 种状态用到的引脚在pinctrl-0 中定义,它是 state_0_node_a节点, 位于pincontroller 节点中的子节点。
  • 第 1 种状态用到的引脚在 pinctrl-1 中定义,它是 state_1_node_a节点, 位于pincontroller 节点中的子节点。

当客户设备处于default状态时,pinctrl子系统会自动根据上述信息把所用引脚复用为uart0功能。

当这个设备处于sleep 状态时,pinctrl子系统会自动根据上述信息把所用引脚配置为高电平。

  • cilent_device节点的格式是有标准的,我们必须按照标准去写,才能正确使用Pinctrl子系统中的功能复用节点。
  • 但是pin controller节点的格式却没有统一的标准,甚至上面的属性值groupfunction也不一定有。

虽然Pinctrl子系统在设备树中的实现不尽相同,但是都是遵循配置功能(function),设置组(groups),引脚设置(高低电平等)的规则来实现的。

甚至我们都不用知道是如何根据设备树中的Pinctrl子系统节点去配置开发板的寄存器的,因为具体的寄存器配置也由BSP工程师在Pinctrl子系统中完成了。

图
如上图对default状态的处理,主要是在platform_deviceplatform_driver匹配的过程中实现的:

  • 先将引脚复用设置为某个状态,复用为某个功能,不用我们自己去调用这些代码,在匹配过程中会自己调用。
  • 复用完成以后,才会调用platform_driver中的probe函数。

如果非要自己去实现复用功能的配置,也有相应的函数:

  • devm_pinctrl_get_select_default(struct device* dev),使用default状态的引脚。
  • pinctrl_get_select(struct device* dev, const char* name),根据name旋转某种状态的引脚。
  • pinctrl_put(struct pinctrl* p),不再使用引脚,退出时调用。

但是,我们写驱动程序时基本不用管是如何使用Pinctrl子系统的,只要知道在切换设备状态时,对应的Pinctrl就会被调用,实现相应的配置。

总之,Pinctrl子系统就用来实现引脚的功能复用的。

🛷GPIO子系统

要让引脚作为通用GPIO引脚,在通过Pinctrl子系统配置好引脚的功能后,还需要配置:

  • 引脚方向:输入还是输出。
  • 读取引脚值:获取电平状态是高还是低。
  • 控制输出:输出高电平还是低电平。

以前我们也是通过操作GPIO的相关寄存器来实现的,对于不同的开发板,它的驱动程序代码也是不一样的。

为了实现和Pinctrl子系统一样的分离,BSP工程时还实现了一个GPIO子系统,使得驱动程序适用于任何开发板,此时我们就可以:

  • 在设备树里指定GPIO引脚。
  • 在驱动程序中,使用GPIO子系统提供的标准函数获得GPIO引脚,设置方向,读取或者设置GPIO的值。

🥅设备树中的GPIO子系统

几乎所有的ARM芯片,GPIO都会分为几组,每组中都有若干个引脚,所以在使用GPIO子系统前,要先确定:

  • 使用的GPIO是哪组?GPIO1还是GPIO2等等。
  • 使用哪个引脚?GPIOx_IO1还是GPIOx_IO2等等。

图
如上图所示由芯片厂家的BSP工程师提供的imx6ull.dtsi设备树文件,存在多个GPIO组,也就是GPIO Controller,芯片有多少组GPIO,在该文件中就存在多少个这样的节点。

暂时我们只需要关心GPIO子系统节点中的两个属性:

  • gpio-contriller:表示这是一个GPIO Controller,该组GPIO中有很多引脚。
  • #gpio-cells = <2>:表示该控制器下每一个引脚要用2个32位的数(cell)来描述。

图
如上图所示我们自己写的dts设备树文件,虽然GPIO子系统的定义是厂家的事,但是使用具体哪个引脚还是由我们在设备树文件中决定的:

  • [<name>-]gpios:该属性就是表明该节点要使用哪组GPIO中的哪个引脚。
  • 属性值:<&gpio5 3 GPIO_ACTIVE_LOW>表示要使用GPIO5_IO3引脚,并且是低电平有效。
  • 这部分代码也是放在客户设备节点中的。

可以看到,属性值中有三个值,为什么是三个?

  • 因为在dtsi中的GPIO子系统中,定义了#gpio-cells = <2>,表示用两个数来描述引脚。
  • 使用哪个引脚必须指明所属的GPIO组,所以第一个数&gpio5是必须有的,不算在这两个数中。
  • 那么这两个数自然就是指表示引脚编号的3和,表示有效电平的GPIO_ACTIVE_LOW了。

🥅驱动程序中使用GPIO子系统

此时在设备树中已经指定了好了GPIO引脚,接下就是在驱动程序中使用这些引脚了,需要调用GPIO子系统提供的标准函数接口来获取引脚信息:

  • 新的基于描述符的(descriptor-based)接口:该套接口都有前缀gpiod_,它使用gpio_desc结构体来表示一个引脚。
  • 老的(legacy)接口:该套接口都有前缀gpio_,它使用一个整数来表示一个引脚。

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

获得GPIO引脚:

descriptor-based legacy
gpiod_get
gpiod_get_index
gpio_request
gpiod_get_array gpio_request_array
devm_gpiod_get
devm_gpiod_get_index
devm_gpiod_get_array

设置方向:

descriptor-based legacy
gpiod_direction_input gpio_direction_input
gpiod_direction_output gpio_direction_output

读值、写值:

descriptor-based legacy
gpiod_get_value gpio_get_value
gpiod_set_value gpio_set_value

释放GPIO:

descriptor-based legacy
gpio_free
gpiod_put
gpio_free
gpiod_put_array gpio_free_array

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

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

  • 设备的销毁函数会自动地释放已经申请了的 GPIO 资源。

  • descriptor-based接口

图
如上图所示,假设现在设备树中有这样一个节点,用来操作红绿蓝三个LED灯,在驱动程序中可以使用以下函数来获取引脚:

图
如上图,使用gpiod_get_index来获取引脚信息。

在写和读引脚值时,gpiod_set_value设置的是逻辑值,而逻辑值并不一定等于物理值。

图
如上图所示,LED2在GPIO5_3是低电平时才亮,高电平时不亮,所以在我们写的设备树节点中led-gpios属性中的第三个参数就要写成GPIO_ACTIVE_LOW

此时使用gpiod_set_value(desc, x)时:

  • 当第二个参数为1,就会将该引脚设置成有效电平GPIO_ACTIVE_LOW低电平,LED灯就亮。
  • 当第二个参数为0,就不会将该引脚设置成有效电平,灯就不亮。

说白了就是,该函数的第二个参数为1,灯就会亮,0就会灭,如果电路上是高电平灯亮,就需要在设备树中将有效电平该为GPIO_ACTIVE_HIGH,此时驱动程序中的代码不用改变,该函数的第二个参数仍然是1。

  • 建议使用新的前缀为gpiod_的这套函数接口。
  • legacy接口:

旧的gpio_函数是没办法根据设备树信息获得引脚的,它需要先知道引脚号,然后才能初始化描述引脚信息的整数。

图

如上图,在sysfs中访问GPIO,获得引脚号:

  • 在开发板的/sys/class/gpio目录下,找到各个gpiochipXXX目录,这样的每一个目录表示GPIO子系统中的一组GPIO。
  • 然后进入某个gpiochip目录下,查看label的内容。
  • 根据label的内容来对比设备树。

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

图

如上图,gpiochip96目录中的label中的GPIO基地址是0x020a8000,对dtsi文件中,发现该地址对应的是gpio4这组GPIO,所以可以得出结论:

  • GPIO4的基准引脚号就是96。
  • 假设引脚是GPIO4_IO14,那么该引脚的引脚号整数就是base number + n = 96 + 14 = 110

可以看到,老的一套接口并不是很好用,远不如新接口使用起来方便。

🛷基于子系统的LED驱动程序

有了Pinctrl子系统和GPIO子系统,对于引脚的配置完全在设备树文件中就可以搞定,驱动程序可以做到完全和硬件分离。

🥅驱动程序

我们知道,设备树中的节点会被转化成platfrom_deviceplatform_driver进行匹配,所以我们要做的就是完成platform_driver的相关代码:

  • 注册一个platform_driver结构体,在probe函数中,获取引脚,注册file_operations结构体。
  • file_operations中,设置方向、进行写值。
  • 此时驱动层不再分为上下两层,而是只有一层,file_operations结构体的注册也在probe中完成。

注册platform_driver结构体:

图
如上图所示,定义platform_driver结构体并进行初始化:

  • 初始化proberemove两个函数指针成员。
  • 初始化driver成员中的namesof_match_table

of_match_table是一个指针,指向struct of_device_id类型的数组,在该数组中的每一个元素都有一个compatible成员:

  • platform_deviceplatform_driver匹配过程中,看的就是这个属性,初始化为Big_Miaomi,led_drv,表示该驱动只支持这一个设备节点。
  • 由于使用的是设备树,所以匹配规则不会用到name成员,这里随意初始化为Big_Miaomi_leds

然后就是在入口函数led_init中使用platform_driver_register注册前面定义的chip_gpio_driver结构体,在出口函数中使用platform_driver_unregister再将该结构体移除,最后再完善以下设备信息。

现在总体框架已经有了,接下来就是实现probe函数和remove函数了。


probe函数:

tu
如上图所示chip_gpio_probe函数,在platform_deviceplatform_driver匹配成功以后,内核会自动调用probe函数:

  • 使用gpiod_get函数,从GPIO子系统中获取引脚信息,存入到全局变量led_gpio中,这是一个struct gpio_desc*结构体,用来描述引脚信息。
  • 使用register_chrdev函数注册file_operations结构体,在注册之前,定义该结构体并且初始化。
  • 创建设备类led_class,并且在/dev目录下创建Big_Miaomi_led0设备节点。

此时file_operations的注册工作放在了probe函数中,不再像之前一样放在驱动成的上层了,所以该结构体的定义也放在这里。

remove函数:

图
如上图所示chip_gpio_remove函数,在卸载驱动程序时,内核会自动调用remove函数:

  • 按照顺序,销毁设备节点led_class,销毁设备类led_class,移除file_operations的注册。卸载顺序和安装时相反。
  • 最后使用gpiod_put释放获取到的GPIO引脚。

open和write:

此时驱动程序的框架是完善了,接下来就是实现file_operations结构体中的openwrite函数了,在应用层调用openwrite系统调用时,会调用到驱动层的这两个函数。

图
如上图led_drv_openled_drv_write代码所示:

  • open:使用GPIO子系统中的gpiod_direction_output函数初始化该引脚的方向,0表示输出。
  • write:使用GPIO子系统中的gpiod_set_value像该引脚写值,status来自应用层,1表示灯亮,0表示灯灭,这是一个逻辑值。

ledtest.c:

体
如上图所示测试程序,仍然使用以前的LED程序,在命令行中输入./ledtest /dev/Big_Miaomi_led0 on灯亮,输入./ledtest /dev/Big_Miaomi_led0 off灯灭。

Makefile:

图
使用上图Makefile文件,将驱动程序应用层测试程序编译成ledtest可执行程序,将驱动程序编译成led_drv.ko文件。

图
如上图所示,将这些文件上传到Linux服务器上进行编译,然后拷贝到网络根文件系统中。

🥅设备树文件

Pinctrl信息:

首先是要使用Pinctrl子系统,将引脚配置为GPIO功能,所以要在我们的设dts设备树文件中创建一个Pinctrl子节点,通过该子节点才能使用到Pinctrl子系统。

但是,这个Pinctrl子节点该怎么写呢?对于IMX6ULL开发板,NXP公司提供了图形化界面工具来配置引脚功能:

图
如上图所示,选择好要使用的引脚后,并且配置和要使用的功能,此时就会生成一个设备树文件代码。

图
如上图所示,会生成一个dtsi文件,该文件中的代码如上,很多都是该开发板中所必须的,但是红色框中的部分是根据我们图形化界面的配置生成的。

  • 将红色框中的代码,复制到开发板所用的dts文件中相应的位置。

图
如上图,打开我们自己的dts设备树文件,找到&iomuxc_snvs节点,在该节点中将前面图形化界面生成的Pinctrl节点代码复制过来,形成一个新的子节点。

  • 节点名称写成Big_Miaomi_leds

如果你使用的开发板没有图形化工具,那么还有几种方式来写Pinctrl节点:

  • 参考芯片厂家提供的文档。
  • 参考别人写的dts文件。
  • 在网上搜索怎么写。

设备节点信息:

虽然使用了Pinctrl子系统,但是真正的LED节点还没有创建,所以在dts设备树文件的根节点下创建LED子节点:

tu
如上图所示,在根节点下创建Big_Miaomi_LED子节点,用来表示LED设备:

  • compatible:属性值必须和驱动程序中of_device_id数组中的值一样,所以是"Big_Miaomi,led_drv",用来匹配驱动程序。
  • pinctrl-names:属性值只有一个default,表示该设备只有一个默认状态。
  • pinctrl-0:属性值是<&Big_Miomi_leds>,表示0号default默认状态,使用前面我们增加的pinctrl节点来实现功能选择,选择为GPIO功能。
  • led-gpios:属性值是<&gpio5 3 GPIO_ACTIVE_LOW>,表示使用GPIO5_3引脚,有效电平是低电平。
    • 使用几个cells来描述引脚是由该组GPIO所在的GPIO子系统中的#gpio-cells属性值决定的,可以查看dtsi文件。

此时我们的设备树文件就写好了,使用Pinctrl子系统选择为GPIO功能,使用GPIO子系统选择GPIO5_3引脚为输出引脚,并且是低电平有效。

图
如上图所示,将设备树文件编译好的dtb文件拷贝到网络根文件目录下。

图
如上图,在开发板上,将我们编译好的dtb文件拷贝到开发板的/boot目录下,然后重启开发板,使用新的设备树文件。

图
如上图,此时在开发板的/sys/firmware/devicetree/base/目录下就有了我们在设备树文件中定义的Big_Miaomi_LED@0 节点。

图
如上图所示,在开发板上挂载网络文件系统后,进入/mnt目录下,安装驱动程序,然后在/dev/路径下就有了字符设备文件Big_Miaomi_led0,再执行测试程序,可以成功控制LED灯的亮灭,本喵这里就不贴图了。

🛷总结

要知道Pinctrl子系统的用来选择复用功能的,GPIO子系统是用来控制某个引脚的,现在不必纠结它的原理,只需要知道怎么用就行。

要会在设备树文件中添加pincontroller节点和设备节点,还要会驱动程序中调用GPIO子系统的标志接口函数来获取引脚信息,以及操作引脚。

最近更新

  1. TCP协议是安全的吗?

    2024-01-08 17:38:04       14 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-08 17:38:04       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-08 17:38:04       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-08 17:38:04       18 阅读

热门阅读

  1. 使用redis时快速考虑的问题

    2024-01-08 17:38:04       26 阅读
  2. CISSP 第6章: 密码学与对称加密算法

    2024-01-08 17:38:04       28 阅读
  3. SpringBoot-Redis

    2024-01-08 17:38:04       37 阅读
  4. Keras内置数据集

    2024-01-08 17:38:04       30 阅读
  5. 云水苍苍走四方

    2024-01-08 17:38:04       28 阅读
  6. MW Open - 高效率的API接口管理工具

    2024-01-08 17:38:04       32 阅读
  7. Android debug带系统签名

    2024-01-08 17:38:04       45 阅读