在前面驱动LED的所有案例中,都是在驱动程序中去设置每个引脚的复用功能,这会导致所编写的驱动程序移植困难,可重用性差,缺乏对引脚的统一管理,容易出现引脚的重复定义等等弊病。为此,Linux内核引入了pinctrl子系统和GPIO子系统的概念。pinctrl子系统主要用于芯片引脚功能的管理,它基本上是由芯片厂商来实现的(详见官网https://wiki.stmicroelectronics.cn/stm32mpu/wiki/Pinctrl_device_tree_configuration),而GPIO子系统则是负责IO引脚的控制动作。他们都依赖设备树来实现。
下面先来看pinctrl子系统。简单来说它用于管理芯片引脚并自动完成引脚的初始化,开发者只需要在设备树中按照规定的格式写出相应的配置参数即可。
本例中用到的pinctrl子系统文件位于源码arch/arm/boot/dts/目录下,名称为stm32mp157-pinctrl.dtsi。这个文件是芯片厂商官方将芯片的通用部分单独提出来而形成的一些设备树配置。在该文件中,可以看到有如下定义。
soc { pinctrl: pin-controller@50002000 { #address-cells = <1>; #size-cells = <1>; compatible = "st,stm32mp157-pinctrl"; ranges = <0 0x50002000 0xa400>; interrupt-parent = <&exti>; st,syscfg = <&exti 0x60 0xff>; hwlocks = <&hsem 0>; pins-are-numbered; } …… };
在soc节点中汇总了所需引脚的配置信息,pinctrl子系统存储使用到的节点信息。
在stm32mp157a-basic.dts设备树文件中,通过&pinctrl”(追加)方式在“pinctrl”节点下追加内容,其描述形式如下。
&pinctrl { xxx: xxx { pins { pinmux = <STM32_PINMUX('A', 10, ANALOG)>; }; }; };
上述描述了一个外设xxx,其使用的引脚为GPIOA10,复用功能为ANALOG,电气特性没有指定,使用默认值。
每个芯片厂商的pinctrl子节点的编写格式并不相同,它们由芯片厂商自定义,这不属于设备树的规范。如果要添加自己的pinctrl节点,只要按照上面的格式编写即可。关于pinctrl节点如何去描述,可以在内核文档目录中查找芯片产商给出的文档。如ST官方的pinctrl文档为Documentation/devicetree/bindings/pinctrl/st,stm32-pinctrl.txt。
添加子节点时,只需要将引脚信息以一定格式,写入到stm32mp157a-basic.dts设备树文件中的pinctrl子节点即可,比如本例中添加RGB的pinctrl子节点,如下所示。
&pinctrl { pinctrl_rgb_led: rgb_led{ pins { pinmux = <STM32_PINMUX('A', 13, GPIO)>, <STM32_PINMUX('G', 2, GPIO)>, <STM32_PINMUX('B', 5, GPIO)>; drive-push-pull; }; }; };
新增的节点名为“rgb_led”,名字任意选取,长度不要超过32 个字符,最好能表达出节点的信息。“pinctrl_rgb_led”节点标签,“pinctrl_”是固定的格式,后面的内容自己定义,会在后面通过这个标签引用这个节点。在添加完pinctrl子节点后,系统就会根据添加的配置信息将引脚初始化为GPIO功能,并设置为推挽模式。
接下来看GPIO子系统。在没有使用GPIO子系统之前,如果要点亮一个LED,首先需要得到led相关的配置寄存器,再手动地去读、改、写这些配置寄存器,以实现控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统来完成,开发者只需要调用GPIO子系统提供的API函数即可完成对GPIO的控制动作。
在stm32mp157-pinctrl.dtsi文件中的pinctrl 子节点已经记录了GPIO控制器的寄存器地址,下面给出的是GPIOA节点部分的内容。
soc { pinctrl: pin-controller@50002000 { #address-cells = <1>; #size-cells = <1>; compatible = "st,stm32mp157-pinctrl"; ranges = <0 0x50002000 0xa400>; interrupt-parent = <&exti>; st,syscfg = <&exti 0x60 0xff>; hwlocks = <&hsem 0>; pins-are-numbered; gpioa: gpio@50002000 { gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; reg = <0x0 0x400>; clocks = <&rcc GPIOA>; st,bank-name = "GPIOA"; status = "disabled"; }; ………… }; };
compatible :与GPIO子系统的平台驱动做匹配。
ranges :GPIO外设寄存器的基地址,在gpioa的reg属性中GPIOA的寄存器组的映射地址为50002000,范围为0x400。
interrupt-parent :表示中断控制器是exti外设。
clocks :初始化GPIO外设时钟信息。
gpio-controller :表示gpioa是一个GPIO控制器。
#gpio-cells :表示有多少个cells来描述GPIO引脚。
#interrupt-controller : 是中断控制器。
#interrupt-cells : 表示用多少个cells来描述一个中断。
对以上部分只有大致有个了解就可以了,一般芯片产商会将这部分信息完善好。GPIOA这个节点对整个GPIOA进行了描述。使用GPIO子系统时只需要往设备树中添加设备节点即可。在驱动程序中使用GPIO子系统提供的API就可实现对GPIO的控制。
在本例中,基于GPIO子系统的rgb_led设备树节点添加到了stm32mp157a-basic.dts设备树文件的根节点内,如下所示。
rgb_led{ #address-cells = <1>; #size-cells = <1>; pinctrl-names = "default"; compatible = "fire,rgb-led"; pinctrl-0 = <&pinctrl_rgb_led>; rgb_leds = <&gpioa 13 GPIO_ACTIVE_LOW &gpiog 2 GPIO_ACTIVE_LOW &gpiob 5 GPIO_ACTIVE_LOW>; status = "okay"; };
compatible属性值用于与led的平台驱动做匹配。pinctrl-0用于指定前面已经定义好的pinctrl子节点。宏定义GPIO_ACTIVE_LOW指定高电平有效,GPIO_ACTIVE_HIGH指定低电平有效。(注意:在驱动程序中,1表示有效,0表示无效。)
小结一下,先在设备树文件stm32mp157a-basic.dts中包含STM官方的stm32mp157-pinctrl.dtsi文件,然后编写一个pinctrl子节点(见上面的pinctrl_rgb_led),最后编写一个设备树子节点(见上面的rgb_led子节点)来调用它就可以了,余下的事情就是在驱动中去获取这个设备子节点的内容了。pinctrl子系统负责芯片引脚复用功能的选定,GPIO子系统负责引脚输入输了功能的相关配置。
以上完成后,就可以编译设备树了,编译完成后替换开发板上的设备树文件,具体操作可参见“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的相关部分。
接下来看平台驱动程序部分,下面是驱动的全部代码。
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/gpio/consumer.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> static dev_t devid; //设备号 static struct cdev led_cdev; //定义字符型结构体 static int rgb_led_red, rgb_led_green, rgb_led_blue;//定义红绿蓝3个变量 struct class *led_class; //类结构体 struct device_node *rgb_led_device_node; //rgb_led的设备树节点结构体 //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { 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; n = copy_from_user(&value, buf, cnt); switch(value) //根据应用空间的值判断具体操作 { case 0: //全部点亮三个LED gpio_set_value(rgb_led_red, 0); gpio_set_value(rgb_led_blue, 0); gpio_set_value(rgb_led_green, 0); break; case 1: //点亮红色LED gpio_set_value(rgb_led_red, 0); break; case 2: //点亮绿色LED gpio_set_value(rgb_led_green, 0); break; case 3: //点亮蓝色LED gpio_set_value(rgb_led_blue, 0); break; case 4: //熄灭红色LED gpio_set_value(rgb_led_red, 1); break; case 5: //熄灭绿色LED gpio_set_value(rgb_led_green, 1); break; case 6: //熄灭蓝色LED gpio_set_value(rgb_led_blue, 1); break; case 7: //全部熄灭三个LED gpio_set_value(rgb_led_red, 1); gpio_set_value(rgb_led_blue, 1); gpio_set_value(rgb_led_green, 1); break; default: //全部熄灭 gpio_set_value(rgb_led_red, 1); gpio_set_value(rgb_led_blue, 1); gpio_set_value(rgb_led_green, 1); 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 *pdv) { //获取rgb_led的设备树节点 rgb_led_device_node = of_find_node_by_path("/rgb_led"); if(rgb_led_device_node == NULL) { printk(KERN_ERR "\t get rgb_led failed! \n"); return -1; } //获取rgb_led节点的红绿蓝灯子节点 rgb_led_red = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 0); rgb_led_green = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 1); rgb_led_blue = of_get_named_gpio(rgb_led_device_node, "rgb-gpios", 2); //申请红绿蓝灯子节点 if(gpio_request(rgb_led_red, "red")) { printk("get gpio_red failed! \n"); } if(gpio_request(rgb_led_green, "green")) { printk("get gpio_green failed! \n"); } if(gpio_request(rgb_led_blue, "blue")) { printk("get gpio_blue failed! \n"); } //设置红绿蓝引脚为输出方向,并输出高电平 gpio_direction_output(rgb_led_red, 1); gpio_direction_output(rgb_led_green, 1); gpio_direction_output(rgb_led_blue, 1); //申请主设备号 if (alloc_chrdev_region(&devid, 0, 1, "led") < 0) { printk("fail to alloc devid\n"); return -EFAULT; } led_cdev.owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&led_cdev, &led_dev_fops); //填充上面申请到的主设备号到字符设备 if (cdev_add(&led_cdev, devid, 1) < 0) { printk("fail to add cdev\n"); return -EFAULT; } //创建一个类 led_class = class_create(THIS_MODULE, "my_leds"); //创建一个设备节点 device_create(led_class, NULL, devid, NULL, "led"); printk("platform driver probed!\n"); return 0; } //remove函数中,删除设备并释放设备号 static int led_pdrv_remove(struct platform_device *pdev) { //释放红绿蓝子节点 gpio_free(rgb_led_red); gpio_free(rgb_led_green); gpio_free(rgb_led_blue); unregister_chrdev_region(devid, 1); //释放主设备号 cdev_del(&led_cdev); //删除字符设备 device_destroy(led_class, devid); //销毁设备节点 class_destroy(led_class); //销毁类 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) { 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驱动 printk("led platform driver exited!\n"); } module_init(led_pdrv_init); module_exit(led_pdrv_exit); MODULE_LICENSE("GPL");
在驱动中,主要是利用GPIO子系统提供的API函数进行操作,下面就来看一下这些常用的API函数。
1、 获取GPIO编号函数:static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index),参数np指定设备节点,即通过of_find_node_by_path函数查找节点的返回值。参数propname指定GPIO属性名,要与设备树中定义的属性名一致。参数index为索引值,当设备树中的一条引脚属性包含有多个引脚时,该参数用于指定获取那个引脚。如果获取成功则返回获取的GPIO编号,失败则返回一个负数。
2、GPIO申请函数:static inline int gpio_request(unsigned gpio, const char *label),参数gpio为要申请的GPIO编号,该值是函数of_get_named_gpio的返回值。参数label为引脚名字,相当于为申请得到的引脚取了个别名。如果申请成功则返回0,失败返回一个负数。该函数主要用来确认内核没有使用过要申请的引脚,一般在获取到引脚后都应该申请一下。
3、GPIO释放函数:static inline void gpio_free(unsigned gpio),参数gpio为要释放的GPIO编号。该函数与gpio_request互为反函数,一个申请,一个释放。一个GPIO只能被申请一次,当不再使用某一个引脚时应该将其释放。
4、GPIO输出设置函数:static inline int gpio_direction_output(unsigned gpio , int value),参数gpio为要设置的GPIO的编号,参数value指定输出值,1为高电平,0为低电平。如果设置成功则返回0,失败返回一个负数。
5、GPIO输入设置函数:static inline int gpio_direction_input(unsigned gpio),参数gpio为要设置的GPIO的编号,如果设置成功则返回0,失败返回一个负数。
6、获取GPIO引脚值函数:static inline int gpio_get_value(unsigned gpio),参数gpio为要设置的GPIO的编号,返回值为获取得到的引脚状态,失败返回一个负数。
7、设置GPIO输出值:static inline int gpio_direction_output(unsigned gpio, int value),参数gpio为要设置的GPIO的编号,参数value为输出值,1为高电平,0为低电平。如果设置成功则返回0,失败返回一个负数。
对比上面的平台驱动与前面“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的平台驱动可以看到,在引入了GPIO子系统之后,由于不再涉及寄存器操作,程序变得简单多了,也更有利于移植。
配套的Makefile文件内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/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 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 = 0; 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子系统来实现,才是其最终方式,也是最为推荐的方式。
标签:led,pinctrl,Pinctrl,LED,rgb,gpio,GPIO,节点 From: https://www.cnblogs.com/fxzq/p/18295226