pinctrl和gpio子系统实验
上一章我们编写了基于设备树的LED驱动,但是驱动的本质还是没变,都是配置LED灯
所使用的GPIO寄存器,驱动开发方式和裸机基本没啥区别。Linux是一个庞大而完善的系统,
尤其是驱动框架,像GPIO这种最基本的驱动不可能采用“原始”的裸机驱动开发方式,否则
就相当于你买了一辆车,结果每天推着车去上班。Linux内核提供了pinctrl和gpio子系统用于
GPIO驱动,本章我们就来学习一下如何借助pinctrl和gpio子系统来简化GPIO驱动开发。
1 pinctrl子系统
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。
1.2 I.MX6ULL的pinctrl子系统驱动
1、PIN配置信息详解
要使用pinctrl子系统,我们需要在设备树里面设置PIN的配置信息,毕竟pinctrl子系统要根据你提供的信息来配置PIN功能,一般会在设备树里面创建一个节点来描述PIN的配置信息。打开imx6ull.dtsi文件,找到一个叫做iomuxc的节点,如下所示:
iomuxc: iomuxc@020e0000 {compatible = "fsl,imx6ul-iomuxc";reg = <0x020e0000 0x4000>;
}
iomuxc节点就是I.MX6ULL的IOMUXC外设对应的节点,看起来内容很少,没看出什么跟PIN的配置有关的内容啊,别急!打开imx6ull-alientek-emmc.dts,找到如下所示内容:
&iomuxc {pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;imx6ul-evk {pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058>;};
......pinctrl_flexcan1: flexcan1grp {fsl,pins = <MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020>;};
......pinctrl_wdog: wdoggrp {fsl,pins = <MX6UL_PAD_LCD_RESET__WDOG1_WDOG_ANY 0x30b0>;};};
};
示例代码就是向iomuxc节点追加数据,不同的外设使用的PIN不同、其配置也不同,因此一个萝卜一个坑,将某个外设所使用的所有PIN都组织在一个子节点里面。示例代码中pinctrl_hog_1子节点就是和热插拔有关的PIN集合,比如USB OTG的ID引脚。pinctrl_flexcan1子节点是flexcan1这个外设所使用的PIN,pinctrl_wdog子节点是wdog外设所使用的PIN。如果需要在iomuxc中添加我们自定义外设的PIN,那么需要新建一个子节点,然后将这个自定义外设的所有PIN配置信息都放到这个子节点中。
将其与示例代码结合起来就可以得到完成的iomuxc节点,如下所示:
iomuxc: iomuxc@020e0000 {compatible = "fsl,imx6ul-iomuxc";reg = <0x020e0000 0x4000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;imx6ul-evk {pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT 0x17059MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x17059MX6UL_PAD_GPIO1_IO00__ANATOP_OTG1_ID 0x13058>;
......};};
};
第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的宏定
义内容如下:
#define MX6UL_PAD_UART1_RTS_B__UART1_DCE_RTS 0x0090 0x031C 0x0620 0x0 0x3
#define MX6UL_PAD_UART1_RTS_B__UART1_DTE_CTS 0x0090 0x031C 0x0000 0x0 0x0
#define MX6UL_PAD_UART1_RTS_B__ENET1_TX_ER 0x0090 0x031C 0x0000 0x1 0x0
#define MX6UL_PAD_UART1_RTS_B__USDHC1_CD_B 0x0090 0x031C 0x0668 0x2 0x1
#define MX6UL_PAD_UART1_RTS_B__CSI_DATA05 0x0090 0x031C 0x04CC 0x3 0x1
#define MX6UL_PAD_UART1_RTS_B__ENET2_1588_EVENT1_OUT 0x0090 0x031C 0x0000 0x4 0x0
#define MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x0090 0x031C 0x0000 0x5 0x0
#define MX6UL_PAD_UART1_RTS_B__USDHC2_CD_B 0x0090 0x031C 0x0674 0x8 0x2
示例代码中一共有8个以“MX6UL_PAD_UART1_RTS_B”开头的宏定义,大家仔细观察应该就能发现,这8个宏定义分别对应UART1_RTS_B这个PIN的8个复用IO。查阅《I.MX6ULL参考册》可以知UART1_RTS_B的可选复用IO如图所示:
示例代码的宏定义 MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 表示将UART1_RTS_B这个IO复用为GPIO1_IO19。此宏定义后面跟着5个数字,也就是这个宏定义的具体值,如下所示:
综上所述可知:
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这个寄存器的位域图,如图所示:
因此可知,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内核相应的驱动文件就
会根据这些值来做相应的初始化。接下来就找一下哪个驱动文件来做这一件事情,iomuxc节点
中compatible属性的值为“fsl,imx6ul-iomuxc”,在Linux内核中全局搜索“fsl,imx6ul-iomuxc”
字符串就会找到对应的驱动文件。在文件drivers/pinctrl/freescale/pinctrl-imx6ul.c中有如下内容:
static struct of_device_id imx6ul_pinctrl_of_match[] = {{ .compatible = "fsl,imx6ul-iomuxc", .data = &imx6ul_pinctrl_info, },{ .compatible = "fsl,imx6ull-iomuxc-snvs", .data = &imx6ull_snvs_pinctrl_info, },{ /* sentinel */ }
};static int imx6ul_pinctrl_probe(struct platform_device *pdev)
{const struct of_device_id *match;struct imx_pinctrl_soc_info *pinctrl_info;match = of_match_device(imx6ul_pinctrl_of_match, &pdev->dev);if (!match)return -ENODEV;pinctrl_info = (struct imx_pinctrl_soc_info *) match->data;return imx_pinctrl_probe(pdev, pinctrl_info);
}static struct platform_driver imx6ul_pinctrl_driver = {.driver = {.name = "imx6ul-pinctrl",.owner = THIS_MODULE,.of_match_table = of_match_ptr(imx6ul_pinctrl_of_match),},.probe = imx6ul_pinctrl_probe,.remove = imx_pinctrl_remove,
};
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配置工作。
platform_driver 是平台设备驱动,这个是我们后面章节要讲解的内容,platform_driver是个结构体,有个probe成员变量。在这里大家只需要知道,当设备和驱动匹配成功以后platform_driver的probe成员变量所代表的函数就会执行,设置probe成员变量为imx6ul_pinctrl_probe函数,因此在本章实验中imx6ul_pinctrl_probe这个函数就会执行,可以认为imx6ul_pinctrl_probe函数就是I.MX6ULL这个SOC的PIN配置入口函数。以此为入口,如图所示的函数调用路径:
在图中函数imx_pinctrl_parse_groups负责获取设备树中关于PIN的配置信息,也就是我们前面分析的那6个u32类型的值。处理过程如下所示:
/** Each pin represented in fsl,pins consists of 5 u32 PIN_FUNC_ID * and 1 u32 CONFIG, so 24 types in total for each pin. */
#define FSL_PIN_SIZE 24
#define SHARE_FSL_PIN_SIZE 20 static int imx_pinctrl_parse_groups(struct device_node *np, struct imx_pin_group *grp, struct imx_pinctrl_soc_info *info, u32 index)
{ int size, pin_size; const __be32 *list; int i; u32 config; ...... for (i = 0; i < grp->npins; i++) { u32 mux_reg = be32_to_cpu(*list++); u32 conf_reg; unsigned int pin_id; struct imx_pin_reg *pin_reg; struct imx_pin *pin = &grp->pins[i]; ...... pin_id = (mux_reg != -1) ? mux_reg / 4 : conf_reg / 4; pin_reg = &info->pin_regs[pin_id]; pin->pin = pin_id; grp->pin_ids[i] = pin_id; pin_reg->mux_reg = mux_reg; pin_reg->conf_reg = conf_reg; pin->input_reg = be32_to_cpu(*list++); pin->mux_mode = be32_to_cpu(*list++); pin->input_val = be32_to_cpu(*list++); /* SION bit is in mux register */ config = be32_to_cpu(*list++); if (config & IMX_PAD_SION) pin->mux_mode |= IOMUXC_CONFIG_SION; pin->config = config & ~IMX_PAD_SION; ...... } return 0;
}
设备树中的mux_reg和conf_reg值会保存在info参数中,input_reg、mux_mode、input_val和config值会保存在grp参数中。
获取mux_reg、conf_reg、input_reg、mux_mode和input_val值。
获取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结构体
如下所示:
struct pinctrl_desc {const char *name;struct pinctrl_pin_desc const *pins;unsigned int npins;const struct pinctrl_ops *pctlops;const struct pinmux_ops *pmxops;const struct pinconf_ops *confops;struct module *owner;
#ifdef CONFIG_GENERIC_PINCONFunsigned int num_custom_params;const struct pinconf_generic_params *custom_params;const struct pin_config_item *custom_conf_items;
#endif
};
这三个“_ops”结构体指针非常重要!!!因为这三个结构体就是PIN控制器的“工具”,这三个结构体里面包含了很多操作函数,通过这些操作函数就可以完成对某一个PIN的配置。pinctrl_desc结构体需要由用户提供,结构体里面的成员变量也是用户提供的。但是这个用户并不是我们这些使用芯片的程序员,而是半导体厂商,半导体厂商发布的Linux内核源码中已经把这些工作做完了。比如在imx_pinctrl_probe函数中可以找到如下所示代码:
int imx_pinctrl_probe(struct platform_device *pdev, struct imx_pinctrl_soc_info *info)
{ struct device_node *dev_np = pdev->dev.of_node; struct device_node *np; struct imx_pinctrl *ipctl; struct resource *res; struct pinctrl_desc *imx_pinctrl_desc; ......imx_pinctrl_desc = devm_kzalloc(&pdev->dev, sizeof(*imx_pinctrl_desc), GFP_KERNEL); if (!imx_pinctrl_desc) return -ENOMEM; ......imx_pinctrl_desc->name = dev_name(&pdev->dev); imx_pinctrl_desc->pins = info->pins; imx_pinctrl_desc->npins = info->npins; imx_pinctrl_desc->pctlops = &imx_pctrl_ops; imx_pinctrl_desc->pmxops = &imx_pmx_ops; imx_pinctrl_desc->confops = &imx_pinconf_ops; imx_pinctrl_desc->owner = THIS_MODULE; ......ipctl->pctl = pinctrl_register(imx_pinctrl_desc, &pdev->dev, ipctl); ......
}
定义结构体指针变量imx_pinctrl_desc。
向指针变量imx_pinctrl_desc分配内存。
初始化imx_pinctrl_desc结构体指针变量,重点是pctlops、pmxops和confops这三个成员变量,分别对应imx_pctrl_ops、imx_pmx_ops和imx_pinconf_ops这三个结构体。
调用函数pinctrl_register向Linux内核注册imx_pinctrl_desc,注册以后Linux内核就有了对I.MX6ULL的PIN进行配置的工具。
imx_pctrl_ops、imx_pmx_ops和imx_pinconf_ops这三个结构体定义如下:
static const struct pinctrl_ops imx_pctrl_ops = {.get_groups_count = imx_get_groups_count,.get_group_name = imx_get_group_name,.get_group_pins = imx_get_group_pins,.pin_dbg_show = imx_pin_dbg_show,.dt_node_to_map = imx_dt_node_to_map,.dt_free_map = imx_dt_free_map,
};......static const struct pinmux_ops imx_pmx_ops = {.get_functions_count = imx_pmx_get_funcs_count,.get_function_name = imx_pmx_get_func_name,.get_function_groups = imx_pmx_get_groups,.set_mux = imx_pmx_set,.gpio_request_enable = imx_pmx_gpio_request_enable,.gpio_set_direction = imx_pmx_gpio_set_direction,
};......static const struct pinconf_ops imx_pinconf_ops = {.pin_config_get = imx_pinconf_get,.pin_config_set = imx_pinconf_set,.pin_config_dbg_show = imx_pinconf_dbg_show,.pin_config_group_dbg_show = imx_pinconf_group_dbg_show,
};
示例代码中这三个结构体下的所有函数就是I.MX6ULL的PIN配置函数,我们就此打住,不再去分析这些函数了,否则本章就没完没了了,有兴趣的可以去看一下。
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_”。添
加完成以后如下所示:
pinctrl_test: testgrp { /* 具体的PIN信息 */ };
2、添加“fsl,pins”属性
设备树是通过属性来保存信息的,因此我们需要添加一个属性,属性名字一定要为“fsl,pins”,
因为对于I.MX系列SOC而言,pinctrl驱动程序是通过读取“fsl,pins”属性值来获取PIN的配
置信息,完成以后如下所示:
pinctrl_test: testgrp {
fsl,pins = < /* 设备所使用的PIN配置信息 */ >; };
3、在“fsl,pins”属性中添加PIN配置信息
pinctrl_test: testgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config是具体设置值*/ >; }
至此,我们已经在imx6ull-alientek-emmc.dts文件中添加好了test设备所使用的PIN配置信
息。
2 gpio子系统
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。
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设置如下:
pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
......>;
};
设置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卡设备节点,如下所示:
&usdhc1 {pinctrl-names = "default", "state_100mhz", "state_200mhz";pinctrl-0 = <&pinctrl_usdhc1>;pinctrl-1 = <&pinctrl_usdhc1_100mhz>;pinctrl-2 = <&pinctrl_usdhc1_200mhz>;/* pinctrl-3 = <&pinctrl_hog_1>; */cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;keep-power-in-suspend;enable-sdio-wakeup;vmmc-supply = <®_sd1_vmmc>;status = "okay";
};
屏蔽行本来没有,是作者添加的,usdhc1节点作为SD卡设备总节点,usdhc1节点需要描述SD卡所有的信息,因为驱动要使用。本行就是描述SD卡的CD引脚pinctrl信息所在的子节点,因为SD卡驱动需要根据pincrtl节点信息来设置CD引脚的复用功能等。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。
属性“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,在里面找到如下所示内容
gpio1: gpio@0209c000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x0209c000 0x4000>;interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;gpio-controller;#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;
};
gpio1节点信息描述了GPIO1控制器的所有信息,重点就是GPIO1外设寄存器基地址以及
兼容属性。关于I.MX 系列 SOC 的 GPIO 控制器绑定信息请查看文档
Documentation/devicetree/bindings/gpio/ fsl-imx-gpio.txt。
设置gpio1节点的compatible属性有两个,分别为“fsl,imx6ul-gpio”和“fsl,imx35-
gpio”,在Linux内核中搜索这两个字符串就可以找到I.MX6UL的GPIO驱动程序。
的reg属性设置了GPIO1控制器的寄存器基地址为0X0209C000,大家可以打开《I.MX6ULL参考手册》找到“Chapter 28:General Purpose Input/Output(GPIO)”章节第28.5小节,有如图所示的寄存器地址表
“gpio-controller”表示gpio1节点是个GPIO控制器。
“#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驱动程序简介
gpio1 节点的 compatible属性描述了兼容性,在 Linux内核中搜索“fsl,imx6ul-gpio”和
“fsl,imx35-gpio”这两个字符串,查找GPIO驱动文件。drivers/gpio/gpio-mxc.c就是I.MX6ULL
的GPIO驱动文件,在此文件中有如下所示of_device_id匹配表:
static const struct of_device_id mxc_gpio_dt_ids[] = {{ .compatible = "fsl,imx1-gpio", .data = &mxc_gpio_devtype[IMX1_GPIO], },{ .compatible = "fsl,imx21-gpio", .data = &mxc_gpio_devtype[IMX21_GPIO], },{ .compatible = "fsl,imx31-gpio", .data = &mxc_gpio_devtype[IMX31_GPIO], },{ .compatible = "fsl,imx35-gpio", .data = &mxc_gpio_devtype[IMX35_GPIO], },{ /* sentinel */ }
};
compatible值为“fsl,imx35-gpio”,和gpio1的compatible属性匹配,因此gpio-mxc.c就是I.MX6ULL的GPIO控制器驱动文件。gpio-mxc.c所在的目录为drivers/gpio,打开这个目录可以看到很多芯片的gpio驱动文件, “gpiolib”开始的文件是gpio驱动的核心文件,如图所示
我们重点来看一下gpio-mxc.c这个文件,在gpio-mxc.c文件中有如下所示内容:
static struct platform_driver mxc_gpio_driver = {.driver = {.name = "gpio-mxc",.of_match_table = mxc_gpio_dt_ids,},.probe = mxc_gpio_probe,.id_table = mxc_gpio_devtype,
};
可以看出 GPIO 驱动也是个平台设备驱动,因此当设备树中的设备节点与驱动的of_device_id匹配以后probe函数就会执行,在这里就是mxc_gpio_probe函数,这个函数就是I.MX6ULL的GPIO驱动入口函数。我们简单来分析一下mxc_gpio_probe这个函数,函数内容如下:
static int mxc_gpio_probe(struct platform_device *pdev)
{struct device_node *np = pdev->dev.of_node;struct mxc_gpio_port *port;struct resource *iores;int irq_base;int err;mxc_gpio_get_hw(pdev);port = devm_kzalloc(&pdev->dev, sizeof(*port), GFP_KERNEL);if (!port)return -ENOMEM;iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);port->base = devm_ioremap_resource(&pdev->dev, iores);if (IS_ERR(port->base))return PTR_ERR(port->base);port->irq_high = platform_get_irq(pdev, 1);port->irq = platform_get_irq(pdev, 0);if (port->irq < 0)return port->irq;/* disable the interrupt and clear the status */writel(0, port->base + GPIO_IMR);writel(~0, port->base + GPIO_ISR);if (mxc_gpio_hwtype == IMX21_GPIO) {/** Setup one handler for all GPIO interrupts. Actually* setting the handler is needed only once, but doing it for* every port is more robust and easier.*/irq_set_chained_handler(port->irq, mx2_gpio_irq_handler);} else {/* setup one handler for each entry */irq_set_chained_handler(port->irq, mx3_gpio_irq_handler);irq_set_handler_data(port->irq, port);if (port->irq_high > 0) {/* setup handler for GPIO 16 to 31 */irq_set_chained_handler(port->irq_high,mx3_gpio_irq_handler);irq_set_handler_data(port->irq_high, port);}}err = bgpio_init(&port->bgc, &pdev->dev, 4,port->base + GPIO_PSR,port->base + GPIO_DR, NULL,port->base + GPIO_GDIR, NULL, 0);if (err)goto out_bgio;port->bgc.gc.to_irq = mxc_gpio_to_irq;port->bgc.gc.base = (pdev->id < 0) ? of_alias_get_id(np, "gpio")* 32 : pdev->id * 32;err = gpiochip_add(&port->bgc.gc);if (err)goto out_bgpio_remove;irq_base = irq_alloc_descs(-1, 0, 32, numa_node_id());if (irq_base < 0) {err = irq_base;goto out_gpiochip_remove;}port->domain = irq_domain_add_legacy(np, 32, irq_base, 0,&irq_domain_simple_ops, NULL);if (!port->domain) {err = -ENODEV;goto out_irqdesc_free;}/* gpio-mxc can be a generic irq chip */mxc_gpio_init_gc(port, irq_base);list_add_tail(&port->node, &mxc_gpio_ports);return 0;
......
}
函数第一行,设备树节点指针。
第二行,定义一个结构体指针port,结构体类型为mxc_gpio_port。gpio-mxc.c的重点工
作就是维护mxc_gpio_port,mxc_gpio_port就是对I.MX6ULL GPIO的抽象。mxc_gpio_port结
构体定义如下
struct mxc_gpio_port {struct list_head node;void __iomem *base;int irq;int irq_high;struct irq_domain *domain;struct bgpio_chip bgc;u32 both_edges;
};
mxc_gpio_port的bgc成员变量很重要,因为稍后的重点就是初始化bgc。
继续回到mxc_gpio_probe函数函数,调用mxc_gpio_get_hw函数获取gpio的硬件相关数据,其实就是gpio的寄存器组,函数mxc_gpio_get_hw里面有如下代码:
static void mxc_gpio_get_hw(struct platform_device *pdev)
{const struct of_device_id *of_id = of_match_device(mxc_gpio_dt_ids, &pdev->dev);enum mxc_gpio_hwtype hwtype;......if (hwtype == IMX35_GPIO)mxc_gpio_hwdata = &imx35_gpio_hwdata;else if (hwtype == IMX31_GPIO)mxc_gpio_hwdata = &imx31_gpio_hwdata;elsemxc_gpio_hwdata = &imx1_imx21_gpio_hwdata;mxc_gpio_hwtype = hwtype;
}
注意mxc_gpio_hwdata是个全局变量,如果硬件类型是IMX35_GPIO的话设置
mxc_gpio_hwdat为imx35_gpio_hwdata。对于I.MX6ULL而言,硬件类型就是IMX35_GPIO,
imx35_gpio_hwdata是个结构体变量,描述了GPIO寄存器组,内容如下:
static struct mxc_gpio_hwdata imx35_gpio_hwdata = {.dr_reg = 0x00,.gdir_reg = 0x04,.psr_reg = 0x08,.icr1_reg = 0x0c,.icr2_reg = 0x10,.imr_reg = 0x14,.isr_reg = 0x18,.edge_sel_reg = 0x1c,.low_level = 0x00,.high_level = 0x01,.rise_edge = 0x02,.fall_edge = 0x03,
};
大家将imx35_gpio_hwdata中的各个成员变量和图中的GPIO寄存器表对比就会发现,imx35_gpio_hwdata 结构体就是 GPIO 寄存器组结构。这样我们后面就可以通过
mxc_gpio_hwdata这个全局变量来访问 GPIO的相应寄存器了。
继续回到示例代码 的 mxc_gpio_probe 函数中,调用函数platform_get_resource获取设备树中内存资源信息,也就是reg属性值。前面说了reg属性指定了GPIO1控制器的寄存器基地址为0X0209C000,在配合前面已经得到的mxc_gpio_hwdata,这样Linux内核就可以访问gpio1的所有寄存器了。
调用devm_ioremap_resource函数进行内存映射,得到0x0209C000在Linux内核中的虚拟地址。
通过platform_get_irq函数获取中断号,第422行获取高16位GPIO的中断号,第423行获取底16位GPIO中断号。
操作GPIO1的IMR和ISR这两个寄存器,关闭GPIO1所有IO中断,并且清除状态寄存器。
设置对应GPIO的中断服务函数,不管是高16位还是低16位,中断服务函数都是mx3_gpio_irq_handler。
bgpio_init函数第一个参数为bgc,是bgpio_chip结构体指针。bgpio_chip结构体有个gc成员变量,gc是个gpio_chip结构体类型的变量。gpio_chip结构体是抽象出来的GPIO控制器,gpio_chip结构体如下所示(有缩减):
struct gpio_chip {const char *label;struct device *dev;struct module *owner;struct list_head list;int (*request)(struct gpio_chip *chip, unsigned offset);void (*free)(struct gpio_chip *chip, unsigned offset);int (*get_direction)(struct gpio_chip *chip, unsigned offset);int (*direction_input)(struct gpio_chip *chip, unsigned offset);int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);int (*get)(struct gpio_chip *chip, unsigned offset);void (*set)(struct gpio_chip *chip, unsigned offset, int value);......
};
可以看出,gpio_chip大量的成员都是函数,这些函数就是GPIO操作函数。bgpio_init函数
主要任务就是初始化 bgc->gc。bgpio_init 里面有三个 setup 函数:bgpio_setup_io、
bgpio_setup_accessors和bgpio_setup_direction。这三个函数就是初始化bgc->gc中的各种有关
GPIO的操作,比如输出,输入等等。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函数,调用函数gpiochip_add向Linux内核注册gpio_chip,也就是port->bgc.gc。注册完成以后我们就可以在驱动中使用gpiolib提供的各个API函数。
2.3 gpio子系统API函数
对于驱动开发人员,设置好设备树以后就可以使用gpio子系统提供的API函数来操作指定
的GPIO,gpio子系统向驱动开发人员屏蔽了具体的读写寄存器过程。这就是驱动分层与分离
的好处,大家各司其职,做好自己的本职工作即可。gpio子系统提供的常用的API函数有下面
几个:
gpio_request
函数用于申请一个 GPIO 管脚,在使用 GPIO 前必须调用该函数进行申请。函数原型为:
int gpio_request(unsigned gpio, const char *label)
参数
gpio:要申请的 GPIO 标号,可通过 of_get_named_gpio 从设备树获取。
label:为 GPIO 设置名称。
返回值
0 表示成功,非 0 表示失败。
gpio_free
函数用于释放不再使用的 GPIO。函数原型为:
void gpio_free(unsigned gpio)
参数
gpio:要释放的 GPIO 标号。
返回值
无。
gpio_direction_input
函数用于设置 GPIO 为输入模式。函数原型为:
int gpio_direction_input(unsigned gpio)参数
gpio:要设置为输入的 GPIO 标号。
返回值
0 表示成功,负值表示失败。
gpio_direction_output
函数用于设置 GPIO 为输出模式并指定默认输出值。函数原型为:
int gpio_direction_output(unsigned gpio, int value)参数
gpio:要设置为输出的 GPIO 标号。
value:默认输出值。
返回值
0 表示成功,负值表示失败。
gpio_get_value
是一个宏,用于获取 GPIO 的值。定义如下:
#define gpio_get_value __gpio_get_value
int __gpio_get_value(unsigned gpio)参数
gpio:要读取的 GPIO 标号。
返回值
非负值表示 GPIO 值,负值表示失败。
gpio_set_value
是一个宏,用于设置 GPIO 的值。定义如下:
#define gpio_set_value __gpio_set_value
void __gpio_set_value(unsigned gpio, int value)参数
gpio:要设置的 GPIO 标号。
value:要设置的值。
返回值
无。
2.4 设备树中添加gpio节点模板
继续完成test设备,我们已经讲解了如何创建test设备的pinctrl节点。本节我们来学习一下如何创建test设备的GPIO节点。
1.创建test设备节点
在根节点“/”下创建test设备子节点,节点内容如下:
test {/* 节点内容 */
};
2.添加pinctrl信息
将已创建的pinctrl_test节点信息添加到test设备节点中,节点内容如下:
test {pinctrl-names = "default";pinctrl-0 = <&pinctrl_test>;/* 其他节点内容 */
};
pinctrl-names
属性描述pinctrl名字为“default”。pinctrl-0
节点引用pinctrl_test节点,表示test设备的PIN信息保存在pinctrl_test节点中。
3.添加GPIO属性信息
在test节点中添加GPIO属性信息,指定test设备所使用的GPIO引脚,节点内容如下:
test {pinctrl-names = "default";pinctrl-0 = <&pinctrl_test>;gpio = <&gpio1 0 GPIO_ACTIVE_LOW>;
};
gpio
属性指定test设备所使用的GPIO引脚为GPIO1_IO00,且为低电平有效。
2.5 与gpio相关的OF函数
在示例代码中,我们定义了一个名为“gpio”的属性,gpio属性描述了test这个设备所使用的GPIO。在驱动程序中需要读取gpio属性内容,Linux内核提供了几个与GPIO有关的OF函数,常用的几个OF函数如下所示:
of_gpio_named_count函数
该函数用于统计设备树中指定属性包含的GPIO数量,包括空值。例如属性gpios = <0 &gpio1 1 2 0 &gpio2 3 4>
会被统计为4个GPIO(其中两个为无效值)。
函数原型为:
int of_gpio_named_count(struct device_node *np, const char *propname)
参数说明:
np
:设备节点指针propname
:目标属性名称 返回值:成功返回GPIO数量,失败返回错误码
of_gpio_count函数
功能与of_gpio_named_count
类似,但固定统计gpios
属性中的GPIO数量。适用于标准GPIO属性命名场景。
函数原型为:
int of_gpio_count(struct device_node *np)
参数说明:
np
:设备节点指针 返回值:成功返回GPIO数量,失败返回错误码
of_get_named_gpio函数
该函数用于从设备树属性中解析出特定索引的GPIO编号,是驱动开发中的核心接口。支持解析形如<&gpio5 7 GPIO_ACTIVE_LOW>
的配置项。
函数原型为:
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
参数说明:
np
:设备节点指针propname
:包含GPIO配置的属性名index
:GPIO在属性中的位置索引 返回值:成功返回GPIO编号,失败返回错误码
3 硬件原理图分析
参考之前章节。
4 实验程序编写
本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 5_gpioled。
本章实验我们继续研究LED灯,在第四十四章实验中我们通过设备树向dtsled.c文件传递
相应的寄存器物理地址,然后在驱动文件中配置寄存器。本章实验我们使用pinctrl和gpio子系
统来完成LED灯驱动。
4.1 修改设备树文件
添加pinctrl节点
在I.MX6U-ALPHA开发板的设备树文件imx6ull-alientek-emmc.dts
中,找到iomuxc
节点下的imx6ul-evk
子节点,添加名为pinctrl_led
的子节点,内容如下:
pinctrl_led: ledgrp {fsl,pins = <MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */>;
};
此处将GPIO1_IO03
复用为GPIO功能,电气属性值设置为0x10B0
,用于控制LED灯。
添加LED设备节点
在根节点/
下创建名为gpioled
的LED设备节点,内容如下:
gpioled {#address-cells = <1>;#size-cells = <1>;compatible = "atkalpha-gpioled";pinctrl-names = "default";pinctrl-0 = <&pinctrl_led>;led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;status = "okay";
}
pinctrl-0
属性指定了LED灯使用的PIN对应的pinctrl节点。led-gpio
属性定义了LED灯使用的GPIO为GPIO1_IO03
,低电平有效。驱动程序将通过该属性获取GPIO编号,以便通过GPIO子系统API进行操作。
检查PIN是否被其他外设使用
在设备树驱动开发中,PIN的复用配置是关键步骤。若引脚被其他外设占用,驱动可能无法正常申请资源。以下是详细检查方法:
检查pinctrl设置
在设备树文件中查找目标引脚(例如GPIO1_IO03
)是否被其他外设的pinctrl
节点占用。例如:
pinctrl_tsc: tscgrp {fsl,pins = <MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0xb0MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0xb0MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0xb0 // 需屏蔽此行MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0xb0>;
};
若引脚被占用(如作为TSC接口),需注释或删除相关行(使用/* */
)。
检查GPIO占用情况
搜索设备树中(在imx6ull-alientek-emmc.dts中搜索“gpio1 3”)是否直接引用了该GPIO(如gpio1 3
):
&tsc {xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; // 需屏蔽此行status = "okay";
};
发现后同样需要屏蔽或删除相关配置。
验证设备树修改
- 重新编译设备树:
make dtbs
- 启动系统后检查节点是否存在:
ls /proc/device-tree/gpioled
若节点存在且驱动功能正常,则修改成功。
注意:需确保引脚配置与硬件实际连接一致,避免功能冲突。
4.2 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 cant not found!\r\n");return -EINVAL;} else {printk("gpioled node has been found!\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;}printk("led-gpio num = %d\r\n", gpioled.led_gpio);/* 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);
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
在设备结构体gpioled_dev中加入led_gpio这个成员变量,此成员变量保存LED
等所使用的GPIO编号。
将设备结构体变量gpioled设置为filp的私有数据private_data。
通过读取filp的private_data成员变量来得到设备结构体变量,也就是gpioled。
这种将设备结构体设置为filp私有数据的方法在Linux内核驱动里面非常常见。
直接调用gpio_set_value函数来向GPIO写入数据,实现开/关LED的效果。
不需要我们直接操作相应的寄存器。
获取节点“/gpioled”。
通过函数of_get_named_gpio函数获取LED所使用的LED编号。相当于将
gpioled节点中的“led-gpio”属性值转换为对应的LED编号。
调用函数gpio_direction_output设置GPIO1_IO03这个GPIO为输出,并且默认
高电平,这样默认就会关闭LED灯。
可以看出gpioled.c文件中的内容和前一章节的dtsled.c差不多,只是取消掉了配置寄存
器的过程,改为使用Linux内核提供的API函数。在GPIO操作上更加的规范化,符合Linux
代码框架,而且也简化了GPIO驱动开发的难度,以后我们所有例程用到GPIO的地方都采用
此方法。
4.3 编写测试APP
本章直接使用前面的的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即
可。
5 运行测试
5.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和前面基本一样,只是将obj-m变量的值改为gpioled.o,Makefile内容如下所示:
2、编译测试APP
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
5.2 运行测试
将上一小节编译出来的gpioled.ko和ledApp这两个文件拷贝到rootfs/lib/modules/4.1.15目
录中,重启开发板,进入到目录lib/modules/4.1.15中,输入如下命令加载gpioled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe gpioled.ko //加载驱动
驱动加载成功以后会在终端中输出一些信息
可以看出,gpioled这个节点找到了,并且GPIO1_IO03这个GPIO的编号为3。驱动加载成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/gpioled 1 //打开LED灯
./ledApp /dev/gpioled 0 //关闭LED灯
rmmod gpioled.ko//卸载驱动