4412开发板学习之Linux驱动开发(七):字符设备驱动的基本操作(生成设备节点、编写设备驱动)

生成字符类设备节点

设备类的概念

前面介绍的设备中的模型:bus,device,driver都是有比较明确的定义,bus代表总线,device代表实际的设备和接口,driver代表驱动
class是设备类,它是一个抽象的概念,没有对应的实体,它是提供给用户接口相似的一类设备的集合。常见的有输入子系统input、usb、串口tty、块设备block等
misc杂项设备class这个部分,系统对这部分进行了打包
使用ls /sys/class查看设备类
在这里插入图片描述

生成字符类设备节点

相关函数、结构体及头文件

  • 函数class_create创建class类文件
    #define class_create(owner, name)
    参数1:一般是THIS_MODULE
    参数2:设备名称
    返回一个设备类,用于设备节点文件的创建
    返回一个class结构体变量

  • class结构体变量
    class是设备驱动模型中通用的设备类结构
    在include/linux/device.h中

	struct class {
		const char		*name;
		struct module		*owner;
			
		struct class_attribute		*class_attrs;
		struct device_attribute		*dev_attrs;
		struct bin_attribute		*dev_bin_attrs;
		struct kobject			*dev_kobj;
			
		int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
		char *(*devnode)(struct device *dev, mode_t *mode);
			
		void (*class_release)(struct class *class);
		void (*dev_release)(struct device *dev);
			
		int (*suspend)(struct device *dev, pm_message_t state);
		int (*resume)(struct device *dev);
			
		const struct kobj_ns_type_operations *ns_type;
		const void *(*namespace)(struct device *dev);
			
		const struct dev_pm_ops *pm;
			
		struct subsys_private *p;
	};
  • 老版本:创建设备class函数class_device_create
    头文件:include/linux/device.h
    参数1:class结构体变量
    参数2:父设备NULL
    参数3:dev_t设备号
    参数4:数据NULL
    参数5:设备节点名称
  • 释放设备class函数class_destroy
    extern void class_destroy(struct class *cls);
    参数1:myclass
  • 创建设备节点函数device_create
    头文件:include/linux/device.h
    extern struct device *device_create(struct class *cls, struct device *parent,dev_t devt, void *drvdata,const char *fmt, …)attribute((format(printf, 5, 6)));
    参数1:设备所属的类
    参数2:设备的父设备,NULL
    参数3:设备号
    参数4:设备数据,NULL
    参数5:设备名称
  • 摧毁设备节点函数device_destory
    参数1:设备所属于的类
    参数2:设备号
  • 加载模块的时候还可以使用命令生成设备节点(老的方法,用的少了,知道就好)
    mknod dev/test0 c 249 0
    两数字分别为主设备号和次设备号,文件名为节点名

实验

代码

#include <linux/init.h>
#include <linux/module.h>
/*定义module_param module_param_array的头文件*/
#include <linux/moduleparam.h>
/*定义module_param module_param_array中的参数perm的头文件*/
#include <linux/stat.h>
/*三个字符设备注册函数*/
#include <linux/fs.h>
/*宏定义MKDEV的头文件,MKDEV转换设备号数据类型*/
#include <linux/kdev_t.h>
/*定义字符设备的结构体*/
#include <linux/cdev.h>
/*分配内存空间函数头文件*/
#include <linux/slab.h>
/*包含结构体class以及相关函数的头文件*/
#include <linux/device.h>

#define DEVICE_NEME "chardevnode"
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0
#define DEV_MINOR 0
#define REGDEV_SIZE 3000
MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("GYY");

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/*输入主设备号*/
module_param(numdev_major,int,S_IRUSR);
/*输入次设备号*/
module_param(numdev_minor,int,S_IRUSR);

static struct class *myclass ;
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

struct reg_dev *my_devices; 

struct file_operations my_fops = {
	.owner = THIS_MODULE
};
/*设备注册到系统*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devnum = MKDEV(numdev_major,numdev_minor+index);
	/*数据初始化*/
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/*注册到系统*/
	err = cdev_add(&dev->cdev,devnum,1);
	/*打印信息*/
	if(err)
	{
		printk(KERN_EMERG "cdev_add is failed ,err : %d ,index : %d \n",err,index);
	}
	else
	{
		printk(KERN_EMERG "cdev_add is success ,minor : %d \n",numdev_minor+index);
	}
}

static int chardev_init(void)
{
	int ret,i;
	dev_t num_dev;
	
	printk(KERN_EMERG "chardev_init enter!\n");
	
	printk(KERN_EMERG "numdev_major is %d \n",numdev_major);
	printk(KERN_EMERG "numdev_minor is %d \n",numdev_minor);
	/*如果传入参数的话静态分配设备号*/
	if(numdev_major)
	{
		num_dev = MKDEV(numdev_major,numdev_minor);//转换为内核的设备号格式(高12位为主设备号,低20位为次设备号)
		/*静态分配设备号*/
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NEME);
	}
	/*不传入参数的话动态分配设备号*/
	else
	{
		/*动态分配设备号*/
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NEME);
		numdev_major = MAJOR(num_dev);//使用MAJOR宏定义获取主设备号
		printk(KERN_EMERG "alloc_chrdev_region req %d\n",numdev_major);//打印主设备号
	}
	/*如果分配设备号失败*/
	if(ret<0)
	{
		printk(KERN_EMERG "alloc_chrdev_region req %d is failed",numdev_major);
		return 0;
	}
	/**/
	myclass = class_create(THIS_MODULE,DEVICE_NEME);
	
	/*给my_devices结构体分配内存*/
	my_devices = kmalloc(DEVICE_MINOR_NUM*sizeof(struct reg_dev),GFP_KERNEL);
	/*如果内存分配失败,返回错误*/
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/*清空分配到的内存空间的数据*/
	memset(my_devices,0,DEVICE_MINOR_NUM*sizeof(struct reg_dev));
	
	/*设备初始化,一个一个初始化*/
	for(i=0;i<DEVICE_MINOR_NUM;i++)
	{
		/*分配内存空间*/
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		/*清空分配到的内存空间的数据*/
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/*设备注册到系统*/
		reg_init_cdev(&my_devices[i],i);
		
		/*生成设备节点*/
		device_create(myclass,NULL, MKDEV(numdev_major,numdev_minor+i),NULL,DEVICE_NEME"%d",i);
	}
	/*打印初始化函数信息*/
	printk(KERN_EMERG "ascdev_init\n");
	return 0;
	
	fail:
		/*注销设备号*/
		unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
		printk(KERN_EMERG "kmalloc is failed!\n");
		return ret;
}

static void chardev_exit(void)
{
	int i;
	printk(KERN_EMERG "chardev World exit!\n");
	/*卸载字符设备(一个一个卸载)*/
	for(i=0;i<DEVICE_MINOR_NUM;i++)
	{
		/*卸载字符设备*/
		cdev_del(&my_devices[i].cdev);
		/*摧毁设备节点*/
		device_destroy(myclass,MKDEV(numdev_major,numdev_minor+i));
	}
	
	/*释放内存*/
	kfree(my_devices);
	/*释放设备class*/
	class_destroy(myclass);
	/*注销设备号*/
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

module_init(chardev_init);
module_exit(chardev_exit);

代码分析

这个代码是在前面注册设备的代码的基础上修改的,修改的地方并不多,只是添加了类的注册与释放以及在初始化时生成设备节点,在最后卸载模块的时候摧毁设备节点
在使用循环对设备进行初始化时,每注册一个设备就生成一个一个设备节点

/*生成设备节点*/
device_create(myclass,NULL, MKDEV(numdev_major,numdev_minor+i),NULL,DEVICE_NEME"%d",i);

myclass是我们先前注册的字符设备类
父设备写NULL
dev_t设备号我们使用MKDEV宏定义来生成(高12为主设备号,低20位为次设备号)
数据写NULL
设备节点名称我们使用DEVICE_NEME加数字后缀,生成的应当是chardevnode0、chardevnode1…
在我们卸载模块的时候我们释放了申请的内存空间、释放了设备类、摧毁了生成的设备节点

实验效果

首先我们查看一下设备类和设备节点
在这里插入图片描述
在这里插入图片描述
加载模块
在这里插入图片描述
查看设备类
在这里插入图片描述
在这里插入图片描述
生成了chardevnode设备类
查看设备节点
在这里插入图片描述
在这里插入图片描述
生成了我们想要的两个设备节点
卸载模块
在这里插入图片描述

编写字符驱动

说明

编写字符驱动与我们前面所做的编写杂项设备驱动的工作相同,就是给文件操作系统调用open、close、ioctl实现底层的操作,也就是实现file_operations中的部分函数

  • 打开函数
    int (*open) (struct inode *, struct file *)
  • 释放close函数
    int (*release) (struct inode *, struct file *)
  • io控制函数
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)
  • 读函数
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
  • 写函数
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
  • 定位函数
    loff_t (*llseek) (struct file *, loff_t, int)

如果需要不同的设备节点有不同的功能,只需要在注册设备的时候添
加不同的file_operations结构体即可

实验

代码

驱动代码

#include <linux/init.h>
#include <linux/module.h>
/*定义module_param module_param_array的头文件*/
#include <linux/moduleparam.h>
/*定义module_param module_param_array中的参数perm的头文件*/
#include <linux/stat.h>
/*三个字符设备注册函数*/
#include <linux/fs.h>
/*宏定义MKDEV的头文件,MKDEV转换设备号数据类型*/
#include <linux/kdev_t.h>
/*定义字符设备的结构体*/
#include <linux/cdev.h>
/*分配内存空间函数头文件*/
#include <linux/slab.h>
/*包含结构体class以及相关函数的头文件*/
#include <linux/device.h>

#define DEVICE_NEME "chardevnode"
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0
#define DEV_MINOR 0
#define REGDEV_SIZE 3000
MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("GYY");

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/*输入主设备号*/
module_param(numdev_major,int,S_IRUSR);
/*输入次设备号*/
module_param(numdev_minor,int,S_IRUSR);

static struct class *myclass ;
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

struct reg_dev *my_devices; 
/*file_operations中的函数*/
/*打开操作*/
static int chardevnode_open (struct inode * pinode, struct file *pfile)
{
	printk(KERN_EMERG "chardevnode_open is success! \n");
	return 0;
}

/*关闭操作*/
static int chardevnode_release (struct inode *pinode, struct file *pfile)
{
	printk(KERN_EMERG "chardevnode_release is success! \n");
	return 0;

}

/*IO操作*/
static long chardevnode_ioctl (struct file *pfile, unsigned int cmd, unsigned long arg)
{
	printk(KERN_EMERG "chardevnode_ioctl is success! cmd is %d ,arg is %d \n",cmd,arg);
	return 0;
}

/*读操作*/
static ssize_t chardevnode_read (struct file *pfile, char __user *buf, size_t count, loff_t *ops)
{
	return 0;
}

/*写操作*/
static ssize_t chardevnode_write (struct file *pfile, const char __user *buf, size_t count, loff_t *ops)
{
	return 0;
}

/*定位操作*/
static loff_t chardevnode_llseek (struct file *pfile, loff_t offset, int n)
{
	return 0;
}

struct file_operations my_fops = {
	.owner = THIS_MODULE,
	.open = chardevnode_open,
	.release = chardevnode_release,
	.unlocked_ioctl = chardevnode_ioctl,
	.read = chardevnode_read,
	.write = chardevnode_write,
	.llseek = chardevnode_llseek
};
/*设备注册到系统*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devnum = MKDEV(numdev_major,numdev_minor+index);
	/*数据初始化*/
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/*注册到系统*/
	err = cdev_add(&dev->cdev,devnum,1);
	/*打印信息*/
	if(err)
	{
		printk(KERN_EMERG "cdev_add is failed ,err : %d ,index : %d \n",err,index);
	}
	else
	{
		printk(KERN_EMERG "cdev_add is success ,minor : %d \n",numdev_minor+index);
	}
}

static int chardev_init(void)
{
	int ret,i;
	dev_t num_dev;
	
	printk(KERN_EMERG "chardev_init enter!\n");
	
	printk(KERN_EMERG "numdev_major is %d \n",numdev_major);
	printk(KERN_EMERG "numdev_minor is %d \n",numdev_minor);
	/*如果传入参数的话静态分配设备号*/
	if(numdev_major)
	{
		num_dev = MKDEV(numdev_major,numdev_minor);//转换为内核的设备号格式(高12位为主设备号,低20位为次设备号)
		/*静态分配设备号*/
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NEME);
	}
	/*不传入参数的话动态分配设备号*/
	else
	{
		/*动态分配设备号*/
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NEME);
		numdev_major = MAJOR(num_dev);//使用MAJOR宏定义获取主设备号
		printk(KERN_EMERG "alloc_chrdev_region req %d\n",numdev_major);//打印主设备号
	}
	/*如果分配设备号失败*/
	if(ret<0)
	{
		printk(KERN_EMERG "alloc_chrdev_region req %d is failed",numdev_major);
		return 0;
	}
	/**/
	myclass = class_create(THIS_MODULE,DEVICE_NEME);
	
	/*给my_devices结构体分配内存*/
	my_devices = kmalloc(DEVICE_MINOR_NUM*sizeof(struct reg_dev),GFP_KERNEL);
	/*如果内存分配失败,返回错误*/
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/*清空分配到的内存空间的数据*/
	memset(my_devices,0,DEVICE_MINOR_NUM*sizeof(struct reg_dev));
	
	/*设备初始化,一个一个初始化*/
	for(i=0;i<DEVICE_MINOR_NUM;i++)
	{
		/*分配内存空间*/
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		/*清空分配到的内存空间的数据*/
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/*设备注册到系统*/
		reg_init_cdev(&my_devices[i],i);
		
		/*生成设备节点*/
		device_create(myclass,NULL, MKDEV(numdev_major,numdev_minor+i),NULL,DEVICE_NEME"%d",i);
	}
	/*打印初始化函数信息*/
	printk(KERN_EMERG "ascdev_init\n");
	return 0;
	
	fail:
		/*注销设备号*/
		unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
		printk(KERN_EMERG "kmalloc is failed!\n");
		return ret;
}

static void chardev_exit(void)
{
	int i;
	printk(KERN_EMERG "chardev World exit!\n");
	/*卸载字符设备(一个一个卸载)*/
	for(i=0;i<DEVICE_MINOR_NUM;i++)
	{
		/*卸载字符设备*/
		cdev_del(&my_devices[i].cdev);
		/*摧毁设备节点*/
		device_destroy(myclass,MKDEV(numdev_major,numdev_minor+i));
		//device_destroy(myclass,MKDEV(numdev_major,numdev_minor+i));
	}
	
	/*释放内存*/
	kfree(my_devices);
	/*释放设备class*/
	class_destroy(myclass);
	/*注销设备号*/
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

module_init(chardev_init);
module_exit(chardev_exit);

应用程序代码

#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>


int main()
{
	int fd;
	char *hello_node0 = "/dev/chardevnode0";
	char *hello_node1 = "/dev/chardevnode1";
	/*O_RDWR只读打开,O_NDELAY非阻塞方式打开*/
	if((fd = open(hello_node0,O_RDWR|O_NDELAY)) < 0)
	{
		printf("APP open %s failed\n",hello_node0);
	}
	else
	{
		printf("APP open %s success\n",hello_node0);
		ioctl(fd,1,6);
	}
	close(fd);
	
	/*O_RDWR只读打开,O_NDELAY非阻塞方式打开*/
	if((fd = open(hello_node1,O_RDWR|O_NDELAY)) < 0)
	{
		printf("APP open %s failed\n",hello_node1);
	}
	else
	{
		printf("APP open %s success\n",hello_node1);
		ioctl(fd,1,6);
	}
	close(fd);
}

代码分析

  • 驱动代码
    在驱动代码中我们实现了上面提到的六个函数,当然我们只是写了很简单的打印,表示驱动正确的进入了这些函数
    在chardevnode_open、chardevnode_release函数中我们写了打印信息,所以当应用程序调用open和close函数的时候将会有打印信息
    我们在chardevnode_ioctl函数中打印了指令和参数,当我们调用ioctl函数时将把它的参数打印出来
    我们定义了file_operations结构体类型的my_fops来注册驱动的各种功能
  • 应用代码
    在应用代码中我们做了很简单的事情,我们分别使用open来打开设备节点,然后使用ioctl进行操作,最后使用close关闭设备节点

实验效果

生成设备节点
在这里插入图片描述
执行应用函数
在这里插入图片描述
说明应用程序执行成功
卸载模块
在这里插入图片描述

发布了123 篇原创文章 · 获赞 598 · 访问量 34万+

猜你喜欢

转载自blog.csdn.net/a568713197/article/details/89820050