欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux 字符设备驱动--PWM的蜂鸣器

程序员文章站 2022-03-22 16:56:57
...

Linux 字符设备驱动学习

一、基本概念

  1. linux系统,一切皆文件,字符设备就是实现一切皆文件,用户可以通过访问文件的方式,来访问硬件/内核。

  2. 驱动:硬件 = 1:n 一个驱动程序,可以管理很多硬件
    给驱动程序 分配一个主设备号, 给该驱动管理的每个硬件 分配一个次设备号。
    同一类硬件,使用相同的驱动,所以 拥有相同的主设备号。

  3. 设备号devno,请求取 主设备major, 次设备号minor:
    major = devno >> 20;
    minor = devno<<12>>12;

  4. 已知major,minor,求 devno: devno = major <<20 | minor;

  5. 实现一个字符设备驱动
    a) 申请设备号
    b) 创建字符设备驱动对象
    c) 在用户层创建一个文件节点

Linux 字符设备驱动--PWM的蜂鸣器
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);

Linux 字符设备驱动--PWM的蜂鸣器
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:从中可以看出设备驱动的核心:申请设备结构体资源后填充设备编号和操作集
  1. 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,被关闭后会释放该结构体
  1. 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结构的指针	
};
......
}

Linux 字符设备驱动--PWM的蜂鸣器
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):主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
  1. 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");

相关标签: 驱动