i.MX8QM NXP 开发板有 8 组 GPIO 每组 GPIO 32 bit,本文使用 i.MX8QM NXP 开发板的 GPIO1, 在 i.MX8QM 上实现 GPIO 控制配置,在应用层通过 APP 控制 LED 的亮灭。本次控制LVDS 的 LED 指示灯,使用 platform 框架,注册字符设备,platform 框架用来匹配设备树,驱动可以通过 of 函数获取设备树参数,字符设备给用户层提供函数接口,可以用 open 函数打开设备,用 read 和 write 函数读写数据,与驱动内核进行联系,从而控制 GPIO 电平变化,软件框架图如下所示:
LVDS 的 led 灯连接脚为 LVDS0_BL_PWM,连接 i.mx8qm 的 GPIO1_04 引脚,如下图所示:
图片来源:取自 NXP 官网 imx8qm 原理图
二、GPIO 驱动编写
1. 模块入口 / 出口
如下代码, module_init(gpio_train_init) 指示驱动入口函数为 gpio_train_init h函数。module_exit (gpio_train_exit) 指示驱动出口函数为 gpio_train_exit 函数。__init 是告知编译器,将变量或函数放在一个特殊的区域,这个区域定义在 vmlinux.lds 中。__init 将函数放在代码段的一个子段“.init.text”(初始化代码段)中,_exit 标记的函数只有对模块才起作用,是指明函数是放在代码段的“.exit.text”中。
在入口函数中使用 platform_driver_register 进行 platform 驱动注册,在出口函数中使用 platform_driver_unregister 函数进行 platform 驱动注销。MODULE_LICENSE 声明模块许可证,通常使用 GPL 即可。
static int __init gpio_train_init(void) { return platform_driver_register(&gpio_driver); } static void __exit gpio_train_exit(void) { platform_driver_unregister(&gpio_driver); } module_init(gpio_train_init); module_exit(gpio_train_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Andrew"); |
2. Platform 驱动框架
结构体定义如下代码所示。.driver 配置了驱动名称和 compatible 内容,.of_math_table 是模块匹配表,其赋值为 of_device_id 类型数组变量,数组 of_device_id 最后一行为空,.compatible 表示匹配属性,当设备树节点的 compatible 内容属性与此驱动匹配表内容一样时,表示驱动可以使用此设备节点的定义属性,并调用 probe 函数进一步初始化驱动。.probe 表示设备驱动于设备树成功匹配后的调用函数,.remove 表示设备卸载时的调用函数。
static const struct of_device_id gpiotrain_of_mach[] = { {.compatible = "gpio-trainled"}, {}, }; static struct platform_driver gpio_driver ={ .driver = { .name = "gpio-train", .of_match_table = gpiotrain_of_mach, }, .probe = gpio_probe, .remove = gpio_remove, }; |
2.2 probe 函数
驱动 probe 函数为 gpio_probe,此函数包含字符设备初始化(1)和 gpio 的初始化(2),字符设备和 GPIO 框架下文分析,具体代码如下所示:
static int gpio_probe(struct platform_device *dev) { // int ret; // struct pwm_state state; // /*创建设备号*/ if(gpiodev.major) (1) { gpiodev.devid = MKDEV(gpiodev.major,0); register_chrdev_region(gpiodev.devid, 1 , "gpio_train"); } else { alloc_chrdev_region(&gpiodev.devid, 0 , 1 , "gpio_train"); gpiodev.major = MKDEV(gpiodev.devid,0); }
/*初始化 cdev*/ gpiodev.cdev.owner = THIS_MODULE; cdev_init(&gpiodev.cdev,&gpiodev_fops); /*添加一个 cdev*/ cdev_add(&gpiodev.cdev,gpiodev.devid, 1); /*创建类*/ gpiodev.class = class_create(THIS_MODULE,"gpio_train"); if (IS_ERR(gpiodev.class)){ return PTR_ERR(gpiodev.class); } /*创建设备*/ gpiodev.device = device_create(gpiodev.class, NULL, gpiodev.devid, NULL, "gpio_train"); if(IS_ERR(gpiodev.device)){ return PTR_ERR(gpiodev.device); } gpiodev.node = of_find_node_by_path("/gpio_train"); (2) if(gpiodev.node == NULL) { printk("gpio-train node nost find!\r\n"); return -EINVAL; } gpiodev.led0 = of_get_named_gpio(gpiodev.node,"gpio", 0); printk("of gpio_train gpio: %d\r\n",gpiodev.led0); if(gpiodev.led0 < 0) { printk("can't get led-gpio \r\n"); return -EINVAL; } gpio_request(gpiodev.led0,"led0"); gpio_direction_output(gpiodev.led0, 1); printk("gpio_train probe is over!"); return 0; } |
2.3 remove 函数
驱动 remove 函数为 gpio_remove,此函数用于注销和删除 probe 函数所注册的内容,主要是字符设备的注销和 GPIO 的设置,具体代码如下所示:
static int gpio_remove(struct platform_device *dev) { gpio_set_value(gpiodev.led0, 1);
cdev_del(&gpiodev.cdev); unregister_chrdev_region(gpiodev.devid, 1); device_destroy(gpiodev.class, gpiodev.devid); class_destroy(gpiodev.class); return 0; } |
3. 字符设备驱动
注册字符设备框架,主要是为了使用字符设备的 write 函数,在用户层调用 write 可以向驱动函数 write 传值,驱动函数根据从用户层接收到的数据,进行控制 GPIO 电平变化。
3.1 字符设备创建和注销
① 设备出入口函数:module_init(init 函数),module_exit(exit 函数):
module_init(gpio_train_init); module_exit(gpio_train_exit); |
② 设备许可函数,MODULE_LICENSE ( "GPL" ):
MODULE_LICENSE("GPL"); |
③ 设备结构体变量,包含不限于 dev_t,struct cdev,struct class *class,struct device *device,int major:
//设备结构体 struct gpiodev_dev { dev_t devid; struct cdev cdev; struct class *class; struct device *device; struct device_node *node; struct pwm_device *pwm; int major; int led0; }; |
④ 设备接口函数及其结构体变量,static struct file_operations gpiodev_fops,包含但不限于:
static struct file_operations gpiodev_fops = { .owner = THIS_MODULE, .open = gpio_open, .write = gpio_write, .read = gpio_read, }; |
⑤ 创建设备号,alloc_chrdev_region 获取设备号,MKDEV 函数设备号转 dev_t:
alloc_chrdev_region(&gpiodev.devid, 0 , 1 , "gpio_train"); gpiodev.major = MKDEV(gpiodev.devid,0); |
⑥ 初始化 cdev:
/*初始化 cdev*/ gpiodev.cdev.owner = THIS_MODULE; cdev_init(&gpiodev.cdev,&gpiodev_fops); |
⑦ 添加 cdev:
/*添加一个 cdev*/ cdev_add(&gpiodev.cdev,gpiodev.devid, 1); |
⑧ 创建类:
/*创建类*/ gpiodev.class = class_create(THIS_MODULE,"gpio_train"); if (IS_ERR(gpiodev.class)){ return PTR_ERR(gpiodev.class); } |
⑨ 创建设备:
/*创建设备*/ gpiodev.device = device_create(gpiodev.class, NULL, gpiodev.devid, NULL, "gpio_train"); if(IS_ERR(gpiodev.device)){ return PTR_ERR(gpiodev.device); } |
⑩ 注销:
cdev_del(&gpiodev.cdev); unregister_chrdev_region(gpiodev.devid, 1); device_destroy(gpiodev.class, gpiodev.devid); class_destroy(gpiodev.class); |
3.2 write 函数
write 函数用于控制 GPIO 电平变化,当应用层使用 write 函数时,对应驱动此函数。
static ssize_t gpio_write(struct file *file, const char __user *buff, size_t cnt, loff_t *offt) { int flag; unsigned char writbuff[2]; unsigned char state;
flag = copy_from_user(writbuff, buff, cnt); if(flag < 0) { printk("write fail\r\n"); return -EFAULT; }
state = writbuff[0]; printk("receipt data is: %c\r\n",state); if(state == 1) { gpio_set_value(gpiodev.led0, 0); printk("gpio_set_value data is: 0"); } else if(state == 0) { gpio_set_value(gpiodev.led0, 1); printk("gpio_set_value data is: 1"); } return 0; } |
copy_from_user 接收来自用户空间的信息,gpio_set_value 设置 GPIO 管脚的输出电平,当接收到用户层传输 1 时,gpio 输出低电平,当接收到用户层传输 0 时,gpio 输出高电平
4. GPIO 子系统
使用 of_find_node_by_path 函数从设备树查找 /gpio_train 节点,查找成功时返回找到的节点,失败时返回NULL:
gpiodev.node = of_find_node_by_path("/gpio_train"); if(gpiodev.node == NULL) { printk("gpio-train node nost find!\r\n"); return -EINVAL; } |
从设备树节点中获取 GPIO 管脚号:
gpiodev.led0 = of_get_named_gpio(gpiodev.node,"gpio", 0); printk("of gpio_train gpio: %d\r\n",gpiodev.led0); if(gpiodev.led0 < 0) { printk("can't get led-gpio \r\n"); return -EINVAL; } |
申请 GPIO 管脚:
gpio_request(gpiodev.led0,"led0"); |
设置 GPIO 管脚模式和输出电平:
gpio_direction_output(gpiodev.led0, 1); |
5. Kernel 配置
5.1 编写 makefile
obj-$(CONFIG_GPIO_TRAIN_LED) += gpio_train.o |
5.2 编写 kconfig
① 新文件:
menuconfig GPIO_TRAIN
bool "GPIO Train support"
depends on OF_GPIO
if GPIO_TRAIN
config GPIO_TRAIN_LED
tristate "GPIO Train block support"
default n
help
This is enable GPIO Train support for the leds class
endif
② 添加节点:
config GPIO_TRAIN_LED
tristate "Train GPIO"
depends on OF_GPIO
select GPIOLIB_IRQCHIP
help
train_gpio enable
5.3 文件引用
配置自己编写的 makefile 和 kconfig 的目录的上一级目录下的 makefile 和 kconfig:
① 添加 Makefile 包含:
# GPIO Train odj-$(CONFIG_GPIO_TRAIN) += gpio_train/ |
② 增加 makefile 配置:
obj-$(CONFIG_GPIO_TRAIN_LED) += gpio_train.o |
③ 添加Kconfig 包含:
source "drivers/gpio/gpio_train/Kconfig" |
④ 增加 Kconfig 配置:
config GPIO_TRAIN_LED tristate "Train GPIO" depends on OF_GPIO select GPIOLIB_IRQCHIP help train_gpio enable |
5.4设备树
增加 gpio 设备树节点 gpio_train,增加 pinctrl gpio 管脚配置节点 pinctrl_train,把 gpio1_4 设置为输出模式。
gpio_train { #address-cells = <1>; //用来描述子节点 "reg" 属性的地址表中用来描述首地址的 cell 的数量 #size-cells = <1>;//用来描述子节点 "reg" 属性的地址表中用来描述地址长度的 cell 的数量。 compatible = "gpio-trainled"; pinctrl-name = "default"; pinctrl-0 = <&pinctrl_train>; gpio = <&lsio_gpio1 4 GPIO_ACTIVE_LOW>; //pwms = <&pwm_lvds0 0 100000 0>; status = "okay"; };
pinctrl_train: traingrp { fsl,pins = < /* IMX8QM_QSPI1A_DQS_LSIO_GPIO4_IO22 0x06000021*/ IMX8QM_LVDS0_GPIO00_LSIO_GPIO1_IO04 0x00000020 /*IMX8QM_LVDS0_GPIO00_LVDS0_PWM0_OUT 0x00000020*/ /*IMX8QM_LVDS1_GPIO00_LVDS1_PWM0_OUT 0x00000020*/ >; };
|
|
6. APP 编写
使用 open 函数打开 GPIO 的设备节点,打开后使用 write 函数向内核驱动写字节数,写的内容由用胡决定,写 1 时 gpio 电平拉低,写 0 时 gpio 拉高,例如终端运行 ./app /dev/gpio_train 0 时,GPIO 电平拉高。
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** 使用方法 :./ledApp /dev/platled 0 关闭 LED ./ledApp /dev/platled 1 打开 LED
***************************************************************/ int main(int argc, char *argv[]) { int fd, retvalue; char *filename; unsigned char databuf[2];
if(argc != 3){ printf("Error Usage!\r\n"); return -1; }
filename = argv[1];
/* 打开led驱动 */ fd = open(filename, O_RDWR); if(fd < 0){ printf("file %s open failed!\r\n", argv[1]); return -1; }
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ retvalue = write(fd, databuf, sizeof(databuf)); if(retvalue < 0){ printf("LED Control Failed!\r\n"); close(fd); return -1; } printf("Filename: %s write: %s\r\n ", filename, databuf[0]); retvalue = close(fd); /* 关闭文件 */ if(retvalue < 0){ printf("file %s close failed!\r\n", argv[1]); return -1; } return 0; } |
以上为 i.MX8QM 的 GPIO 驱动的编写以及测试 APP 的应用编写!
参考文献:
《iMX8QM_RM_Rev_E.pdf》
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.6.pdf》
评论