Linux字符设备驱动基础知识

Linux字符设备驱动基础知识


  在Linux设备驱动中,字符设备驱动较为基础,字符设备即一个一个的字节,按照字节流进行读写操作的设备,读写数据具有一定的先后顺序,例如I2C、SPI、LCD等都属于字符设备。

  在Linux中一切皆文件,设备驱动加载成功后会在/dev目录下生成相应的文件,应用程序通过对这个名为、dev/xxx(xxx为具体设备驱动的名称)的文件进行相应操作即可实现对硬件的操作。

cdev 结构体

  在字符设备驱动程序的管理核心是字符设备,内核为字符设备驱动抽象出了一个具体的数据结构 struct cdev 定义如下:

\include\linux\cdev.h
/*
 *  字符设备的内核抽象
 */
struct cdev {
    struct kobject kobj;                /* 内嵌的内核(kobject)对象 */
    struct module *owner;               /* 所属模块在内核的对象指针 */
    const struct file_operations *ops;  /* 文件操作结构体,用于实现与硬件的一系列操作 */
    struct list_head list;              /* 用来将已经向内核注册的所有字符设备形成链接 */
    dev_t dev;                          /* 设备号 */
    unsigned int count;                 /* 隶属于同一设备号的次设备号的个数 */
};

  cdev结构体的dev_t成员定义设备号,为了方便管理,Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,共为32位,其中高12位为主设备号,低20位为次设备号,因此Linux系统中主设备号范围为0-4095。

  主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

  可使用以下宏对设备号进行操作:

\include\linux\kdev_t.h
#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))       /* 获取主设备号 */
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))        /* 获取次设备号 */
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))            /* 使用主设备号和次设备号生成dev_t */

操作cdev的相关函数

\include\linux\cdev.h
/*
 * 用于初始化cdev的成员,并建立cdev与file_operations之间的连接
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;       /* 将传入的文件操作结构体指针赋值给cdev的ops成员 */
}
/*
 * 动态申请一个cdev结构体内存
 */
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

/*
 * 释放一个cdev
 */
void cdev_put(struct cdev *p)
{
	if (p) {
		struct module *owner = p->owner;
		kobject_put(&p->kobj);
		module_put(owner);
	}
}

/*
 * 向系统注册一个字符设备
 */
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
	int error;

	p->dev = dev;
	p->count = count;

	error = kobj_map(cdev_map, dev, count, NULL,
			 exact_match, exact_lock, p);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

/*
 * 删除一个字符设备
 */
void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

分配和释放设备号

  在调用cdev_add()函数向内核注册字符设备之前,需要先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号。

\fs\char_dev.c
/*
 * 对已知起始设备的设备号申请设备号
 */
int register_chrdev_region(dev_t from, unsigned count, const char *name);

/*
 * 起始设备的设备号未知时,向内核动态申请未被占用的设备号
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name);
/*
 * 释放之前申请的设备号
 */
void unregister_chrdev_region(dev_t from, unsigned count);

file_operations 结构体

  file_operation是将Linux系统调用和驱动程序关联起来的关键数据结构。这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

\include\linux\fs.h
/*
 * 文件操作结构体,用于实现与硬件的一系列操作
 */
struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*mremap)(struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	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 **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
};

结构体常用成员的主要含义

  • struct module *owner

功能:owner并不是一个操作,它是指向拥有这个结构的模块的指针,主要作用是在设备还在被使用时阻止其被卸载,一般为 THIS_MODULE。定义在<linux/module.h>中的宏。

  • loff_t (*llseek) (struct file *, loff_t, int)

功能: 用于改变文件的当前读/写位置,并将新位置返回,出错时,返回一个负值。


参数1: 指针指向进行读取目标文件的结构体;


参数2: 文件定位的目标偏移量。


参数3: 对文件定位的起始地址,这个值可以为文件开头(SEEK_SET)、当前位置(SEEK_CUR)、文件末尾(SEEK_END)。

  • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)

功能: 用于从设备文件中获取数据,成功时返回读取的字节数,出错时返回一个负值。(此操作为阻塞操作)


参数1: 为进行读取信息的目标文件;


参数2: 为对应放置信息的缓冲区(用户空间内存地址);


参数3: 要读取的信息长度;


参数4: 读取位置相对于文件开头的偏移,读取信息后,指针会移动读取信息的长度值的距离;

  • ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)

功能: 用于向设备文件写入数据,成功时返回写入的字节数,若用户未实现此函数的回调函数,用户运行write()时将得到-EINVAL返回值。(此操作为阻塞操作)


参数1: 为目标文件结构体指针;


参数2: 为要写入文件的信息缓冲区;


参数3: 为要写如信息的长度;


参数4: 为当前的偏移位置,这个值通常是用来判断写文件是否越界;

  • unsigned int (*poll) (struct file *, struct poll_table_struct *)

功能: 轮询函数,用于查询设备是否能够进行非阻塞读写,函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。


参数1: 为文件对象结构指针;


参数2: 轮询表指针。

  • long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long)

功能: 用于提供对设备的控制功能,与应用程序的ioctl函数对应。

  • long (*compat_ioctl) (struct file *, unsigned int, unsigned long)

功能: 与unlocked_ioctl功能类似,区别在与64位系统会调用此函数,32位系统调用unlocked_ioctl。

  • int (*mmap) (struct file *, struct vm_area_struct *)

功能: 用于将设备内存映射到进程空间(用户空间)。


参数1: 为文件对象结构指针;


参数2: 为进程地址空间。

  • int (*open) (struct inode *, struct file *)

功能: 打开设备文件,与release文件对应。


参数1: 为文件索引节点,文件索引节点只有一个,无论用户打开多少个文件,都只对应一个inode结构;


参数2: 为文件对象结构体,只要打开一个文件,就对应一个file结构体,file结构体通常用于追踪文件在运行时的状态信息。

  • int (*release) (struct inode *, struct file *)

功能: 用于释放(关闭)设备文件,常与应用程序的close函数对应。


参数1: 同open;


参数2: 同open。

  • int (*fasync) (int, struct file *, int)

功能: 用于刷新待处理的数据,类似于将内存缓冲区的数据刷新至磁盘内,允许进程把所有的脏缓冲区刷新到磁盘。

  • int (*aio_fsync) (struct kiocb *, int datasync)

功能: 与fasync类似,aio_fsync用于异步刷新。

字符设备驱动开发

设备驱动的加载与卸载

  linux设备驱动一般有两种方式运行,一种是将驱动跟内核一起编译,此时linux将会自动加载运行;另外一种是将驱动程序编译为模块(.ko文件)后,使用驱动加载命令 insmod 将相应的模块加载即可。

  在这里一般会使用两个宏

\include\linux\init.h
module_init(x)
module_exit(x)

   module_init 宏用于向linux内核注册一个模块的加载函数,参数x为需要注册的具体函数。在命令行调用 insmod 命令时,x将会被调用。

   module_exit 宏用于向linux内核卸载一个模块,参数为具体需要卸载的函数。在命令行调用 rmmod 命令时,x将会被调用。

相关加载卸载命令

  • 加载驱动模块命令:
insmod xxx.ko

  insmod命令不能解决模块的依赖关系,即:a.ko依赖于b.ko模块,必须使用 insmod 命令加载b.ko模块,然后再加载a.ko模块。

  • 驱动卸载命令:
rmmod xxx.ko
modprobe -r xxx.ko

字符设备注册注销

  对字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。同样,卸载驱动时也需要注销字符设备。相应函数如下:

\include\linux\fs.h
/*
 * 字符设备驱动注册
 */
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

/*
 * 字符设备驱动卸载
 */
static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}

当前设备被用掉的设备号查看命令 cat /proc/devices

创建设备节点文件命令

  驱动加载成功之后需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序通过操作这个节点文件来完成对具体设备的操作。

/* 创建chrdev设备节点文件,c表示字符设备,a为主设备号,与/proc/devices下的加载的设备号一致,b为次设备号 */
mknod /dev/chrdev c a b

分配和释放设备号

动态分配设备号

\fs\char_dev.c
/*
 * 分配设备号
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

创建设备

\driver\base\core.c
/* 
 * 创建设备
 */
struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
{
	va_list vargs;
	struct device *dev;

	va_start(vargs, fmt);
	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
	va_end(vargs);
	return dev;
}

创建类

\linux\devices.h
/*
 * 创建类
 */
#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

参考

《Linux设备驱动开发详解 基于最新的Linux4.0内核.pdf》

《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.pdf》

🎃

🎐

🎨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值