天天看点

linux驱动: 字符设备驱动模板背景驱动模板测试代码测试结果

背景

最近学习字符设备驱动,其大致的框架与流程都基本搞懂了,为了方便以后代码重用,写了一个较为完善的模板,

功能包括自动创建字符设备,/dev下自动创建设备文件,实现了open、read、write、release、ioctl等函数功能,

以后如果需要写如:led、key、lcd等的字符设备驱动,就不需要从O开始,可以直接用来修改调试,

代码中有比较清晰的注释,以及错误处理与回收机制,并且经过了初步测试,是通过的

该模板的作用:编译生成.ko文件后,通过insmod *.ko加载模块,会自动在/dev目录下生成相应的设备文件,模板中设备文件名称为firdev,

同样,卸载模块时会自动将其删除,这主要是device与class这两个结构体的功劳,代码中都有注释…

驱动模板

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/ioctl.h>

/* 字符设备数量、名称、以及缓存大小 */
#define CHRDEV_CNT		1
#define CHRDEV_NAME		"chrdev"
#define BUF_SIZE		256

#define CHRDEV_MAGIC	'k'		//指令类型
#define CHRDEV_MAXNR	 3		//最大指令序号
/* ioctl 自定义指令 */			
#define CMD_OPEN		(_IO(CHRDEV_MAGIC, 1))
#define CMD_CLOSE		(_IO(CHRDEV_MAGIC, 2))
#define CMD_SET			(_IO(CHRDEV_MAGIC, 3))

/* 自定义字符设备结构体 */
struct chr_dev {
	dev_t devnum;			//设备号
	struct cdev *pcdev;		//cdev
	struct class *class;	//类
	struct device *device;	//设备
	char   kbuf[BUF_SIZE];	//数据存储区
};

struct 	chr_dev firdev = {
	.devnum = 0,
};
/*
  * @brief  文件打开函数
  * @param  inode : 传递给驱动的inode
			file  : 要打开的设备文件	
  * @retval 0 成功, 其他 失败
  */ 
static int chrdev_open(struct inode *inode, struct file *file)
{
	file->private_data = &firdev; 		//设置私有数据
	firdev.devnum = inode->i_rdev;		//获取设备号
	printk(KERN_INFO "chrdev_open,devnum:%x\n", firdev.devnum);
	return 0;
}
/*
  * @brief  文件关闭函数
  * @param  inode : 传递给驱动的inode
			file  : 要关闭的设备文件	
  * @retval 0 成功, 其他 失败
  */ 
static int chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "chrdev_release\n");
	return 0;
}
/*
  * @brief  读函数, 将内核中的数据拷贝到应用层
  * @param  file: 要打开的设备文件
			buf : 返回给用户空间的数据缓冲区
			cnt : 要读取的数据长度
			offt: 相对于文件首地址的偏移	
  * @retval 读取的字节数, 负值表示读取失败
  */ 
ssize_t chrdev_read(struct file *filp, char __user *ubuf, size_t size, loff_t *ppos)
{
	int ret = -1;
	unsigned long p =  *ppos;
	unsigned int count = size;
	struct chr_dev *dev = filp->private_data;	//获取私有数据
	//有效长度判断
	 if (p >= BUF_SIZE)
		return 0;
	if (count > BUF_SIZE - p)
		count = BUF_SIZE - p;
	//一定要用如下拷贝函数, 从内核空间拷贝至用户
	if(copy_to_user(ubuf, (void*)(dev->kbuf + p), count)) {
		printk(KERN_ERR "copy_to_user fail\n");
		return -EINVAL;
	}
	else {
		*ppos += count;
		ret = count;
		printk(KERN_INFO "to user success...\n");
	}	
	return ret;
}
/*
  * @brief  写函数, 将应用层传递过来的数据复制到内核中
  * @param  filp: 打开的文件描述符
			buf : 要写给设备写入的数据
			cnt : 要写入的数据长度
			offt: 相对于文件首地址的偏移	
  * @retval 写入的字节数, 负值表示写入失败
  */ 
static ssize_t chrdev_write(struct file *filp, const char __user *ubuf, size_t size, loff_t *ppos)
{
	int ret = -1;
	unsigned long p =  *ppos;
	unsigned int count = size;
	struct chr_dev *dev = filp->private_data;
	//有效长度判断
	if(p >= BUF_SIZE)
		return 0;
	if (count > BUF_SIZE - p)
		count = BUF_SIZE - p;
	//一定要用如下拷贝函数,从用户空间拷贝至内核
	if (copy_from_user(dev->kbuf + p, ubuf, count)) {
		printk(KERN_ERR "copy_from_user fail\n");
		return -EINVAL;
	}
	else {
		*ppos += count;
		ret = count;
		printk(KERN_INFO "from user success...\n");
	}
	return ret;
}
/*
  * @brief  文件定位函数
  * @param  filp: 打开的文件描述符
			whence: 偏移起始位置
			offset: 偏移的步数
  * @retval 偏移的字节数, 负值表示偏移失败
  */ 
static loff_t chrdev_llseek(struct file *filp, loff_t whence, int offset)
{
	unsigned int newpos = 0;

	switch(whence) {
		case SEEK_SET: newpos = offset; 				break;
		case SEEK_CUR: newpos = filp->f_pos + offset; 	break;
		case SEEK_END: newpos = BUF_SIZE -1 + offset;	break;
		default: return -EINVAL;
	}
	if ((newpos < 0) || (newpos > BUF_SIZE))
		return -EINVAL;
	
	filp->f_pos = newpos;
	
	return newpos;
}
/*
  * @brief  IO控制函数
  * @param  filp: 打开的文件描述符
			cmd: 命令
			arg: 参数
  * @retval 0表示执行成功, 负值表示失败
  */ 
long chrdev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	if (_IOC_TYPE(cmd) != CHRDEV_MAGIC) 
		return -EINVAL;
  	if (_IOC_NR(cmd) > CHRDEV_MAXNR) 
		return -EINVAL;
		
	switch(cmd) {
		case CMD_OPEN:
			printk("IO open device!\n");
			return 0;
		case CMD_CLOSE:
			printk("IO close device!\n");
			return 0;
		case CMD_SET:
			printk("IO setup device, arg:%d\n", arg);
			return 0;
		default:
			return -EINVAL;
	}
	return 0;
}
//文件操作结构体, 外部操作此模块的接口, 需要我们填充
//.owner:指向拥有这个结构的模块的指针,用来在它的操作还在被使用时阻止模块被卸载
static struct file_operations chrdev_fops = {
	.owner 		= THIS_MODULE,
	//应用层间接调用的就是如下接口
	.open		= chrdev_open,		//打开设备时调用
	.release	= chrdev_release,	
	.write 		= chrdev_write,
	.read		= chrdev_read,
	.llseek		= chrdev_llseek,
	.unlocked_ioctl = chrdev_ioctl,
};

//模块加载函数
static int __init chrdev_init(void)
{
	int ret;
	// 1.分配设备号
	ret = alloc_chrdev_region(&firdev.devnum, 0, CHRDEV_CNT, CHRDEV_NAME);
	if(ret < 0) {
		printk(KERN_ERR "alloc_chrdev_region fail\n");
		goto chrdev_fail; //如果执行到这里, 可以直接返回
	}
	printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(firdev.devnum), MINOR(firdev.devnum));
	
	// 2.初始化cdev, 并添加到系统
	// cdev绑定file_operations与dev_t, 添加进系统从而产生了联系
	firdev.pcdev = cdev_alloc();	
	//firdev.pcdev->owner = THIS_MODULE;
	//firdev.pcdev->ops = &chrdev_fops;		//将cdev和file_operations进行绑定
	cdev_init(firdev.pcdev, &chrdev_fops);//这条语句可代替上面两条语句
	ret = cdev_add(firdev.pcdev, firdev.devnum, CHRDEV_CNT);//将cdev结构体加入到系统中去
	if (ret) {
		printk(KERN_ERR "Unable to cdev_add\n");
		goto cdev_fail;//如果执行到这里, 说明前面设备号分配成功了, 需要释放掉
	}
	printk(KERN_INFO "cdev_add success\n");
	
	// 3.创建类
	// 注册字符设备驱动完成后, 添加设备类的操作, 让内核帮我们发信息
	// 给udev,让udev自动创建和删除设备文件
	firdev.class = class_create(THIS_MODULE, CHRDEV_NAME);
	if (IS_ERR(firdev.class)) {
		goto class_fail;//如果执行到这里, 说明设备号与cdev都分配成功了, 需要释放掉
	}
	
	// 4.创建设备
	// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
	// 所以我们这里要的文件名是/dev/firdev
	firdev.device = device_create(firdev.class, NULL, firdev.devnum, NULL, CHRDEV_NAME);
	if (IS_ERR(firdev.device)) {
		goto device_fail;
	}
	return 0;

device_fail:
	device_destroy(firdev.class, firdev.devnum);
class_fail:
	cdev_del(firdev.pcdev);
cdev_fail:
	unregister_chrdev_region(firdev.devnum, CHRDEV_CNT);
chrdev_fail:	
	return -EINVAL;
}

//模块卸载函数, 注销要跟创建时倒着来
static void __exit chrdev_exit(void)
{
	// 销毁设备,即把创建的设备文件删掉
	device_destroy(firdev.class, firdev.devnum);
	// 销毁类,释放资源
	class_destroy(firdev.class);
	// 注销字符设备驱动结构
	cdev_del(firdev.pcdev);
	// 然后注销申请到的设备号
	unregister_chrdev_region(firdev.devnum, CHRDEV_CNT);
}

//模块加载与卸载时会调用如下接口
module_init(chrdev_init);
module_exit(chrdev_exit);

//下面这些都是跟模块相关, 需要加上才能编译
MODULE_LICENSE("GPL");				// 模块许可证
MODULE_AUTHOR("author");			// 模块作者
MODULE_DESCRIPTION("description");	// 模块信息
MODULE_ALIAS("alias");				// 模块别名

           

测试代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "streng.h"

int main()
{
    int  fd;
	char Buf[128] = "This is char dev!";/*初始化Buf*/

	printf("BUF: %s\n",Buf);
	fd = open("/dev/chrdev", O_RDWR);/*打开设备文件*/
	if (fd < 0) {
		printf("Open chrdev Error!\n");
		return -1;
	}
	write(fd, Buf, sizeof(Buf));    /*写入设备*/
	lseek(fd, 0, SEEK_SET);         /*需要重新定位文件位置*/
	
	strcpy(Buf,"Buf is NULL!");    /*清除Buf*/
	printf("BUF: %s\n",Buf);
	
	read(fd, Buf, sizeof(Buf));     /*读出设备*/
	printf("BUF: %s\n",Buf);       /*检测结果*/

    while(1) {
        int  cmd, arg;
        printf("Input cmd:");
        scanf("%d", &cmd);
        if(cmd == 1) {
            ioctl(fd, CMD_CLOSE);  
			printf("cmd:%d\n", cmd);
        } 
        else if(cmd == 2) {
            ioctl(fd, CMD_OPEN); 
			printf("cmd:%d\n", cmd);
        } 
        else if(cmd == 3) {
            printf("Input data:");
            scanf("%d", &arg);
            ioctl(fd, CMD_SET, arg); 
        }
        else {
            close(fd);
            break;
        }
    }
	return 0;	
}

           

测试结果

将驱动模板编译生成.ko文件,我的是first_drv.ko,拷贝至开发板

然后执行:insmod first_drv.ko

[email protected]-IMX6U:/mnt/ttt# insmod first_drv.ko 
major = 249, minor = 0.
cdev_add success
           

模块加载成功,cat /proc/devices可看到firdev驱动,并且ls /dev可看到firdev目录,这就是自动生成的设备文件

然后测试代码测试,如下:

linux驱动: 字符设备驱动模板背景驱动模板测试代码测试结果

至此,字符设备驱动学习告一段落!