嵌入式Linux中的LED驱动控制(使用Pinctrl和GPIO子系统)
在前面驱动LED的所有案例中,都是在驱动程序中去设置每个引脚的复用功能,这会导致所编写的驱动程序移植困难,可重用性差,缺乏对引脚的统一管理,容易出现引脚的重复定义等等弊病。为此,Linux内核引入了pinctrl子系统和GPIO子系统的概念。pinctrl子系统主要用于芯片引脚功能的管理,它基本上是由芯片厂商来实现的(NXP提供了工具软件i.MX PinsTool),而GPIO子系统则是负责IO引脚的控制动作。他们都依赖设备树来实现。
下面先来看pinctrl子系统。简单来说它用于管理芯片引脚并自动完成引脚的初始化,开发者只需要在设备树中按照规定的格式写出相应的配置参数即可。
本例中用到的pinctrl子系统文件位于源码arch/arm/boot/dts/目录下,名称为imx6ull.dtsi。这个文件是芯片厂商官方将芯片的通用部分单独提出来而形成的一些设备树配置。在该文件中,可以看到有如下定义。
iomuxc: iomuxc@20e0000 { compatible = "fsl,imx6ul-iomuxc"; reg = <0x20e0000 0x4000>; };
在imx6ull-mmc-npi.dts设备树文件中,通过“&iomuxc”(追加)方式在“iomuxc”节点下追加内容。以串口uart1为例,在正常使用的时候我们将引脚设置为发送引脚、接收引脚,而在系统进入休眠模式时,为了节省功耗可以将这两个引脚设置为其他模式,如设置为GPIO 功能并设置为高电平等。其描述形式如下。
&iomuxc { pinctrl-names = "default","sleep","init"; pinctrl-0 = <&pinctrl_uart1>; pinctrl-1 =<&xxx>; pinctrl-2 =<&yyy>; …… pinctrl_uart1: uart1_grp { fsl,pins = < MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1B0B1 MX6UL_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种状态需要使用到的引脚配置。一般情况下,pinctrl子节点的格式规范如下。
pinctrl_ 自定义名字: 自定义名字{ fsl,pins = < 引脚复用宏定义 PAD(引脚)属性 引脚复用宏定义 PAD(引脚)属性 >; };
每个芯片厂商的pinctrl子节点的编写格式并不相同,它们由芯片厂商自定义,这不属于设备树的规范。如果要添加自己的pinctrl节点,只要按照上面的格式编写即可。
引脚的配置信息由两部分组成,即一个宏定义和一个十六进制数组成。它配置控制引脚所需要用到的各个寄存器的地址及写入寄存器的值。下面以上前面UART1的第一条配置信息为例进行说明。第一条配置信息中的”MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX"实际上是一个宏定义,它位于头文件”arch/arm/boot/dts/imx6ul-pinfunc.h“中,其定义格式如下所示。

宏定义“MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX”把引脚复用为UART1的TX引脚,宏定义后面有5个参数,其名字依次为mux_reg、conf_reg、input_reg、mux_mode、input_val。参数介绍如下:
1. mux_reg和mux_mode :mux_reg是引脚复用选择寄存器偏移地址,mux_mode是引脚复用选择寄存器模式选择位的值。引脚复用选择寄存器IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA定义可查芯片手册,如下所示。

mux_reg的值0x0084与IM6ULL用户手册偏移地址一致,mux_mode的值为0,将引脚选择为UART1_TX功能。
2. conf_reg是引脚(PAD)属性控制寄存器偏移地址。与引脚复用选择寄存器不同,引脚属性寄存器应当根据实际需要灵活的配置,所以它的值并不包含在宏定义中,而是在定义pinctrl节点时,放在引脚复用定义之后(见上面pinctrl子节点的格式规范图)。conf_reg的值0x0310对应了UART1_TX引脚的引脚属性寄存器的偏移地址(可查芯片手册)。
3. input_reg和input_val分别是输入选择寄存器偏移地址和输入选择寄存器的值。这个寄存器只有某些用作输入的引脚才有。这里UART1_TX用作输出,所以这两个参数都是零。
明白了pinctrl节点的配置方法后,就可以写本例的控制RGB点亮的pinctrl节点了。如下所示。
pinctrl_rgb_led:rgb_led{ fsl,pins = < MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x0000F838 MX6UL_PAD_CSI_HSYNC__GPIO4_IO20 0x0000F838 MX6UL_PAD_CSI_VSYNC__GPIO4_IO19 0x0000F838 >; };
新增的节点名为“rgb_led”,名字任意选取,长度不要超过32个字符,最好能表达出节点的信息。“pinctrl_rgb_led”是节点标签,“pinctrl_”是固定的格式,后面的内容自己定义,在后面通过这个标签引用这个节点。在添加完pinctrl子节点后,系统就会根据添加的配置信息将引脚初始化为GPIO功能,并设置为推挽模式。注意,上述内容要写在设备树文件arch/arm/boot/dts/imx6ull-mmc-npi.dts中的&iomuxc节点内容的最后。
接下来看GPIO子系统。在没有使用GPIO子系统之前,如果要点亮一个LED,首先需要得到led相关的配置寄存器,再手动地去读、改、写这些配置寄存器,以实现控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统来完成,开发者只需要调用GPIO子系统提供的API函数即可完成对GPIO的控制动作。
在imx6ull.dtsi文件中的GPIO子节点已经记录了GPIO控制器的寄存器地址,下面给出的是GPIO4节点部分的内容。
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>; };
compatible:与GPIO子系统的平台驱动做匹配。
reg:GPIO寄存器的基地址,GPIO4的寄存器组是的映射地址为0x20a8000-0x20ABFFF。
interrupts:描述中断相关的信息。
clocks:初始化GPIO外设时钟信息。
gpio-controller:表示gpio4是一个GPIO控制器。
#gpio-cells:表示有多少个cells来描述GPIO引脚。
interrupt-controller:表示gpio4也是个中断控制器。
#interrupt-cells: 表示用多少个cells来描述一个中断。
gpio-ranges:将gpio编号转换成pin引脚,<&iomuxc 0 94 17>,表示将gpio4的第0个引脚引脚映射为97,17表示的是引脚的个数。
gpio4这个节点对整个gpio4进行了描述。使用GPIO子系统时需要往设备树中添加设备节点,在驱动程序中使用GPIO子系统提供的API实现控制GPIO的效果。
在本例中,基于GPIO子系统的rgb_led设备树节点添加到了imx6ull-mmc-npi.dts设备树文件的根节点内,如下所示。
rgb_led{ compatible = "fire,rgb-led"; pinctrl-name = "default"; pinctrl-0 = <&pinctrl_rgb_led>; rgb-gpios = <&gpio1 4 GPIO_ACTIVE_LOW>, <&gpio4 20 GPIO_ACTIVE_LOW>, <&gpio4 19 GPIO_ACTIVE_LOW>; status = "okay"; };
在上面的节点中,compatible属性值用于与led的平台驱动做匹配。pinctrl-0用于指定前面已经定义好的pinctrl子节点。接下来分别指定了3个引脚来驱动RGB灯。以第一行<&gpio1 4 GPIO_ACTIVE_LOW>为例,gpio1是端口,4是引脚,GPIO_ACTIVE_LOW是一个宏定义,指定低电平有效。同里,GPIO_ACTIVE_HIGH指定高电平有效。(注意:在驱动程序中,1表示有效,0表示无效。)特别提示,引脚在驱动中是按顺序来索引的,即第一行为0,以此类推。在驱动中通过搜索“-gpios”前的名称(本例中为rgb)来匹配引脚,也就是说“-gpios”是固定格式。
小结一下,先在设备树文件imx6ull-mmc-npi.dts中包含STM官方的imx6ull.dtsi文件,然后通过追加方式在&iomuxc下编写一个pinctrl子节点(见上面的pinctrl_rgb_led),最后编写一个设备树子节点(见上面的rgb_led子节点)来调用它就可以了,余下的事情就是在驱动中去获取这个设备子节点的内容了。pinctrl子系统负责芯片引脚复用功能的选定,GPIO子系统负责引脚输入输了功能的相关配置。
以上完成后,就可以编译设备树了,编译完成后替换开发板上的设备树文件,具体操作可参见“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的相关部分。
接下来看平台驱动程序部分,下面是驱动的全部代码,文件名为led.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <asm/io.h> #include <linux/device.h> #include <linux/platform_device.h> struct rgb_led_dev { dev_t devid; //设备号 struct cdev led_cdev; //定义字符型结构体 struct gpio_desc *rgb_led_red, *rgb_led_green, *rgb_led_blue;//定义红绿蓝3个变量 }; static struct class *led_class; //类结构体 //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { struct rgb_led_dev *led_dev; if (!inode->i_cdev) { pr_err("led_open: i_cdev is NULL\n"); return -ENODEV; } led_dev = container_of(inode->i_cdev, struct rgb_led_dev, led_cdev); //把led结构体保存在file结构体的私有变量中 filp->private_data = led_dev; return 0; } //实现write函数,为file_oprations结构体成员函数 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char value; //用于保存接收到的数据 unsigned long n; //以下把私有变量private_data中的值赋值给指针led(该值在上面open函数中存入) struct rgb_led_dev *led_dev = filp->private_data; n = copy_from_user(&value, buf, cnt); switch(value) //根据应用空间的值判断具体操作 { case 0: //全部点亮三个LED gpiod_set_value(led_dev->rgb_led_red, 1); gpiod_set_value(led_dev->rgb_led_blue, 1); gpiod_set_value(led_dev->rgb_led_green, 1); break; case 1: //点亮红色LED gpiod_set_value(led_dev->rgb_led_red, 1); break; case 2: //点亮绿色LED gpiod_set_value(led_dev->rgb_led_green, 1); break; case 3: //点亮蓝色LED gpiod_set_value(led_dev->rgb_led_blue, 1); break; case 4: //熄灭红色LED gpiod_set_value(led_dev->rgb_led_red, 0); break; case 5: //熄灭绿色LED gpiod_set_value(led_dev->rgb_led_green, 0); break; case 6: //熄灭蓝色LED gpiod_set_value(led_dev->rgb_led_blue, 0); break; case 7: //全部熄灭三个LED gpiod_set_value(led_dev->rgb_led_red, 0); gpiod_set_value(led_dev->rgb_led_blue, 0); gpiod_set_value(led_dev->rgb_led_green, 0); break; default: //全部熄灭 gpiod_set_value(led_dev->rgb_led_red, 0); gpiod_set_value(led_dev->rgb_led_blue, 0); gpiod_set_value(led_dev->rgb_led_green, 0); break; } return cnt; } //实现release函数,为file_oprations结构体函数 static int led_release(struct inode *inode, struct file *filp) { return 0; } //填充一个file_oprations类型的结构体,名为led_dev_fops,包含上述声明的成员函数 static struct file_operations led_dev_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, .release = led_release, }; /*----------------平台驱动函数集-----------------*/ static int led_pdrv_probe(struct platform_device *pdev) { struct rgb_led_dev *led_dev; //以下动态申请led结构体大小的内存 led_dev = devm_kzalloc(&pdev->dev, sizeof(struct rgb_led_dev), GFP_KERNEL); if(!led_dev) return -ENOMEM; //获取rgb_led节点的红绿蓝灯子节点 led_dev->rgb_led_red = gpiod_get_index(&pdev->dev, "rgb", 0, GPIOD_OUT_LOW); if (IS_ERR(led_dev->rgb_led_red)) { dev_err(&pdev->dev, "Failed to get red LED GPIO\n"); return PTR_ERR(led_dev->rgb_led_red); } led_dev->rgb_led_green = gpiod_get_index(&pdev->dev, "rgb", 1, GPIOD_OUT_LOW); if (IS_ERR(led_dev->rgb_led_green)) { dev_err(&pdev->dev, "Failed to get green LED GPIO\n"); return PTR_ERR(led_dev->rgb_led_green); } led_dev->rgb_led_blue = gpiod_get_index(&pdev->dev, "rgb", 2, GPIOD_OUT_LOW); if (IS_ERR(led_dev->rgb_led_blue)) { dev_err(&pdev->dev, "Failed to get blue LED GPIO\n"); return PTR_ERR(led_dev->rgb_led_blue); } //设置红绿蓝引脚为输出方向,并输出高电平 // gpiod_direction_output(led_dev->rgb_led_red, 0); // gpiod_direction_output(led_dev->rgb_led_green, 0); // gpiod_direction_output(led_dev->rgb_led_blue, 0); //申请主设备号 if (alloc_chrdev_region(&led_dev->devid, 0, 1, "led") < 0) { printk("fail to alloc devid\n"); return -EFAULT; } led_dev->led_cdev.owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&led_dev->led_cdev, &led_dev_fops); //填充上面申请到的主设备号到字符设备 if (cdev_add(&led_dev->led_cdev, led_dev->devid, 1) < 0) { printk("fail to add cdev\n"); return -EFAULT; } //创建一个设备节点 device_create(led_class, NULL, led_dev->devid, NULL, "led"); //以下把LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中,后面移除时会用到 platform_set_drvdata(pdev, led_dev); printk("platform driver probed!\n"); return 0; } //remove函数中,删除设备并释放设备号 static int led_pdrv_remove(struct platform_device *pdev) { //platform_get_drvdata,获取当前LED灯对应的结构体 struct rgb_led_dev *led_dev = platform_get_drvdata(pdev); gpiod_put(led_dev->rgb_led_red); gpiod_put(led_dev->rgb_led_green); gpiod_put(led_dev->rgb_led_blue); unregister_chrdev_region(led_dev->devid, 1); //释放主设备号 cdev_del(&led_dev->led_cdev); //删除字符设备 device_destroy(led_class, led_dev->devid); //销毁设备节点 printk("platform driver removed!\n"); return 0; } //填充of_device_id结构体,名为rgb_led,用于指明匹配表 static const struct of_device_id rgb_led[] = { {.compatible = "fire,rgb-led"}, //匹配内容 {/* sentinel */} }; //以下填充一个platform_driver结构体 struct platform_driver led_platform_driver = { .probe = led_pdrv_probe, //指定probe函数成员 .remove = led_pdrv_remove, //指定remove函数成员 .driver = { .name = "rgb-leds-platform", //指定设备名称 .owner = THIS_MODULE, .of_match_table = rgb_led, //指定匹配表名称 } }; //以下定义模块的入口函数 static int __init led_pdrv_init(void) { //创建一个类 led_class = class_create(THIS_MODULE, "my_leds"); platform_driver_register(&led_platform_driver);//注册一个platform驱动 printk("led platform driver initted!\n"); return 0; } //以下定义模块的出口函数 static void __exit led_pdrv_exit(void) { platform_driver_unregister(&led_platform_driver); //释放一个platform驱动 class_destroy(led_class); //销毁类 printk("led platform driver exited!\n"); } module_init(led_pdrv_init); module_exit(led_pdrv_exit); MODULE_LICENSE("GPL"); MODULE_INFO(intree,"Y");
GPIO子系统提供了两套接口函数,一套是新的基于描述符(Descriptor-based),另一套是旧的基于引脚编号(Legacy)。本例使用的是新的接口函数,下面就来看一下这些API函数。
1、获取gpio引脚函数:struct gpio_desc *gpiod_get_index(struct device *dev, const char *con_id, unsigned int idx, enum gpiod_flags flags),参数dev指定设备指针(即调用者)。参数con_id是gpio的连接ID,用于匹配设备树属性名(即前面提到的“-gpios”前的字符串)。参数idx是con_id指定的gpio数组中的索引,从0(即第一行)开始。参数flags是gpio的初始化标志,使用宏表示,一般有:GPIOD_ASIS(不改变方向),GPIOD_IN(输入方向),GPIOD_OUT_LOW(输出方向,逻辑低电平),GPIOD_OUT_HIGH(输出方向,逻辑高电平)等。
2、设置引脚方向函数:int gpiod_direction_output(struct gpio_desc *desc, int value),参数desc为gpio描述符, 就是前面获取到的gpio引脚。参数value为输出的初始逻辑值。在本例中可以不使用该函数,因为前面的gpiod_get_index函数实际上已经指定引脚为输出逻辑低电平了。返回值为0时表示成功,为负数时表示失败。
3、设置引脚值函数:void gpiod_set_value(struct gpio_desc *desc, int value),参数desc同第2中一样。参数value同样为逻辑值。
4、释放gpio引脚函数:void gpiod_put(struct gpio_desc *desc),参数desc同第2中一样。
对比上面的平台驱动与前面“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的平台驱动可以看到,在设备树中引入了Pinctrl子系统之后,就不需要再在驱动中去配置引脚的复用和属性了,也不必再去手动管理端口的时钟,而是交由内核及PM去管理,实现了端口时钟的低功耗动态管理,非常方便有效。同理,在设备树中引入了GPIO子系统之后,由于不再涉及寄存器的操作,程序变得简单多了,也更有利于移植。
配套的Makefile文件内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := led.o all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
接下来是应用程序,文件名为app.c。
#include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; unsigned char val = 7; fd = open("/dev/led", O_RDWR); //打开设备节点 if( fd < 0 ) printf("can`t open\n"); if( argc != 3 ) //命令参数不对时提示 { printf("Usage :\n"); printf("%s <all|red|green|blue> <on|off>\n", argv[0]); return 0; } if(strcmp(argv[1], "all") == 0) { if(strcmp(argv[2], "on") == 0) val = 0; //值为0时全部点亮 else val = 7; //值为7时全部熄灭 } else if(strcmp(argv[1], "red") == 0) { if(strcmp(argv[2], "on") == 0) val = 1; //值为1时红色点亮 else val = 4; //值为4时红色熄灭 } else if(strcmp(argv[1], "green") == 0) { if(strcmp(argv[2], "on") == 0) val = 2; //值为2时绿色点亮 else val = 5; //值为5时绿色熄灭 } else if(strcmp(argv[1], "blue") == 0) { if(strcmp(argv[2], "on") == 0) val = 3; //值为3时蓝色点亮 else val = 6; //值为6时蓝色熄灭 } write(fd, &val, 1); //把值写入设备节点 close(fd); //关闭设备节点 return 0; }
可以看出应用程序与“嵌入式Linux中的LED驱动控制(设备树方式)”中的一样,并没有改动过。完成后,先执行make命令编译驱动程序,若成功会生成名为led.ko的驱动模块文件。然后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。实验结果与“嵌入式Linux中的LED驱动控制”一文中的完全一样。
对于在嵌入式Linux下控制LED,虽然前面给出过很多种方式,但无疑使用Pinctl和GPIO子系统来实现,才是其最终方式,也是最为推荐的方式。

浙公网安备 33010602011771号