【正点原子MP157连载】第二十五章 pinctrl和gpio子系统实验-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
在这里插入图片描述

第二十五章 pinctrl和gpio子系统实验

上一章我们编写了基于设备树的LED驱动,但是驱动的本质还是没变,都是配置LED灯所使用的GPIO寄存器,驱动开发方式和裸机基本没啥区别。Linux是一个庞大而完善的系统,尤其是驱动框架,像GPIO这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。Linux内核提供了pinctrl和gpio子系统用于GPIO驱动,本章我们就来学习一下如何借助pinctrl和gpio子系统来简化GPIO驱动开发。

25.1 pinctrl子系统
25.1.1 pinctrl子系统简介
Linux驱动讲究驱动分离与分层,pinctrl和gpio子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架,关于驱动的分离与分层我们后面会讲。本来pinctrl和gpio子系统应该放到驱动分离与分层章节后面讲解,但是不管什么外设驱动,GPIO驱动基本都是必须的,而pinctrl和gpio子系统又是GPIO驱动必须使用的,所以就将pintrcl和gpio子系统这一章节提前了。
我们先来回顾一下上一章是怎么初始化LED灯所使用的GPIO,步骤如下:
①、修改设备树,添加相应的节点,节点里面重点是设置reg属性,reg属性包括了GPIO相关寄存器。
②、 获取reg属性中GPIOI_MODER、GPIOI_OTYPER、GPIOI_OSPEEDR、GPIOI_PUPDR和GPIOI_BSRR这些寄存器的地址,并且初始化它们,这些寄存器用于设置PI0这个PIN的复用功能、上下拉、速度等。
③、 在②里面将PI0这个PIN设置为通用输出功能,因此需要设置PI0这个GPIO相关的寄存器,也就是设置GPIOI_MODER寄存器。
④、在②里面将PI0这个PIN设置为高速、上拉和推挽模式,就要需要设置PI0的GPIOI_OTYPER、GPIOI_OSPEEDR和GPIOI_PUPDR这些寄存器。
总结一下,②中完成对PI0这个PIN的获取相关的寄存器地址,③和④设置这个PIN的复用功能、上下拉等,比如将GPIOI_0这个PIN设置为通用输出功能。如果使用过STM32单片机的话应该都记得,STM32单片机也是要先设置某个PIN的复用功能、速度、上下拉等,然后再设置PIN所对应的GPIO,STM32MP1是从STM32单片机发展而来的,设置GPIO是一样。其实对于大多数的32位SOC而言,引脚的设置基本都是这两方面,因此Linux内核针对PIN的复用配置推出了pinctrl子系统,对于GPIO的电气属性配置推出了gpio子系统。本小节我们来学习pinctrl子系统,下一小节再学习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。
25.1.2 STM32MP1的pinctrl子系统驱动
1、PIN配置信息详解
要使用pinctrl子系统,我们需要在设备树里面设置PIN的配置信息,毕竟pinctrl子系统要根据你提供的信息来配置PIN功能,一般会在设备树里面创建一个节点来描述PIN的配置信息。打开stm32mp151.dtsi文件,找到一个叫做pinctrl的节点,如下所示:

示例代码25.1.2.1 pinctrl节点内容1

1814        pinctrl: pin-controller@50002000 {
1815            #address-cells = <1>;
1816            #size-cells = <1>;
1817            compatible = "st,stm32mp157-pinctrl";
1818            ranges = <0 0x50002000 0xa400>;
1819            interrupt-parent = <&exti>;
1820            st,syscfg = <&exti 0x60 0xff>;
1821            hwlocks = <&hsem 0 1>;
1822            pins-are-numbered;
......
1968        };

第1815~1816行,#address-cells属性值为1和#size-cells属性值为1,也就是说pinctrl下的所有子节点的reg第一位是起始地址,第二位为长度。
第1818行,ranges属性表示STM32MP1的GPIO相关寄存器起始地址,STM32MP1系列芯片最多拥有176个通用GPIO,分为12组,分别为:PA0PA15、PB0PB15、PC0PC15、PD0PD15、PE0PE15、PF0PF15、PG0PG15、PH0PH15、PJ0PJ15、PK0PK7、PZ0~PZ7。
其中PAPK这9组GPIO的寄存器都在一起,起始地址为0X50002000,终止地址为0X5000C3FF。这个可以在《STM32MP157参考手册》里面找到。PZ组寄存器起始地址为0X54004000,终止地址为0X540043FF,所以stm32mp151.dtsi文件里面还有个名为“pinctrl_z”的子节点来描述PZ组IO。pinctrl节点用来描述PAPK这11组IO,因此ranges属性中的0x50002000表示起始地址,0xa400表示寄存器地址范围。
第1819行,interrupt-parent属性值为“&exti”,父中断为exti。
后面的gpiox子节点先不分析,这些子节点都是gpio子系统的内容,到后面在去分析。这个节点看起来,根本没有PIN先关的配置,别急!先打开stm32mp15-pinctrl.dtsi文件,你们能找到如下内容:

    示例代码25.1.2.2 pinctrl节点内容2
1   &pinctrl {
......
534     m_can1_pins_a: m-can1-0 {
535         pins1 {
536             pinmux = <STM32_PINMUX('H', 13, AF9)>; 	/* CAN1_TX */
537             slew-rate = <1>;
538             drive-push-pull;
539             bias-disable;
540         };
541         pins2 {
542             pinmux = <STM32_PINMUX('I', 9, AF9)>; 	/* CAN1_RX */
543             bias-disable;
544         };
545     };
......
554     pwm1_pins_a: pwm1-0 {
555         pins {
556             pinmux = <STM32_PINMUX('E', 9, AF1)>, /* TIM1_CH1 */
557                  <STM32_PINMUX('E', 11, AF1)>, 		/* TIM1_CH2 */
558                  <STM32_PINMUX('E', 14, AF1)>; 		/* TIM1_CH4 */
559             bias-pull-down;
560             drive-push-pull;
561             slew-rate = <0>;
562         };
563     };
......
1411};

示例代码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信息:
每个pincrtl节点必须至少包含一个子节点来存放pincrtl相关信息,也就是pinctrl集,这个集合里面存放当前外设用到哪些引脚(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,示例代码25.1.2.2中第536行的pinmux属性内容如下:
pinmux = <STM32_PINMUX(‘H’, 13, AF9)>;
可以看出,这里使用STM32_PINMUX这宏来配置引脚和引脚的复用功能,此宏定义在include/dt-bindings/pinctrl/stm32-pinfunc.h文件里面,内容如下:

示例代码25.1.2.3 STM32_PINMUX宏定义
32 #define PIN_NO(port, line)   (((port) - 'A') * 0x10 + (line))
33 
34 #define STM32_PINMUX(port, line, mode) (((PIN_NO(port, line)) << 8) 
| (mode))
可以看出,STM32_PINMUX宏有三个参数,这三个参数含义如下所示:
port:表示用那一组GPIO(例:H表示为GPIO第H组,也就是GPIOH)。

line:表示这组GPIO的第几个引脚(例:13表示为GPIOH_13,也就是PH13)。
mode:表示当前引脚要做那种复用功能(例:AF9表示为用第9个复用功能),这个需要查阅STM32MP1数据手册来确定使用哪个复用功能。打开《STM32MP157A&D数据手册》,一个IO最大有16种复用方法:AF0~AF15,打开第4章“Pinouts, pin description and alternate functions”,其中“Table 8. Alternate function AF0 to AF7”描述的是AF0~AF7这8种复用方式,“Table 9. Alternate function AF8 to AF15”描述的是AF8~AF15这8中复用方式。找到图25.1.2.1所示内容:
在这里插入图片描述

图25.1.2.1 PH13引脚复用功能
可以看出,PH13要设置为FDCAN1的TX引脚,就要使用AF9这个复用配置,这个就是如何在pinmux属性中添加一个引脚的复用配置。从图25.1.2.1中可以看出,如果PH13也可以复用为UART4的TX引脚,只要设置为AF8即可,此时相关的引脚配置如下所示:

示例代码25.1.2.3 myuart4 pinctrl节点
1   &pinctrl {
2       
3       myuart4_pins_a: myuart4-0 {
4           pins{
5               pinmux = <STM32_PINMUX('H', 13, AF8)>;
6           };
7       };
8   };
这时候大家可能会奇怪?PH13做了FDCAN1的TX功能,还能做UART4的TX功能,不是冲突了吗?这是不会冲突的,因为pinctrl驱动只会把设备树pinctrl节点解析出来的数据存储到一个链表里,只有当外设调用这个pinctrl节点的时候才会真的使用。但是,如果你要同时使用FDCAN1和UART4的话就会出问题,因此PH13同一时间只能用于一个外设,所以为了方便开发,还是建议大家一个PIN最好只能被一个外设使用。 
stm32-pinfunc.h文件里面定义了一个PIN 的所有配置项AF0~AF9,如下所示:
示例代码25.1.2.4 stm32-pinfunc.h文件
10  /*  define PIN modes */
11  #define GPIO    	0x0
12  #define AF0 		0x1
13  #define AF1 		0x2
14  #define AF2 		0x3
15  #define AF3 		0x4
16  #define AF4 		0x5
17  #define AF5 		0x6
18  #define AF6 		0x7
19  #define AF7 		0x8
20  #define AF8 		0x9
21  #define AF9 		0xa
22  #define AF10    	0xb
23  #define AF11    	0xc
24  #define AF12    	0xd
25  #define AF13    	0xe
26  #define AF14    	0xf
27  #define AF15    	0x10
28  #define ANALOG  	0x11
29  #define RSVD    	0x12
可以看出,除了A0~AF15,还有GPIO、ANALOG这两个,如果一个PIN只是作为最基本的GPIO功能,那么就是用“GPIO”;如果这个引脚要用作模拟功能,比如ADC采集引脚,那么就设置为“ANALOG”。
2、电气属性配置

接下来了解一下PIN的电气特性如何设置,电气特性在pinctrl子系统里不是必须的,可以不配置,但是pinmux属性是必须要设置的。stm32-pinctrl.yaml文件里面也描述了如何设置STM32的电气属性,如表25.1.2.1所示:
在这里插入图片描述
表25.1.2.1 pinctrl的电气特性
表25.1.2.1里的bootlean类型表示了在pinctrl子系统只要定义这个电气属性就行了,例如:我要禁用内部电压,只要在PIN的配置集里添加“bias-disable”即可,这个时候bias-pull-down和bias-pull-up都不能使用了,因为已经禁用了内部电压,所以不能配置上下拉。enum类型使用方法更简单跟C语言的一样,比如要设置PIN速度为最低就可以使用“slew-rate=<0>”。在示例代码25.1.2.3里添加引脚的电气特性组合成,如示例代码25.1.2.6所示:

示例代码25.1.2.6 添加电气特性的myuart4-0节点
1   &pinctrl {
2        
3       myuart4_pins_a: myuart4-0 {
4           pins1{
5               pinmux = <STM32_PINMUX('H', 13, AF8)>;
6               bias-pull-up;
7               drive-push-pull;
8             };
9       };
10  };
在第6~7行里给myuart4-0添加了两个电气属性分别为内部上拉和推挽输出,这样我们就设置好一个PIN配置了。

2、PIN驱动程序讲解
本小节会涉及到Linux驱动分层与分离、平台设备驱动等还未讲解的知识,所以本小节教程可以不用看,不会影响后续的实验。如果对Linux内核的pinctrl子系统实现原理感兴趣的话可以看本小节。
所有的东西都已经准备好了,包括寄存器地址和寄存器值,Linux内核相应的驱动文件就会根据这些值来做相应的初始化。接下来就找一下哪个驱动文件来做这一件事情,pinctrl节点中compatible属性的值为“st,stm32mp157-pinctrl”,在Linux内核中全局搜索“pinctrl”字符串就会找到对应的驱动文件。在文件drivers/pinctrl/stm32/pinctrl-stm32mp157.c中有如下内容:

示例代码25.1.2.7 pinctrl-stm32mp157.c文件代码段
2323    static struct stm32_pinctrl_match_data stm32mp157_match_data = {
2324        .pins = stm32mp157_pins,
2325        .npins = ARRAY_SIZE(stm32mp157_pins),
2326    };
2327
2328    static struct stm32_pinctrl_match_data stm32mp157_z_match_data = {
2329        .pins = stm32mp157_z_pins,
2330        .npins = ARRAY_SIZE(stm32mp157_z_pins),
2331        .pin_base_shift = STM32MP157_Z_BASE_SHIFT,
2332    };
2333
2334    static const struct of_device_id stm32mp157_pctrl_match[] = {
2335        {
2336            .compatible = "st,stm32mp157-pinctrl",
2337            .data = &stm32mp157_match_data,
2338        },
2339        {
2340            .compatible = "st,stm32mp157-z-pinctrl",
2341            .data = &stm32mp157_z_match_data,
2342        },
2343        { }
2344    };
2345
2346    static const struct dev_pm_ops stm32_pinctrl_dev_pm_ops = {
2347         SET_LATE_SYSTEM_SLEEP_PM_OPS(NULL, stm32_pinctrl_resume)
2348    };
2349
2350    static struct platform_driver stm32mp157_pinctrl_driver = {
2351        .probe = stm32_pctl_probe,
2352        .driver = {
2353            .name = "stm32mp157-pinctrl",
2354            .of_match_table = stm32mp157_pctrl_match,
2355            .pm = &stm32_pinctrl_dev_pm_ops,
2356        },
2357    };
2358
2359    static int __init stm32mp157_pinctrl_init(void)
2360    {
2361        return platform_driver_register(&stm32mp157_pinctrl_driver);
2362    }
2363    arch_initcall(stm32mp157_pinctrl_init);
2364

第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里面,函数内容如下所示:

示例代码25.1.2.8 stm32_pctl_probe代码段
1452    int stm32_pctl_probe(struct platform_device *pdev)
1453    {
......
1458        struct stm32_pinctrl *pctl;
......
1530
1531        pctl->pctl_desc.name = dev_name(&pdev->dev);
1532        pctl->pctl_desc.owner = THIS_MODULE;
1533        pctl->pctl_desc.pins = pins;
1534        pctl->pctl_desc.npins = pctl->npins;
1535        pctl->pctl_desc.link_consumers = true;
1536        pctl->pctl_desc.confops = &stm32_pconf_ops;
1537        pctl->pctl_desc.pctlops = &stm32_pctrl_ops;
1538        pctl->pctl_desc.pmxops = &stm32_pmx_ops;
1539        pctl->dev = &pdev->dev;
1540        pctl->pin_base_shift = pctl->match_data->pin_base_shift;
1541
1542        pctl->pctl_dev = devm_pinctrl_register(&pdev->dev, 
1543                         &pctl->pctl_desc, pctl);
......
1600}

第1458行,看它的结构体的名字就知道是ST官方自定义的一个结构体类型,用于存放STM32相关PIN属性集合。第22.5.1小节我们驱动代码也添加了自己的结构体,它们都有自己的结构体这样做有啥好处?可以实现一个驱动代码“通杀”多个设备。要想驱动能通用,就要用结构体来保存数据和驱动里面尽量不要使用全局变量(在pinctrl驱动里就没有一个全局变量,全部使用结构体来描述一个物体,物体的所有属性都作为结构体成员变量)。接着我们去看下stm32_pinctrl结构体是如何定义的,如示例代码25.1.2.9所示:

示例代码25.1.2.9 stm32_pinctrl结构体代码段
100 struct stm32_pinctrl {
......
103     struct pinctrl_desc pctl_desc;
......
107		struct stm32_gpio_bank *banks;
......
120 };

第103行,pinctrl_desc结构体用来描述PIN控制器,PIN控制器,PIN控制器用于配置SOC的PIN复用功能和电气特性。
第107行,这个stm32_gpio_bank结构体,是用来注册GPIO驱动。到后面GPIO子系统在说。
pinctrl_desc结构体内容如下所示:

示例代码25.1.2.10 pinctrl_desc结构体
130 struct pinctrl_desc {
131     const char *name;
132     const struct pinctrl_pin_desc *pins;
133     unsigned int npins;
134     const struct pinctrl_ops *pctlops;
135     const struct pinmux_ops *pmxops;
136     const struct pinconf_ops *confops;
137     struct module *owner;
138 #ifdef CONFIG_GENERIC_PINCONF
139     unsigned int num_custom_params;
140     const struct pinconf_generic_params *custom_params;
141     const struct pin_config_item *custom_conf_items;
142 #endif
143     bool link_consumers;
144 };

第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,三个结构体如下:

示例代码25.1.2.11 stm32_pconf_ops、stm32_prtrl_ops和stm32_pmx_ops结构体
714     static const struct pinctrl_ops stm32_pctrl_ops = {
715         .dt_node_to_map     	= stm32_pctrl_dt_node_to_map,
716         .dt_free_map        	= pinctrl_utils_free_map,
717         .get_groups_count   	= stm32_pctrl_get_groups_count,
718         .get_group_name     	= stm32_pctrl_get_group_name,
719         .get_group_pins     	= stm32_pctrl_get_group_pins,
720     };
......
865     static const struct pinmux_ops stm32_pmx_ops = {
866         .get_functions_count 	= stm32_pmx_get_funcs_cnt,
867         .get_function_name  	= stm32_pmx_get_func_name,
868         .get_function_groups 	= stm32_pmx_get_func_groups,
869         .set_mux        		= stm32_pmx_set_mux,
870         .gpio_set_direction 	= stm32_pmx_gpio_set_direction,
871         .strict        	 		= true,
872     };
......
1238    static const struct pinconf_ops stm32_pconf_ops = {
1239        .pin_config_group_get	= stm32_pconf_group_get,
1240        .pin_config_group_set   	= stm32_pconf_group_set,
1241        .pin_config_dbg_show    	= stm32_pconf_dbg_show,
1242        .pin_config_set     		= stm32_pconf_set,
1243    };
pinctrl_desc结构体初始化完成以后,需要调用pinctrl_register或者devm_pinctrl_register函数就能够向Linux内核注册一个PIN控制器,示例代码25.1.2.8中的1542行就是向Linux内核注册PIN控制器。
总结一下,pinctrl驱动流程如下:

1、定义pinctrl_desc结构体。
2、初始化结构体,重点是pinconf_ops、pinmux_ops和pinctrl_ops这三个结构体成员变量,但是这部分半导体厂商帮我们搞定。
3、调用devm_pinctrl_register函数完成PIN控制器注册。
25.1.3 设备树中添加pinctrl节点模板
我们已经对pinctrl有了比较深入的了解,接下来我们学习一下如何在设备树中添加某个外设的PIN信息。比如我们需要将PG11这个PIN复用为UART4_TX引脚,pinctrl节点添加过程如下:
1、创建对应的节点
在pinctrl节点下添加一个“uart4_pins”节点:

	示例代码25.1.3.1 uart4_pins设备节点
1   &pinctrl {
2       uart4_pins: uart4-0 {
3       /* 具体的PIN信息 */
4       };
5   };

2、添加“pins”属性
添加一个“pins”子节点,这个子节点是真正用来描述PIN配置信,要注意,同一个pins子节点下的所有PIN电气属性要一样。如果某个外设所用的PIN可能有不同的配置,那么就需要多个pins子节点,例如UART4的TX和RX引脚配置不同,因此就有pins1和pins2两个子节点。这里我们只添加UART4的TX引脚,所以添加完pins1子节点以后如下所示:

示例代码25.1.3.2 uart4_pins设备节点下的pins1属性
1   &pinctrl {
2       uart4_pins: uart4-0 {
3           pins1{
4      			/* UART4 TX引脚的PIN配置信息 */
5           };
6       };
7   };

3、在“pins”节点中添加PIN配置信息
最后在“pins”节点中添加具体的PIN配置信息,完成以后如下所示:

示例代码25.1.3.3完整的uart4_pins设备pinctrl子节点
1   &pinctrl {
2       gpio_led: gpio-led-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功能,比如将PI0复用为GPIO的时候就需要设置pinmux属性为:<STM32_PINMUX('I', 0, GPIO)>,但是!对于STM32MP1而言,如果一个IO用作GPIO功能的时候不需要创建对应的pinctrl节点!

25.2 gpio子系统
25.2.1 gpio子系统简介
上一小节讲解了pinctrl子系统,pinctrl子系统重点是设置PIN(有的SOC叫做PAD)的复用和电气属性,如果pinctrl子系统将一个PIN复用为GPIO的话,那么接下来就要用到gpio子系统了。gpio子系统顾名思义,就是用于初始化GPIO并且提供相应的API函数,比如设置GPIO为输入输出,读取GPIO的值等。gpio子系统的主要目的就是方便驱动开发者使用gpio,驱动开发者在设备树中添加gpio相关信息,然后就可以在驱动程序中使用gpio子系统提供的API函数来操作GPIO,Linux内核向驱动开发者屏蔽掉了GPIO的设置过程,极大的方便了驱动开发者使用GPIO。
25.2.2 STM32MP1的gpio子系统驱动
1、设备树中的gpio信息
首先肯定是GPIO控制器的节点信息,以PI0这个引脚所在的GPIOI为例,打开stm32mp151.dtsi,在里面找到如下所示内容:

示例代码25.2.2.1 gpioi控制器节点
1814    pinctrl: pin-controller@50002000 {
1815        #address-cells = <1>;
1816        #size-cells = <1>;
1817        compatible = "st,stm32mp157-pinctrl";
......
1912        gpioi: gpio@5000a000 {
1913            gpio-controller;
1914            #gpio-cells = <2>;
1915            interrupt-controller;
1916            #interrupt-cells = <2>;
1917            reg = <0x8000 0x400>;
1918            clocks = <&rcc GPIOI>;
1919            st,bank-name = "GPIOI";
1920            status = "disabled";
1921        };
1944    };
第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参考手册》,找到“Table 9. Register boundary addresses”章节的2.5.2 Memory map and register boundary addresses小节,如图25.2.2.1所示:

在这里插入图片描述

图25.2.2.1 GPIOI寄存器表
从图25.2.2.1可以看出,GPIOI控制器的基地址就是0X5000A000,这个地址是基于pinctrl的地址0X50002000+0x8000 = 0X5000A000。
第1918行,clocks属性指定这个GPIOI控制器的时钟。
示例代码25.2.2.1中的是GPIOI控制器节点,当某个具体的引脚作为GPIO使用的时候还需要进一步设置。ST官方EVK开发板将PG1用作SD卡的检测(CD)引脚,PG1复用为GPIO功能,通过读取这个GPIO的高低电平就可以知道SD卡有没有插入。但是,SD卡驱动程序怎么知道CD引脚连接的PG1呢?这里肯定需要设备树来告诉驱动,在设备树中的SD卡节点下添加一个属性来描述SD卡的CD引脚就行了,SD卡驱动直接读取这个属性值就知道SD卡的CD引脚使用的是哪个GPIO了。ST官方EVK开饭的SD卡连接在STM32MP157的sdmmc1接口上,在stm32mp15xx-edx.dtsi中找到名为“sdmmc1”的节点,这个节点就是SD卡设备节点,如下所示:

示例代码25.2.2.2 设备树中SD卡节点
333 &sdmmc1 {
334     pinctrl-names = "default", "opendrain", "sleep";
335     pinctrl-0 = <&sdmmc1_b4_pins_a &sdmmc1_dir_pins_a>;
336     pinctrl-1 = <&sdmmc1_b4_od_pins_a &sdmmc1_dir_pins_a>;
337     pinctrl-2 = <&sdmmc1_b4_sleep_pins_a &sdmmc1_dir_sleep_pins_a>;
338     cd-gpios = <&gpiog 1 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
339     disable-wp;
......
351     status = "okay";
352 };
第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.3 gpio_lookup_flag枚举类型
8  enum gpio_lookup_flags {
9   	GPIO_ACTIVE_HIGH    	= (0 << 0),
10  	GPIO_ACTIVE_LOW     	= (1 << 0),
11  	GPIO_OPEN_DRAIN    	= (1 << 1),
12  	GPIO_OPEN_SOURCE    	= (1 << 2),
13  	GPIO_PERSISTENT     	= (0 << 3),
14  	GPIO_TRANSITORY     	= (1 << 3),
15  	GPIO_PULL_UP         	= (1 << 4),
16  	GPIO_PULL_DOWN      	= (1 << 5),
17  	GPIO_LOOKUP_FLAGS_DEFAULT   = GPIO_ACTIVE_HIGH | GPIO_PERSISTENT,
18 };
我们可以通过或运算组合不同的配置内容,示例代码25.2.2.2中的338行, “GPIO_ACTIVE_LOW”表示低电平有效,“GPIO_PULL_UP”表示上拉,所以PG1引脚默认上拉,而且电平有效(当PG1被拉低的时候表示SD卡插入)。
这里也可以看出,把PG1用作GPIO的时候不需要添加其对应的pincrl节点!

2、GPIO驱动程序简介
在25.1.2小节里已经分析过pinctrl驱动代码了,前面一小节说过了STM32MP1的pinctrl驱动和gpio驱动是同一个驱动文件,都为pinctrl-stm32mp157.c,所以他们的入口函数都是stm32_pctl_probe,找到如下代码所示:

示例代码25.2.2.3 stm32_pctl_probe代码段
1452    int stm32_pctl_probe(struct platform_device *pdev)
1453    {
......
1585    for_each_available_child_of_node(np, child) {
1586        if (of_property_read_bool(child, "gpio-controller")) {
1587            ret = stm32_gpiolib_register_bank(pctl, child);
1588            if (ret) {
1589                of_node_put(child);
1590                return ret;
1591            }
1592        }
1593    }
......
1600}
第1586行,判断设备树节点,是否有gpio-controller。如果存在,那么这个节点就是一个GPIO控制器节点。

第1587行,stm32_gpiolib_register_bank函数用来注册GPIO驱动,包括生成回调函数,注册的过程是跟pinctrl驱动注册是一样的。都是创建自己的结构体,然后初始化结构体,调用内核的注册函数,这样把自己的结构体注册到内核。这边就不去分析了,大家可以试下分析GPIO驱动代码。
25.2.3 gpio子系统API函数
对于驱动开发人员,设置好设备树以后就可以使用gpio子系统提供的API函数来操作指定的GPIO,gpio子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离的好处,大家各司其职,做好自己的本职工作即可。gpio子系统提供的常用的API函数有下面几个:
1、gpio_request函数
gpio_request函数用于申请一个GPIO管脚,在使用一个GPIO之前一定要使用gpio_request进行申请,函数原型如下:
int gpio_request(unsigned gpio, const char *label)
函数参数和返回值含义如下:
gpio:要申请的gpio标号,使用of_get_named_gpio函数从设备树获取指定GPIO属性信息,此函数会返回这个GPIO的标号。
label:给gpio设置个名字。
返回值:0,申请成功;其他值,申请失败。
2、gpio_free函数
如果不使用某个GPIO了,那么就可以调用gpio_free函数进行释放。函数原型如下:
void gpio_free(unsigned gpio)
函数参数和返回值含义如下:
gpio:要释放的gpio标号。
返回值:无。
3、gpio_direction_input函数
此函数用于设置某个GPIO为输入,函数原型如下所示:
int gpio_direction_input(unsigned gpio)
函数参数和返回值含义如下:
gpio:要设置为输入的GPIO标号。
返回值:0,设置成功;负值,设置失败。
4、gpio_direction_output函数
此函数用于设置某个GPIO为输出,并且设置默认输出值,函数原型如下:
int gpio_direction_output(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio:要设置为输出的GPIO标号。
value:GPIO默认输出值。
返回值:0,设置成功;负值,设置失败。
5、gpio_get_value函数
此函数用于获取某个GPIO的值(0或1),此函数是个宏,定义所示:
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)
函数参数和返回值含义如下:
gpio:要获取的GPIO标号。
返回值:非负值,得到的GPIO值;负值,获取失败。
6、gpio_set_value函数
此函数用于设置某个GPIO的值,此函数是个宏,定义如下
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)
函数参数和返回值含义如下:
gpio:要设置的GPIO标号。
value:要设置的值。
返回值:无
关于gpio子系统常用的API函数就讲这些,这些是我们用的最多的。
25.2.4 设备树中添加gpio节点模板
本节我们以正点原子STM32MP157开发板上的LED0为例,学习一下如何创建GPIO节点。LED0连接到了PI0引脚上,首先创建一个“led”设备节点。
1、创建led设备节点
在根节点“/”下创建test设备子节点,如下所示:

示例代码25.2.4.1 led设备节点
1 led {
2   /* 节点内容 */
3 };
2、添加GPIO属性信息
在led节点中添加GPIO属性信息,表明test所使用的GPIO是哪个引脚,添加完成以后如下所示:
示例代码25.2.4.2 向led节点添加gpio属性
1 led {
2   compatible = "atk,led";
3   gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
4   status = "okay";
5 };
第3行,led设备所使用的gpio。

关于pinctrl子系统和gpio子系统就讲解到这里,接下来就使用pinctrl和gpio子系统来驱动STM32MP1开发板上的LED灯。
25.2.5 与gpio相关的OF函数
在示例代码25.2.4.2中,我们定义了一个名为“gpio”的属性,gpio属性描述了led这个设备所使用的GPIO。在驱动程序中需要读取gpio属性内容,Linux内核提供了几个与GPIO有关的OF函数,常用的几个OF函数如下所示:
1、of_gpio_named_count函数
of_gpio_named_count函数用于获取设备树某个属性里面定义了几个GPIO信息,要注意的是空的GPIO信息也会被统计到,比如:
gpios = <0
&gpio1 1 2
0
&gpio2 3 4>;
上述代码的“gpios”节点一共定义了4个GPIO,但是有2个是空的,没有实际的含义。通过of_gpio_named_count函数统计出来的GPIO数量就是4个,此函数原型如下:
int of_gpio_named_count(struct device_node *np, const char *propname)
函数参数和返回值含义如下:
np:设备节点。
propname:要统计的GPIO属性。
返回值:正值,统计到的GPIO数量;负值,失败。
2、of_gpio_count函数
和of_gpio_named_count函数一样,但是不同的地方在于,此函数统计的是“gpios”这个属性的GPIO数量,而of_gpio_named_count函数可以统计任意属性的GPIO信息,函数原型如下所示:
int of_gpio_count(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:正值,统计到的GPIO数量;负值,失败。
3、of_get_named_gpio函数
此函数获取GPIO编号,因为Linux内核中关于GPIO的API函数都要使用GPIO编号,此函数会将设备树中类似<&gpioi 0(GPIO_ACTIVE_LOW | GPIO_PULL_UP)>的属性信息转换为对应的GPIO编号,此函数在驱动中使用很频繁!函数原型如下:
int of_get_named_gpio(struct device_node *np,
const char *propname,
int index)
函数参数和返回值含义如下:
np:设备节点。
propname:包含要获取GPIO信息的属性名。
index:GPIO索引,因为一个属性里面可能包含多个GPIO,此参数指定要获取哪个GPIO的编号,如果只有一个GPIO信息的话此参数为0。
返回值:正值,获取到的GPIO编号;负值,失败。
25.3 硬件原理图分析
本实验的硬件原理参考21.2小节即可。
25.4 实验程序编写
本实验对应的例程路径为:开发板光盘 1、程序源码2、Linux驱动例程5_gpioled。
本章实验我们继续研究LED灯,在第二十四章实验中我们通过设备树向dtsled.c文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验我们使用gpio子系统来完成LED灯驱动。
25.4.1 修改设备树文件
在stm32mp157d-atk.dts文件的根节点“/”下创建LED灯节点,节点名为“gpioled”,节点内容如下:

示例代码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”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图25.4.1.1所示:

在这里插入图片描述

图25.4.1.1 gpioled子节点
25.4.2 LED灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,本章实验在第四十四章实验驱动文件dtsled.c的基础上修改而来。新建名为“5_gpioled”文件夹,然后在5_gpioled文件夹里面创建vscode工程,工作区命名为“gpioled”。工程创建好以后新建gpioled.c文件,在gpioled.c里面输入如下内容:

示例代码25.4.1.2 gpioled.c驱动文件代码
1   #include <linux/types.h>
2   #include <linux/kernel.h>
3   #include <linux/delay.h>
4   #include <linux/ide.h>
5   #include <linux/init.h>
6   #include <linux/module.h>
7   #include <linux/errno.h>
8   #include <linux/gpio.h>
9   #include <linux/cdev.h>
10  #include <linux/device.h>
11  #include <linux/of.h>
12  #include <linux/of_address.h>
13  #include <linux/of_gpio.h>
14  #include <asm/mach/map.h>
15  #include <asm/uaccess.h>
16  #include <asm/io.h>
17  /***************************************************************
18  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
19  文件名  		: gpioled.c
20  作者      	: 正点原子Linux团队
21  版本      	: V1.0
22  描述      	: gpio子系统驱动LED灯。
23  其他      	: 无
24  论坛      	: www.openedv.com
25  日志      	: 初版V1.0 2020/12/30 正点原子Linux团队创建
26  ***************************************************************/
27  #define GPIOLED_CNT     	1           		/* 设备号个数 	*/
28  #define GPIOLED_NAME    	"gpioled"   	/* 名字 		*/
29  #define LEDOFF           	0           		/* 关灯 		*/
30  #define LEDON            	1           		/* 开灯 		*/
31  
32  /* gpioled设备结构体 */
33  struct gpioled_dev{
34      dev_t devid;            		/* 设备号   	*/
35      struct cdev cdev;      	 	/* cdev   	*/
36      struct class *class;    		/* 类     	*/
37      struct device *device;  		/* 设备    	*/
38      int major;              		/* 主设备号 	*/
39      int minor;              		/* 次设备号 	*/
40      struct device_node  *nd; 	/* 设备节点 	*/
41      int led_gpio;           		/* led所使用的GPIO编号	*/
42  };
43  
44  struct gpioled_dev gpioled; 	/* led设备 */
45  
46  /*
47   * @description  	: 打开设备
48   * @param – inode	: 传递给驱动的inode
49   * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
50   *                    一般在open的时候将private_data指向设备结构体。
51   * @return       	: 0 成功;其他 失败
52   */
53  static int led_open(struct inode *inode, struct file *filp)
54  {
55      filp->private_data = &gpioled; /* 设置私有数据 */
56      return 0;
57  }
58  
59  /*
60   * @description  	: 从设备读取数据 
61   * @param - filp 	: 要打开的设备文件(文件描述符)
62   * @param - buf  	: 返回给用户空间的数据缓冲区
63   * @param - cnt  	: 要读取的数据长度
64   * @param - offt 	: 相对于文件首地址的偏移
65   * @return        	: 读取的字节数,如果为负值,表示读取失败
66   */
67  static ssize_t led_read(struct file *filp, char __user *buf, 
size_t cnt, loff_t *offt)
68  {
69      return 0;
70  }
71  
72  /*
73   * @description  	: 向设备写数据 
74   * @param – filp	: 设备文件,表示打开的文件描述符
75   * @param - buf  	: 要写给设备写入的数据
76   * @param - cnt  	: 要写入的数据长度
77   * @param - offt 	: 相对于文件首地址的偏移
78   * @return        	: 写入的字节数,如果为负值,表示写入失败
79   */
80  static ssize_t led_write(struct file *filp, const char __user *buf, 
size_t cnt, loff_t *offt)
81  {
82      int retvalue;
83      unsigned char databuf[1];
84      unsigned char ledstat;
85      struct gpioled_dev *dev = filp->private_data;
86  
87      retvalue = copy_from_user(databuf, buf, cnt); 
88      if(retvalue < 0) {
89          printk("kernel write failed!\r\n");
90          return -EFAULT;
91      }
92  
93      ledstat = databuf[0];       /* 获取状态值 */
94  
95      if(ledstat == LEDON) {  
96          gpio_set_value(dev->led_gpio, 0);   /* 打开LED灯 */
97      } else if(ledstat == LEDOFF) {
98          gpio_set_value(dev->led_gpio, 1);   /* 关闭LED灯 */
99      }
100     return 0;
101 }
102 
103 /*
104  * @description  	: 关闭/释放设备
105  * @param – filp	: 要关闭的设备文件(文件描述符)
106  * @return        	: 0 成功;其他 失败
107  */
108 static int led_release(struct inode *inode, struct file *filp)
109 {
110     return 0;
111 }
112 
113 /* 设备操作函数 */
114 static struct file_operations gpioled_fops = {
115     .owner = THIS_MODULE,
116     .open = led_open,
117     .read = led_read,
118     .write = led_write,
119     .release =  led_release,
120 };
121 
122 /*
123  * @description 	: 驱动出口函数
124  * @param       	: 无
125  * @return      	: 无
126  */
127 static int __init led_init(void)
128 {
129     int ret = 0;
130     const char *str;
131 
132     /* 设置LED所使用的GPIO 		*/
133     /* 1、获取设备节点:gpioled 	*/
134     gpioled.nd = of_find_node_by_path("/gpioled");
135     if(gpioled.nd == NULL) {
136         printk("gpioled node not find!\r\n");
137         return -EINVAL;
138     }
139 
140     /* 2.读取status属性 */
141     ret = of_property_read_string(gpioled.nd, "status", &str);
142     if(ret < 0) 
143         return -EINVAL;
144 
145     if (strcmp(str, "okay"))
146         return -EINVAL;
147     
148     /* 3、获取compatible属性值并进行匹配 */
149     ret = of_property_read_string(gpioled.nd, "compatible", &str);
150     if(ret < 0) {
151         printk("gpioled: Failed to get compatible property\n");
152         return -EINVAL;
153     }
154 
155     if (strcmp(str, "alientek,led")) {
156         printk("gpioled: Compatible match failed\n");
157         return -EINVAL;
158     }
159 
160     /* 4、 获取设备树中的gpio属性,得到LED所使用的LED编号 */
161     gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
162     if(gpioled.led_gpio < 0) {
163         printk("can't get led-gpio");
164         return -EINVAL;
165     }
166     printk("led-gpio num = %d\r\n", gpioled.led_gpio);
167 
168     /* 5.向gpio子系统申请使用GPIO */
169     ret = gpio_request(gpioled.led_gpio, "LED-GPIO");
170     if (ret) {
171         printk(KERN_ERR "gpioled: Failed to request led-gpio\n");
172         return ret;
173     }
174 
175     /* 6、设置PI0为输出,并且输出高电平,默认关闭LED灯 */
176     ret = gpio_direction_output(gpioled.led_gpio, 1);
177     if(ret < 0) {
178         printk("can't set gpio!\r\n");
179     }
180 
181     /* 注册字符设备驱动 */
182     /* 1、创建设备号 */
183     if (gpioled.major) {        /*  定义了设备号 */
184         gpioled.devid = MKDEV(gpioled.major, 0);
185         ret = register_chrdev_region(gpioled.devid, GPIOLED_CNT, 
GPIOLED_NAME);
186         if(ret < 0) {
187             pr_err("cannot register %s char driver [ret=%d]\n", 
GPIOLED_NAME, GPIOLED_CNT);
188             goto free_gpio;
189         }
190     } else {                        /* 没有定义设备号 */
191         ret = alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, 
GPIOLED_NAME);    /* 申请设备号 */
192         if(ret < 0) {
193             pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", 
GPIOLED_NAME, ret);
194             goto free_gpio;
195         }
196         gpioled.major = MAJOR(gpioled.devid); /* 获取分配好的主设备号 */
197         gpioled.minor = MINOR(gpioled.devid); /* 获取分配好的次设备号 */
198     }
199     printk("gpioled major=%d,minor=%d\r\n",gpioled.major, 
gpioled.minor);   
200     
201     /* 2、初始化cdev */
202     gpioled.cdev.owner = THIS_MODULE;
203     cdev_init(&gpioled.cdev, &gpioled_fops);
204     
205     /* 3、添加一个cdev */
206     cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
207     if(ret < 0)
208         goto del_unregister;
209         
210     /* 4、创建类 */
211     gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
212     if (IS_ERR(gpioled.class)) {
213         goto del_cdev;
214     }
215 
216     /* 5、创建设备 */
217     gpioled.device = device_create(gpioled.class, NULL,
                      gpioled.devid, NULL, GPIOLED_NAME);
218     if (IS_ERR(gpioled.device)) {
219         goto destroy_class;
220     }
221     return 0;
222     
223 destroy_class
224     class_destroy(gpioled.class);
225 del_cdev:
226     cdev_del(&gpioled.cdev);
227 del_unregister:
228     unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);
229 free_gpio:
230     gpio_free(gpioled.led_gpio);
231     return -EIO;
232 }
233 
234 /*
235  * @description 	: 驱动出口函数
236  * @param       	: 无
237  * @return      	: 无
238  */
239 static void __exit led_exit(void)
240 {
241     /* 注销字符设备驱动 */
242     cdev_del(&gpioled.cdev);			/*  删除cdev 	*/
243     unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); 
244     device_destroy(gpioled.class, gpioled.devid);	
245     class_destroy(gpioled.class);	/* 注销类 		*/
246     gpio_free(gpioled.led_gpio); 	/* 释放GPIO 	*/
247 }
248 
249 module_init(led_init);
250 module_exit(led_exit);
251 MODULE_LICENSE("GPL");
252 MODULE_AUTHOR("ALIENTEK");
253 MODULE_INFO(intree, "Y");

第41行,在设备结构体gpioled_dev中加入led_gpio这个成员变量,此成员变量保存LED等所使用的GPIO编号。
第55行,将设备结构体变量gpioled设置为filp的私有数据private_data。
第85行,通过读取filp的private_data成员变量来得到设备结构体变量,也就是gpioled。这种将设备结构体设置为filp私有数据的方法在Linux内核驱动里面非常常见。
第96、98行,直接调用gpio_set_value函数来向GPIO写入数据,实现开/关LED的效果。不需要我们直接操作相应的寄存器。
第134行,获取节点“/gpioled”。
第141~146行,获取“status”属性的值,判断属性是否“okay”。
第149~153行,获取compatible属性值并进行匹配。
第161行,通过函数of_get_named_gpio函数获取LED所使用的LED编号。相当于将gpioled节点中的“led-gpio”属性值转换为对应的LED编号。
第169行,通过函数gpio_request向GPIO子系统申请使用PI0。
第176行,调用函数gpio_direction_output设置PI0这个GPIO为输出,并且默认高电平,这样默认就会关闭LED灯。
可以看出gpioled.c文件中的内容和第二十四章的dtsled.c差不多,只是取消掉了配置寄存器的过程,改为使用Linux内核提供的API函数。在GPIO操作上更加的规范化,符合Linux代码框架,而且也简化了GPIO驱动开发的难度,以后我们所有例程用到GPIO的地方都采用此方法。
25.4.3 编写测试APP
本章直接使用第四十二章的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即可。
25.5 运行测试
25.5.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第二十章实验基本一样,只是将obj-m变量的值改为gpioled.o,Makefile内容如下所示:

示例代码25.5.1.1 Makefile文件
1  KERNELDIR := /home/zuozhongkai/linux/my_linux/linux-5.4.31
...... 
4  obj-m := gpioled.o
......
11 clean:
12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,设置obj-m变量的值为gpioled.o。
输入如下命令编译出驱动模块文件:

make -j32
编译成功以后就会生成一个名为“gpioled.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
25.5.2 运行测试
将上一小节编译出来的gpioled.ko和ledApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载gpioled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe gpioled //加载驱动
驱动加载成功以后会在终端中输出一些信息,如图25.5.2.1所示:
在这里插入图片描述

图25.5.2.1 驱动加载成功以后输出的信息
从图25.5.2.1可以看出,gpioled这个节点找到了,并且PI0这个GPIO的编号为128。驱动加载成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/gpioled 1 //打开LED灯
输入上述命令以后观察开发板上的红色LED灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
./ledApp /dev/gpioled 0 //关闭LED灯
输入上述命令以后观察开发板上的红色LED灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
rmmod gpioled.ko

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值