关于字符设备驱动
一. 使用一个结构 struct cdev 描述字符设备
struct cdev { struct kobject kobj; //文件系统相关的sysfs---4 一般由系统来管理,不需要我们自己添加 struct module *owner; //THIS_MODULE ---用于模块计数 const struct file_operations *ops; //操作方法集 ====>操作的是硬件, 向下操作 struct list_head list; //用于管理字符设备的链表 dev_t dev; //设备号 ==== 把软件和硬件结合起来 ===>主设备号<<20+次设备号 unsigned int count; //隶属于同一主设备号的次设备号的个数 };
这个结构中的 struct file_operations 成员是操作字符设备的方法集,这个结构中包含的都是函数指针,是在驱动程序中自己实现操作底层硬件的接口。
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 *); 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 (*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); }; // 这里只是贴出部分
二. 关于设备号 是通过一个宏制作的 如下图所示:
设备号包含主设备号+此设备号;主设备号用于描述是哪一类设备,次设备号描述具体哪个设备;设备号 devnum = MKDEV(主设备号, 次设备号)
#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)) // 用来制作设备号的
可以看出使用 4 字节 32 bit 描述一个设备号 高12 bit 描述主设备号,低20 bit 描述次设备号
三。 字符设备的操作流程:
1. 自己指定静态的主设备号,但是不一定申请成功
(1)定义一个字符设备:struct cdev *cdev;
(2)为字符设备分配空间:cdev_alloc(void);
(3)初始化字符设备对象(对硬件操作的方法集): cdev_init(struct cdev *, const struct file_operations *);
(4)向内核申请一个设备号:register_chrdev_region(dev_t from, unsigned count, const char * name);
(5)添加当前的字符设备到内核中:cdev_add(struct cdev *p, dev_t dev , unsigned count);
(6)卸载字符设备对象:
a) cdev_del(struct cdev *); 删除设备
b) unregister_chrdev_region(dev_t from, unsigned count); // 释放申请的设备号
设备号是固定的:查看设备号:【 cat /proc/devices 】
经过上面的 步骤 操作就可以在内核中注册一个设备并申请设备号,供这个模块使用;要想在应用层使用这个设备还需要,创建这个设备的节点(应用层操作的文件)。
需要手动创建:
在应用层只需要打开 【 /dev/mychardev0 】这个设备就可以实现操作底层了,操作底层的具体实现就这与这个设备关联的驱动程序中实现的
2. 动态获取主设备号,不用手动创建设备节点(mknod )
(1) // 动态获取主设备号,并注册到内核中
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
参数: major :主设备号,填 0 时表示自动获取,不是 0 表示静态申请自己指定设备号,静态申请时成功返回 0
name:告诉内核这个设备的名称,在 /proc/devices 下查看设备的名字
fops :就是上面说的文件操作方法集,在驱动程序中重新实现,操作硬件,然后在应用层直接调用相应的IO 操作函数即可。
返回值: 动态:返回值是动态申请到的设备号
静态;成功返回0;负值和错误码表示失败
(2)创建一个目录
这是一个宏函数参数:owner :这是一个宏,推向编译模块时自动创建的__thismodule
clsname:创建在 /sys/class/clsname 目录下
返回值: struct class * 的结构指针
(2)创建设备节点:
struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
参数: class :指向应该注册此设备的 struct class 的指针,也就是上面的 class_create 的返回值
parent:指向此新设备的父结构设备的指针(如果有的话)
devt :添加的字符设备的设备号
drvdata :要添加到回调设备中的数据
fmt :用于设备名称的字符串,也就是设备节点的名称,在应用层操作的设备节点
如:
过程:在加载模块时向内核申请主设备号,并注册到内核中,这个步骤完成了(a)定义并分配字符设备的空间;(b)向内核申请设备号,并添加到内核中;
接下来就是为创建的设备节点创建一个目录,最后就是创建设备节点(如: /dev/Demochar0);在这 3 个步骤中,只要后面其中一个步骤出错就要释放上面申请的资源。
3. 对于注册设备号,创建目录,创建设备节点,在卸载设备时就要相应的释放设备号,删除目录和删除设备节点
(1)删除设备节点 :void device_destroy(struct class *class, dev_t devt)
参数: class:模块入口时创建的目录
devt: 指定设备的设备号(这个设备号包含主设备号和次设备号)
(2)删除目录:void class_destroy(struct class *cls)
参数: cls : 模块入口时创建的目录,
(3)释放设备号等:static inline void unregister_chrdev(unsigned int major, const char *name)
参数:major :删除设备的主设备号(通过 MKDEV() 制作)
name : 注册设备号时,传递给内核的名字,要释放这个资源
对于模块的入口主要是初始化一些操作,模块的处出口主要是把入口申请的资源释放