字符设备驱动

参考:正点原子,驱动开发指南

字符设备驱动

1、字符设备驱动简介

2、字符设备驱动开发步骤

  • 驱动模块的加载和卸载
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的
具体函数,当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用。module_exit()
函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使
用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。字符设备驱动模块加载和卸载模板如下所示:

1/* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4   /* 入口函数具体内容 */
5   return 0;
6 }
7
8 /* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11   /* 出口函数具体内容 */
12 }
13
  • 字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

1 static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
2 static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

  • major :主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
    部分,关于设备号后面会详细讲解。
  • name:设备名字,指向一串字符串。
  • fops :结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:

  • major :要注销的设备对应的主设备号。
  • name :要注销的设备对应的设备名。

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块
的出口函数 xxx_exit 中进行

示例代码 40.2.2.1 加入字符设备注册和注销
1 static struct file_operations test_fops;
2
3 /* 驱动入口函数 */
4 static int __init xxx_init(void)
5 {
6    /* 入口函数具体内容 */
7   int retvalue = 0;
8
9   /* 注册字符设备驱动 */
10  retvalue = register_chrdev(200, "chrtest", &test_fops);
11  if(retvalue < 0){
12      /*  字符设备注册失败, 自行处理 */
13   }
14  return 0;
15 }
16
17 /* 驱动出口函数 */
18 static void __exit xxx_exit(void)
19 {
20  /* 注销字符设备驱动 */
21  unregister_chrdev(200, "chrtest");
22 }
23
24 /* 将上面两个函数指定为驱动的入口和出口函数 */
25 module_init(xxx_init);
26 module_exit(xxx_exit);
  • 实现字符设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,在示例代码 40.2.2.1 中我们定义了
file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、
release、read 和 write 等具体的设备操作函数。本节小节我们就完成变量 test_fops 的初始化,设
置好针对 chrtest 设备的操作函数。在初始化 test_fops 之前我们要分析一下需求,也就是要对
chrtest 这个设备进行哪些操作,只有确定了需求以后才知道我们应该实现哪些操作函数

struct file_operations {
	struct module *owner;
	//owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
	loff_t (*llseek) (struct file *, loff_t, int);  
    //llseek 函数用于修改文件当前的读写位置。
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	//read 函数用于读取设备文件。
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	//write 函数用于向设备文件写入(发送)数据。
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	//poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	//unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	//compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,
    //32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是
    //unlocked_ioctl。
	int (*mmap) (struct file *, struct vm_area_struct *);
	//mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧
    //缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应
    //用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
	int (*open) (struct inode *, struct file *);
	//open 函数用于打开设备文件。
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	//release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	//fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
	int (*aio_fsync) (struct kiocb *, int datasync);
	//aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的
    //数据。
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	int (*show_fdinfo)(struct seq_file *m, struct file *f);
	/* get_lower_file is for stackable file system */
	struct file* (*get_lower_file)(struct file *f);
};
  • 添加LICENSE和作者信息
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息

3、linux设备号

为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了
一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下

  • 设备号的组成
  • 设备号的分派
  • 静态分配设备号

本小节讲的设备号分配主要是主设备号的分配。前面讲解字符设备驱动的时候说过了,注
册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态的指定一个
设备号,比如选择 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉
了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉
的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个
主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。

  • 动态分配设备号

静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用
的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字
符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。
卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

函数 alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:

  • dev:保存申请到的设备号。
  • baseminor :次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这
    些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递
    增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
  • count :要申请的设备号数量。
  • name:设备名字。
    注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
  • from:要释放的设备号。
  • count :表示从 from 开始,要释放的设备号数量。

4、实例代码

#include 
#include 
#include 
#include 
#include 
#include 
/***************************************************************
Copyright © XXXXXXX, Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbase.c
作者	  	: 
版本	   	: V1.0
描述	   	: chrdevbase驱动文件。
其他	   	: 无
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2020/5/26 仇嘉斌创建
***************************************************************/

#define CHRDEVBASE_MAJOR	200				/* 主设备号 */
#define CHRDEVBASE_NAME		"chrdevbase" 	/* 设备名     */

static char readbuf[100];		/* 读缓冲区 */
static char writebuf[100];		/* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));
	retvalue = copy_to_user(buf, readbuf, cnt);
	if(retvalue == 0){
		printk("kernel senddata ok!\r\n");
	}else{
		printk("kernel senddata failed!\r\n");
	}
	
	//printk("chrdevbase read!\r\n");
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	/* 接收用户空间传递给内核的数据并且打印出来 */
	retvalue = copy_from_user(writebuf, buf, cnt);
	if(retvalue == 0){
		printk("kernel recevdata:%s\r\n", writebuf);
	}else{
		printk("kernel recevdata failed!\r\n");
	}
	
	//printk("chrdevbase write!\r\n");
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase release!\r\n");
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};


/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
	if(retvalue < 0){
		printk("chrdevbase driver register failed\r\n");
	}
	printk("chrdevbase init!\r\n");
	return 0;
}


/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
	printk("chrdevbase exit!\r\n");
}


module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiabinly");

字符设备驱动开发实验

你可能感兴趣的:(字符设备驱动)