Linux 字符设备驱动--PWM的蜂鸣器
程序员文章站
2022-03-22 16:56:57
...
Linux 字符设备驱动学习
一、基本概念
-
linux系统,一切皆文件,字符设备就是实现一切皆文件,用户可以通过访问文件的方式,来访问硬件/内核。
-
驱动:硬件 = 1:n 一个驱动程序,可以管理很多硬件
给驱动程序 分配一个主设备号, 给该驱动管理的每个硬件 分配一个次设备号。
同一类硬件,使用相同的驱动,所以 拥有相同的主设备号。 -
设备号devno,请求取 主设备major, 次设备号minor:
major = devno >> 20;
minor = devno<<12>>12; -
已知major,minor,求 devno: devno = major <<20 | minor;
-
实现一个字符设备驱动
a) 申请设备号
b) 创建字符设备驱动对象
c) 在用户层创建一个文件节点
6. 实现字符设备驱动程序
a) 向内核申请一个设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
dev_t *dev: 设备号,内核将 设备号放在 dev里面
存放 第一个设备号
baseminor: 起始的次设备号, 次设备号的范围0-2^20, baseminor可以指定从哪里开始申请
count: 你要申请多少个次设备号
name : 给设备号取个名字
返回值, 0成功 <0 失败
b) 创建一个字符设备对象,然后初始化对象的属性和方法
1) struct cdev chdevobj; //定义
2) void cdev_init(struct cdev * cdev,struct file_operations * fops)
等同于 cdev.ops = & fops;
3) int cdev_add(struct cdev p[],dev_t devno,unsigned count)
count: 这次添加 多少个 字符设备对象 到内核中
返回值: 0-成功 <0 失败
c) 在用户层创建一个设备文件节点
1)创建一个类class --- cls = class_create(THIS_MODULE,"chdev class");
2)在类中,创建一个文件节点--- device_create(cls,NULL,devno,NULL,"chdev%d",0);
7. struct cdev 结构体
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
Notes:从中可以看出设备驱动的核心:申请设备结构体资源后填充设备编号和操作集
- struct file 结构体
struct file { //表示一个打开的文件
......
const struct file_operations *f_op; // 文件操作集
fmode_t f_mode; // 文件模式,是否可读写
loff_t f_pos; // 当前读/写位置
unsigned int f_flags; // 文件标志,是否阻塞式操作
/***********************************************************************
私有数据 --- 内核从来不会访问 这个变量, 是给 驱动开发者使用。
##一般我们在open的时候, 将一个空间存入该地址. 在 close read write ioctl的时候取出来使用,在close释放
***********************************************************************/
void *private_data; // 驱动可以用作任意目的或者忽略该字段,可以指向分配的数据,但release时需要释放,一般用于保存硬件资源
......
}
Notes:是一个内核结构,在open是创建,传递给该文件上进行操作的所有函数,知道最后的close,被关闭后会释放该结构体
- struct inode 结构体
struct inode { //代表一个屋物理设备节点
......
const struct inode_operations *i_op;
dev_t i_rdev; // 对表示设备文件的inode结构,该字段包含了正在的设备编号
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; // 字符设备的内核的内部结构,当inode指向一个字符设备文件时,该字段包含指向struct cdev结构的指针
};
......
}
10. file_operation结构体:把系统调用和驱动程序关联起来的关键数据结构。
struct file_operations ***_ops={
.owner = THIS_MODULE,
.llseek = ***_llseek,
.read = ***_read,
.write = ***_write,
.mmap = ***_mmap;
.ioctl = ***_ioctl,
.open = ***_open,
.release = ***_release,
};
a) struct module *owner :根本不是一个操作;它是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载。
b) loff_t (*llseek) (struct file * filp , loff_t p, int orig):用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值。
c) ssize_t (*read) (struct file * filp, char __user * buffer, size_t size,loff_t * p):用来从设备中获取数据。
d) ssize_t (*write) (struct file * filp, const char __user *buffer, size_t count, loff_t * ppos):用来发送数据给设备。 读写皆为阻塞操作
e) int (*mmap) (struct file *, struct vm_area_struct *):用来请求将设备内存映射到进程的地址空间。
f) int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg):ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写)。另外, 几个 ioctl 命令被内核识别而不必引用 fops 表。
g) int (*open) (struct inode * inode , struct file * filp ):打开一个字符设备,尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法。int (*release) (struct inode *, struct file *):当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:void release(struct inode inode,struct file *file):主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
- 8、9、10三者关系:
a) file_operations绑定于file
b) inode与file:
i. inode为真实设备
ii. file为进程相关的文件描述符,和inode为 多为一的关系
c) 设备打开是首先由设备路径名去索引对应文件系统,找到真实的文件,并打开,这样当进程再次打开时则不用在重复该动作,直接在进程对应的file索引表就可以找到设备并返回索引号地址给用户态用。当打开文件时由inode找到真实设备,在chrdev_open中通过inode找到真实设备的i_cdev,将其指定的文件操作集分享给file用于绑定设备的操作集,并由其调用设备真实的open(用i_cdev 马上open也可以),file与进程相关,file为内核结构,最终映射为进程打开文件集的索引地址给用户态使用即FILE *fp,这样后面在用户态操作read/write等函数时由于之前已经绑定了文件真实的read/write等就可以直接使用了
二、 相关操作接口
a) dev_t --- 设备号的数据类型,32位,前12位为主设备号,后20位位次设备号
b) int MAJOR(dev_t dev);
由设备号抽取主设备号
主设备号标志设备对应的驱动程序
c) int MINOR(devt dev); 由设备号抽取次设备号 此设备号标志设备文件所指的设备
d) dev_t MKDEV(unsigned int major, unsigned int minor); 由主/次设备号构造dev_t
e) int register_chrdev_region(dev_t from, unsigned count, const char *name);
作用:指定设备编号来静态注册一个字符设备
from:注册的指定其实设备编号,比如:MKDEV(100,0),表示起始设备对应的主设备号为100,次设备号为0
count:需要连续注册的次设备号的个数,比如上例次设备号的起始为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations的操作方法
*name:字符设备名称,用户态访问的标志
返回值小于0表示注册失败
f) int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
作用:动态分配一个字符设备,注册成功则将主次设备号放入dev
*dev:存放其实设备编号的指针,注册成功即被赋值为设备号,由MAJOR/MINOR抽离主次设备号
baseminor:起始次设备号
count:需要连续注册的次设备号数量
name:字符设备名称
g) void cdev_init(struct cdev *cdev, const struct file_operations *fops); 作用:初始化cdev结构体,并将fops放入cdev->ops 绑定操作集到对应的设备号
h) int cdev_add(struct cdev *p, dev_t dev, unsigned count); 作用:将cdev结构体添加到系统中,并将dev(注册号的设备)放入cdev->dev里,count(次设备号个数)放入cdev->count
i) int cdev_del(struct cdev *cdev, dev_t dev, unsigned count); 作用:将系统中的cdev结构体删除 卸载
j) void unregister_chrdev_region(dev_t from, unsigned count);
作用:注销字符设备
from:注销指定起始设备号
count:需要连续注销的次设备号个数,与注册对应
三、一个简单的实例代码:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
/*
如何实现 字符设备驱动程序
1.向内核 申请一个 设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
dev_t *dev: 设备号,内核将 设备号放在 dev里面
存放 第一个设备号
baseminor: 起始的次设备号, 次设备号的范围0-2^20, baseminor可以指定从哪里开始申请
count: 你要申请多少个次设备号
name : 给设备号取个名字
返回值, 0成功 <0 失败
2.创建一个 字符设备 对象,然后初始化对象的属性和方法
1) struct cdev chdevobj;
2) void cdev_init(struct cdev * cdev,struct file_operations * fops)
等同于 chdevobj.ops = &chdev_ops;
3) int cdev_add(struct cdev p[],dev_t devno,unsigned count)
count: 这次添加 多少个 字符设备对象 到内核中
返回值: 0-成功 <0 失败
3.在用户层创建一个设备文件节点
1)创建一个类class
cls = class_create(THIS_MODULE,"chdev class");
2)在类 中,创建一个 文件节点
device_create(cls,NULL,devno,NULL,"chdev%d",0);
*/
int chdev_open(struct inode *inode, struct file *file){
printk("%s->%d\n",__FUNCTION__,__LINE__);
return 0;
}
int chdev_close (struct inode *inode, struct file *file){
printk("%s->%d\n",__FUNCTION__,__LINE__);
return 0;
}
struct file_operations chdev_ops = {
.open = chdev_open,
.release = chdev_close,
};
/*字符设备驱动对象*/
struct cdev chdevobj;
struct class *cls;
int module_fun_init(void){
dev_t devno;
int major,minor;
/* 1.向内核申请设备号*/
if(alloc_chrdev_region(&devno,0,1,"chdev test") <0){
printk("alloc_chrdev_region err %s->%d\n",__FUNCTION__,__LINE__);
return -12;
}
/*操作设备号*/
major = MAJOR(devno) ; //major = devno >> 20;
minor = MINOR(devno); //devno <<12>>12;
devno = MKDEV(major,minor); //major<<20 | minor;
chdevobj.dev = devno;
printk("%s->%d get major%d minor%d\n",__FUNCTION__,__LINE__,major,minor);
/*初始化字符设备驱动对象*/
cdev_init(&chdevobj,&chdev_ops);
if(cdev_add(&chdevobj,devno,1) <0){
printk("cdev_add err %s->%d\n",__FUNCTION__,__LINE__);
return -14;
}
/*第三步, 1.创建一个类*/
cls = class_create(THIS_MODULE,"chdev class");
if(!cls){
printk("class_create err %s->%d\n",__FUNCTION__,__LINE__);
return -16;
}
/*创建一个节点/文件,在类下
在 /dev/目录下,会创建 /dev/chdev0 文件节点 */
device_create(cls,NULL,devno,NULL,"chdev%d",0);
return 0;
}
void module_fun_exit(void){
cdev_del(&chdevobj); //注销字符设备
unregister_chrdev_region(chdevobj.dev, 1); // 释放原先申请的设备号,设备号,释放个数
printk("%s->%d\n",__FUNCTION__,__LINE__);
}
module_init(module_fun_init);
module_exit(module_fun_exit);
MODULE_AUTHOR("YinFei.Hu <aaa@qq.com>");
MODULE_DESCRIPTION("Simple character device driver");
MODULE_LICENSE("GPL");