Linux設備驅動——字符設備驅動

介紹

字符設備:是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先後數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制檯和LED設備等
每一個字符設備或塊設備都在/dev目錄下對應一個設備文件。linux用戶程序通過設備文件(或稱設備節點)來使用驅動程序操作字符設備和塊設備。

字符設備驅動模型

這裏寫圖片描述

這裏寫圖片描述

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設備驅動–字符設備模型

發佈了64 篇原創文章 · 獲贊 2 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章