pinctrl和gpio子系统实验(芯片原厂做的,类似STM32的HAL库,我们只需调用API来直接操作GPIO)

上一章我们编写了基于设备树的LED 驱动,但是驱动的本质还是没变,都是配置LED 灯所使用的GPIO 寄存器,驱动开发方式和裸机基本没啥区别,这种配置方式比较繁琐。Linux 内核提供了pinctrl和gpio子系统用于GPIO 驱动,简化驱动开发。

pinctrl子系统

pinctrl子系统简介(半导体厂商写的)

Linux 驱动讲究驱动分离与分层,pinctrl 和gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。

我们先来回顾一下上一章是怎么初始化LED 灯所使用的GPIO,步骤如下:

  • ①、修改设备树,添加相应的节点,节点里面重点是设置reg 属性,reg 属性包括了GPIO相关寄存器。
  • ②、获取reg 属性中IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 和
    IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 这两个寄存器地址,并且初始化这两个寄存器,这两个寄存器用于设置GPIO1_IO03 这个PIN 的复用功能、上下拉、速度等。
  • ③、在②里面将GPIO1_IO03 这个PIN 复用为了GPIO 功能,因此需要设置GPIO1_IO03这个GPIO 相关的寄存器,也就是GPIO1_DR 和GPIO1_GDIR 这两个寄存器。

pinctrl 子系统主要工作内容如下:

  • ①、获取设备树中pin 信息。
  • ②、根据获取到的pin 信息来设置pin 的复用功能
  • ③、根据获取到的pin 信息来设置pin 的电气特性,比如上/下拉、速度、驱动能力等。

对于我们使用者来讲,只需要在设备树里面设置好某个pin的相关属性即可,其他的初始化工作均由pinctrl 子系统来完成(半导体厂商帮我们完成),pinctrl 子系统源码目录为drivers/pinctrl。

I.MX6ULL的pinctrl子系统驱动分析

0、通过compatbile属性查找对应驱动文件

在这里插入图片描述

1、PIN配置信息详解(获取寄存器地址)

要使用pinctrl 子系统,我们需要在设备树里面设置PIN 的配置信息,毕竟pinctrl 子系统要根据你提供的信息来配置PIN 功能,一般会在设备树里面创建一个节点来描述PIN 的配置信息。打开imx6ull.dtsi 文件,找到一个叫做iomuxc 的节点,如下所示:

756 iomuxc: iomuxc@020e0000 {
757 	compatible = "fsl,imx6ul-iomuxc";
758 	reg = <0x020e0000 0x4000>;
759 };

iomuxc 节点就是I.MX6ULL 的IOMUXC 外设对应的节点,看起来内容很少,继续打开imx6ull-alientek-emmc.dts,找到如下所示内容:

311 &iomuxc {
312 pinctrl-names = "default";
313 pinctrl-0 = <&pinctrl_hog_1>;
314 imx6ul-evk {
315 pinctrl_hog_1: hoggrp-1 {
316 fsl,pins = <
317 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
318 MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
319 MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
320 MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
321 >;
322 };
......
371 pinctrl_flexcan1: flexcan1grp{
372 fsl,pins = <
373 MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020
374 MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020
375 >;
376 };
......
587 pinctrl_wdog: wdoggrp {
588 fsl,pins = <
589 MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY 0x30b0
590 >;
591 };
592 };
593 };

示例代码45.1.2.2 就是向iomuxc 节点追加数据,不同的外设使用的PIN 不同、其配置也不同,将某个外设所使用的所有PIN 都组织在一个子节点里面。示例代码45.1.2.2 中pinctrl_hog_1 子节点就是和热插拔有关的PIN 集合,比如USB OTG 的ID 引脚。

pinctrl_flexcan1 子节点是flexcan1 这个外设所使用的PIN,pinctrl_wdog 子节点是wdog 外设所使用的PIN。如果需要在iomuxc 中添加我们自定义外设的PIN,那么需要新建一个子节点,然后将这个自定义外设的所有PIN 配置信息都放到这个子节点中。

将其与示例代码45.1.2.1 结合起来就可以得到完成的iomuxc 节点,如下所示:

1 iomuxc: iomuxc@020e0000 {
2 compatible = "fsl,imx6ul-iomuxc";
3 reg = <0x020e0000 0x4000>;
4 pinctrl-names = "default";
5 pinctrl-0 = <&pinctrl_hog_1>;
6 imx6ul-evk {
7 pinctrl_hog_1: hoggrp-1 {
8 fsl,pins = <
9 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059
10 MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059
11 MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059
12 MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058
13 >;
......
16 };
17 };
18 };

第2 行,compatible 属性值为“fsl,imx6ul-iomuxc”,前面讲解设备树的时候说过,Linux 内核会根据compatbile 属性值来查找对应的驱动文件,所以我们在Linux 内核源码中全局搜索字符串“fsl,imx6ul-iomuxc”就会找到I.MX6ULL 这颗SOC 的pinctrl 驱动文件。稍后我们会讲解这个pinctrl 驱动文件。
第9~12 行,pinctrl_hog_1 子节点所使用的PIN 配置信息,我们就以第9 行的UART1_RTS_B这个PIN 为例,讲解一下如何添加PIN 的配置信息,UART1_RTS_B 的配置信息如下:

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059

首先说明一下,UART1_RTS_B 这个PIN 是作为SD 卡的检测引脚,也就是通过此PIN 就可以检测到SD 卡是否有插入。UART1_RTS_B 的配置信息分为两部分:
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 和0x17059

我们重点来看一下这两部分是什么含义,前面说了,对于一个PIN 的配置主要包括两方面,一个是设置这个PIN 的复用功能,另一个就是设置这个PIN 的电气特性。所以我们可以大胆的猜测UART1_RTS_B 的这两部分配置信息一个是设置UART1_RTS_B 的复用功能,一个是用来设置UART1_RTS_B 的电气特性。

首先来看一下MX6UL_PAD_UART1_RTS_B__GPIO1_IO19,这是一个宏定义,定义在文件
arch/arm/boot/dts/imx6ul-pinfunc.h 中,imx6ull.dtsi 会引用imx6ull-pinfunc.h 这个头文件,而imx6ull-pinfunc.h 又会引用imx6ul-pinfunc.h 这个头文件(绕啊绕!)。从这里可以看出,可以在设备树中引用C 语言中.h 文件中的内容。MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 的宏定义内容如下:

190 #define MX6UL_PAD_UART1_RTS_B__UART1_DCE_RTS 0x0090 0x031C 0x0620 0x0 0x3
191 #define MX6UL_PAD_UART1_RTS_B__UART1_DTE_CTS 0x0090 0x031C 0x0000 0x0 0x0
192 #define MX6UL_PAD_UART1_RTS_B__ENET1_TX_ER 0x0090 0x031C 0x0000 0x1 0x0
193 #define MX6UL_PAD_UART1_RTS_B__USDHC1_CD_B 0x0090 0x031C 0x0668 0x2 0x1
194 #define MX6UL_PAD_UART1_RTS_B__CSI_DATA05 0x0090 0x031C 0x04CC 0x3 0x1
195 #define MX6UL_PAD_UART1_RTS_B__ENET2_1588_EVENT1_OUT 0x0090 0x031C 0x0000 0x4 0x0
196 #define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0
197 #define MX6UL_PAD_UART1_RTS_B__USDHC2_CD_B 0x0090 0x031C 0x0674 0x8 0x2

示例代码45.1.2.4 中一共有8 个以“MX6UL_PAD_UART1_RTS_B”开头的宏定义,大家仔细观察应该就能发现,这8 个宏定义分别对应UART1_RTS_B 这个PIN 的8 个复用IO。查阅《I.MX6ULL 参考手册》可以知UART1_RTS_B 的可选复用IO 如图45.1.2.1 所示:
在这里插入图片描述
示例代码196 行的宏定义MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 表示将UART1_RTS_B 这个IO 复用为GPIO1_IO19。此宏定义后面跟着5 个数字,也就是这个宏定义的具体值,如下所示:

0x0090 0x031C 0x0000 0x5 0x0

这5 个值的含义如下所示:

<mux_reg conf_reg input_reg mux_mode input_val>

综上所述可知:

0x0090:mux_reg 寄存器偏移地址,设备树中的iomuxc 节点就是IOMUXC 外设对应的节点,根据其reg 属性可知IOMUXC 外设寄存器起始地址为0x020e0000 。因此0x020e0000+0x0090=0x020e0090,IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B 寄存器地址
正好是0x020e0090 ,大家可以在《IMX6ULL 参考手册》中找到
IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B 这个寄存器的位域图,如图45.1.2.2 所示:
在这里插入图片描述
因此可知,0x020e0000+mux_reg 就是PIN 的复用寄存器地址。
0x031C:conf_reg 寄存器偏移地址,和mux_reg 一样,0x020e0000+0x031c=0x020e031c,这个就是寄存器IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B 的地址。
0x0000:input_reg 寄存器偏移地址,有些外设有input_reg 寄存器,有input_reg 寄存器的外设需要配置input_reg 寄存器。没有的话就不需要设置,UART1_RTS_B 这个PIN 在做GPIO1_IO19 的时候是没有input_reg 寄存器,因此这里intput_reg 是无效的。

0x5 :mux_reg 寄存器值,在这里就相当于设置
IOMUXC_SW_MUX_CTL_PAD_UART1_RTS_B 寄存器为0x5,也即是设置UART1_RTS_B 这个PIN 复用为GPIO1_IO19。
0x0:input_reg 寄存器值,在这里无效。

这就是宏MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 的含义,看的比较仔细的同学应该会发现并没有conf_reg 寄存器的值,config_reg 寄存器是设置一个PIN 的电气特性的,这么重要的寄存器怎么没有值呢?回到示例代码45.1.2.3 中,第9 行的内容如下所示:

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059

MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 我们上面已经分析了,就剩下了一个0x17059,反应快的同学应该已经猜出来了,0x17059 就是conf_reg 寄存器值!此值由用户自行设置,通过此值来设置一个IO 的上/下拉、驱动能力和速度等。在这里就相当于设置寄存器
IOMUXC_SW_PAD_CTL_PAD_UART1_RTS_B 的值为0x17059。

2、PIN 驱动程序流程讲解(流程图,了解)

本小节会涉及到Linux 驱动分层与分离、平台设备驱动等还未讲解的知识,所以本小节教程可以不用看,不会影响后续的实验。如果对Linux 内核的pinctrl 子系统实现原理感兴趣的话可以看本小节。

所有的东西都已经准备好了,包括寄存器地址和寄存器值Linux 内核相应的驱动文件就会根据这些值来做相应的初始化。接下来就找一下哪个驱动文件来做这一件事情,iomuxc 节点中compatible 属性的值为“fsl,imx6ul-iomuxc”,在Linux 内核中全局搜索“fsl,imx6ul-iomuxc”字符串就会找到对应的驱动文件。在文件drivers/pinctrl/freescale/pinctrl-imx6ul.c 中有如下内容:

326 static struct of_device_id imx6ul_pinctrl_of_match[] = {
327 { .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },
328 { .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },
329 { /* sentinel */ }
330 };
331
332 static int imx6ul_pinctrl_probe(struct platform_device *pdev)
333 {
334 const struct of_device_id *match;
335 struct imx_pinctrl_soc_info *pinctrl_info;
336
337 match = of_match_device(imx6ul_pinctrl_of_match, &pdev->dev);
338
339 if (!match)

第326~330 行,of_device_id 结构体数组,第四十三章讲解设备树的时候说过了,of_device_id里面保存着这个驱动文件的兼容性值,设备树中的compatible 属性值会和of_device_id 中的所有兼容性字符串比较,查看是否可以使用此驱动。imx6ul_pinctrl_of_match 结构体数组一共有两个兼容性字符串,分别为“fsl,imx6ul-iomuxc”和“fsl,imx6ull-iomuxc-snvs”,因此iomuxc 节点与此驱动匹配,所以pinctrl-imx6ul.c 会完成I.MX6ULL 的PIN 配置工作。

第347~355 行,platform_driver 是平台设备驱动,这个是我们后面章节要讲解的内容,platform_driver 是个结构体,有个probe 成员变量。在这里大家只需要知道,当设备和驱动匹配成功以后platform_driver 的probe 成员变量所代表的函数就会执行,在353 行设置probe 成员变量为imx6ul_pinctrl_probe 函数,因此在本章实验中imx6ul_pinctrl_probe 这个函数就会执行,可以认为imx6ul_pinctrl_probe 函数就是I.MX6ULL 这个SOC 的PIN 配置入口函数。以此为入口,如图45.1.2.3 所示的函数调用路径:
在这里插入图片描述
在图45.1.2.3 中函数imx_pinctrl_parse_groups 负责获取设备树中关于PIN 的配置信息,也就是我们前面分析的那6个u32 类型的值。处理过程如下所示:

488 /*
489 * Each pin represented in fsl,pins consists of 5 u32 PIN_FUNC_ID
490 * and 1 u32 CONFIG, so 24 types in total for each pin.
491 */

第496 和497 行,设备树中的mux_reg 和conf_reg 值会保存在info 参数中,input_reg、mux_mode、input_val 和config 值会保存在grp 参数中。
第560~564 行,获取mux_reg、conf_reg、input_reg、mux_mode 和input_val 值。
第570 行,获取config 值。
接下来看一下函数pinctrl_register,此函数用于向Linux 内核注册一个PIN 控制器,此函数原型如下:

struct pinctrl_dev *pinctrl_register(struct pinctrl_desc *pctldesc,
struct device *dev,
void *driver_data)

参数pctldesc 非常重要,因为此参数就是要注册的PIN 控制器,PIN 控制器用于配置SOC的PIN 复用功能和电气特性。参数pctldesc 是pinctrl_desc 结构体类型指针,pinctrl_desc 结构体如下所示:

128 struct pinctrl_desc {
129 const char *name;
130 struct pinctrl_pin_desc const *pins;
131 unsigned int npins;
132 const struct pinctrl_ops *pctlops;
133 const struct pinmux_ops *pmxops;
134 const struct pinconf_ops *confops;
135 struct module *owner;
136 #ifdef CONFIG_GENERIC_PINCONF
137 unsigned int num_custom_params;
138 const struct pinconf_generic_params *custom_params;
139 const struct pin_config_item *custom_conf_items;
140 #endif
141 };

第132~124 行,这三个“_ops”结构体指针非常重要!!!因为这三个结构体就是PIN 控制器的“工具”,这三个结构体里面包含了很多操作函数,通过这些操作函数就可以完成对某一个PIN 的配置。pinctrl_desc 结构体需要由用户提供,结构体里面的成员变量也是用户提供的。但是这个用户并不是我们这些使用芯片的程序员,而是半导体厂商,半导体厂商发布的Linux 内核源码中已经把这些工作做完了。比如在imx_pinctrl_probe 函数中可以找到如下所示代码:

648 int imx_pinctrl_probe(struct platform_device *pdev,
649 struct imx_pinctrl_soc_info *info)
650 {
651 struct device_node *dev_np = pdev->dev.of_node;
652 struct device_node *np;
653 struct imx_pinctrl *ipctl;
654 struct resource *res;
655 struct pinctrl_desc *imx_pinctrl_desc;
......
663
664 imx_pinctrl_desc = devm_kzalloc(&pdev->dev, sizeof(*imx_pinctrl_desc),
665 GFP_KERNEL);
666 if (!imx_pinctrl_desc)
667 return -ENOMEM;
......
705
706 imx_pinctrl_desc->name = dev_name(&pdev->dev);
707 imx_pinctrl_desc->pins = info->pins;
708 imx_pinctrl_desc->npins = info->npins;
709 imx_pinctrl_desc->pctlops = &imx_pctrl_ops;
710 imx_pinctrl_desc->pmxops = &imx_pmx_ops;
711 imx_pinctrl_desc->confops = &imx_pinconf_ops;
712 imx_pinctrl_desc->owner = THIS_MODULE;
......
723 ipctl->pctl = pinctrl_register(imx_pinctrl_desc, &pdev->dev, ipctl);
......
732 }

第655 行,定义结构体指针变量imx_pinctrl_desc。
第664 行,向指针变量imx_pinctrl_desc 分配内存。
第706~712 行,初始化imx_pinctrl_desc 结构体指针变量,重点是pctlops、pmxops 和confops这三个成员变量,分别对应imx_pctrl_ops、imx_pmx_ops 和imx_pinconf_ops 这三个结构体。
第723 行,调用函数pinctrl_register 向Linux 内核注册imx_pinctrl_desc,注册以后Linux 内核就有了对I.MX6ULL 的PIN 进行配置的工具。
imx_pctrl_ops、imx_pmx_ops 和imx_pinconf_ops 这三个结构体定义如下:

174 static const struct pinctrl_ops imx_pctrl_ops = {
175 .get_groups_count = imx_get_groups_count,
176 .get_group_name = imx_get_group_name,
177 .get_group_pins = imx_get_group_pins,
178 .pin_dbg_show = imx_pin_dbg_show,
179 .dt_node_to_map = imx_dt_node_to_map,
180 .dt_free_map = imx_dt_free_map,
181
182 };
......
374 static const struct pinmux_ops imx_pmx_ops = {
375 .get_functions_count = imx_pmx_get_funcs_count,
376 .get_function_name = imx_pmx_get_func_name,
377 .get_function_groups = imx_pmx_get_groups,
378 .set_mux = imx_pmx_set,
379 .gpio_request_enable = imx_pmx_gpio_request_enable,
380 .gpio_set_direction = imx_pmx_gpio_set_direction,
381 };
......
481 static const struct pinconf_ops imx_pinconf_ops = {
482 .pin_config_get = imx_pinconf_get,
483 .pin_config_set = imx_pinconf_set,
484 .pin_config_dbg_show = imx_pinconf_dbg_show,
485 .pin_config_group_dbg_show = imx_pinconf_group_dbg_show,
486 };

示例代码45.1.2.9 中这三个结构体下的所有函数就是I.MX6ULL 的PIN 配置函数,我们就此打住,不再去分析这些函数了,否则本章就没完没了了,有兴趣的可以去看一下。

设备树中添加pinctrl节点模板(半导体厂商瑞芯微、海思厂商写的)

我们已经对pinctrl 有了比较深入的了解,接下来我们学习一下如何在设备树中添加某个外设的PIN 信息。关于I.MX 系列SOC 的pinctrl 设备树绑定信息可以参考文档
Documentation/devicetree/bindings/pinctrl/fsl,imx-pinctrl.txt。这里我们虚拟一个名为“test”的设备,test 使用了GPIO1_IO00 这个PIN 的GPIO 功能,pinctrl 节点添加过程如下:

1、创建对应的节点
同一个外设的PIN 都放到一个节点里面,打开imx6ull-alientek-emmc.dts,在iomuxc 节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点,注意!节点前缀一定要为“pinctrl_”。添加完成以后如下所示:

1 pinctrl_test: testgrp {
2 /* 具体的PIN信息*/
3 };

2、添加“fsl,pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl,pins”,因为对于I.MX 系列SOC 而言,pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取PIN 的配置信息,完成以后如下所示:

1 pinctrl_test: testgrp {
2 fsl,pins = <
3 /* 设备所使用的PIN配置信息*/
4 >;
5 };

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

1 pinctrl_test: testgrp {
2 fsl,pins = <
3 MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config是具体设置值*/
4 >;
5 };

至此,我们已经在imx6ull-alientek-emmc.dts 文件中添加好了test 设备所使用的PIN 配置信息。

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。

在这里插入图片描述

I.MX6ULL 的gpio子系统驱动

1、设备树中的gpio 信息

I.MX6ULL-ALPHA 开发板上的UART1_RTS_B 做为SD 卡的检测引脚,UART1_RTS_B 复用为GPIO1_IO19,通过读取这个GPIO 的高低电平就可以知道SD 卡有没有插入。首先肯定是将UART1_RTS_B 这个PIN 复用为GPIO1_IO19,并且设置电气属性,也就是上一小节讲的pinctrl 节点。打开imx6ull-alientek-emmc.dts,UART1_RTS_B 这个PIN 的pincrtl 设置如下:

316 pinctrl_hog_1: hoggrp-1 {
317 fsl,pins = <
318 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
......
322 >;
323 };

第318 行,设置UART1_RTS_B 这个PIN 为GPIO1_IO19。

pinctrl 配置好以后就是设置gpio 了,SD 卡驱动程序通过读取GPIO1_IO19 的值来判断SD卡有没有插入,但是SD 卡驱动程序怎么知道CD 引脚连接的GPIO1_IO19 呢?肯定是需要设备树告诉驱动啊!在设备树中SD 卡节点下添加一个属性来描述SD 卡的CD 引脚就行了,SD卡驱动直接读取这个属性值就知道SD 卡的CD 引脚使用的是哪个GPIO 了。SD 卡连接在
I.MX6ULL 的usdhc1 接口上,在imx6ull-alientek-emmc.dts 中找到名为“usdhc1”的节点,这个节点就是SD 卡设备节点,如下所示:

760 &usdhc1 {
761 pinctrl-names = "default", "state_100mhz", "state_200mhz";
762 pinctrl-0 = <&pinctrl_usdhc1>;
763 pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
764 pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
765 /* pinctrl-3 = <&pinctrl_hog_1>; */
766 cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;
767 keep-power-in-suspend;
768 enable-sdio-wakeup;
769 vmmc-supply = <&reg_sd1_vmmc>;
770 status = "okay";
771 };

第765 行,此行本来没有,是作者添加的,usdhc1 节点作为SD 卡设备总节点,usdhc1 节点需要描述SD 卡所有的信息,因为驱动要使用。本行就是描述SD 卡的CD 引脚pinctrl 信息所在的子节点,因为SD 卡驱动需要根据pincrtl 节点信息来设置CD 引脚的复用功能等。
762~ 764行的pinctrl-0~2 都是SD 卡其他PIN 的pincrtl 节点信息。但是大家会发现,其实在usdhc1 节点中并没有“pinctrl-3 = <&pinctrl_hog_1>”这一行,也就是说并没有指定CD 引脚的pinctrl 信息,那么SD 卡驱动就没法设置CD 引脚的复用功能啊?这个不用担心,因为在“iomuxc”节点下引用了pinctrl_hog_1 这个节点,所以Linux 内核中的iomuxc 驱动就会自动初始化pinctrl_hog_1节点下的所有PIN。

第766 行,属性“cd-gpios”描述了SD 卡的CD 引脚使用的哪个IO。属性值一共有三个,我们来看一下这三个属性值的含义,“&gpio1”表示CD 引脚所使用的IO 属于GPIO1 组,“19”表示GPIO1 组的第19 号IO,通过这两个值SD 卡驱动程序就知道CD 引脚使用了GPIO1_IO19这GPIO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。

根据上面这些信息,SD 卡驱动程序就可以使用GPIO1_IO19 来检测SD 卡的CD 信号了,打开imx6ull.dtsi,在里面找到如下所示内容:

504 gpio1: gpio@0209c000 {
505 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
506 reg = <0x0209c000 0x4000>;
507 interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
508 <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
509 gpio-controller;
510 #gpio-cells = <2>;
511 interrupt-controller;
512 #interrupt-cells = <2>;
513 };

gpio1 节点信息描述了GPIO1 控制器的所有信息,重点就是GPIO1 外设寄存器基地址以及兼容属性。关于I.MX 系列SOC 的GPIO 控制器绑定信息请查看文档Documentation/devicetree/bindings/gpio/ fsl-imx-gpio.txt。

第505 行,设置gpio1 节点的compatible 属性有两个,分别为“fsl,imx6ul-gpio”和“fsl,imx35-gpio”,在Linux 内核中搜索这两个字符串就可以找到I.MX6UL 的GPIO 驱动程序。

第506 行,reg 属性设置了GPIO1 控制器的寄存器基地址为0X0209C000,大家可以打开《I.MX6ULL 参考手册》找到“Chapter 28:General Purpose Input/Output(GPIO)”章节第28.5 小节,有如图45.2.2.1 所示的寄存器地址表:
在这里插入图片描述

从图45.2.2.1 可以看出,GPIO1 控制器的基地址就是0X0209C000。
第509 行,“gpio-controller”表示gpio1 节点是个GPIO 控制器。
第510 行,“#gpio-cells”属性和“#address-cells”类似,#gpio-cells 应该为2,表示一共有两个cell,第一个cell 为GPIO 编号,比如“&gpio1 3”就表示GPIO1_IO03。第二个cell 表示GPIO 极性,如果为0(GPIO_ACTIVE_HIGH) 的话表示高电平有效,如果为1(GPIO_ACTIVE_LOW)的话表示低电平有效。

2、GPIO 驱动程序简介

本小节会涉及到Linux 驱动分层与分离、平台设备驱动等还未讲解的知识,所以本小节教程可以不用看,不会影响后续的实验。如果对Linux 内核的GPIO 子系统实现原理感兴趣的话可以看本小节。

gpio1 节点的compatible 属性描述了兼容性,在Linux 内核中搜索“fsl,imx6ul-gpio”和“fsl,imx35-gpio”这两个字符串,查找GPIO 驱动文件。drivers/gpio/gpio-mxc.c 就是I.MX6ULL的GPIO 驱动文件,在此文件中有如下所示of_device_id 匹配表:

152 static const struct of_device_id mxc_gpio_dt_ids[] = {
153 { .compatible = "fsl,imx1-gpio", .data = &mxc_gpio_devtype[IMX1_GPIO], },
154 { .compatible = "fsl,imx21-gpio", .data = &mxc_gpio_devtype[IMX21_GPIO], },
155 { .compatible = "fsl,imx31-gpio", .data = &mxc_gpio_devtype[IMX31_GPIO], },
156 { .compatible = "fsl,imx35-gpio", .data = &mxc_gpio_devtype[IMX35_GPIO], },
157 { /* sentinel */ }
158 };

第156 行的compatible 值为“fsl,imx35-gpio”,和gpio1 的compatible 属性匹配,因此gpio-mxc.c 就是I.MX6ULL 的GPIO 控制器驱动文件。gpio-mxc.c 所在的目录为drivers/gpio,打开这个目录可以看到很多芯片的gpio 驱动文件,“gpiolib”开始的文件是gpio 驱动的核心文件,如图45.2.2.2 所示:
在这里插入图片描述
我们重点来看一下gpio-mxc.c 这个文件,在gpio-mxc.c 文件中有如下所示内容:

496 static struct platform_driver mxc_gpio_driver = {
497 .driver = {
498 .name = "gpio-mxc",
499 .of_match_table = mxc_gpio_dt_ids,
500 },
501 .probe = mxc_gpio_probe,
502 .id_table = mxc_gpio_devtype,
503 };

可以看出GPIO 驱动也是个平台设备驱动,因此当设备树中的设备节点与驱动的of_device_id 匹配以后probe 函数就会执行,在这里就是mxc_gpio_probe 函数,这个函数就是I.MX6ULL 的GPIO 驱动入口函数。我们简单来分析一下mxc_gpio_probe 这个函数,函数内容如下:

403 static int mxc_gpio_probe(struct platform_device *pdev)
404 {
405 struct device_node *np = pdev->dev.of_node;
406 struct mxc_gpio_port *port;
407 struct resource *iores;
408 int irq_base;
409 int err;
410
411 mxc_gpio_get_hw(pdev);
412
413 port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);
414 if (!port)
415 return -ENOMEM;
416
417 iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
418 port->base = devm_ioremap_resource(&pdev->dev, iores);
419 if (IS_ERR(port->base))
420 return PTR_ERR(port->base);
421
422 port->irq_high = platform_get_irq(pdev, 1);
423 port->irq = platform_get_irq(pdev, 0);
424 if (port->irq < 0)

第405 行,设备树节点指针。
第406 行,定义一个结构体指针port,结构体类型为mxc_gpio_port。gpio-mxc.c 的重点工作就是维护mxc_gpio_port,mxc_gpio_port 就是对I.MX6ULL GPIO 的抽象。mxc_gpio_port 结构体定义如下:

61 struct mxc_gpio_port {
62 struct list_head node;
63 void __iomem *base;
64 int irq;
65 int irq_high;
66 struct irq_domain *domain;
67 struct bgpio_chip bgc;
68 u32 both_edges;
69 };

mxc_gpio_port 的bgc 成员变量很重要,因为稍后的重点就是初始化bgc。
继续回到mxc_gpio_probe 函数函数,第411 行调用mxc_gpio_get_hw 函数获取gpio 的硬件相关数据,其实就是gpio 的寄存器组,函数mxc_gpio_get_hw 里面有如下代码:

364 static void mxc_gpio_get_hw(struct platform_device *pdev)
365 {
366 const struct of_device_id *of_id =
367 of_match_device(mxc_gpio_dt_ids, &pdev->dev);
368 enum mxc_gpio_hwtype hwtype;
......
383

注意第385 行,mxc_gpio_hwdata 是个全局变量,如果硬件类型是IMX35_GPIO 的话设置mxc_gpio_hwdat 为imx35_gpio_hwdata。对于I.MX6ULL 而言,硬件类型就是IMX35_GPIO,imx35_gpio_hwdata 是个结构体变量,描述了GPIO 寄存器组,内容如下:

101 static struct mxc_gpio_hwdata imx35_gpio_hwdata = {
102 .dr_reg = 0x00,
103 .gdir_reg = 0x04,
104 .psr_reg = 0x08,
105 .icr1_reg = 0x0c,
106 .icr2_reg = 0x10,
107 .imr_reg = 0x14,
108 .isr_reg = 0x18,
109 .edge_sel_reg = 0x1c,
110 .low_level = 0x00,
111 .high_level = 0x01,
112 .rise_edge = 0x02,
113 .fall_edge = 0x03,
114 };

大家将imx35_gpio_hwdata 中的各个成员变量和图45.2.2.1 中的GPIO 寄存器表对比就会发现,imx35_gpio_hwdata 结构体就是GPIO 寄存器组结构。这样我们后面就可以通过mxc_gpio_hwdata 这个全局变量来访问GPIO 的相应寄存器了。

继续回到示例代码45.2.2.5 的mxc_gpio_probe 函数中,第417 行,调用函数
platform_get_resource 获取设备树中内存资源信息,也就是reg 属性值。前面说了reg 属性指定了GPIO1 控制器的寄存器基地址为0X0209C000,在配合前面已经得到的mxc_gpio_hwdata,这样Linux 内核就可以访问gpio1 的所有寄存器了。
第418 行,调用devm_ioremap_resource 函数进行内存映射,得到0x0209C000 在Linux 内核中的虚拟地址。
第422、423 行,通过platform_get_irq 函数获取中断号,第422 行获取高16 位GPIO 的中断号,第423 行获取底16 位GPIO 中断号。
第428、429 行,操作GPIO1 的IMR 和ISR 这两个寄存器,关闭GPIO1 所有IO 中断,并且清除状态寄存器。
第438~448 行,设置对应GPIO 的中断服务函数,不管是高16 位还是低16 位,中断服务函数都是mx3_gpio_irq_handler。
第450~453 行,bgpio_init 函数第一个参数为bgc,是bgpio_chip 结构体指针。bgpio_chip结构体有个gc 成员变量,gc 是个gpio_chip 结构体类型的变量。gpio_chip 结构体是抽象出来的GPIO 控制器,gpio_chip 结构体如下所示(有缩减):

74 struct gpio_chip {
75 const char *label;
76 struct device *dev;
77 struct module *owner;
78 struct list_head list;
79
80 int (*request)(struct gpio_chip *chip,
81 unsigned offset);
82 void (*free)(struct gpio_chip *chip,
83 unsigned offset);
84 int (*get_direction)(struct gpio_chip *chip,
85 unsigned offset);
86 int (*direction_input)(struct gpio_chip *chip,
87 unsigned offset);
88 int (*direction_output)(struct gpio_chip *chip,
89 unsigned offset, int value);
90 int (*get)(struct gpio_chip *chip,
91 unsigned offset);
92 void (*set)(struct gpio_chip *chip,
93 unsigned offset, int value);
......
145 };

可以看出,gpio_chip 大量的成员都是函数,这些函数就是GPIO 操作函数。bgpio_init 函数主要任务就是初始化bgc->gc 。bgpio_init 里面有三个setup 函数:bgpio_setup_io 、bgpio_setup_accessors 和bgpio_setup_direction。这三个函数就是初始化bgc->gc 中的各种有关GPIO 的操作,比如输出,输入等等。第451~453 行的GPIO_PSR、GPIO_DR 和GPIO_GDIR 都是I.MX6ULL 的GPIO 寄存器。这些寄存器地址会赋值给bgc 参数的reg_dat、reg_set、reg_clr和reg_dir 这些成员变量。至此,bgc 既有了对GPIO 的操作函数,又有了I.MX6ULL 有关GPIO的寄存器,那么只要得到bgc 就可以对I.MX6ULL 的GPIO 进行操作。

继续回到mxc_gpio_probe 函数,第461 行调用函数gpiochip_add 向Linux 内核注册gpio_chip,也就是port->bgc.gc。注册完成以后我们就可以在驱动中使用gpiolib 提供的各个API 函数。

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 函数就讲这些,这些是我们用的最多的。

设备树中添加gpio节点模板

继续完成45.1.3 中的test 设备,在45.1.3 中我们已经讲解了如何创建test 设备的pinctrl 节点。本节我们来学习一下如何创建test 设备的GPIO 节点。

1、创建test 设备节点
在根节点“/”下创建test 设备子节点,如下所示:

1 test {
2 /* 节点内容*/
3 };

2、添加pinctrl 信息
在45.1.3 中我们创建了pinctrl_test 节点,此节点描述了test 设备所使用的GPIO1_IO00 这个PIN 的信息,我们要将这节点添加到test 设备节点中,如下所示:

1 test {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_test>;
4 /* 其他节点内容*/
5 };

第2 行,添加pinctrl-names 属性,此属性描述pinctrl 名字为“default”。
第3 行,添加pinctrl-0 节点,此节点引用45.1.3 中创建的pinctrl_test 节点,表示tset 设备的所使用的PIN 信息保存在pinctrl_test 节点中。

3、添加GPIO 属性信息
我们最后需要在test 节点中添加GPIO 属性信息,表明test 所使用的GPIO 是哪个引脚,添加完成以后如下所示:

1 test {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_test>;
4 gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
5 };

第4 行,test 设备所使用的gpio。
关于pinctrl 子系统和gpio 子系统就讲解到这里,接下来就使用pinctrl 和gpio 子系统来驱动I.MX6ULL-ALPHA 开发板上的LED 灯。

与gpio 相关的OF函数

在示例代码45.2.4.3 中,我们定义了一个名为“gpio”的属性,gpio 属性描述了test 这个设备所使用的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 编号,此函数会将设备树中类似<&gpio5 7 GPIO_ACTIVE_LOW>的属性信息转换为对应的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 编号;负值,失败。

硬件原理图分析

本章实验硬件原理图参考8.3 小节即可。

实验程序编写(使用子系统点灯,类似HAL库,调用API操作GPIO)

本章实验我们继续研究LED 灯,在第四十四章实验中我们通过设备树向dtsled.c 文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验我们使用pinctrl和gpio子系统来完成LED 灯驱动。

本实验对应的例程路径为:开发板光盘-> 2、Linux 驱动例程-> 5_gpioled。

修改设备树文件

1、添加pinctrl 节点

I.MX6U-ALPHA 开发板上的LED 灯使用了GPIO1_IO03 这个PIN,打开imx6ull-alientek-emmc.dts,在iomuxc 节点的imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:

1 pinctrl_led: ledgrp {
2 	fsl,pins = <
3 	MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */
4 	>;
5 };

第3 行,将GPIO1_IO03 这个PIN 复用为GPIO1_IO03,电气属性值为0X10B0。

2、添加LED 设备节点

在根节点“/”下创建LED 灯节点,节点名为“gpioled”,节点内容如下:

1 gpioled {
2 	#address-cells = <1>;
3 	#size-cells = <1>;
4 	compatible = "atkalpha-gpioled";
5 	pinctrl-names = "default";
6 	pinctrl-0 = <&pinctrl_led>;
7 	led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
8 	status = "okay";
9 };

第6 行,pinctrl-0 属性设置LED 灯所使用的PIN 对应的pinctrl 节点。
第7 行,led-gpio 属性指定了LED 灯所使用的GPIO,在这里就是GPIO1 的IO03,低电平有效。稍后编写驱动程序的时候会获取led-gpio 属性的内容来得到GPIO 编号,因为gpio 子系统的API 操作函数需要GPIO 编号。

3、检查PIN 是否被其他外设使用

这一点非常重要!!!

很多初次接触设备树的驱动开发人员很容易因为这个小问题栽了大跟头!因为我们所使用的设备树基本都是在半导体厂商提供的设备树文件基础上修改而来的,而半导体厂商提供的设备树是根据自己官方开发板编写的,很多PIN 的配置和我们所使用的开发板不一样。比如A 这个引脚在官方开发板接的是I2C 的SDA,而我们所使用的硬件可能将A 这个引脚接到了其他的外设,比如LED 灯上,接不同的外设,A 这个引脚的配置就不同。一个引脚一次只能实现一个功能,如果A 引脚在设备树中配置为了I2C 的SDA 信号,那么A 引脚就不能再配置为GPIO,否则的话驱动程序在申请GPIO 的时候就会失败。检查PIN 有没有被其他外设使用包括两个方面:

  • ①、检查pinctrl 设置。
  • ②、如果这个PIN 配置为GPIO 的话,检查这个GPIO 有没有被别的外设使用。

在本章实验中LED 灯使用的PIN 为GPIO1_IO03,因此先检查GPIO_IO03 这个PIN 有没有被其他的pinctrl 节点使用,在imx6ull-alientek-emmc.dts 中找到如下内容:

480 pinctrl_tsc: tscgrp {
481 fsl,pins = <
482 MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0
483 MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0xb0
484 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0xb0
485 MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0xb0
486 >;
487 };

pinctrl_tsc 节点是TSC(电阻触摸屏接口)的pinctrl 节点,从第484 行可以看出,默认情况下GPIO1_IO03 作为了TSC 外设的PIN。所以我们需要将第484 行屏蔽掉!和C 语言一样,在要屏蔽的内容前后加上“/”和“/”符号即可。其实在I.MX6U-ALPHA 开发板上并没有用到TSC接口,所以第482~485 行的内容可以全部屏蔽掉。

因为本章实验我们将GPIO1_IO03 这个PIN 配置为了GPIO,所以还需要查找一下有没有其他的外设使用了GPIO1_IO03,在imx6ull-alientek-emmc.dts 中搜索“gpio1 3”,找到如下内容:

723 &tsc {
724 pinctrl-names = "default";
725 pinctrl-0 = <&pinctrl_tsc>;
726 xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
727 measure-delay-time = <0xffff>;
728 pre-charge-time = <0xfff>;
729 status = "okay";
730 };

tsc 是TSC 的外设节点,从726 行可以看出,tsc 外设也使用了GPIO1_IO03,同样我们需要将这一行屏蔽掉。然后在继续搜索“gpio1 3”,看看除了本章的LED 灯以外还有没有其他的地方也使用了GPIO1_IO03,找到一个屏蔽一个。

设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb 文件启动Linux 系统。启动成功以后进入“/proc/device-tree”目录中查看“gpioled”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图45.4.1.1 所示:
在这里插入图片描述

LED灯驱动程序编写

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

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: gpioled.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: 采用pinctrl和gpio子系统驱动LED灯。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/7/13 左忠凯创建
***************************************************************/
#define GPIOLED_CNT			1		  	/* 设备号个数 */
#define GPIOLED_NAME		"gpioled"	/* 名字 */
#define LEDOFF 				0			/* 关灯 */
#define LEDON 				1			/* 开灯 */

/* gpioled设备结构体 */
struct gpioled_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	int led_gpio;			/* led所使用的GPIO编号		*/
};

struct gpioled_dev gpioled;	/* led设备 */

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &gpioled; /* 设置私有数据 */
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;
	struct gpioled_dev *dev = filp->private_data;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		gpio_set_value(dev->led_gpio, 0);	/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		gpio_set_value(dev->led_gpio, 1);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.read = led_read,
	.write = led_write,
	.release = 	led_release,
};

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init led_init(void)
{
	int ret = 0;

	/* 设置LED所使用的GPIO */
	/* 1、获取设备节点:gpioled */
	gpioled.nd = of_find_node_by_path("/gpioled");//根节点下添加的
	if(gpioled.nd == NULL) {
		printk("gpioled node not find!\r\n");
		return -EINVAL;//错误处理 添加GOTO跳转
	} else {
		printk("gpioled node find!\r\n");
	}

	/* 2、 获取设备树中的gpio属性,得到LED所使用的LED编号 */
	gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
	if(gpioled.led_gpio < 0) {
		printk("can't get led-gpio");
		return -EINVAL;//错误处理 添加GOTO跳转
	}
	printk("led-gpio num = %d\r\n", gpioled.led_gpio);
    //视频里讲 此处还需要申请IO 防止冲突 使用的API是 gpio_request
	/* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */
	ret = gpio_direction_output(gpioled.led_gpio, 1);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
	}

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (gpioled.major) {		/*  定义了设备号 */
		gpioled.devid = MKDEV(gpioled.major, 0);
		register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
	} else {						/* 没有定义设备号 */
		alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);	/* 申请设备号 */
		gpioled.major = MAJOR(gpioled.devid);	/* 获取分配号的主设备号 */
		gpioled.minor = MINOR(gpioled.devid);	/* 获取分配号的次设备号 */
	}
	printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);	
	
	/* 2、初始化cdev */
	gpioled.cdev.owner = THIS_MODULE;
	cdev_init(&gpioled.cdev, &gpioled_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);

	/* 4、创建类 */
	gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
	if (IS_ERR(gpioled.class)) {
		return PTR_ERR(gpioled.class);
	}

	/* 5、创建设备 */
	gpioled.device = device_create(gpioled.class, NULL, gpioled.devid, NULL, GPIOLED_NAME);
	if (IS_ERR(gpioled.device)) {
		return PTR_ERR(gpioled.device);
	}
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit led_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&gpioled.cdev);/*  删除cdev */
	unregister_chrdev_region(gpioled.devid, GPIOLED_CNT); /* 注销设备号 */

	device_destroy(gpioled.class, gpioled.devid);//摧毁设备
	class_destroy(gpioled.class);//摧毁类
}//视频里申请完需要释放IO  gpio_free()

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

第41 行,在设备结构体gpioled_dev 中加入led_gpio 这个成员变量,此成员变量保存LED等所使用的GPIO 编号。
第55 行,将设备结构体变量gpioled 设置为filp 的私有数据private_data。
第85 行,通过读取filp 的private_data 成员变量来得到设备结构体变量,也就是gpioled。这种将设备结构体设置为filp 私有数据的方法在Linux 内核驱动里面非常常见。
第96、97 行,直接调用gpio_set_value 函数来向GPIO 写入数据,实现开/关LED 的效果。不需要我们直接操作相应的寄存器
第133 行,获取节点“/gpioled”。
第142 行,通过函数of_get_named_gpio 函数获取LED 所使用的LED 编号。相当于将gpioled 节点中的“led-gpio”属性值转换为对应的LED 编号。
第150 行,调用函数gpio_direction_output 设置GPIO1_IO03 这个GPIO 为输出,并且默认高电平,这样默认就会关闭LED 灯。

可以看出gpioled.c 文件中的内容和第四十四章的dtsled.c 差不多,只是取消掉了配置寄存器的过程,改为使用Linux 内核提供的API 函数。在GPIO 操作上更加的规范化,符合Linux代码框架,而且也简化了GPIO 驱动开发的难度,以后我们所有例程用到GPIO 的地方都采用此方法。

编写测试APP

本章直接使用第四十二章的测试APP,将上一章的ledApp.c 文件复制到本章实验工程下即可。

运行测试

编译驱动程序和测试APP

1、编译驱动程序
编写Makefile 文件,本章实验的Makefile 文件和第四十章实验基本一样,只是将obj-m 变量的值改为gpioled.o,Makefile 内容如下所示:

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)

obj-m := gpioled.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第4 行,设置obj-m 变量的值为gpioled.o。
输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“gpioled.ko”的驱动模块文件。

2、编译测试APP
输入如下命令编译测试ledApp.c 这个测试程序:

arm-linux-gnueabihf-gcc ledApp.c -o ledApp

编译成功以后就会生成ledApp 这个应用程序。

运行测试

将上一小节编译出来的gpioled.ko 和ledApp 这两个文件拷贝到rootfs/lib/modules/4.1.15 目录中,重启开发板,进入到目录lib/modules/4.1.15 中,输入如下命令加载gpioled.ko 驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe gpioled.ko //加载驱动

驱动加载成功以后会在终端中输出一些信息,如图45.5.2.1 所示:
在这里插入图片描述

从图45.5.2.1 可以看出,gpioled 这个节点找到了,并且GPIO1_IO03 这个GPIO 的编号为3。驱动加载成功以后就可以使用ledApp 软件来测试驱动是否工作正常,输入如下命令打开LED灯:

./ledApp /dev/gpioled 1 //打开LED 灯

输入上述命令以后观察I.MX6U-ALPHA 开发板上的红色LED 灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED 灯:

./ledApp /dev/gpioled 0 //关闭LED 灯

输入上述命令以后观察I.MX6U-ALPHA 开发板上的红色LED 灯是否熄灭。如果要卸载驱动的话输入如下命令即可:

rmmod gpioled.ko
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值