在上一篇笔记中,学习编写了基于设备树的LED驱动,但是驱动的本质还是没变,都是配置LED灯
所使用的GPIO寄存器,驱动开发方式和裸机基本没区别 。Linux是一个庞大而完善的系统,尤其是驱动框架,像GPIO这种最基本的驱动不可能采用裸机驱动开发方式。Linux内核提供了pinctrl和gpio子系统用于GPIO驱动,本章就学习一下如何借助pinctrl和gpio子系统来简化GPIO驱动开发。
pinctrl子系统
pinctrl子系统简介
Linux驱动讲究驱动分离与分层 ,pinctrl和gpio子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
先来回顾一下上一篇笔记中是怎么初始化LED灯所使用的GPIO,步骤如下:
- 修改设备树,添加相应的节点,节点里面重点是设置reg属性,reg属性包括了GPIO相关寄存器。
- 获取 reg属性中GPIOI_MODER、GPIOI_OTYPER、GPIOI_OSPEEDR、GPIOI_PUPDR和GPIOI_BSRR这些寄存器的地址,并且初始化它们,这些寄存器用于设置PI0这个PIN的复用功能、上下拉、速度等。
- 在2里面将PI0这个PIN设置为通用输出功能,因此需要设置PI0这个GPIO相关的寄存器,也就是设置GPIOI_MODER寄存器。
- 在2里面将PI0这个PIN设置为高速、上拉和推挽模式,就要需要设置PI0的GPIOI_OTYPER、GPIOI_OSPEEDR和GPIOI_PUPDR这些寄存器。
对于大多数的32位 SOC而言,引脚的设置基本都是这两方面,因此Linux内核针对PIN的复用配置推出了pinctrl子系统,对于GPIO的电气属性配置推出了gpio子系统。
大多数SOC的pin都是支持复用的,比如STM32MP1的PI0既可以作为普通的GPIO使
用,也可以作为SPI2的NSS引脚、TIM5的CH4引脚等等。此外还需要配置pin的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置pin的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如pin功能冲突)。pinctrl子系统就是为了解决这个问题而引入的,pinctrl子系统主要工作内容如下:
- 获取设备树中pin信息。
- 根据获取到的pin信息来设置pin的复用功能。
- 根据获取到的pin信息来设置pin的电气特性,比如上/下拉、速度、驱动能力等。
对于使用者来讲,只需要在设备树里面设置好某个pin的相关属性即可,其他的初始化工作均由pinctrl子系统来完成,pinctrl子系统源码目录为drivers/pinctrl。
STM32MP1的pinctrl子系统驱动
PIN配置信息详解
要使用pinctrl子系统,需要在设备树里面设置PIN的配置信息,一般会在设备树里面创建一个节点来描述PIN的配置信息 。打开stm32mp151.dtsi文件,找到一个叫做pinctrl的节点,如下所示:
第1815-1816行,#address-cells属性值为1和#size-cells属性值为1,也就是说pinctrl下的所有子节点的reg第一位是起始地址,第二位为长度。
第1818行,ranges属性表示STM32MP1的GPIO相关寄存器起始地址,STM32MP1系列芯片最多拥有176个通用 GPIO,分为12组,分别为:PA0-PA15、PB0-PB15、PC0-PC15、PD0-PD15、PE0-PE15、PF0-PF15、PG0-PG15、PH0-PH15、PI0-PI15、PJ0-PJ15、PK0-PK7、PZ0-PZ7。
其中PA-PK这11组 GPIO的寄存器都在一起,起始地址为0X50002000,终止地址为0X5000C3FF。PZ组寄存器起始地址为0X54004000,终止地址为0X540043FF,所以stm32mp151.dtsi文件里面还有个名为"pinctrl_z"的子节点来描述PZ组 IO。pinctrl节点用来描述PA-PK这11组IO,因此ranges属性中的0x50002000表示起始地址, 0xa400表示寄存器地址范围。
第1819行,interrupt-parent属性值为"&exti",父中断为exti。
后面的gpiox子节点先不分析,这些子节点都是gpio子系统的内容,到后面在去分析。打开stm32mp15-pinctrl.dtsi文件,能找到如下内容:
示例代码25.1.2.2就是向pinctrl节点追加数据,不同的外设使用的PIN不同、其配置也不同,将某个外设所使用的所有PIN都组织在一个子节点里面 。示例代码25.1.2.2中m_can1_pins_a子节点就是CAN1的所有相关IO的PIN集合,pwm1_pins_a子节点就是PWM1相关IO的PIN集合。绑定文档 Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.yaml描述了如何在设备树中设置STM32的PIN信息。
集合里面存放当前外设用到哪些引脚(PIN)、这些引脚应该怎么配置、复用相关的配置、上下拉、
默认输出高电平还是低电平。一般这个存放pincrtl集的子节点名字是"pins",如果某个外设用到多种配置不同的引脚那么就需要多个pins子节点 ,比如示例代码25.1.2.2中第535行和541行的pins1和pins2这两个子节点分别描述PH13和PI9的配置方法,由于PH13和PI9这两个IO的配置不同,因此需要两个pins子节点来分别描述 。第555-562行的pins子节点是描述PWM1的相关引脚,包括PE9、PE11、PE14,由于这三个引脚的配置是一模一样的,因此只需要一个pins子节点就可以了。
上面讲了,在pins子节点里面存放外设的引脚描述信息,这些信息包括:
1、pinmux属性
此属性用来存放外设所要使用的所有IO,通过STM32_PINMUX宏来配置引脚和引脚的复用功能,定义在include/dt-bindings/pinctrl/stm32-pinfunc.h文件里面,内容如下:
可看出有3个参数,含义如下:
- port:表示用哪一组 GPIO(例:H表示为GPIO第H组,也就是GPIOH)。
- line:表示这组GPIO的第几个引脚 (例:13表示为GPIOH_13,也就是PH13)。
- mode:表示当前引脚要做那种复用功能 (例:AF9表示为用第9个复用功能 ),这个需要查阅STM32MP1数据手册来确定使用哪个复用功能。
这里要注意,比如PH13做了FDCAN1的TX功能,还能做UART4的TX功能,这是不会冲突的,因为pinctrl驱动只会把设备树pinctrl节点解析出来的数据存储到一个链表里,只有当外设调用这个pinctrl节点的时候才会真的使用 。但是,如果要同时使用FDCAN1和UART4的话就会出问题,因此PH13同一时间只能用于一个外设,所以为了方便开发,还是建议一个PIN最好只能被一个外设使用。
2、电气属性配置
接下来了解一下PIN的电气特性如何设置,电气特性在pinctrl子系统里不是必须的,可以不配置,但是pinmux属性是必须要设置的 。stm32-pinctrl.yaml文件里面也描述了如何设置STM32的电气属性,如下图所示:
上图中bootlean类型表示了在pinctrl子系统只要定义这个电气属性就行了,例如:要禁用内部电压,只要在PIN的配置集里添加"bias-disable"即可,这个时候bias-pull-down和bias-pull-up都不能使用了,因为已经禁用了内部电压,所以不能配置上下拉。enum类型使用方法更简单跟C语言的一样,比如要设置PIN速度为最低就可以使用"slew-rate=<0>"。
PIN驱动程序讲解
所有的东西都已经准备好了,包括寄存器地址和寄存器值,Linux内核相应的驱动文件就会根据这些值来做相应的初始化。接下来就找一下哪个驱动文件来做这一件事情,pinctrl节点中compatible属性的值为"st,stm32mp157-pinctrl",在Linux内核中全局搜索"pinctrl"字符串就会找到对应的驱动文件 。在文件drivers/pinctrl/stm32/pinctrl-stm32mp157.c中有如下内容:
第2334-2344行,of_device_id结构体类型的数组,在讲解设备树的时候说过,of_device_id里面保存着这个驱动文件的兼容性值,设备树中的compatible属性值会和of_device_id中的所有兼容性字符串比较,查看是否可以使用此驱动 。stm32mp157_pctrl_match结构体数组一共有两个兼容,分别为"st,stm32mp157-pinctrl"和 st,stm32mp157-z-pinctrl",设备树也定义了这两个兼容性值,因此pinctrl和pinctrl_z节点都会和此驱动匹配,所以pinctrl-stm32mp157.c会完成STM32MP1的PIN配置工作。
第2350-2357行,platform_driver是平台设备驱动,这个是后面章节要讲解的内容,platform_driver是个结构体,有个probe成员变量。在这里只需要知道,当设备和驱动匹配成功以后platform_driver的probe成员变量所代表的函数就会执行,在2351行设置probe成员变量为stm32_pctl_probe函数,因此在本章实验中stm32_pctl_probe这个函数就会执行,可以认为stm32_pctl_probe函数就是STM32MP157这个 SOC的PIN配置入口函数。
第2359-2362行,就是一个简单的驱动入口函数,platform_driver_register函数是一个标准的平台设备驱动注册函数,用于向Linux内核注册一个platform_driver,这里就是将stm32mp157_pinctrl_driver注册到Linux内核总线,关于平台设备驱动后面章节会详细讲解。
重点来分析一下stm32_pctl_probe函数,函数定义在drivers/pinctrl/stm32/pinctrl-stm32.c里面,函数内容如下所示:
第1458行,看它的结构体的名字就知道是ST官方自定义的一个结构体类型,用于存放STM32相关PIN属性集合 。要想驱动能通用,就要用结构体来保存数据和驱动里面尽量不要使用全局变量(在pinctrl驱动里就没有一个全局变量,全部使用结构体来描述一个物体,物体的所有属性都作为结构体成员变量 )。接着去看下stm32_pinctrl结构体是如何定义的,如下图所示:
第103行,pinctrl_desc结构体用来描述PIN控制器,PIN控制器用于配置SOC的PIN复用功能和电气特性。
第107行,这个stm32_gpio_bank结构体,是用来注册GPIO驱动。到后面GPIO子系统再说。
pinctrl_desc结构体内容如下所示:
第134-136行,这三个"_ops"结构体指针非常重要!因为这三个结构体就是PIN控制器的"工具",这三个结构体里面包含了很多操作函数,Linux内核初始化PIN最终使用的就是这些操作函数 。因此编写一个SOC的PIN控制器驱动的核心就是实现pinctrl_desc里面的pctlops、pmxops和confops ,pinctrl_desc结构体需要由用户提供,结构体里面的成员变量也是用户编写的。但是这个用户并不是我们这些使用芯片的程序员,而是半导体厂商,半导体厂商发布的Linux内核源码中已经把这些工作做完了。
示例代码 25.1.2.8里,第1536-1538行,给这三个结构体赋值分别对应stm32_pconf_ops、stm32_prtrl_ops和stm32_pmx_ops,三个结构体如下:
pinctrl_desc结构体初始化完成以后,需要调用pinctrl_register或者devm_pinctrl_register函
数就能够向Linux内核注册一个PIN控制器,示例代码25.1.2.8中的1542行就是向Linux内核注册PIN控制器。
总结一下,pinctrl驱动流程如下:
- 定义pinctrl_desc结构体。
- 初始化结构体, 重点是pinconf_ops、pinmux_ops和pinctrl_ops这三个结构体成员变量,但是这部分半导体厂商帮我们搞定。
- 调用devm_pinctrl_register函数完成PIN控制器注册。
设备树中添加pinctrl节点模板
接下来学习一下如何在设备树中添加某个外设的PIN信息 。比如需要将PG11这个PIN复用为UART4_TX引脚,pinctrl节点添加过程如下:
- 创建对应节点:在pinctrl节点下添加"uart4_pins"节点。
- 添加"pins"属性:这个子节点是真正来描述PIN配置的,同一个pins子节点需要所有PIN电气属性一样。
- 在"pins"节点中添加PIN配置信息。
最终结果如下:
c
示例代码25.1.3.3完整的uart4_pins设备pinctrl子节点
1 &pinctrl {
2 uart4_pins: uart4-0 {
3 pins1{
4 pinmux = <STM32_PINMUX('G', 11, AF6)>; /* UART4_TX */
5 bias-disable;
6 drive-push-pull;
7 };
8 };
9 };
按理来说,将IO用作GPIO功能也需要创建对应pinctrl节点,并将IO复用为GPIO,但是对于STM32MP1而言,如果IO用作GPIO功能,就可以不需要创建对应的pinctrl节点。
gpio子系统
gpio子系统简介
上一小节讲解了pinctrl子系统,pinctrl子系统重点是设置PIN(有的SOC叫做PAD)的复用和电气属性 ,如果pinctrl子系统将一个PIN复用为GPIO的话,那么接下来就要用到gpio子系统了。gpio子系统顾名思义,就是用于初始化GPIO并且提供相应的API函数,比如设置GPIO为输入输出,读取GPIO的值等 。gpio子系统的主要目的就是方便驱动开发者使用gpio,驱动开发者在设备树中添加gpio相关信息,然后就可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,Linux内核向驱动开发者屏蔽掉了GPIO的设置过程,极大的方便了驱动开发者使用GPIO。
STM32MP1的gpio子系统驱动
设备树中的gpio信息
以PI0引脚所在的GPIOI为例,打开stm32mp151.dtsi,找到如下内容:
第1912-1921行就是GPIOI的控制器信息,属于pincrtl的子节点,因此对于STM32MP1而言,pinctrl和gpio这两个子系统的驱动文件是一样的,都为pinctrl-stm32mp157.c ,所以在注册pinctrl驱动顺便会把gpio驱动程序一起注册。绑定文档
Documentation/devicetree/bindings/gpio/gpio.txt详细描述了gpio控制器节点各个属性信息。
第1913行,"gpio-controller"表示gpioi节点是个GPIO控制器,每个GPIO控制器节点必须包含"gpio-controller"属性。
第1914行,"#gpio-cells"属性和"#address-cells"类似,#gpio-cells应该为2,表示一共有两个cell,第一个cell为GPIO编号,比如"&gpioi 0"就表示PI0。第二个cell表示GPIO极性,如果为0(GPIO_ACTIVE_HIGH)的话表示高电平有效,如果为1(GPIO_ACTIVE_LOW)的话表示低电平有效。
第1917行,reg属性设置了GPIOI控制器的寄存器基地址偏移为0X800,因此GPIOI寄存器地址为0X50002000+0X800=0X5000A000,可以打《STM32MP157参考手册》,就可以找到GPIOI控制器的基地址就是0X5000A000。
第1918行,clocks属性指定这个GPIOI控制器的时钟。
示例代码25.2.2.1中的是GPIOI控制器节点,当某个具体的引脚作为GPIO使用的时候还需要进一步设置。ST官方EVK开发板将PG1用作SD卡的检测(CD)引脚, PG1复用为GPIO功能,通过读取这个 GPIO的高低电平就可以知道SD卡有没有插入 。这里肯定需要设备树来告诉驱动,在设备树中的SD卡节点下添加一个属性来描述SD卡的CD引脚就行了,SD卡驱动直接读取这个属性值就知道SD卡的CD引脚使用的是哪个GPIO了 。 ST官方EVK开饭的SD卡连接在STM32MP157的sdmmc1接口上,在stm32mp15xx-edx.dtsi中找到名为"sdmmc1"的节点,这个节点就是SD卡设备节点,如下所示:
第338行,**属性"cd-gpios"描述了SD卡的CD引脚使用的哪个IO。**属性值一共有三个,来看一下这三个属性值的含义,"&gpiog"表示CD引脚所使用的IO属于GPIOG组,"1"表示 GPIOG组的第1号 IO,通过这两个值SD卡驱动程序就知道CD引脚使用了PG1这个GPIO。最后一个是"GPIO_ACTIVE_LOW | GPIO_PULL_UP",Linux内核定义在include/linux/gpio/machine.h文件中定义了枚举类型gpio_lookup_flags,内容如下:
可以通过或运算组合不同的配置内容,示例代码25.2.2.2中的338行,"GPIO_ACTIVE_LOW"表示低电平有效,"GPIO_PULL_UP"表示上拉,所以PG1引脚默认上拉,而且低电平有效(当 PG1被拉低的时候表示SD卡插入)。
GPIO驱动程序简介
前面一小节说过了STM32MP1的pinctrl驱动和gpio驱动是同一个驱动文件,都为pinctrl-stm32mp157.c,所以入口函数都是stm32_pctl_probe ,找到如下代码所示:
第1586行,判断设备树节点,是否有gpio-controller。如果存在,那么这个节点就是一个
GPIO控制器节点。
第1587行,stm32_gpiolib_register_bank函数用来注册GPIO驱动,包括生成回调函数,注册的过程是跟pinctrl驱动注册是一样的。都是创建自己的结构体,然后初始化结构体,调用内核的注册函数,这样把自己的结构体注册到内核。
gpio子系统API函数
对于驱动开发人员,设置好设备树以后就可以使用gpio子系统提供的API函数来操作指定的GPIO,gpio子系统向驱动开发人员屏蔽了具体的读写寄存器过程。gpio子系统提供的常用API有如下几个。
gpio_request函数
该函数用于申请一个GPIO管脚,在使用一个GPIO之前一定要使用gpio_request进行申请,函数原型如下:
c
int gpio_request(unsigned gpio, const char *label)
函数参数和返回值含义如下:
- gpio:要申请的gpio标号,使用of_get_named_gpio函数从设备树获取指定GPIO属性信息,此函数会返回这个GPIO的标号。
- label:给gpio设置个名字。
- 返回值:0,申请成功;其他值,申请失败。
gpio_free函数
如果不使用某个GPIO了,那么就可以调用gpio_free函数进行释放。函数原型如下:
c
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
- gpio:要释放的gpio标号。
- 返回值:无。
gpio_direction_input函数
此函数用于设置某个GPIO为输入 ,函数原型如
下所示:
c
int gpio_direction_input(unsigned gpio)
函数参数和返回值含义如下:
- gpio:要设置为输入的GPIO标号。
- 返回值:0,设置成功;负值,设置失败。
gpio_direction_output函数
此函数用于设置某个GPIO为输出,并且设置默认输出值,函数原型如下:
c
int gpio_direction_output(unsigned gpio, int value)
函数参数和返回值含义如下:
- gpio:要设置为输出的GPIO标号。
- value:GPIO默认输出值。
- 返回值:0,设置成功;负值,设置失败。
gpio_get_value函数
此函数用于获取某个GPIO的值 (0或1),此函数是个宏,定义如下所示:
c
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
- gpio:要获取的GPIO标号。
- 返回值:非负值,得到的GPIO值;负值,获取失败。
gpoio_set_value函数
此函数用于设置某个GPIO的值,此函数是个宏,定义如下:
c
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
- gpio:要设置的GPIO标号。
- value:要设置的值。
- 返回值:无。
设备树中添加gpio节点模板
以正点原子STM32MP157开发板的LED0为例,学习创建GPIO节点。LED0连到PI0引脚上:
- 创建led设备节点。
- 添加GPIO属性信息。
完成后如下所示:
c
示例代码25.2.4.2 向led节点添加gpio属性
1 led {
2 compatible = "atk,led";
3 gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
4 status = "okay";
5 };
gpio相关OF函数
在驱动程序中需要读取gpio属性内容, Linux内核提供了几个与GPIO有关的OF函数,常用的几个OF函数如下所示:
of_gpio_named_count函数
该函数用于获取设备树某个属性里面定义了几个 GPIO信息,要注意的是空的GPIO信息也会被统计到。此函数原型如下:
c
int of_gpio_named_count(struct device_node *np, const char *propname)
函数参数和返回值含义如下:
- np:设备节点。
- propname:要统计的GPIO属性。
- 返回值:正值,统计到的GPIO数量;负值,失败。
of_gpio_count函数
此函数统计的是"gpios"这个属性的GPIO数量,而of_gpio_named_count函数可以统计任意属性的GPIO信息,函数原型如下所示:
c
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
- np:设备节点。
- 返回值:正值,统计到的GPIO数量;负值,失败。
of_get_named_gpio函数
此函数获取GPIO编号,因为Linux内核中关于GPIO的API函数都要使用GPIO编号,此函数会将设备树中类似<&gpioi 0(GPIO_ACTIVE_LOW | GPIO_PULL_UP)>的属性信息转换为对应的GPIO编号,此函数在驱动中使用很频繁!函数原型如下:
c
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
函数参数和返回值含义如下:
- np:设备节点。
- propname:包含要获取GPIO信息的属性名。
- index:GPIO索引,因为一个属性里面可能包含多个GPIO,此参数指定要获取哪个GPIO的编号,如果只有一个GPIO信息的话此参数为0。
- 返回值:正值,获取到的GPIO编号;负值,失败。
硬件原理图
就是之前的LED原理图。
实验程序编写
修改设备树文件
在stm32mp157d-atk.dts文件的根节点"/"下创建LED灯节点,节点名为"gpioled",节点内容如下:
c
示例代码25.4.1.1 创建LED灯节点
1 gpioled {
2 compatible = "alientek,led";
3 status = "okay";
4 led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
5 };
第4行,led-gpio属性指定了LED灯所使用的GPIO,在这里就是GPIOI的0号,低电平有效。稍后编写驱动程序的时候会获取led-gpio属性的内容来得到GPIO编号 ,因为gpio子系统的API操作函数需要GPIO编号。设备树编写完成以后使用"make dtbs"命令重新编译设备树,然后使用新编译出来的stm32mp157d-atk.dtb文件启动Linux系统。启动成功以后进入"/proc/device-tree"目录中查看"gpioled"节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证)。
LED灯驱动程序编写
在之前设备树类似裸机开发的LED驱动基础上进行改写。
主要的区别在于:
需要再色河北结构体gpioled_dev中添加led_gpio这个成员,保存LED使用的GPIO编号;然后将gpioled这个设备结构体变量设为filp的私有数据private_data;之后读取private_data得到设备结构体变量gpioled;然后就可以直接在led_write中使用gpio_set_value来完成LED开关。
之后在led_init中,通过of_find_node_by_path("/gpioled")来获取设备节点并保存到gpioled.nd中;之后通过of_property_read_string读取status属性;通过of_property_read_string获取compatible属性;通过of_get_named_gpio读取LED编号,存入gpioled.led_gpio中;之后通过gpio_request申请使用GPIO,再由gpio_direction_output输出高电平来默认关闭LED。
编写测试APP
可以直接用之前的ledApp.c文件。
运行测试
编译驱动程序和测试APP
驱动只需要把Makefile中obj-m的值改为gpioled.o然后"make -j8"即可,APP可以用如下命令编译:
|-------------------------------------------------|
| arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp |
运行测试
将之前编译得到的gpioled.ko和ledApp拷贝到rootfs/lib/modules/5.4.31目录中,然后重启开发板,进入/lib/modules/5.4.31目录,输入如下命令加载gpioled.ko:
|----------------------------------------------------|
| depmod //第一次加载驱动的时候需要运行此命令 modprobe gpioled //加载驱动 |
加载成功后可以通过如下命令打开和关闭LED:
|-------------------------------------------------------------------|
| ./ledApp /dev/gpioled 1 //打开LED灯 ./ledApp /dev/gpioled 0 //关闭LED灯 |
可以通过如下命令卸载驱动:
|-----------------|
| rmmod dtsled.ko |
总结
本章通过Linux最常用的pinctrl和gpio子系统,结合设备树来控制LED灯。主要是通过修改设备树stm32mp157d-atk.dts添加gpioled节点;然后在驱动程序中,在gpioled_dev结构体中添加int led_gpio记录GPIO编号并申请结构体gpioled代表led设备;led_write中,就可以通过private_data获取设备信息,然后由gpio_set_value来控制LED;led_init中就通过OF函数找到节点之后,获取status和compatible属性来匹配,最终由of_get_named_gpio读取LED编号,并由gpio_request申请对LED的使用并通过gpio_direction_output输出高电平。