本帖最后由 正点原子 于 2020-10-26 11:24 编辑
1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
4)对正点原子Linux感兴趣的同学可以加群讨论:876919289
5)关注正点原子公众号,获取最新资料
第四十五章 pinctrl和gpio子系统实验
上一章我们编写了基于设备树的LED驱动,但是驱动的本质还是没变,都是配置LED灯所使用的GPIO寄存器,驱动开发方式和裸机基本没啥区别。Linux是一个庞大而完善的系统,尤其是驱动框架,像GPIO这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则就相当于你买了一辆车,结果每天推着车去上班。Linux内核提供了pinctrl和gpio子系统用于GPIO驱动,本章我们就来学习一下如何借助pinctrl和gpio子系统来简化GPIO驱动开发。
45.1 pinctrl子系统
45.1.1 pinctrl子系统简介
Linux驱动讲究驱动分离与分层,pinctrl和gpio子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架,关于驱动的分离与分层我们后面会讲。本来pinctrl和gpio子系统应该放到驱动分离与分层章节后面讲解,但是不管什么外设驱动,GPIO驱动基本都是必须的,而pinctrl和gpio子系统又是GPIO驱动必须使用的,所以就将pintrcl和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这两个寄存器。
总结一下,②中完成对GPIO1_IO03这个PIN的初始化,设置这个PIN的复用功能、上下拉等,比如讲GPIO_IO03这个PIN设置为GPIO功能。③中完成对GPIO的初始化,设置GPIO为输入/输出等。如果使用过STM32的话应该都记得,STM32也是要先设置某个PIN的复用功能、速度、上下拉等,然后再设置PIN所对应的GPIO。其实对于大多数的32位SOC而言,引脚的设置基本都是这两方面,因此Linux内核针对PIN的配置推出了pinctrl子系统,对于GPIO的配置推出了gpio子系统。本节我们来学习pinctrl子系统,下一节再学习gpio子系统。
大多数SOC的pin都是支持复用复用的,比如I.MX6ULL的GPIO1_IO03既可以作为普通的GPIO使用,也可以作为I2C1的SDA等等。此外我们还需要配置pin的电气特性,比如上/下拉、速度、驱动能力等等。传统的配置pin的方式就是直接操作相应的寄存器,但是这种配置方式比较繁琐、而且容易出问题(比如pin功能冲突)。pinctrl子系统就是为了解决这个问题而引入的,pinctrl子系统主要工作内容如下:
①、获取设备树中pin信息。
②、根据获取到的pin信息来设置pin的复用功能
③、根据获取到的pin信息来设置pin的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个pin的相关属性即可,其他的初始化工作均由pinctrl子系统来完成,pinctrl子系统源码目录为drivers/pinctrl。
45.1.2 I.MX6ULL的pinctrl子系统驱动
1、PIN配置信息详解
要使用pinctrl子系统,我们需要在设备树里面设置PIN的配置信息,毕竟pinctrl子系统要根据你提供的信息来配置PIN功能,一般会在设备树里面创建一个节点来描述PIN的配置信息。打开imx6ull.dtsi文件,找到一个叫做iomuxc的节点,如下所示:
示例代码45.1.2.1 iomuxc节点内容1
- 756 iomuxc: iomuxc@020e0000 {
- 757 compatible ="fsl,imx6ul-iomuxc";
- 758 reg =<0x020e00000x4000>;
- 759};
复制代码
iomuxc节点就是I.MX6ULL的IOMUXC外设对应的节点,看起来内容很少,没看出什么跟PIN的配置有关的内容啊,别急!打开imx6ull-alientek-emmc.dts,找到如下所示内容:
示例代码45.1.2.2 iomuxc节点内容2
- 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节点,如下所示:
示例代码45.1.2.3 完整的iomuxc节点
- 1 iomuxc: iomuxc@020e0000 {
- 2 compatible ="fsl,imx6ul-iomuxc";
- 3 reg =<0x020e00000x4000>;
- 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的宏定义内容如下:
示例代码45.1.2.4 UART1_RTS_B 引脚定义
- 190 #define MX6UL_PAD_UART1_RTS_B__UART1_DCE_RTS 0x00900x031C0x06200x00x3
- 191 #define MX6UL_PAD_UART1_RTS_B__UART1_DTE_CTS 0x00900x031C0x00000x00x0
- 192 #define MX6UL_PAD_UART1_RTS_B__ENET1_TX_ER 0x00900x031C0x00000x10x0
- 193 #define MX6UL_PAD_UART1_RTS_B__USDHC1_CD_B 0x00900x031C0x06680x20x1
- 194 #define MX6UL_PAD_UART1_RTS_B__CSI_DATA05 0x00900x031C0x04CC0x30x1
- 195 #define MX6UL_PAD_UART1_RTS_B__ENET2_1588_EVENT1_OUT 0x00900x031C0x00000x40x0
- 196 #define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x00900x031C0x00000x50x0
- 197 #define MX6UL_PAD_UART1_RTS_B__USDHC2_CD_B 0x00900x031C0x06740x80x2
复制代码
示例代码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所示:
图45.1.2.1 UART1_RTS_B引脚复用
示例代码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所示:
图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中有如下内容:
示例代码45.1.2.5 pinctrl-imx6ul.c文件代码段
- 326staticstruct 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
- 332staticint imx6ul_pinctrl_probe(struct platform_device *pdev)
- 333{
- 334conststruct of_device_id *match;
- 335struct imx_pinctrl_soc_info *pinctrl_info;
- 336
- 337 match = of_match_device(imx6ul_pinctrl_of_match,&pdev->dev);
- 338
- 339if(!match)
- 340return-ENODEV;
- 341
- 342 pinctrl_info =(struct imx_pinctrl_soc_info *) match->data;
- 343
- 344return imx_pinctrl_probe(pdev, pinctrl_info);
- 345}
- 346
- 347staticstruct platform_driver imx6ul_pinctrl_driver ={
- 348.driver ={
- 349.name ="imx6ul-pinctrl",
- 350.owner = THIS_MODULE,
- 351.of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),
- 352},
- 353.probe = imx6ul_pinctrl_probe,
- 354.remove = imx_pinctrl_remove,
- 355};
复制代码
第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 imx6ul_pinctrl_probe函数执行流程
在图45.1.2.3中函数imx_pinctrl_parse_groups负责获取设备树中关于PIN的配置信息,也就是我们前面分析的那6个u32类型的值。处理过程如下所示:
示例代码45.1.2.6 imx_pinctrl_parse_groups函数代码段
- 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 */
- 492 #define FSL_PIN_SIZE 24
- 493 #define SHARE_FSL_PIN_SIZE 20
- 494
- 495staticint imx_pinctrl_parse_groups(struct device_node *np,
- 496struct imx_pin_group *grp,
- 497struct imx_pinctrl_soc_info *info,
- 498 u32 index)
- 499{
- 500int size, pin_size;
- 501const __be32 *list;
- 502int i;
- 503 u32 config;
- ......
- 537
- 538for(i =0; i < grp->npins; i++){
- 539 u32 mux_reg = be32_to_cpu(*list++);
- 540 u32 conf_reg;
- 541unsignedint pin_id;
- 542struct imx_pin_reg *pin_reg;
- 543struct imx_pin *pin =&grp->pins;
- 544
- ......
- 555
- 556 pin_id =(mux_reg !=-1)? mux_reg /4: conf_reg /4;
- 557 pin_reg =&info->pin_regs[pin_id];
- 558 pin->pin = pin_id;
- 559 grp->pin_ids= pin_id;
- 560 pin_reg->mux_reg = mux_reg;
- 561 pin_reg->conf_reg = conf_reg;
- 562 pin->input_reg = be32_to_cpu(*list++);
- 563 pin->mux_mode = be32_to_cpu(*list++);
- 564 pin->input_val = be32_to_cpu(*list++);
- 565
- 566/* SION bit is in mux register */
- 567 config = be32_to_cpu(*list++);
- 568if(config & IMX_PAD_SION)
- 569 pin->mux_mode |= IOMUXC_CONFIG_SION;
- 570 pin->config = config &~IMX_PAD_SION;
- ......
- 574}
- 575
- 576return0;
- 577}
复制代码
第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结构体如下所示:
示例代码45.1.2.7 pinctrl_desc结构体
- 128struct pinctrl_desc {
- 129constchar*name;
- 130struct pinctrl_pin_desc const*pins;
- 131unsignedint npins;
- 132conststruct pinctrl_ops *pctlops;
- 133conststruct pinmux_ops *pmxops;
- 134conststruct pinconf_ops *confops;
- 135struct module *owner;
- 136 #ifdef CONFIG_GENERIC_PINCONF
- 137unsignedint num_custom_params;
- 138conststruct pinconf_generic_params *custom_params;
- 139conststruct pin_config_item *custom_conf_items;
- 140 #endif
- 141};
复制代码
第132~124行,这三个“_ops”结构体指针非常重要!!!因为这三个结构体就是PIN控制器的“工具”,这三个结构体里面包含了很多操作函数,通过这些操作函数就可以完成对某一个PIN的配置。pinctrl_desc结构体需要由用户提供,结构体里面的成员变量也是用户提供的。但是这个用户并不是我们这些使用芯片的程序员,而是半导体厂商,半导体厂商发布的Linux内核源码中已经把这些工作做完了。比如在imx_pinctrl_probe函数中可以找到如下所示代码:
示例代码45.1.2.8 imx_pinctrl_probe函数代码段
- 648int imx_pinctrl_probe(struct platform_device *pdev,
- 649struct imx_pinctrl_soc_info *info)
- 650{
- 651struct device_node *dev_np = pdev->dev.of_node;
- 652struct device_node *np;
- 653struct imx_pinctrl *ipctl;
- 654struct resource *res;
- 655struct pinctrl_desc *imx_pinctrl_desc;
- ......
- 663
- 664 imx_pinctrl_desc = devm_kzalloc(&pdev->dev,sizeof(*imx_pinctrl_desc),
- 665 GFP_KERNEL);
- 666if(!imx_pinctrl_desc)
- 667return-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这三个结构体定义如下:
示例代码45.1.2.9 imx_pctrl_ops、imx_pmx_ops和imx_pinconf_ops结构体
- 174staticconststruct 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};
- ......
- 374staticconststruct 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};
- ......
- 481staticconststruct 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配置函数,我们就此打住,不在去分析这些函数了,否则本章就没完没了了,有兴趣的可以去看一下。
45.1.3 设备树中添加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_”。添加完成以后如下所示:
示例代码45.1.2.10 test设备pinctrl节点
- 1 pinctrl_test: testgrp {
- 2/* 具体的PIN信息 */
- 3};
复制代码
2、添加“fsl,pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl,pins”,因为对于I.MX系列SOC而言,pinctrl驱动程序是通过读取“fsl,pins”属性值来获取PIN的配置信息,完成以后如下所示:
示例代码45.1.2.11 添加"fsl,pins"属性
- 1 pinctrl_test: testgrp {
- 2 fsl,pins =<
- 3 /* 设备所使用的PIN配置信息 */
- 4 >;
- 5};
复制代码
3、在“fsl,pins”属性中添加PIN配置信息
最后在“fsl,pins”属性中添加具体的PIN配置信息,完成以后如下所示:
示例代码45.1.2.13 完整的test设备pinctrl子节点
- 1 pinctrl_test: testgrp {
- 2 fsl,pins =<
- 3 MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config是具体设置值*/
- 4 >;
- 5};
复制代码
至此,我们已经在imx6ull-alientek-emmc.dts文件中添加好了test设备所使用的PIN配置信息。
45.2 gpio子系统
45.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。
45.2.2 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设置如下:
示例代码45.2.2.1 SD卡CD引脚PIN配置参数
- 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卡设备节点,如下所示:
示例代码45.2.2.2 设备树中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 =<®_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,在里面找到如下所示内容:
示例代码45.2.2.2 gpio1节点
- 504 gpio1: gpio@0209c000 {
- 505 compatible ="fsl,imx6ul-gpio","fsl,imx35-gpio";
- 506 reg =<0x0209c0000x4000>;
- 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寄存器表
从图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的话表示高电平有效,如果为1的话表示低电平有效。
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匹配表:
示例代码45.2.2.3 mxc_gpio_dt_ids匹配表
- 152staticconststruct 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所示:
图45.2.2.2 gpio核心驱动文件
我们重点来看一下gpio-mxc.c这个文件,在gpio-mxc.c文件中有如下所示内容:
示例代码45.2.2.4 mxc_gpio_driver结构体
- 496staticstruct 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这个函数,函数内容如下:
示例代码45.2.2.5 mxc_gpio_probe函数
- 403staticint mxc_gpio_probe(struct platform_device *pdev)
- 404{
- 405struct device_node *np = pdev->dev.of_node;
- 406struct mxc_gpio_port *port;
- 407struct resource *iores;
- 408int irq_base;
- 409int err;
- 410
- 411 mxc_gpio_get_hw(pdev);
- 412
- 413 port = devm_kzalloc(&pdev->dev,sizeof(*port), GFP_KERNEL);
- 414if(!port)
- 415return-ENOMEM;
- 416
- 417 iores = platform_get_resource(pdev, IORESOURCE_MEM,0);
- 418 port->base = devm_ioremap_resource(&pdev->dev, iores);
- 419if(IS_ERR(port->base))
- 420return PTR_ERR(port->base);
- 421
- 422 port->irq_high = platform_get_irq(pdev,1);
- 423 port->irq = platform_get_irq(pdev,0);
- 424if(port->irq <0)
- 425return port->irq;
- 426
- 427/* disable the interrupt and clear the status */
- 428 writel(0, port->base + GPIO_IMR);
- 429 writel(~0, port->base + GPIO_ISR);
- 430
- 431if(mxc_gpio_hwtype == IMX21_GPIO){
- 432/*
- 433 * Setup one handler for all GPIO interrupts. Actually
- 434 * setting the handler is needed only once, but doing it for
- 435 * every port is more robust and easier.
- 436 */
- 437 irq_set_chained_handler(port->irq, mx2_gpio_irq_handler);
- 438}else{
- 439/* setup one handler for each entry */
- 440 irq_set_chained_handler(port->irq, mx3_gpio_irq_handler);
- 441 irq_set_handler_data(port->irq, port);
- 442if(port->irq_high >0){
- 443/* setup handler for GPIO 16 to 31 */
- 444 irq_set_chained_handler(port->irq_high,
- 445 mx3_gpio_irq_handler);
- 446 irq_set_handler_data(port->irq_high, port);
- 447}
- 448}
- 449
- 450 err = bgpio_init(&port->bgc,&pdev->dev,4,
- 451 port->base + GPIO_PSR,
- 452 port->base + GPIO_DR,NULL,
- 453 port->base + GPIO_GDIR,NULL,0);
- 454if(err)
- 455goto out_bgio;
- 456
- 457 port->bgc.gc.to_irq = mxc_gpio_to_irq;
- 458 port->bgc.gc.base =(pdev->id <0)? of_alias_get_id(np,"gpio")
- 459*32:pdev->id *32;
- 460
- 461 err = gpiochip_add(&port->bgc.gc);
- 462if(err)
- 463goto out_bgpio_remove;
- 464
- 465 irq_base = irq_alloc_descs(-1,0,32, numa_node_id());
- 466if(irq_base <0){
- 467 err = irq_base;
- 468goto out_gpiochip_remove;
- 469}
- 470
- 471 port->domain = irq_domain_add_legacy(np,32, irq_base,0,
- 472&irq_domain_simple_ops,NULL);
- 473if(!port->domain){
- 474 err =-ENODEV;
- 475goto out_irqdesc_free;
- 476}
- 477
- 478/* gpio-mxc can be a generic irq chip */
- 479 mxc_gpio_init_gc(port, irq_base);
- 480
- 481 list_add_tail(&port->node,&mxc_gpio_ports);
- 482
- 483return0;
- ......
- 494}
复制代码
第405行,设备树节点指针。
第406行,定义一个结构体指针port,结构体类型为mxc_gpio_port。gpio-mxc.c的重点工作就是维护mxc_gpio_port,mxc_gpio_port就是对I.MX6ULL GPIO的抽象。mxc_gpio_port结构体定义如下:
示例代码45.2.2.6 mxc_gpio_port结构体
- 61struct 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里面有如下代码:
示例代码45.2.2.7 mxc_gpio_get_hw函数
- 364staticvoid mxc_gpio_get_hw(struct platform_device *pdev)
- 365{
- 366conststruct of_device_id *of_id =
- 367 of_match_device(mxc_gpio_dt_ids,&pdev->dev);
- 368enum mxc_gpio_hwtype hwtype;
- ......
- 383
- 384if(hwtype == IMX35_GPIO)
- 385 mxc_gpio_hwdata =&imx35_gpio_hwdata;
- 386elseif(hwtype == IMX31_GPIO)
- 387 mxc_gpio_hwdata =&imx31_gpio_hwdata;
- 388else
- 389 mxc_gpio_hwdata =&imx1_imx21_gpio_hwdata;
- 390
- 391 mxc_gpio_hwtype = hwtype;
- 392}
复制代码
注意第385行,mxc_gpio_hwdata是个全局变量,如果硬件类型是IMX35_GPIO的话设置mxc_gpio_hwdat为imx35_gpio_hwdata。对于I.MX6ULL而言,硬件类型就是IMX35_GPIO,imx35_gpio_hwdata是个结构体变量,描述了GPIO寄存器组,内容如下:
示例代码45.2.2.8 imx35_gpio_hwdata结构体
- 101staticstruct 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结构体如下所示(有缩减):
示例代码45.2.2.9 gpio_chip结构体
- 74struct gpio_chip {
- 75constchar *label;
- 76struct device *dev;
- 77struct module *owner;
- 78struct list_head list;
- 79
- 80int (*request)(struct gpio_chip *chip,
- 81 unsigned offset);
- 82void (*free)(struct gpio_chip *chip,
- 83 unsigned offset);
- 84int (*get_direction)(struct gpio_chip *chip,
- 85 unsigned offset);
- 86int (*direction_input)(struct gpio_chip *chip,
- 87 unsigned offset);
- 88int (*direction_output)(struct gpio_chip *chip,
- 89 unsigned offset,int value);
- 90int (*get)(struct gpio_chip *chip,
- 91 unsigned offset);
- 92void (*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函数。
45.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函数就讲这些,这些是我们用的最多的。
45.2.4 设备树中添加gpio节点模板
继续完成45.1.3中的test设备,在45.1.3中我们已经讲解了如何创建test设备的pinctrl节点。本节我们来学习一下如何创建test设备的GPIO节点。
1、创建test设备节点
在根节点“/”下创建test设备子节点,如下所示:
示例代码45.2.4.1 test设备节点
2、添加pinctrl信息
在45.1.3中我们创建了pinctrl_test节点,此节点描述了test设备所使用的GPIO_IO00这个PIN的信息,我们要将这节点添加到test设备节点中,如下所示:
示例代码45.2.4.2 向test节点添加pinctrl信息
- 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是哪个引脚,添加完成以后如下所示:
示例代码45.2.4.3 向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灯。
45.2.5 与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)
函数参数和返回值含义如下:
nd:设备节点。
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)
复制代码
函数参数和返回值含义如下:
nd:设备节点。
返回值:正值,统计到的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)
复制代码
函数参数和返回值含义如下:
nd:设备节点。
propname:包含要获取GPIO信息的属性名。
index:GPIO索引,因为一个属性里面可能包含多个GPIO,此参数指定要获取哪个GPIO的编号,如果只有一个GPIO信息的话此参数为0。
返回值:正值,获取到的GPIO编号;负值,失败。
45.3硬件原理图分析
本章实验硬件原理图参考8.3小节即可。
45.4 实验程序编写
本实验对应的例程路径为:开发板光盘->2、Linux驱动例程->5_gpioled。
本章实验我们继续研究LED灯,在第四十四章实验中我们通过设备树向dtsled.c文件传递相应的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验我们使用pinctrl和gpio子系统来完成LED灯驱动。
45.4.1 修改设备树文件
1、添加pinctrl节点
I.MX6U-ALPHA开发板上的LED灯使用了GPIO1_IO03这个PIN,打开imx6ull-alientek-emmc.dts,在iomuxc节点的imx6ul-evk子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:
示例代码45.4.1.1 GPIO1_IO03 pinctrl节点
- 1 pinctrl_led: ledgrp {
- 2 fsl,pins =<
- 3 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0/* LED0 */
- 4>;
- 5};
复制代码
第3行,将GPIO1_IO03这个PIN复用为GPIO1_IO03,也就是GPIO,GPI1O_IO03这个PIN的电气属性值为0X10B0,也就是设置IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03寄存器的值为0X10B0。
2、添加LED设备节点
在根节点“/”下创建LED灯节点,节点名为“gpioled”,节点内容如下:
示例代码45.4.1.2 创建LED灯节点
- 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}
复制代码
第7行,pinctrl-0属性设置LED灯所使用的PIN对应的pinctrl节点。
第8行,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中找到如下内容:
示例代码45.4.1.3 pinctrl_tsc节点
- 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”,找到如下内容:
示例代码45.4.1.4 tsc节点
- 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,找到一个屏蔽一个。
设备树编写完成以后使用“makedtbs”命令重新编译设备树,然后使用新编译出来的imx6ull-alientek-emmc.dtb文件启动Linux系统。启动成功以后进入“/proc/device-tree”目录中查看“gpioled”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图45.4.1.1所示:
图45.4.1.1 gpio子节点
45.4.2 LED灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,本章实验在第四十四章实验驱动文件dtsled.c的基础上修改而来。新建名为“5_gpioled”文件夹,然后在5_gpioled文件夹里面创建vscode工程,工作区命名为“gpioled”。工程创建好以后新建gpioled.c文件,在gpioled.c里面输入如下内容:
示例代码45.4.2.1 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作者 : 左忠凯
- 21版本 : V1.0
- 22描述 : 采用pinctrl和gpio子系统驱动LED灯。
- 23其他 : 无
- 24论坛 : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
- 25日志 : 初版V1.0 2019/7/13 左忠凯创建
- 26 ***************************************************************/
- 27 #define GPIOLED_CNT 1 /* 设备号个数 */
- 28 #define GPIOLED_NAME "gpioled" /* 名字 */
- 29 #define LEDOFF 0 /* 关灯 */
- 30 #define LEDON 1 /* 开灯 */
- 31
- 32/* gpioled设备结构体 */
- 33struct gpioled_dev{
- 34 dev_t devid; /* 设备号 */
- 35struct cdev cdev; /* cdev */
- 36struct class *class; /* 类 */
- 37struct device *device; /* 设备 */
- 38int major; /* 主设备号 */
- 39int minor; /* 次设备号 */
- 40struct device_node *nd; /* 设备节点 */
- 41int led_gpio; /* led所使用的GPIO编号 */
- 42};
- 43
- 44struct 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 */
- 53staticint led_open(struct inode *inode,struct file *filp)
- 54{
- 55 filp->private_data =&gpioled;/* 设置私有数据 */
- 56return0;
- 57}
- 58
- 59/*
- 60 * @description : 从设备读取数据
- 61 * @param – filp : 要打开的设备文件(文件描述符)
- 62 * @param - buf : 返回给用户空间的数据缓冲区
- 63 * @param - cnt : 要读取的数据长度
- 64 * @param – offt : 相对于文件首地址的偏移
- 65 * @return : 读取的字节数,如果为负值,表示读取失败
- 66 */
- 67static ssize_t led_read(struct file *filp,char __user *buf,
- size_t cnt, loff_t *offt)
- 68{
- 69return0;
- 70}
- 71
- 72/*
- 73 * @description : 向设备写数据
- 74 * @param - filp : 设备文件,表示打开的文件描述符
- 75 * @param - buf : 要写给设备写入的数据
- 76 * @param - cnt : 要写入的数据长度
- 77 * @param – offt : 相对于文件首地址的偏移
- 78 * @return : 写入的字节数,如果为负值,表示写入失败
- 79 */
- 80static ssize_t led_write(struct file *filp,constchar __user *buf,
- size_t cnt, loff_t *offt)
- 81{
- 82int retvalue;
- 83unsignedchar databuf[1];
- 84unsignedchar ledstat;
- 85struct gpioled_dev *dev = filp->private_data;
- 86
- 87 retvalue = copy_from_user(databuf, buf, cnt);
- 88if(retvalue <0){
- 89 printk("kernel write failed!\r\n");
- 90return-EFAULT;
- 91}
- 92
- 93 ledstat = databuf[0]; /* 获取状态值 */
- 94
- 95if(ledstat == LEDON){
- 96 gpio_set_value(dev->led_gpio,0);/* 打开LED灯 */
- 97}elseif(ledstat == LEDOFF){
- 98 gpio_set_value(dev->led_gpio,1);/* 关闭LED灯 */
- 99}
- 100return0;
- 101}
- 102
- 103/*
- 104 * @description : 关闭/释放设备
- 105 * @param – filp : 要关闭的设备文件(文件描述符)
- 106 * @return : 0 成功;其他失败
- 107 */
- 108staticint led_release(struct inode *inode,struct file *filp)
- 109{
- 110return0;
- 111}
- 112
- 113/* 设备操作函数 */
- 114staticstruct 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 */
- 127staticint __init led_init(void)
- 128{
- 129int ret =0;
- 130
- 131/* 设置LED所使用的GPIO */
- 132/* 1、获取设备节点:gpioled */
- 133 gpioled.nd = of_find_node_by_path("/gpioled");
- 134if(gpioled.nd ==NULL){
- 135 printk("gpioled node not find!\r\n");
- 136return-EINVAL;
- 137}else{
- 138 printk("gpioled node find!\r\n");
- 139}
- 140
- 141/* 2、获取设备树中的gpio属性,得到LED所使用的LED编号 */
- 142 gpioled.led_gpio = of_get_named_gpio(gpioled.nd,"led-gpio",0);
- 143if(gpioled.led_gpio <0){
- 144 printk("can't get led-gpio");
- 145return-EINVAL;
- 146}
- 147 printk("led-gpio num = %d\r\n", gpioled.led_gpio);
- 148
- 149/* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */
- 150 ret = gpio_direction_output(gpioled.led_gpio,1);
- 151if(ret <0){
- 152 printk("can't set gpio!\r\n");
- 153}
- 154
- 155/* 注册字符设备驱动 */
- 156/* 1、创建设备号 */
- 157if(gpioled.major){ /* 定义了设备号 */
- 158 gpioled.devid = MKDEV(gpioled.major,0);
- 159 register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
- 160}else{ /* 没有定义设备号 */
- 161 alloc_chrdev_region(&gpioled.devid,0, GPIOLED_CNT, GPIOLED_NAME); /* 申请设备号 */
- 162 gpioled.major = MAJOR(gpioled.devid);/* 获取分配号的主设备号 */
- 163 gpioled.minor = MINOR(gpioled.devid);/* 获取分配号的次设备号 */
- 164}
- 165 printk("gpioled major=%d,minor=%d\r\n",gpioled.major, gpioled.minor);
- 166
- 167/* 2、初始化cdev */
- 168 gpioled.cdev.owner = THIS_MODULE;
- 169 cdev_init(&gpioled.cdev,&gpioled_fops);
- 170
- 171/* 3、添加一个cdev */
- 172 cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);
- 173
- 174/* 4、创建类 */
- 175 gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
- 176if(IS_ERR(gpioled.class)){
- 177return PTR_ERR(gpioled.class);
- 178}
- 179
- 180/* 5、创建设备 */
- 181 gpioled.device = device_create(gpioled.class,NULL, gpioled.devid,NULL, GPIOLED_NAME);
- 182if(IS_ERR(gpioled.device)){
- 183return PTR_ERR(gpioled.device);
- 184}
- 185return0;
- 186}
- 187
- 188/*
- 189 * @description : 驱动出口函数
- 190 * @param : 无
- 191 * @return : 无
- 192 */
- 193staticvoid __exit led_exit(void)
- 194{
- 195/* 注销字符设备驱动 */
- 196 cdev_del(&gpioled.cdev); /* 删除cdev */
- 197 unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);/* 注销 */
- 198
- 199 device_destroy(gpioled.class, gpioled.devid);
- 200 class_destroy(gpioled.class);
- 201}
- 202
- 203 module_init(led_init);
- 204 module_exit(led_exit);
- 205 MODULE_LICENSE("GPL");
- 206 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的地方都采用此方法。
44.4.3 编写测试APP
本章直接使用第四十二章的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即可。
45.5 运行测试
45.5.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为gpioled.o,Makefile内容如下所示:
示例代码45.5.1.1 Makefile文件
- 1 KERNELDIR:= /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
- ......
- 4 obj-m := gpioled.o.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-linux-gnueabihf-gcc ledApp.c -o ledApp
复制代码
编译成功以后就会生成ledApp这个应用程序。
45.5.2 运行测试
将上一小节编译出来的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 驱动加载成功以后输出的信息
从图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灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
|