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 |
ops | file_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 驱动框架