驱动程序开发:pinctrl和gpio子系统之新字符设备驱动之LED点灯
Linux 驱动讲究驱动分离与分层, pinctrl 和 gpio 子系统就是驱动分离与分层思想下的产物,驱动分离与分层其实就是按照面向对象编程的设计思想而设计的设备驱动框架。
一、修改设备树文件
1、在.dts文件中的 iomuxc 节点内 imx6ul-evk 子节点下创建一个名为“pinctrl_gpioled”的子节点,如下图所示。
(将 GPIO1_IO03 这个 PIN 复用为 GPIO1_IO03,电气属性值为 0X10B0。)
2、、在.dts文件中的根节点“/”下创建 LED 灯节点,节点名为“gpioled”。
(pinctrl-0 属性设置 LED 灯所使用的 PIN 对应的 pinctrl 节点,led-gpio 属性指定了 LED 灯所使用的 GPIO,在这里就是 GPIO1 的 IO03,低电平有效。稍后编写驱动程序的时候会获取 led-gpio 属性的内容来得到 GPIO 编号,因为 gpio 子系统的 API 操作函数需要 GPIO 编号。)
3、检查 PIN 有没有被其他外设使用,否则的话驱动程序在申请 GPIO 的时候就会失败。
检查方法是:
①检查 pinctrl 设置。
②检查这个PIN还有没有别的外设也配置成GPIO的。
如下图操作。
二、编写驱动程序
这里就不详细说明了,程序的注释有很详细的步骤说明,具体API函数使用大家可以自行查看内核源码的例程案例程序。
/*
* 根据linux内核的程序查找所使用函数的对应头文件。
*/
#include <linux/module.h> // MODULE_LICENSE,MODULE_AUTHOR
#include <linux/init.h> // module_init,module_exit
#include <linux/kernel.h> // printk
#include <linux/fs.h> // struct file_operations
#include <linux/slab.h> //kmalloc, kfree
#include <linux/uaccess.h> // copy_to_user,copy_from_user
#include <linux/io.h> //ioremap,iounmap
#include <linux/cdev.h> //struct cdev,cdev_init,cdev_add,cdev_del
#include <linux/device.h> //class
#include <linux/of.h> //of_find_node_by_path
#include <linux/of_gpio.h> //of_get_named_gpio
#include <linux/gpio.h> //gpio_request,gpio_direction_output,gpio_set_value
#define LED_OFF 0
#define LED_ON 1
/* 1.6 gpioled设备结构体 */
struct gpioled_dev {
dev_t devid; /* 设备号 */
int major; /* 主设备号 */
int ninor; /* 次设备号 */
int count; /* 设备个数 */
char* name; /* 设备名字 */
struct cdev cdev; /* 注册设备结构体 */
struct class *class; /* 类 */
struct device *device; /* 设备 */
struct device_node *nd; /* 设备节点 */
int led_gpio; /* IO的编号 */
};
struct gpioled_dev gpioled; //定义gpioled
/* 6.1 打开字符设备文件 */
static int gpioled_open(struct inode *inde,struct file *filp) {
/* 设置私有类数据 */
filp->private_data = &gpioled;
return 0;
}
/* 6.2 关闭字符设备文件 */
static int gpioled_release(struct inode *inde,struct file *filp) {
/* 提取私有类的属性 */
struct gpioled_dev *dev = (struct gpioled_dev *)filp->private_data;
return 0;
}
/* 6.3 向字符设备文件读取数据 */
static ssize_t gpioled_read(struct file *filp,char __user *buf,
size_t count,loff_t *ppos) {
return 0;
}
/* 6.4 向字符设备文件写入数据 */
static ssize_t gpioled_write(struct file *filp,const char __user *buf,
size_t count,loff_t *ppos) {
/* 提取私有类的属性 */
struct gpioled_dev *dev = (struct gpioled_dev *)filp->private_data;
/* 8.1 应用程序输入参数写入内核驱动程序控制LED */
int ret = 0; //保存调用函数的返回值
u8 databuf[1]; //控制参数是0和1,所以一位字符即可
ret = copy_from_user(databuf,buf,count);
if(ret < 0) {
printk("write kernel failed!\r\n");
return -EFAULT;
}
if(databuf[0] == LED_ON) {
gpio_set_value(dev->led_gpio,0);
} else if(databuf[0] == LED_OFF) {
gpio_set_value(dev->led_gpio,1);
}
return 0;
}
/* 3.1.1 gpioled设备操作集 */
static const struct file_operations gpioled_fops = {
.owner = THIS_MODULE,
.open = gpioled_open,
.release = gpioled_release,
.read = gpioled_read,
.write = gpioled_write,
};
/* 1.1 驱动模块入口函数 */
static int __init gpioled_init(void) {
int ret = 0; //保存调用函数返回值
printk("gpioled_init!\r\n");
/* 2.1 注册设备号 */
gpioled.count = 1; //设置设备个数
gpioled.name = "gpioled"; //设置设备名字
gpioled.major = 0;
/* 如果自设定设备号 */
if(gpioled.major) {
gpioled.devid = MKDEV(gpioled.major,0); //整合设备号
ret = register_chrdev_region(gpioled.devid,gpioled.count,gpioled.name); //注册设备号
} else {
alloc_chrdev_region(&gpioled.devid,0,gpioled.count,gpioled.name);
gpioled.major = MAJOR(gpioled.devid);
gpioled.ninor = MINOR(gpioled.devid);
}
if(ret < 0) {
goto fail_devid; /* 注册设备号失败 */
}
printk("gpioled major = %d, minor = %d \r\n",gpioled.major,gpioled.ninor); //打印主次设备号
/* 3.1 注册或者叫添加字符设备 */
cdev_init(&gpioled.cdev,&gpioled_fops); //初始化cdev结构体
ret = cdev_add(&gpioled.cdev,gpioled.devid,gpioled.count); //添加字符设备
if(ret < 0) {
goto fail_cdev;
}
/* 4.1 自动创建设备节点 */
gpioled.class = class_create(THIS_MODULE,gpioled.name); //创建类
if(IS_ERR(gpioled.class)) {
ret = PTR_ERR(gpioled.class);
goto fail_class;
}
gpioled.device = device_create(gpioled.class,NULL,gpioled.devid,NULL,gpioled.name); //创建设备
if(IS_ERR(gpioled.device)) {
ret = PTR_ERR(gpioled.device);
goto fail_device;
}
/* 5.1 获取设备节点 */
gpioled.nd = of_find_node_by_path("/gpioled"); //根据设备树的设备节点路径获取设备节点
if(gpioled.nd == NULL) {
ret = -EINVAL;
goto fail_findnode;
}
/* 5.2 获取LED对应的设备节点中的GPIO信息 */
gpioled.led_gpio = of_get_named_gpio(gpioled.nd,"led-gpio",0);
if(gpioled.led_gpio < 0) {
printk("can't find led gpio!\r\n");
ret = -EINVAL;
goto fail_getgpio;
}
printk("led gpio num = %d\r\n",gpioled.led_gpio); //打印获取到的gpio编号
/* 5.3 申请IO,不申请也可以直接使用,但是无法检测此IO是否被其他设备使用,避免IO出现重复使用 */
/* 如果申请IO失败的话,大部分原因是IO被其他的设备占用所导致的。
* 方法:(在.dts文件中查询,屏蔽重复段)
* 1、检查复用,也就是如下:
* pinctrl_gpioled: ledgrp {
* fsl,pins = <
* MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0
* >;
* };
*
* 2、检查GPIO的使用,如下:
* led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
*/
ret = gpio_request(gpioled.led_gpio,"led_gpio");
if(ret) {
printk("failed to request the led gpio!\r\n");
ret = -EINVAL;
goto fail_requestgpio;
}
/* 5.4 使用IO,设置其IO为输出 */
ret = gpio_direction_output(gpioled.led_gpio,1); //设置led对应的GPIO1-IO3为输出模式,并输出高电平,LED默认关闭状态
if(ret) {
printk("failed to drive the led reset gpio!\r\n");
goto fail_setouput;
}
/* 5.5 设置IO的值,设置低电平,点亮LED */
gpio_set_value(gpioled.led_gpio,0);
return 0;
fail_setouput: //设置IO输出失败
gpio_free(gpioled.led_gpio);
fail_requestgpio: //申请IO失败
fail_getgpio: //获取LED对应的设备节点中的GPIO信息失败
fail_findnode: //获取设备节点失败
device_destroy(gpioled.class,gpioled.devid);
fail_device: //创建设备失败
class_destroy(gpioled.class);
fail_class: //创建类失败
cdev_del(&gpioled.cdev);
fail_cdev: //注册设备或者叫添加设备失败
unregister_chrdev_region(gpioled.devid,gpioled.count);
fail_devid: //分配设备号失败
return ret;
}
/* 1.2 驱动模块出口函数 */
static void __exit gpioled_exit(void) {
printk("gpioled_exit!\r\n");
/* 7.6 关闭LED灯 */
gpio_set_value(gpioled.led_gpio,1);
/* 7.5 释放IO */
gpio_free(gpioled.led_gpio);
/* 7.4 摧毁设备 */
device_destroy(gpioled.class,gpioled.devid);
/* 7.3 摧毁类 */
class_destroy(gpioled.class);
/* 7.2 注销字符设备 */
cdev_del(&gpioled.cdev);
/* 7.1 注销设备号*/
unregister_chrdev_region(gpioled.devid,gpioled.count);
}
/* 1.3 注册驱动模块入口函数 */
module_init(gpioled_init);
/* 1.4 注册驱动模块出口函数 */
module_exit(gpioled_exit);
/* 1.5 驱动许可证 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("djw");
三、编写APP应用程序
/*
* 头文件可以通过linux手册查找,man no <xx>
* 1是普通的命令
* 2是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文件)
* 3是库函数,如printf,fread
* 4是特殊文件,也就是/dev下的各种设备文件
* 5是指文件的格式,比如passwd, 就会说明这个文件中各个字段的含义
* 6是给游戏留的,由各个游戏自己定义
* 7是附件还有一些变量,比如向environ这种全局变量在这里就有说明
* 8是系统管理用的命令,这些命令只能由root使用,如ifconfig
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
* argc:应用程序参数个数
* argv[]:具体打参数内容,字符串形式
* ./ledAPP <filename> <0:1> 0为关,1为开
* ./ledAPP /dev/led 0 表示关灯
* ./ledAPP /dev/led 1 表示开灯
*/
int main(int argc, char *argv[])
{
int fd, ret; //fd保存调用文件操作函数的文件描述符的临时变量,ret保存调用函数返回值临时变量
char *filename; //保存文件名信息的指针变量
unsigned char databuf[1]; //保存终端输入的开关灯变量
/* 判断终端输入的参数个数是否为3个 */
if(argc != 3) {
printf("ERROR USAGE!\r\n");
return -1;
}
/* 保存第二个输入的参数,文件名信息,此参数是保存对应创建文件节点的绝对路径的 */
filename = argv[1];
fd = open(filename,O_RDWR); //使用读写方式打开文件
if(fd < 0) {
printf("file %s open failed!\r\n",filename);
return -1;
}
/* 保存第三个输入参数,也就是led的状态参数 */
databuf[0] = atoi(argv[2]);
/* 将led的状态参数写入fd设备文件中,其会在驱动程序中以调用函数buf形参写入驱动程序的变量中 */
ret = write(fd, databuf, sizeof(databuf));
if(ret < 0) {
printf("LED control failed!\r\n");
close(fd); //如果写入操作失败了,那么就关闭设备文件
return -1;
}
close(fd); //程序运行完成,关闭设备文件
return 0;
}
四、操作步骤
步骤:
1、将驱动程序进行make操作,编译生成xxx.ko文件
2、使用交叉编译器将应用程序编译成可执行文件,如:我这里使用 arm-linux-gnueabihf-gcc xxxAPP.c -o xxxAPP
3、将.dts设备树文件进行编译,如make dtbs生成.dtb文件
4、将xxx.ko、xxxAPP和.dtb三个文件个文件分别拷贝到驱动模块目录和tftp目录中。
5、打开开发板并使用Linux系统选择通过TFTP从网络启动和使用NFS挂载网络根文件系统。
6、在PC机的串口终端中先输入depmod,后输入modprobe xxx.ko加载驱动文件,最后输入lsmod查看当前系统中存在的模块。
7、在PC机的串口终端中输入cat /proc/device-tree查找设备数的设备节点。
8、在PC机的串口终端中输入./xxxApp /dev/xxx 0或./xxxApp /dev/xxx 1指令来使用应用程序对驱动程序进行读写等操作。
9、在PC机的串口终端中输入rmmod xxx.ko卸载驱动模块。