Linux设备驱动——字符设备驱动

介绍

字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

字符设备驱动模型

Linux设备驱动——字符设备驱动_第1张图片

1、cdev结构体

struct cdev {  
    struct kobject kobj;   /*内嵌的kobject结构,用于内核设备驱动模型的管理*/ 

    struct module *owner;  /*指向包含该结构的模块的指针,用于引用计数*/   

    const struct file_operations *ops;   /*指向字符设备操作函数集的指针*/  
    struct list_head list; /*该结构将使用该驱动的字符设备连接成一个链表*/       
                           /*与cdev对应的字符设备文件的inode->i_devices的链表头*/

    dev_t dev;             /*该字符设备的起始设备号,一个设备可能有多个设备号*/  
    unsigned int count;    /*使用该字符设备驱动的设备数量*/  
}; 

cdev 结构体的dev_t 成员定义了设备号,为32位,其中12位是主设备号,20位是次设备号
我们只需使用二个简单的宏就可以从dev_t 中获取主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
相反地,可以通过主次设备号来生成dev_t:
MKDEV(int major,int minor)

2、分配cdev

struct cdev btn_cdev;
/*申请设备号,如果申请失败采用动态申请方式*/ 
if(major)
{
    //静态
    dev_id = MKDEV(major, 0);
    register_chrdev_region(dev_id, 1, "button");
} 
else 
{     
    //动态
    alloc_chardev_region(&dev_id, 0, 1, "button");
    major = MAJOR(dev_id);
}

静态申请:

int register_chrdev_region(dev_t from, unsigned count, const char *name);
/*功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增加)*/

from :要分配的设备编号范围的初始值(次设备号常设为0);
Count:连续编号范围.
name:编号相关联的设备名称. (/proc/devices);
静态申请相对较简单,但是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

动态申请:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/
//返回值小于0表示分配失败,系统自动返回没有占用的设备号

baseminor:是请求的最小的次编号;
count:是请求的连续设备编号的总数;
name:为设备名
然后通过major=MMOR(dev)获取主设备号
动态申请简单,易于驱动推广,但是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。

销毁设备号

void unregister_chrdev_region(dev_t first, unsigned int count);

first为第一个设备号,count为申请的设备数量

3、初始化c_dev

内核在内部使用类型 struct cdev 的结构来代表字符设备.有 2 种方法来分配和初始化一个这些结构.
①如果你想在运行时获得一个独立的 cdev 结构, 你可以为此使用这样的代码:

struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;

②另一种是把cdv结构嵌入到你自己封装的设备结构中,这时需要使用下面的方法来分配和初始化:

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

cdev_ init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。

cdev_init()操作源代码:

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;  
}  

4、注册cdev

int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
//向系统添加一个cdev,完成字符设备的注册。

dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1。

5、硬件初始化

6、设备操作

file_operations结构

struct file_operations {
    struct module *owner;  //THIS_MODULE, <linux/module.h> 中定义的宏.

    /*用于修改文件当前的读写位置*/
    loff_t(*llseek) (struct file *, loff_t, int);

    ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);

    ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);

    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *, 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(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
    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);
};

参考:file_operations中各项解析

file结构,文件结构代表一个打开的文件

struct file{
    mode_t fmode; //文件模式,如FMODE_READ,FMODE_WRITE*/

    ......
    loff_t f_pos; //当前读写位置.off_t是一个64位的数,驱动可以读这个值,如果它需要知道文件中的当前位置,但是正常地不应该改变它。

    unsigned int f_flags; //文件标志,例如O_RDONLY,O_NONBLOCK,和O_SYNC.驱动应当检查 O_NONBLOCK标志来看是否是请求非阻塞操作

    struct file_operations *f_op;

    void *private_data; //非常重要,用于存放转换后的设备描述结构指针*/
    .......
};

inode 结构
内核用inode 结构在内部表示文件,它是实实在在的表示物理硬件上的某一个文件,且一个文件仅有一个inode与之对应,同样它有二个比较重要的成员:

struct inode{
    dev_t i_rdev;   //设备编号

    struct cdev *i_cdev; 
};
//从inode中获取主次设备号
unsigned int imajor(struct inode *inode);
unsigned int iminor(struct inode *inode);

7、字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请cdev 结构的注册
卸载函数中应该实现设备号的释放cdev结构的注销

我们一般习惯将cdev内嵌到另外一个设备相关的结构体里面,该设备包含所涉及的cdev、私有数据及信号量等等信息。常见的设备结构体、模块加载函数、模块卸载函数形式如下:

/*设备结构体*/
struct xxx_dev{
    struct cdev cdev;

    char *data;

    struct semaphore sem;
    ......
};

/*模块加载函数*/
static int __init xxx_init(void)
{
    .......

    初始化cdev结构;

    申请设备号;
    注册设备号;
    申请分配设备结构体的内存; 
    /*非必须*/
 }

/*模块卸载函数*/
static void __exit xxx_exit(void)
{
    .......
    释放原先申请的设备号;
    释放原先申请的内存;
    注销cdev设备;
}

参考:深入浅出:Linux设备驱动之字符设备驱动

实例1


#ifndef MEMDEV_MAJOR
    #define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
#endif

#ifndef MEMDEV_NR_DEVS
    #define MEMDEV_NR_DEVS 2 /*设备数*/
#endif

#ifndef MEMDEV_SIZE
    #define MEMDEV_SIZE 4096
#endif

/*mem设备描述结构体*/
struct mem_dev 
{ 
  char *data; 
  unsigned long size; 
};

static mem_major = MEMDEV_MAJOR;

module_param(mem_major, int, S_IRUGO);

struct mem_dev *gp_mem_dev; /*设备结构体指针*/

struct cdev g_cdev; 

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
    struct mem_dev *dev;

    /*获取次设备号*/
    int num = MINOR(inode->i_rdev);

    if (num >= MEMDEV_NR_DEVS) 
            return -ENODEV;
    dev = &gp_mem_dev[num];

    /*将设备描述结构指针赋值给文件私有数据指针*/
    filp->private_data = dev;

    return 0; 
}

/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p = *ppos; /*记录文件指针偏移位置*/ 
  unsigned int count = size; /*记录需要读取的字节数*/ 
  int ret = 0; /*返回值*/ 
  struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/

  /*判断读位置是否有效*/
  if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/ 
    return 0;
  if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/ 
    count = MEMDEV_SIZE - p;

  /*读数据到用户空间:内核空间->用户空间交换数据*/ 
  if (copy_to_user(buf, (void*)(dev->data + p), count))
  {
    ret = - EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;

    printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
  }

  return ret;
}

/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p = *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/

  /*分析和获取有效的写长度*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/
    count = MEMDEV_SIZE - p;

  /*从用户空间写入数据*/
  if (copy_from_user(dev->data + p, buf, count))
    ret = - EFAULT;
  else
  {
    *ppos += count; /*增加偏移位置*/ 
    ret = count; /*返回实际的写入字节数*/ 

    printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
  }

  return ret;
}

/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{ 
    loff_t newpos; 

    switch(whence) {
      case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/ 
        newpos = offset; /*更新文件指针位置*/
        break;

      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + offset; 
        break;

      case 2: /* SEEK_END */
        newpos = MEMDEV_SIZE -1 + offset;
        break;

      default: /* can't happen */
        return -EINVAL;
    }
    if ((newpos<0) || (newpos>MEMDEV_SIZE))
        return -EINVAL;

    filp->f_pos = newpos;
    return newpos;

}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
  int result;
  int i;

  dev_t devno = MKDEV(mem_major, 0);

  /* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/ 
  /* 静态申请设备号*/
  if(mem_major)
    result = register_chrdev_region(devno, 2, "memdev");
  else /* 动态分配设备号 */
  {
    result = alloc_chrdev_region(&devno, 0, 2, "memdev");
    mem_major = MAJOR(devno); /*获得申请的主设备号*/
  } 

  if (result < 0)
    return result;

  /*初始化cdev结构,并传递file_operations结构指针*/ 
  cdev_init(&g_cdev, &mem_fops); 
  g_cdev.owner = THIS_MODULE; /*指定所属模块*/
  g_cdev.ops = &mem_fops;

  /*注册字符设备*/
  cdev_add(&g_cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

  /*为设备描述结构分配内存*/
  gp_mem_dev = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
  if (!gp_mem_dev) /*申请失败*/
  {
    result = - ENOMEM;
    goto fail_malloc;
  }
  memset(gp_mem_dev, 0, sizeof(struct mem_dev));

  /*为设备分配内存*/
  for (i=0; i < MEMDEV_NR_DEVS; i++) 
  {
        gp_mem_dev[i].size = MEMDEV_SIZE;
        gp_mem_dev[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
        memset(gp_mem_dev[i].data, 0, MEMDEV_SIZE);
  }

  return 0;

fail_malloc: 
  unregister_chrdev_region(devno, 1);

  return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
  cdev_del(&g_cdev); /*注销设备*/
  kfree(gp_mem_dev); /*释放设备结构体内存*/
  unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}

MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);

参考:Linux 内核开发 - 第一个字符驱动程序

Makefile文件如下:

ifneq ($(KERNELRELEASE),)
    obj-m := cdevdemo.o
else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order  Module.symvers

实例2

#include <linux/module.h> 
#include <linux/types.h> 
#include <linux/fs.h> 
#include <linux/errno.h> 
#include <linux/mm.h> 
#include <linux/sched.h> 
#include <linux/init.h> 
#include <linux/cdev.h> 
#include <asm/io.h> 
#include <asm/uaccess.h> 
#include <linux/timer.h> 
#include <asm/atomic.h> 
#include <linux/slab.h> 
#include <linux/device.h> 

#define CDEVDEMO_MAJOR 255 /*预设cdevdemo的主设备号*/ 

static int cdevdemo_major = CDEVDEMO_MAJOR;  

/*设备结构体,此结构体可以封装设备相关的一些信息等 信号量等也可以封装在此结构中,后续的设备模块一般都 应该封装一个这样的结构体,但此结构体中必须包含某些 成员,对于字符设备来说,我们必须包含struct cdev cdev*/  
struct cdevdemo_dev   
{  
    struct cdev cdev;  
};  

struct cdevdemo_dev *cdevdemo_devp; /*设备结构体指针*/  

/*文件打开函数,上层对此设备调用open时会执行*/  
int cdevdemo_open(struct inode *inode, struct file *filp)     
{  
    printk(KERN_NOTICE "======== cdevdemo_open ");  
    return 0;  
}  

/*文件释放,上层对此设备调用close时会执行*/  
int cdevdemo_release(struct inode *inode, struct file *filp)      
{  
    printk(KERN_NOTICE "======== cdevdemo_release ");     
    return 0;  
}  

/*文件的读操作,上层对此设备调用read时会执行*/  
static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)  
{  
    printk(KERN_NOTICE "======== cdevdemo_read ");    
}  

/* 文件操作结构体,文中已经讲过这个结构*/  
static const struct file_operations cdevdemo_fops =  
{  
    .owner = THIS_MODULE,  
    .open = cdevdemo_open,  
    .release = cdevdemo_release,  
    .read = cdevdemo_read,  
};  

/*初始化并注册cdev*/  
static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index)  
{  
    printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1");     
    int err, devno = MKDEV(cdevdemo_major, index);  
    printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2");  

    /*初始化一个字符设备,设备所支持的操作在cdevdemo_fops中*/     
    cdev_init(&dev->cdev, &cdevdemo_fops);  
    printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3");     
    dev->cdev.owner = THIS_MODULE;  
    dev->cdev.ops = &cdevdemo_fops;  
    printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4");     
    err = cdev_add(&dev->cdev, devno, 1);  
    printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5");  
    if(err)  
    {  
        printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index);   
    }  
}  

int cdevdemo_init(void)  
{  
    printk(KERN_NOTICE "======== cdevdemo_init ");    
    int ret;  
    dev_t devno = MKDEV(cdevdemo_major, 0);  

    struct class *cdevdemo_class;  
    /*申请设备号,如果申请失败采用动态申请方式*/  
    if(cdevdemo_major)  
    {  
        printk(KERN_NOTICE "======== cdevdemo_init 1");  
        ret = register_chrdev_region(devno, 1, "cdevdemo");  
    }else  
    {  
        printk(KERN_NOTICE "======== cdevdemo_init 2");  
        ret = alloc_chrdev_region(&devno,0,1,"cdevdemo");  
        cdevdemo_major = MAJOR(devno);  
    }  
    if(ret < 0)  
    {  
        printk(KERN_NOTICE "======== cdevdemo_init 3");  
        return ret;  
    }  
    /*动态申请设备结构体内存*/  
    cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL);  
    if(!cdevdemo_devp)  /*申请失败*/  
    {  
        ret = -ENOMEM;  
        printk(KERN_NOTICE "Error add cdevdemo");     
        goto fail_malloc;  
    }  

    memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev));  
    printk(KERN_NOTICE "======== cdevdemo_init 3");  
    cdevdemo_setup_cdev(cdevdemo_devp, 0);  

    /*下面两行是创建了一个总线类型,会在/sys/class下生成cdevdemo目录 这里的还有一个主要作用是执行device_create后会在/dev/下自动生成 cdevdemo设备节点。而如果不调用此函数,如果想通过设备节点访问设备 需要手动mknod来创建设备节点后再访问。*/  
    cdevdemo_class = class_create(THIS_MODULE, "cdevdemo");  
    device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo");  

    printk(KERN_NOTICE "======== cdevdemo_init 4");  
    return 0;  

fail_malloc:  
        unregister_chrdev_region(devno,1);  
}  

void cdevdemo_exit(void)    /*模块卸载*/  
{  
    printk(KERN_NOTICE "End cdevdemo");   
    cdev_del(&cdevdemo_devp->cdev);  /*注销cdev*/  
    kfree(cdevdemo_devp);       /*释放设备结构体内存*/  
    unregister_chrdev_region(MKDEV(cdevdemo_major,0),1);    //释放设备号 
}  

MODULE_LICENSE("Dual BSD/GPL");  
module_param(cdevdemo_major, int, S_IRUGO);  
module_init(cdevdemo_init);  
module_exit(cdevdemo_exit);  

参考:linux设备驱动–字符设备模型

你可能感兴趣的:(Linux设备驱动——字符设备驱动)