🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🛷Pinctrl子系统
- 🥅设备树中的Pinctrl子系统
- 🛷GPIO子系统
- 🥅设备树中的GPIO子系统
- 🥅驱动程序中使用GPIO子系统
- 🛷基于子系统的LED驱动程序
- 🥅驱动程序
- 🥅设备树文件
- 🛷总结
在前面的LED驱动程序中,有三种实现方式:
- 硬件操作绑定到驱动程序中。
platform_device
提供硬件信息,platform_driver
获取硬件资源,并进行操作。- 使用设备树提供硬件信息,
platform_driver
获取硬件资源,并进行操作。
无论使用哪种方式,都需要通过ioremap
函数将物理地址映射为虚拟地址,这几种方式存在两个问题:
- 和硬件强相关,需要去查看芯片手册,更像是"寄存器"编程。
- 驱动程序不通用,无法做到一套驱动程序适用于所有开发板。
在Linux中,针对开发板的引脚有两个子系统:GPIO子系统和Pinctrl子系统。
🛷Pinctrl子系统
如上图所示,开发板中几乎所有引脚都支持功能复用:
- 要想让
pinA
和pinB
作为通用GPIO口去使用,就需要设置IOMUX
模块,配置这两个引脚的复用功能为GPIO
功能模块。 - 要想让
pinA
和pinB
作为I2C功能去使用,就需要设置IOMUX
模块,配置这两个引脚的复用功能为I2C
功能模块。
GPIO
模块和I2C
模块与可复用的其他功能地位是相等的。
在将引脚复位为某种功能时,首先就需要配置IOMUX
模块,有的时候还需要配置引脚的上拉,下拉,开漏等模式。
大多数芯片是没有单独的IOMUX
模块的,需要配置其他寄存器模块来实现功能复用。
我们现在使用的芯片,动辄上百个引脚,而且配置复用功能的方式和步骤不尽相同,如果我们在编程的时候去对照芯片手册寻找这些寄存器和设置方式的话,无疑会把我们逼疯。
- 所以,各个芯片厂家的BSP工程师就设计了一个
Pinctrl
子系统。
Pinctrl
子系统是一个软件层面的子系统,它将配置引脚复用为各个功能的操作都放在这个子系统中,我们写驱动程序时,只需要直接去使用Pinctrl
子系统就可以实现对不同功能的复用。
🥅设备树中的Pinctrl子系统
如上图所示设备树中代码所示,分为左右两部分:
- 左半部分是
pin_controller
引脚控制子系统,这部分设备树的代码是用来使用Pinctrl
子系统的。 - 右半部分是
client_device
客户设备,是由我们写驱动程序的"客户"实现的。
client_device:
上图右半部分中client_device
节点的名字是device
,代称一个具体设备,如led
,key
等等。
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
,代称功能复用节点,如uart
,pwm
等等。
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
节点的格式却没有统一的标准,甚至上面的属性值group
,function
也不一定有。
虽然Pinctrl
子系统在设备树中的实现不尽相同,但是都是遵循配置功能(function),设置组(groups),引脚设置(高低电平等)的规则来实现的。
甚至我们都不用知道是如何根据设备树中的Pinctrl
子系统节点去配置开发板的寄存器的,因为具体的寄存器配置也由BSP工程师在Pinctrl
子系统中完成了。
如上图对default
状态的处理,主要是在platform_device
和platform_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_device
和platform_driver
进行匹配,所以我们要做的就是完成platform_driver
的相关代码:
- 注册一个
platform_driver
结构体,在probe
函数中,获取引脚,注册file_operations
结构体。 - 在
file_operations
中,设置方向、进行写值。
- 此时驱动层不再分为上下两层,而是只有一层,
file_operations
结构体的注册也在probe
中完成。
注册platform_driver结构体:
如上图所示,定义platform_driver
结构体并进行初始化:
- 初始化
probe
和remove
两个函数指针成员。 - 初始化
driver
成员中的names
和of_match_table
。
of_match_table
是一个指针,指向struct of_device_id
类型的数组,在该数组中的每一个元素都有一个compatible
成员:
platform_device
和platform_driver
匹配过程中,看的就是这个属性,初始化为Big_Miaomi,led_drv
,表示该驱动只支持这一个设备节点。- 由于使用的是设备树,所以匹配规则不会用到
name
成员,这里随意初始化为Big_Miaomi_leds
。
然后就是在入口函数led_init
中使用platform_driver_register
注册前面定义的chip_gpio_driver
结构体,在出口函数中使用platform_driver_unregister
再将该结构体移除,最后再完善以下设备信息。
现在总体框架已经有了,接下来就是实现probe
函数和remove
函数了。
probe函数:
如上图所示chip_gpio_probe
函数,在platform_device
和platform_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
结构体中的open
和write
函数了,在应用层调用open
和write
系统调用时,会调用到驱动层的这两个函数。
如上图led_drv_open
和led_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子节点:
如上图所示,在根节点下创建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
子系统的标志接口函数来获取引脚信息,以及操作引脚。