i.MX6ULL驱动开发 | 02-字符设备驱动框架

Posted Mculover666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 02-字符设备驱动框架相关的知识,希望对你有一定的参考价值。

一、字符设备驱动框架

1. file_operations结构体

在Linux中应用程序运行在用户空间,而驱动程序属于内核的一部分,在内核空间运行。用户需要通过系统调用陷入到内核空间,才能实现对底层驱动的操作。


以open函数为例,当用户在C语言程序中调用open函数时,调用关系链如下图所示:

这就意味着,驱动程序必须提供一些必要的函数,来与open、read、write、close这些函数相对应,确实,这套函数定义在 file_operations结构体中。

file_operations结构体中定义了Linux内核驱动操作函数的集合,在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
;

这些函数的作用如下:

2. cdev结构体

Linux操作系统将字符设备统一用一个cdev结构体进行管理,该结构体对字符设备进行了详细的描述

cdev结构体在<include/linux/cdev.h>文件中,定义如下:

struct cdev 
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
;
成员作用
kobj字符设备驱动中的一个内核对象
owner设备的拥有者,一般为THIS_MODULE
opsfile_operations结构体指针
list双向链表节点,用于挂载到设备链表
dev设备信息
count记录了相同主设备号中次设备号的总数

Linux内核提供了一组函数用来操作cdev结构体, 声明在文件<include/linux/cdev.h>中,实现在文件fs/char_dev.c中:

void cdev_init(struct cdev *, const struct file_operations *);

struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);

int cdev_add(struct cdev *, dev_t, unsigned);

void cdev_del(struct cdev *);

void cd_forget(struct inode *);

(1)cdev_init函数用来初始化cdev结构体的成员,并建立cdev和file_operations之间的连接:

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */
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;

(2)cdev_alloc函数用来动态申请一个cdev内存

/**
 * cdev_alloc() - allocate a cdev structure
 *
 * Allocates and returns a cdev structure, or NULL on failure.
 */
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;

(3)cdev_add函数用来向系统中添加一个cdev,完成字符设备的注册:

/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device
 * @dev: the first device number for which this device is responsible
 * @count: the number of consecutive minor numbers corresponding to this
 *         device
 *
 * cdev_add() adds the device represented by @p to the system, making it
 * live immediately.  A negative error code is returned on failure.
 */
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;

(4)cdev_del函数用于从系统中删除一个cdev,完成字符设备的注销:

/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 */
void cdev_del(struct cdev *p)

	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);

3. 设备号的描述

每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。

这么重要的信息,当然在cdev结构体中的dev成员中,dev_t其实就是一个u32类型,在include/linux/types.h文件中,定义如下:

typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t		dev_t;

在文件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))

可以看到,主设备号占据高12位、范围是0-4095,次设备号占据低20位。

4. 主设备号的分配

主设备号分配有两种方式:静态分配和动态分配。

4.1. 静态分配主设备号

有一些设备号已经被linux内核开发者给分配了,分配的内容在文件Documentation/devices.txt中,但是这些设备号我们依然可以强行使用,只是不规范而已。

如果系统中已经使用了的设备号,可以使用下面的命令查看,那么我们就不能强行使用了,否则会造成冲突。

cat /proc/devices


避开系统已经使用的,只要在范围之内,喜欢哪个用哪个都行

4.2. 动态分配设备号

静态分配设备号简单粗暴,但是很容易造成冲突。

Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动分配一个没有使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。

(1)申请设备号API

设备号申请的函数声明在头文件include/linux/fs.h中,声明如下:

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

实现在fs/char_dev.c文件中:

/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
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;

该函数有四个参数:

  • dev:用于保存申请到的设备号
  • baseminor:次设备号起始地址,一般为0
  • count:要申请的设备数量
  • name:设备名字

(2)释放设备号API

该函数同样声明在include/linux/fs.h文件中,声明如下:

extern void unregister_chrdev_region(dev_t, unsigned);

实现在文件``中:

/**
 * unregister_chrdev_region() - return a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 *
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
 */
void unregister_chrdev_region(dev_t from, unsigned count)

	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) 
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	

该函数有两个参数:

  • from:要释放的设备号
  • count:要释放的设备号数量

5. 设备节点

用户程序要实现对设备的操作,还需要一个在 /dev 目录下的设备节点,那么如何来创建设备节点呢?

5.1. 手动创建设备节点

加载驱动到内核后,可以通过mknod命令,根据设备号手动创建一个设备节点:

mknod <设备目录> <设备类型> <主设备号> <次设备号>

比如:

mknod /dev/hello_drv c 200 0

这样就会在/dev目录下创建一个名为hello_drv的设备节点,显然这样不太方便,如果在驱动加载的时候,自动的创建一个设备节点该有多好。

5.2. 自动创建设备节点

Linux内核提供了自动创建设备节点的机制,具体使用如下,这些API在include/linux/device.h文件中声明。

(1)创建一个设备类

/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */
#define class_create(owner, name)		\\
(						\\
	static struct lock_class_key __key;	\\
	__class_create(owner, name, &__key);	\\
)

该函数会在/sys/class目录下创建一个该类

(2)创建一个设备

struct device *device_create(struct class *cls, struct device *parent,
			     dev_t devt, void *drvdata,
			     const char *fmt, ...);

该函数会在/dev目录下创建该设备节点。

(3)删除设备

extern void device_destroy(struct class *cls, dev_t devt);

(4)删除类

extern void class_destroy(struct class *cls);

注意,在设备驱动注销的时候,需要自动删除设备,也要一起删除设备节点和相关的设备类。

6. 总结

6.1. 字符设备驱动框架架构

6.2. 字符设备驱动调用关系

6.3. 设备节点和设备号之间的关系

二、字符设备驱动实例

1. 模块源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define HELLO_DRV_NAME  "hello drv"
#define HELLO_DRV_CLASS "hello_drv_class"
#define HELLO_DRV_DEVICE "hello_drv0"

#define MEM_SIZE 0x1000

static dev_t hello_drv_dev;
static struct cdev  *hello_drv_cdev;
static struct class *hello_drv_class;
static struct device *hello_drv0;

static int hello_drv_open(struct inode *inode, struct file *filp)

    printk("hello drv open!\\n");
    return 0;


ssize_t hello_drv_read(struct file *filp, char __user *buf, size_t len, loff_t *off)

    printk("hello drv read!\\n");
    return 0;


ssize_t hello_drv_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)

    printk("hello drv write!\\n");
    return 0;


static int hello_drv_release(struct inode *inode, struct file *filp)

    printk("hello drv release!\\n");
    return 0;   


static struct file_operations hello_drv_fops = 
    .owner = THIS_MODULE,
    .open = hello_drv_open,
    .read = hello_drv_read,
    .write = hello_drv_write,
    .release = hello_drv_release,
;

static int __init hello_drv_init(void)

    int ret;

	// 动态申请设备号
    ret = alloc_chrdev_region(&hello_drv_dev, 0, 1, HELLO_DRV_NAME);
    if (ret != 0) 
        printk(KERN_WARNING"alloc_chrdev_region failed!\\n");
        return -1;
    

    // 动态申请cdev
    hello_drv_cdev = cdev_alloc();
    if (!hello_drv_cdev) 
        printk(KERN_WARNING"cdev_alloc failed!\\n");
        return -1;
    

    // 初始化cdev结构体
    hello_drv_cdev->owner = THIS_MODULE;
    hello_drv_cdev->ops = &hello_drv_fops;

    // 将设备添加到内核中
    cdev_add(hello_drv_cdev, hello_drv_dev, 1);

    // 创建设备类
    hello_drv_class = class_create(THIS_MODULE, HELLO_DRV_CLASS);
    if (!hello_drv_class) 
        printk(KERN_WARNING"class_create failed!\\n");
        return -1;
    

    // 创建设备节点
    hello_drv0 = device_create(hello_drv_class, NULL, hello_drv_dev, NULL, HELLO_DRV_DEVICE);
    if (IS_ERR(hello_drv0)) 
        printk(KERN_WARNING"device_create failed!\\n");
        return -1;
    

    printk("hello drv init success!\\n");
    return 0;


static void __exit hello_drv_exit(void)

    // 将设备从内核删除
    cdev_del(hello_drv_cdev);

    // 释放设备号
    unregister_chrdev_region(hello_drv_dev, 1);

    // 删除设备节点
    device_destroy(hello_drv_class, hello_drv_dev);

    // 删除设备类
    class_destroy(hello_drv_class);

    printk("hello drv exit!\\n");


module_init(hello_drv_init);
module_exit(hello_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mculover666");

2. 编译

KERNEL_DIR := /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m := hello_drv.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

3. 加载卸载测试

(1)驱动加载测试

加载之后查看系统当前使用的设备号:

查看驱动自动创建的设备类:

查看驱动自动创建的设备节点:

(2)驱动卸载测试

查看设备节点,已经被删除:

查看设备类,已经被删除:

查看设备号:

这里虽然没有被删除,但是注意到,之前加载后查看的时候是有两个(之前已经卸载了一次),此时只有一个了,所以设备号不会被立即释放,应该是过一会被系统释放,有待进一步验证学习。

4. 应用程序测试

编写一个普通

以上是关于i.MX6ULL驱动开发 | 02-字符设备驱动框架的主要内容,如果未能解决你的问题,请参考以下文章

i.MX6ULL驱动开发 | 03-基于字符设备驱动框架点亮LED

i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发 | 08 -基于pinctrl子系统和gpio子系统点亮LED

i.MX6ULL驱动开发 | 15 - Linux UART 驱动框架

i.MX6ULL驱动开发 | 36 - 注册spilcd为framebuffer设备并使用lvgl测试

i.MX6ULL驱动开发 | 36 - 注册spilcd为framebuffer设备并使用lvgl测试