> 文章列表 > I.MX6ULL内核开发13:pinctrl子系统和gpio子系统-led实验

I.MX6ULL内核开发13:pinctrl子系统和gpio子系统-led实验

I.MX6ULL内核开发13:pinctrl子系统和gpio子系统-led实验

目录

一、pinctrl子系统 

1.1 pinctrl子系统编写格式以及引脚属性介绍

1.1.1 iomux节点介绍

1.1.2 pinctrl子节点编写格式

1.1.3 引脚配置信息介绍

1.2 将RGB灯引脚添加到pinctrl子系统

1.2.1 查找RGB灯使用的引脚

1.2.2找到引脚宏定义 

1.2.3 设置引脚属性

1.2.4 在iomuxc节点中添加pinctrl子节点

二、GPIO子系统

2.1 在设备树种添加RGB灯的设备树节点

2.2 编译、下载设备树验证修改结果

2.3 GPIO子系统常用的API函数

三、实验

3.1 实验代码 

3.1.1 驱动程序

3.1.2 应用程序

3.2 实验准备

3.2.1 Makefile修改

3.3 下载验证


一、pinctrl子系统 

        pinctrl子系统用于管理芯片的引脚。imx6ull芯片上拥有众多的片上外设,大多数外设需要通过芯片的引脚与外部涉笔(器件)相连实现相对应的控制,例如I2C、SPI、LCD、USDHC等等。而芯片的可用引脚(除去电源引脚和特定功能引脚)数量是有限的,芯片的设计厂商为了提高硬件设计的灵活性,一个芯片引脚往往可以作为多个片上外设的功能引脚。

        在驱动程序中需要手动的设置每个引脚的复用功能,不仅增加工作量,编写的驱动程序不方便移植,可重用性差。更糟糕的时缺乏对引脚的统一管理,容易出现引脚的重复定义。且这种重定义引起的错误是很难被发现的。

        pinctrl子系统时由芯片厂商实现,用于帮助管理芯片引脚并自动完成引脚的初始化,所以编写驱动代码的时候知识在设备树中按照规定的格式写出现想要的配置参数即可。

1.1 pinctrl子系统编写格式以及引脚属性介绍

1.1.1 iomux节点介绍

文件位置:/ebf_linux_kernel/arch/arm/boot/dts/imx6ull/dtsi文件中查找iomux节点,可以看到如下定义

iomuxc: iomuxc@20e0000 {compatible = "fsl,imx6ul-iomuxc";reg = <0x20e0000 0x4000>;};
  • compatiable:修饰的是与平台驱动做匹配的名字,这里则是pinctrl子系统的平台驱动做匹配。
  • reg:表示的是引脚配置寄存器的基地址。

imx6ull.dtsi这个文件是芯片厂商官方将芯片的通用部分单独提出来的一些设备树配置。在iomux节点中汇总了所需要引脚的配置信息,pinctrl子系统存储使用者iomux节点信息。

设备树主要的配置文件主要在/arch/arm/boot/dts/imx6ull-mmc-npi.dts中,打开imx6ull-mmc-npi.dts,在文件中搜索“&iomux”找到设备树中引用“iomux”节点的位置如下所示。

&iomuxc {pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;pinctrl_hog_1: hoggrp-1 {fsl,pins = <MX6UL_PAD_UART1_RTS_B__GPIO1_IO19	0x17059 /* SD1 CD */MX6UL_PAD_GPIO1_IO05__USDHC1_VSELECT	0x17059 /* SD1 VSELECT */MX6UL_PAD_GPIO1_IO09__GPIO1_IO09        0x17059 /* SD1 RESET */>;};pinctrl_enet1: enet1grp {fsl,pins = <MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN	0x1b0b0MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER	0x1b0b0MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00	0x1b0b0MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01	0x1b0b0MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN	0x1b0b0MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00	0x1b0b0MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01	0x1b0b0MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1	0x4001b031>;};pinctrl_enet2: enet2grp {fsl,pins = <MX6UL_PAD_GPIO1_IO07__ENET2_MDC		0x1b0b0MX6UL_PAD_GPIO1_IO06__ENET2_MDIO	0x1b0b0MX6UL_PAD_ENET2_RX_EN__ENET2_RX_EN	0x1b0b0MX6UL_PAD_ENET2_RX_ER__ENET2_RX_ER	0x1b0b0MX6UL_PAD_ENET2_RX_DATA0__ENET2_RDATA00	0x1b0b0MX6UL_PAD_ENET2_RX_DATA1__ENET2_RDATA01	0x1b0b0MX6UL_PAD_ENET2_TX_EN__ENET2_TX_EN	0x1b0b0MX6UL_PAD_ENET2_TX_DATA0__ENET2_TDATA00	0x1b0b0MX6UL_PAD_ENET2_TX_DATA1__ENET2_TDATA01	0x1b0b0MX6UL_PAD_ENET2_TX_CLK__ENET2_REF_CLK2	0x4001b031>;};pinctrl_uart1: uart1grp {fsl,pins = <MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1>;};
/*--------------部分省略--------------------------------*/
  • 第2-3行:“pinctrl-names”标识,指定PIN的状态列表,默认设置为“default”。“pinctrl-0=<&pinctrl_hog_1>”的意思的是在默认状态下,将使用pinctrl_hog_1这个设备点来设置GPIO状态。一个引脚可能有多种状态,以串口为例,在正常使用的时候将引脚设置为发送引脚、接收引脚,而在系统进入休眠模式下,为了节省功耗,可以将这两个引脚设置为其他模式。
  • 其余源码都是pinctrl的子节点,它们都是按照一定的格式来编写。
&iomuxc {pinctrl-names = "default","sleep","init";pinctrl-0 = <&pinctrl_uart1>;pinctrl-1=<&xxx>;pinctrl-2=<&yyy>;/*-----------------------省略--------------------------------*/pinctrl_uart1: uart1grp {fsl,pins = <MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1>;};xxx:xxx_grp{...这里设置将引脚设置为其他模式}yyy:yyy_grp{...这里设置将引脚设置为其他模式}...
}
  • pinctrl-names:定义引脚状态
  • pinctrl-0:定义第0种状态需要使用到的引脚配置,可饮用其他节点表示
  • pinctrl-1:定义第1种状态需要使用到的引脚配置。
  • pinctrl-2:定义第2种状态需要使用到的引脚配置。

1.1.2 pinctrl子节点编写格式

以“pinctrl-uart1”节点源码为例介绍pinctrl子节点格式规范编写:

pinctrl子节点格式规范,格式框架如下:

pinctrl_自定义名字: 自定义名字 {fsl,pins = <引脚复用宏定义 PAD(引脚)属性引脚复用宏定义 PAD(引脚)属性>;};

这里需要注意的是每个芯片厂商的pinctrl子节点的编写格式并不相同,这里不属于设备树的规范,是芯片厂商自定义的。如果想添加自己的pinctrl子节点,只需要照着上面的格式编写即可。

1.1.3 引脚配置信息介绍

即上图中标号3处的内容,也是编写的主要内容-提娜佳引脚配置信息。引脚配置信息有两部分组成,一个宏定义和一个16进制数组成。这实际上定义已经配置控制引脚所需要用到的各个寄存器及应写入寄存器的i西南西,以上面第一条配置信息为例。

MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1

MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX  是定义在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内的一个宏定义。

 这里面关于“MX6UL_PAD_UART1_TX_DATA__xxx”命名的宏定义一共8个,表示关于引脚复用寄存器,8个宏是用来定义UART1_TX_DATA引脚的8个复用功能。每个宏定义后面有5个参数,名词依次是mug_reg、conf_reg、input_reg、mux_mod、input_val。

mug_reg   conf_reg   input_reg   mux_mod   input_val
0x0084    0x0310      0x0000      0x0        0x0

如果将宏定义展开则在设备树中每条配置信息实际是6个参数,由于第6个参数设置比较复杂需要根据实际需要设置因此并没有把它放到宏定义里面。以MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 为例,宏定义里面的5个参数介绍如下:

1、  mux_reg和mux_mode:mux_reg 是引脚复用选择寄存器偏移地址,mux_mode是引脚复用选择寄存器模式选择位的值,UART1_TX 引脚复用选择寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA定义如下所示。

 mux_reg = 0x0084 与IM6ULL用户手册偏移地址移植,mux_mode = 0 。 设置复用选择寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA[MUX_MODE] = 0,将其复用位UART1_TX功能。

2、config_reg,引脚(PAD)属性控制控制寄存器偏移地址。与引脚复用选择寄存器不同,引脚属性寄存器应当根据实际需要灵活的配置,所以它的值不包含在宏定义中。

3、input_reg和input_val,input_reg暂且称为输入选择寄存器的偏移地址,input_val是输入选择寄存器的值。这个寄存器只有某些用作输入的引脚才有,正如本例所示,UART1_TX用作输出,所以这两个参数都是0。

1.2 将RGB灯引脚添加到pinctrl子系统

1.2.1 查找RGB灯使用的引脚

       

1.2.2找到引脚宏定义 

这些引脚都将被复用为GPIO,用作驱动LED灯。首先要在“./arch/arm/boot/dts/imx6ul-pinfunc.h”文件内找到对应的宏定义,以CSI_HSYNC引脚为例,在imx6ul-pinfunc.h中直接搜索“CSI_HSYNC”找到如下结果,

 同一个引脚的可选复用功能是连续排布的,要将其复用为GPIO,所以选择“MUX6UL_PAD_CSI_HSYNC_GPIO4_IO20”即可。

1.2.3 设置引脚属性

要写入到设备树中的引脚属性就是引脚属性设置寄存器的值,引脚属性配置项很多,从GPIO1_IO04为例如下所示。

 实际编程中很少手动设置每一个配置项然后将其再组合成一个16进制数,通常情况下,按照官方的设置,如果有需要在对个别参数进行修改。通常情况下用作GOIO的引脚的PAD引脚属性设置为“0x000010B1”。

1.2.4 在iomuxc节点中添加pinctrl子节点

添加子节点,只需要将前面选择好的配置信息按照之前的格式写入到设备树中即可。

&iomuxc {pinctrl-names = "default";pinctrl-0 = <&pinctrl_hog_1>;/*新增加的内容*/pinctrl_rgb_led:rgb_led{fsl,pins = <MX6UL_PAD_GPIO1_IO04__GPIO1_IO04		0x000010B1MX6UL_PAD_CSI_HSYNC__GPIO4_IO20        0x000010B1MX6UL_PAD_CSI_VSYNC__GPIO4_IO19		0x000010B1>;};

新增加的节点名为“rgb_led”,名字任意选取,长度不要超过32个字符。“pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自定义的,通过这个标签引用这个节点。在添加完pinctrl子节点后,系统会根据添加的配置信息将引脚初始化为GPIO子系统相关的内容。

二、GPIO子系统

在没有使用GPIO子系统之前,如过想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现控制LED地目的。有了GPIO子系统后这部分工作由GPIO子系统完成。只需要调用GPIO子系统提供地API函数即可完成GPIO地控制动作。

再imx6ull.dtsi文件中地GPIO子节点记录着GPIO控制器地寄存器地址,下面以GPIO4为例介绍GPIO子节点地相关内容

gpio4: gpio@20a8000 {compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";reg = <0x20a8000 0x4000>;interrupts = <GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 73 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_GPIO4>;gpio-controller;#gpio-cells = <2>;interrupt-controller;#interrupt-cells = <2>;gpio-ranges = <&iomuxc 0 94 17>, <&iomuxc 17 117 12>;
};
  • compatiable:与GPIO子系统地平台驱动做匹配
  • reg:GPIO寄存器地基地址,GPIO4地寄存器组地映射地址是0x20a8000~0x20ABFFF
  • interrupts:描述中断相关地信息
  • clocks:初始化GPIO外设时钟信息
  • gpio-controller:表示gpio4是一个GPIO控制器
  • #gpio-cells:表示有多少个cells来描述GPIO引脚
  • interrupt-controller:表示gpio4也是一个中断控制器
  • #nterrupt-cells:表示用多少个cells来描述一个中断
  • gpio-ranges:将gpio编号转化为pin引脚,<&iomux 0 94 17>,表示将gpio4的第0个引脚映射为97,17表示的是引脚的个数。

gpio4这个节点对整个gpio4进行了描述。使用GPIO子系统时需要往设备树中添加很多设备节点,在驱动程序中使用GPIO子系统提供的API实现GPIO的效果。

2.1 在设备树种添加RGB灯的设备树节点

 相比之前led灯的设备节点(没有使用GPIO子系统),这里只需要增加GPIO属性定义,基于GPIO子系统的rgb_led设备树节点添加到“./arch/arm/boot/dts/imx6ull-mmc-npi.dtb”设备树的根节点内。添加完成后的设备树如下表示。

	/* 添加 rgb_led 节点*/rgb_led{#address-cells = <1>;#size-cells = <1>;pinctrl_names = "default";compatible = "fire,reg_led";pinctrl-0 = <&pinctrl_rgb_led>;regb_led_red = <&gpio1 4 GPIO_ACTIVE_LOW>;	regb_led_green = <&gpio4 20 GPIO_ACTIVE_LOW>;regb_led_blue = <&gpio4 19 GPIO_ACTIVE_LOW>;status = "okay";};
  • 第6行:设置“compatiable”属性值,与led的平台驱动做匹配。
  • 第7行:指定RGB灯的引脚pinctrl信息,上一小节定义了pinctrl节点,并且标签设置为“pinctrl_rgb_led”,在这里引用或者pinctrl信息。
  • 第8-10行,指定引脚使用的哪个GPIO,编写格式如下

  •  标号1:设置引脚名字,如果使用GPIO子系统提供的API操作GPIO,在驱动程序种会用到这个名字,名字是自定义的。
  • 标号2:指定GPIO组
  • 标号3:指定GPIO编号
  • 标号4:这是一个宏定义,指定有效电平,低电平有效选择“GPIO_ACTIVE_LOW”,高电平有效选择“GPIO_ACTIVE_HIGH”

2.2 编译、下载设备树验证修改结果

编译内核时会自动编译设备树,我们可以重新编译内核,这样做的缺点是编译时间过长,在内核目录下执行如下命令,只编译设备树:

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

如果执行了“make distclean”清理了内核,那么就需要在内核目录下执行如下命令重新配置内核(如果编译设备树出错也可以先清理内核然后执行如下命令尝试重新编译)

命令:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后会在“./arch/arm/boot/dts”目录下生成“imx6ull-mmc-npi.dtb”文件,将其替换掉板子/usr/lib/linux-image-4.19.35-imx6/目录下的imx6ull-mmc-npi.dtb文件并输入sudo reboot重启开发板。

重启之后正常情况下会在开发板的“/proc/device-tree”目录下生成“rgb_led”设备树节点,如下所示

2.3 GPIO子系统常用的API函数

1、获取 GPIO 编号函数 of_get_named_gpio  

GPIO 子系统大多数 API 函数会用到 GPIO 编号。GPIO 编号可以通过 of_get_named_gpio 函数从设备树中获取。
文件位置:内核源码/include/linux/of_gpio.h
static inline int of_get_gpio(struct device_node *np, int index)
{return of_get_gpio_flags(np, index, NULL);
}

参数:

  • np:指定设备节点。
  • propname:GPIO属性名,与设备树中定义的属性名对应
  • index:引脚索引值,在设备树中一条引脚属性可以包含很多个引脚,该参数用于指定获取哪个引脚

返回值:

  • 成功:获取的GPIO编号(这里的GPIO编号是根据引脚属性生成一个非负整数)
  • 失败:返回负数。

2、GPIO申请函数gpio_request

文件位置:内核源码/drivers/gpio/gpiolib-legacy.c

int gpio_request(unsigned gpio, const char *label)

参数:

  • gpio:要申请的GPIO编号,该值是函数of_get_named_gpio的返回值
  • label:引脚名子,相当于为申请得到的引脚取了个别名。

返回值:

  • 成功:返回0
  • 失败:返回负数

3、GPIO释放函数

void gpio_free(unsigned gpio)

gpio_free函数与gpio_request是一对相反的函数,一个申请,一个释放。一个GPIO只能申请一次,当不再使用某一个引脚时记得将其释放掉。

参数:

  • gpio:要释放的GPIO编号

返回值:

       无。

4、GPIO输出设置函数gpio_direction_output

用于将引脚设置为输出模式

文件位置:内核源码/include/asm-generic/gpio.h

static inline int gpio_direction_output(unsigned gpio, int value)

函数参数:

  • gpio:要设置的GPIO的编号
  • value:输出值,1,表示高电平。0,表示低电平。

返回值:

  • 成功:返回0
  • 失败:返回负数

5、GPIO输入设置函数gpio_direction_input

用于将引脚设置为输入模式。

文件位置:内核源码/include/asmgeneric/gpio.h

static inline int gpio_direction_input(unsigned gpio)

函数参数:

  • gpio:要设置的GPIO的编号

返回值:

  • 成功:返回0
  • 失败:返回负数

6、获取GPIO引脚值函数gpio_get_value

用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。

文件位置:内核源码/include/asmgeneric/gpio.h

static inline int gpio_get_value_cansleep(unsigned gpio)

函数参数:

  • gpio:要获取的GPIO的编号

返回值:

  • 成功:获取得到的引脚状态‘
  • 失败:返回负数

7、设置GPIO输出值goio_set_value

该函数只用于那些设置为输出模式的GPIO

文件位置:内核源码/include/asmgeneric/gpio.h

static inline int gpio_direction_output(unsigned gpio, int value)

函数参数:

  • gpio:设置的GPIO的编号
  • value:设置的输出值,为1输出高电平,为0输出低电平

返回值:

  • 成功:返回0
  • 失败:返回负数

根据上面这些函数就可以在驱动程序中控制GPIO了。

三、实验

3.1 实验代码 

程序包含两个C语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。另一个是测试程序,用于测试驱动是否正常。

3.1.1 驱动程序

驱动程序大致分为3个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.probe函数,在probe函数中实现字符设备的注册和RGB灯的初始化。第三部分,编写字符设备函数集,实现open和write函数。

平台驱动入口和出口函数实现

源码如下:

/*-------------------------第一部分---------------------------*/
static const struct of_device_id rgb_led[] = {
{ .compatible = "fire,rgb-led"},{ /* sentinel */ }
};/*定义平台设备结构体*/
struct platform_driver led_platform_driver = {.probe = led_probe,.remove =led_remove,.driver = {.name = "rgb-leds-platform",.owner = THIS_MODULE,.of_match_table = rgb_led,}
};/*-------------------------第二部分---------------------------*//*
*驱动初始化函数
*/
static int __init led_platform_driver_init(void)
{int DriverState;DriverState = platform_driver_register(&led_platform_driver);printk(KERN_EMERG "\\tDriverState is %d\\n",DriverState);return 0;
}/*-------------------------第三部分---------------------------*/
/*
*驱动注销函数
*/
static void __exit led_platform_driver_exit(void)
{printk(KERN_EMERG "HELLO WORLD exit!\\n");platform_driver_unregister(&led_platform_driver);	
}module_init(led_platform_driver_init);
module_exit(led_platform_driver_exit);MODULE_LICENSE("GPL");
  • 第一部分:仅实现.probe函数和.driver,当驱动和设备匹配成功后会执行该函数,这个函数的实现后面介绍。.driver描述这个驱动的属性,包括.name驱动的名字,.owner驱动的所有者,.of_match_table驱动匹配表,用于匹配驱动和设备。驱动设备匹配表定义为“rgb_led”在这个表只有一个匹配值为“.compatible = “fire,rgb-led””这个值要与我们在设备树节点的“compatible”属性相同。
  • 第二、三部分:是平台设备的入口和出口函数,函数实现即在入口函数注册平台驱动,在出口函数中注销平台驱动。

平台驱动.probe函数实现

当驱动和设备匹配后首先会执行probe函数,我们在probe函数中实现RGB的初始化、注册一个字符设备。后面将会在字符设备操作函数(open、write)中实现对RGB灯的控制。函数源码如下。

*----------------平台驱动函数集-----------------*/
static int led_probe(struct platform_device *pdv)
{int ret = 0;  //用于保存申请设备号的结果printk(KERN_EMERG "\\t  match successed  \\n");/*-------------------------第一部分----------------------------------------*//*获取RGB的设备树节点*/rgb_led_device_node = of_find_node_by_path("/rgb_led");if(rgb_led_device_node == NULL){printk(KERN_EMERG "\\t  get rgb_led failed!  \\n");}/*-------------------------第二部分----------------------------------------*/rgb_led_red = of_get_named_gpio(rgb_led_device_node, "rgb_led_red", 0);rgb_led_green = of_get_named_gpio(rgb_led_device_node, "rgb_led_green", 0);rgb_led_blue = of_get_named_gpio(rgb_led_device_node, "rgb_led_blue", 0);printk("rgb_led_red = %d,\\n rgb_led_green = %d,\\n rgb_led_blue = %d,\\n", rgb_led_red,\\rgb_led_green,rgb_led_blue);/*-------------------------第三部分----------------------------------------*/gpio_direction_output(rgb_led_red, 1);gpio_direction_output(rgb_led_green, 1);gpio_direction_output(rgb_led_blue, 1);/*-------------------------第四部分----------------------------------------*//*---------------------注册 字符设备部分-----------------*///第一步//采用动态分配的方式,获取设备编号,次设备号为0,//设备名称为rgb-leds,可通过命令cat  /proc/devices查看//DEV_CNT为1,当前只申请一个设备编号ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);if(ret < 0){printk("fail to alloc led_devno\\n");goto alloc_err;}//第二步//关联字符设备结构体cdev与文件操作结构体file_operationsled_chr_dev.owner = THIS_MODULE;cdev_init(&led_chr_dev, &led_chr_dev_fops);//第三步//添加设备至cdev_map散列表中ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);if(ret < 0){printk("fail to add cdev\\n");goto add_err;}//第四步/*创建类 */class_led = class_create(THIS_MODULE, DEV_NAME);/*创建设备*/device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);return 0;add_err://添加设备失败时,需要注销设备号unregister_chrdev_region(led_devno, DEV_CNT);printk("\\n error! \\n");
alloc_err:return -1;}
  • 第一部分:使用of_find_node_by_path函数找到并获取rgb_led在设备树种的设备节点。参数“/rgb_led”是要获取的设备树节点在设备树中的路径,如果要获取的节点嵌套在其他子节点中需要写出节点所在的完美路径。
  • 第二部分:使用函数of_get_named_gpio函数获取GPIO号,读取成功则返回读取得到的GPIO号。“rgb_led_red”指定GPIO的名字,这个参数要与rgb_led设备树节点中GPIO属性名对应,参数“0”指定引脚索引,设备树中一条属性只定义一个引脚,只有一个所以设置为0.
  • 第三部分:将GPIO设置为输出模式,默认输出电平为高电平。
  • 第四部分:字符设备相关内容。

字符设备函数

字符设备函数只需要实现open函数和write函数。函数源码如下

/*--------------------------第一部分-------------------------*/
/*字符设备操作函数集*/
static struct file_operations  led_chr_dev_fops = 
{.owner = THIS_MODULE,.open = led_chr_dev_open,.write = led_chr_dev_write,
};/*---------------------------第二部分--------------------------*/
/*字符设备操作函数集,open函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{printk("\\n open form driver \\n");return 0;
}/*----------------------------第三部分--------------------------*/
/*字符设备操作函数集,write函数*/
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{unsigned char write_data; //用于保存接收到的数据int error = copy_from_user(&write_data, buf, cnt);if(error < 0) {return -1;}/*设置 GPIO1_04 输出电平*/if(write_data & 0x04){gpio_direction_output(rgb_led_red, 0);  // GPIO1_04引脚输出低电平,红灯亮}else{gpio_direction_output(rgb_led_red, 1);    // GPIO1_04引脚输出高电平,红灯灭}/*设置 GPIO4_20 输出电平*/if(write_data & 0x02){gpio_direction_output(rgb_led_green, 0);  // GPIO4_20引脚输出低电平,绿灯亮}else{gpio_direction_output(rgb_led_green, 1);    // GPIO4_20引脚输出高电平,绿灯灭}/*设置 GPIO4_19 输出电平*/if(write_data & 0x01){gpio_direction_output(rgb_led_blue, 0);  // GPIO4_19引脚输出低电平,蓝灯亮}else{gpio_direction_output(rgb_led_blue, 1);    // GPIO4_19引脚输出高电平,蓝灯灭}return 0;
}
  • 第一部分:定义字符设备操作函数集,这里主要实现open函数和write函数即可
  • 第二部分:实现open函数,在平台驱动的probe函数中已经初始化了GPIO。
  • 第三部分:实现write函数,首先使用“copy_from_user”函数将来自应用层的数据“拷贝”打破内核层。得到命令后依此检查后三位,根据命令值使用“gpio_direction_output”函数控制RGB灯的亮灭。

3.1.2 应用程序

应用程序则只需要打开设备节点文件,写入命令后然后关闭设备节点文件即可。 

int main(int argc, char *argv[])
{printf("led_tiny test\\n");
/*----------------第一部分------------------------------------------------*//*判断输入的命令是否合法*/if(argc != 2){printf(" commend error ! \\n");return -1;}/*----------------第二部分------------------------------------------------*//*打开文件*/int fd = open("/dev/rgb-leds", O_RDWR);if(fd < 0){printf("open file : %s failed !\\n", argv[0]);return -1;}/*----------------第三部分------------------------------------------------*/unsigned char commend = atoi(argv[1]);  //将受到的命令值转化为数字;/*判断命令的有效性*//*写入命令*/int error = write(fd,&commend,sizeof(commend));if(error < 0){printf("write file error! \\n");close(fd);/*判断是否关闭成功*/}/*关闭文件*/error = close(fd);if(error < 0){printf("close file error! \\n");}return 0;
}
  •  第一部分:判断命令是否有效。再运行应用程序时要传递一个控制命令,所以参数长度是2.
  • 第二部分:打开设备文件。参数“/dev/rgb-leds”用于指定设备节点文件,设备节点文件名是在驱动程序中设置的,这里保证与驱动一致即可。
  • 第三部分:由于从main函数中获取的参数是字符串,这里首先要将其转化为数字。最后用write函数写入命令后然后关闭文件即可。

3.2 实验准备

3.2.1 Makefile修改

修改Makefile并编译生成驱动程序

KERNEL_DIR=/home/geralt/linux_driver/kernel/ebf_linux_kernel_6ull_depth1/build_image/buildARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
export  ARCH  CROSS_COMPILEobj-m := rgb-leds.oapp_in  = rgb_leds_app.c
app_out = rgb_leds_appall:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules$(CROSS_COMPILE)gcc -o $(app_out) $(app_in).PHONY:clean
clean:$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) cleanrm $(app_out)
  • 第一行:变量“KERNEL_DIR”保存的是内核所在路径,这个需要根据自己内核所在位置设定
  • 第七行:“obj-m:rhb-led.o”中的“rgb-led.o”要与驱动源码名对应。Makefile文件修改完执行如下命令编译驱动。
  • 第9行:“rgb_leds_app.c"是需要编译的应用程序。
  • 第10行:“rgb_leds_app”是编译应用程序后输出的应用程序可执行文件。

命令:

make

正常情况下会在当前目录生成.ko驱动文件和rgb_leds_app应用程序可执行文件。

3.3 下载验证

(1)将前面编译出的.ko驱动和应用程序,添加到开发板中。

(2)执行如下命令加载驱动:

命令:

insmod ./rgb-leds.ko

(3)在驱动程序中,由于在.probe函数中注册字符设备并创建了设备文件,设备和驱动匹配成功后.probe函数执行,所以在"/dev"目录下已经生成了"rgb-leds"设备节点,如下所示。

注意:这里没有生成设备节点,需要取/boot/uEnv.txtx 取消led的设备树插件

驱动加载后直接运行应用程序如下所示。

命令:

./rgb_leds_app <命令>

执行结果如下:

 命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次是红、绿、蓝,1代表亮,0代表灭。