在没有使用GPIO子系统之前,如果我们想点亮一个LED,首先要得到led相关的配置寄存器,再手动地读、改、写这些配置寄存器实现 控制LED的目的。有了GPIO子系统之后这部分工作由GPIO子系统帮我们完成,我们只需要调用GPIO子系统提供的API函数即可完成GPIO的 控制动作。
在imx6ull.dtsi文件中的GPIO子节点记录着GPIO控制器的寄存器地址,下面我们以GPIO4为例介绍GPIO子节点相关内容
imx6ull.dtbi中GPIO4节点内容
| gpio4: gpio@20a8000 {
compatible = “fsl,imx6ul-gpio”, “fsl,imx35-gpio”;
reg = <0x20a8000 0x4000>;
interrupts =
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的效果。
10.2.1. 在设备树中添加RGB灯的设备树节点
相比之前led灯的设备树节点(没有使用GPIO子系统),这里只需要增加GPIO属性定义,基于GPIO子系统的rgb_led设备树节点 添加到“./arch/arm/boot/dts/imx6ull-seeed-npi.dts”设备树的根节点内。 添加完成后的设备树如下所示。
设备树中添加rgb_led节点
| /添加rgb_led节点/ rgb_led{
#address-cells = <1>;#size-cells = <1>;pinctrl-names = "default";compatible = "fire,rgb-led";pinctrl-0 = <&pinctrl_rgb_led>;rgb_led_red = <&gpio1 4 GPIO_ACTIVE_LOW>;rgb_led_green = <&gpio4 20 GPIO_ACTIVE_LOW>;rgb_led_blue = <&gpio4 19 GPIO_ACTIVE_LOW>;status = "okay";
}; | | —- |
- 第6行,设置“compatible”属性值,与led的平台驱动做匹配。
- 第7行,指定RGB灯的引脚pinctrl信息,上一小节我们定义了pinctrl节点,并且标签设置为“pinctrl_rgb_led”, 在这里我们引用了这个pinctrl信息。
- 第8-10行,指定引脚使用的哪个GPIO,编写格式如下所示。

- 标号①,设置引脚名字,如果使用GPIO子系统提供的API操作GPIO,在驱动程序中会用到这个名字,名字是自定义的。
- 标号②,指定GPIO组。
- 标号③,指定GPIO编号。
- 编号④,这是一个宏定义,指定有效电平,低电平有效选择“GPIO_ACTIVE_LOW”高电平有效选择“GPIO_ACTIVE_HIGH”。
10.2.2. 编译、下载设备树验证修改结果
前两小节我们分别在设备树中将RGB灯使用的引脚添加到pinctrl子系统,然后又在设备树中添加了rgb_led设备树节点。 这一小节将会编译、下载修改后的设备树,用新的设备树启动系统,然后检查是否有rgb_led设备树节点产生。
编译内核时会自动编译设备树,我们可以直接重新编译内核,这样做的缺点是编译时间会很长。 在内核目录下(~/ebf-buster-linux)执行如下命令,只编译设备树:
命令:
| make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs | | —- |
如果执行了“make distclean”清理了内核,那么就需要在内核目录下执行如下命令重新配置内核 (如果编译设备树出错也可以先清理内核然后执行如下命令尝试重新编译)。
命令:
| make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs | | —- |
编译成功后会在“./arch/arm/boot/dts”目录下生成“imx6ull-seeed-npi.dtb”文件,将其替换掉板子/boot/dtbs/4.19.71-imx-r1/ 目录下的imx6ull-seeed-npi.dtb文件并重启开发板。
| #这里操作命令仅作为参考,实际根据自己电脑情况进行修改
将生成的设备树拷贝到共享文件夹
cp arch/arm/boot/dts/imx6ull-seeed-npi.dtb /home/Embedfire/wokdfir
挂载nfs共享文件夹(在开发板上)
sudo mount -f nfs 192.168.0.231:/home/Embedfire/wokdfir /mnt
复制设备树到共享文件夹(在开发板上)
cp /mnt/imx6ull-seeed-npi.dtb /boot/dtbs/4.19.71-imx-r1/
重启开发板
reboot | | —- |
使用新的设备树重新启动之后正常情况下会在开发板的“/proc/driver-tree”目录下生成“rgb_led”设备树节点。如下所示。
10.2.3. GPIO子系统常用API函数讲解
之前两小节我们修改设备树并编译、下载到开发板。设备树部分已经完成了,这里介绍GPIO子系统常用的几个API函数, 然后就可以使用GPIO子系统编写RGB驱动了。
1. 获取GPIO编号函数of_get_named_gpio
GPIO子系统大多数API函数会用到GPIO编号。GPIO编号可以通过of_get_named_gpio函数从设备树中获取。
of_get_named_gpio函数(内核源码include/linux/of_gpio.h)
| static inline int of_get_named_gpio(struct device_node np, const char propname, int index) |
|---|
参数:
- np: 指定设备节点。
- propname: GPIO属性名,与设备树中定义的属性名对应。
- index: 引脚索引值,在设备树中一条引脚属性可以包含多个引脚,该参数用于指定获取那个引脚。
返回值:
- 成功: 获取的GPIO编号(这里的GPIO编号是根据引脚属性生成的一个非负整数),
- 失败: 返回负数。
2. GPIO申请函数gpio_request
gpio_request函数(内核源码drivers/gpio/gpiolib-legacy.c)
| static inline int gpio_request(unsigned gpio, const char *label); |
|---|
参数:
- gpio: 要申请的GPIO编号,该值是函数of_get_named_gpio的返回值。
- label: 引脚名字,相当于为申请得到的引脚取了个别名。
返回值:
- 成功: 返回0,
- 失败: 返回负数。
3. GPIO释放函数
gpio_free函数(内核源码drivers/gpio/gpiolib-legacy.c)
| static inline void gpio_free(unsigned gpio); |
|---|
gpio_free函数与gpio_request是一对相反的函数,一个申请,一个释放。一个GPIO只能被申请一次, 当不再使用某一个引脚时记得将其释放掉。
参数:
- gpio: 要释放的GPIO编号。
返回值: 无
4. GPIO输出设置函数gpio_direction_output
用于将引脚设置为输出模式。
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
用于将引脚设置为输入模式。
gpio_direction_input函数(内核源码include/asm-generic/gpio.h)
| static inline int gpio_direction_input(unsigned gpio) |
|---|
函数参数:
- gpio: 要设置的GPIO的编号。
返回值:
- 成功: 返回0
- 失败: 返回负数。
6. 获取GPIO引脚值函数gpio_get_value
用于获取引脚的当前状态。无论引脚被设置为输出或者输入都可以用该函数获取引脚的当前状态。
gpio_get_value函数(内核源码include/asm-generic/gpio.h)
| static inline int gpio_get_value(unsigned gpio); |
|---|
函数参数:
- gpio: 要获取的GPIO的编号。
返回值:
- 成功: 获取得到的引脚状态
- 失败: 返回负数
7. 设置GPIO输出值gpio_set_value
该函数只用于那些设置为输出模式的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
- 失败: 返回负数
10.3. 实验说明与代码讲解
硬件介绍
本节实验使用到 EBF6ULL-PRO 开发板上的 RGB 彩灯
硬件原理图分析
参考”字符设备驱动–点亮LED灯”章节
10.3.1. 实验代码讲解
本章的示例代码目录为:base_code/linux_driver/gpio_subsystem_rgb_led
程序包含两个C语言文件,一个是驱动程序,驱动程序在平台总线基础上编写。 另一个是一个简单的测试程序,用于测试驱动是否正常。
10.3.1.1. 驱动程序讲解
驱动程序大致分为三个部分,第一部分,编写平台设备驱动的入口和出口函数。第二部分,编写平台设备的.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, .driver = { .name = “rgb-leds-platform”, .owner = THIS_MODULE, .of_match_table = rgb_led, } };
/—————————第二部分————————/ /驱动初始化函数/ static int __init led_platform_driver_init(void) { int error;
error = platform_driver_register(&led_platform_driver);printk(KERN_EMERG "\\tDriverState = %d\\n",error);return 0;
}
/—————————第三部分————————/ /驱动注销函数/ static void __exit led_platform_driver_exit(void) { printk(KERN_EMERG “platform_driver_exit!\n”);
platform_driver_unregister(&led_platform_driver);
}
module_init(led_platform_driver_init); module_exit(led_platform_driver_exit);
MODULE_LICENSE(“GPL”); | | —- |
- 第2-15行:为代码的第一部分,仅实现.probe函数和.driver,当驱动和设备匹配成功后会执行该函数, 这个函数的函数实现我们在后面介绍。.driver描述这个驱动的属性,包括.name驱动的名字,.owner驱动的所有者, .of_match_table驱动匹配表,用于匹配驱动和设备。驱动设备匹配表定义为“rgb_led”在这个表里只有一个匹配值 “.compatible = “fire,rgb-led” ”这个值要与我们在设备树中rgb_led设备树节点的“compatible”属性相同。
- 第17-40行:第二、三部分是平台设备的入口和出口函数,函数实现很简单,在入口函数中注册平台驱动,在出口函数中注销平台驱动。
平台驱动.probe函数实现
当驱动和设备匹配后首先会probe函数,我们在probe函数中实现RGB的初始化、注册一个字符设备。 后面将会在字符设备操作函数(open、write)中实现对RGB等的控制。函数源码如下所示。
probe函数实现
| static int led_probe(struct platform_device *pdv) { unsigned int register_data = 0; //用于保存读取得到的寄存器值 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;
} | | —- |
- 第10-14行:使用of_find_node_by_path函数找到并获取rgb_led在设备树中的设备节点。 参数“/rgb_led”是要获取的设备树节点在设备树中的路径,如果要获取的节点嵌套在其他子节点中需要写出节点所在的完整路径。
- 第17-22行:使用函数of_get_named_gpio函数获取GPIO号,读取成功则返回读取得到的GPIO号。 “rgb_led_red”指定GPIO的名字,这个参数要与rgb_led设备树节点中GPIO属性名对应, 参数“0”指定引脚索引,我们的设备树中一条属性中只定义了一个引脚,我们只有一个所以设置为0。
- 第25-27行,将GPIO设置为输出模式,默认输出电平为高电平。
- 第32-65行,字符设备相关内容,这部分内容在字符设备章节已经详细介绍这里不再赘述。
实现字符设备函数
字符设备函数我们只需要实现open函数和write函数。函数源码如下。
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;
} | | —- |
- 代码3-8行:定义字符设备操作函数集,这里主要实现open和write函数即可。
- 代码12-16行:实现open函数,在平台驱动的prob函数中已经初始化了GPIO,这里不用做任何操作
- 代码20-60行:write函数实现也很简单,首先使用“copy_from_user”函数将来自应用层的数据“拷贝”内核层。 得到命令后就依次检查后三位,根据命令值使用“gpio_direction_output”函数控制RGB灯的亮灭。
10.3.1.2. 应用程序讲解
应用程序编写比较简单,我们只需要打开设备节点文件,写入命令然后关闭设备节点文件即可。源码如下所示。
Makefile文件
| int main(int argc, char argv[]) { /判断输入的命令是否合法*/ 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;
} | | —- |
结合代码各部分说明如下:
- 代码4-8行:判断命令是否有效。再运行应用程序时我们要传递一个控制命令,所以参数长度是2。
- 代码11-16行:打开设备文件。参数“/dev/rgb-leds”用于指定设备节点文件,设备节点文件名是在驱动程序中设置的, 这里保证与驱动一致即可。
- 代码18-35行:由于从main函数中获取的参数是字符串,这里首先要将其转化为数字。最后条用write函数写入命令然后关闭文件即可。
10.3.2. Makefile修改说明
修改Makefile并编译生成驱动程序
Makefile程序并没有大的变化,修改后的Makefile如下所示。
Makefile文件
| KERNEL_DIR = /home/fire2/ebf-buster-linux
obj-m := rgb-leds.o
all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONY:clean clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean | | —- |
- 代码第2行:变量“KERNEL_DIR”保存的是内核所在路径,这个需要根据自己内核所在位置设定。
- 代码第4行:“obj-m := rgb-leds.o”中的“rgb-leds.o”要与驱动源码名对应。Makefiel 修改完成后执行如下命令编译驱动。
命令:
| make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- | | —- |
正常情况下会在当前目录生成.ko驱动文件。
编译应用程序
进入应用程序所在目录“~/gpio_subsystem_rgb_led/”执行如下命令:
命令:
| arm-linux-gnueabihf-gcc <源文件名> –o <输出文件名> | | —- |
以本章配套历程为例执行如下命令编译应用程序:
命令:
| arm-linux-gnueabihf-gcc rgb_leds_app.c –o rgb_leds_app | | —- |
10.3.3. 下载验证
前两小节我们已经编译出了.ko驱动和应用程序,将驱动程序和应用程序添加到开发板中(推荐使用之前讲解的NFS共享文件夹), 驱动程序和应用程序在开发板中的存放位置没有限制。我们将驱动和应用都放到开发板的“/home/nfs_share”目录下,如下所示。
执行如下命令加载驱动:
命令:
| insmod ./rgb-leds.ko | | —- |
正常情况下输出结果如下所示。
在驱动程序中,我们在.probe函数中注册字符设备并创建了设备文件,设备和驱动匹配成功后.probe函数已经执行, 所以正常情况下在“/dev/”目录下已经生成了“rgb-leds”设备节点,如下所示。
驱动加载成功后直接运行应用程序如下所示。
命令:
| ./rgb_leds_app <命令> | | —- |
执行结果如下:
命令是一个“unsigned char”型数据,只有后三位有效,每一位代表一个灯,从高到低依次代表红、绿、蓝,1表示亮,0表示灭。 例如命令=4 则亮红灯,命令=7则三个灯全亮。
