開發一個字符設備驅動

1、什麼是字符設備

 1.1 基本概念

  字符設備是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先後數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制檯和LED設備等。

 1.2 數據結構

struct cdev//設備結構體
{
    struct kobject kobj;          /* 內嵌的kobject 對象 */
    struct module *owner;         /*所屬模塊*/
    struct file_operations *ops;  /*文件操作結構體*/
    struct list_head list;
    dev_t dev;                    /*設備號*/
    unsigned int count;
};

2、字符設備驅動開發的基本步驟

 2.1 確定主設備號和次設備號

  設備號是一個32bit的整數,由主設備號和從設備號組成。主設備號是內核識別一類設備的標識。整數(佔12bits),範圍從0到4095,通常使用1到255。次設備號由內核使用,用於正確確定設備文件所指的設備。整數(佔20bits),範圍從0到1048575,一般使用0到255。
  設備號需要向內核申請,有靜態申請和動態申請兩種方式。靜態申請就是自己定義一個值,然後調用相關的函數向內核申請,這種方法可能會和別的設備號衝突,一般不使用;動態申請就是直接調用相關的函數,由內核返回一個設備號給你。下面第三節會介紹這些函數的,再此之前先來看看幾個宏定義。

將主設備號和次設備號轉換成dev_t類型的宏:
MKDEV (int major,int minor);
如dev_t devno = MKDEV(232, 0);
從dev_t獲得主設備號和次設備號的宏:
MAJOR(dev_t);
  
/*以上幾個宏定義如下:*/
#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))

 2.2 實現字符驅動程序

  2.2.1 實現file_operations結構體

  首先,我們先來看看3.0內核下../include/linux/fs.h中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 *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_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 *, 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 (*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);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
};

  在kernel 3.0中已經完全刪除了struct file_operations 中的ioctl 函數指針,剩下unlocked_ioctl和compat_ioctl。主要改進就是不再需要上內核鎖 (調用之前不再先調用lock_kernel()然後再unlock_kernel())。
  一般我們只需要實現以下四個函數就行了:
  (1)ssize_t (read) (struct file , char __user , size_t, loff_t );
  應用程序調用read()函數時,驅動的這個函數會被調用。
  (2)ssize_t (write) (struct file , const char __user , size_t, loff_t );
  應用程序調用write()函數時,驅動的這個函數會被調用。
  (3) unsigned int (poll) (struct file , struct poll_table_struct *);
  當應用程序需要支持select或者poll機制時,驅動的poll函數需要被實現,具體看我的另一篇博客《select機制的驅動實現及原理》。
  (4)long (unlocked_ioctl) (struct file , unsigned int, unsigned long);
  當應用程序調用ioctl函數時,驅動的這個函數會被調用。它的原型如下:

long (*ioctl)(struct file *file, unsigned int cmd, unsigned long arg) //kernel 3.0後
int (*ioctl) (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) //kernel 3.0前

  這裏詳細介紹一下cmd參數。
  在編寫ioctl代碼之前,需要選擇對應不同命令的編號。爲了防止對錯誤的設備使用正確的命令,命令號應該在系統範圍內唯一,這種錯誤匹配並不是不會發生,程序可能發現自己正在試圖對FIFO和audio等這類非串行設備輸入流修改波特率,如果每一個ioctl命令都是唯一的,應用程序進行這種操作時就會得到一個EINVAL錯誤,而不是無意間成功地完成了意想不到的操作。
  儘管ioctl系統調用絕大部分用於操作設備,但是還有一些命令是可以由內核識別的。要注意,當這些命令用於我們的設備時,他們會在我們自己的文件操作被調用之前被解碼。所以,如果你爲自己的ioctl命令選用了與這些預定義命令相同的編號,就永遠不會收到該命令的請求,而且由於ioctl編號衝突,應用程序的行爲將無法預測。
  以上兩段話來自ldd3,說了爲什麼cmd在系統裏要有唯一性。爲了做到唯一性,內核約定cmd被分爲四個字段,具體含義如下圖:

設備類型(幻數) 序列號 方向 數據尺寸
8 bit 8 bit 2 bit 8~14 bit

  幻數:說得再好聽的名字也只不過是個0~0xff的數,佔8bit(_IOC_TYPEBITS)。一個設備對應一個幻數。
  序列號:用這個數來給自己的命令編號,佔8bit(_IOC_NRBITS)
  方向:讀或者寫,方向是以應用層的角度來描述的。
  數據尺寸:所涉及的用戶數據大小。系統並不強制使用這個位字段,也就是說,內核不會檢測這個位字段。
  我們開發驅動時要按Linux內核的約定方法爲驅動程序選擇ioctl編號,應該首先看看include/asm/ioctl.h和Doucumention/ioctl-number.txt這兩個文件。頭文件定義了要使用的位字段:類型(幻數)、序數、傳送方向以及參數大小等。ioctl-number.txt文件中羅列了內核所使用的幻數,選擇自己的幻數要避免和內核衝突。
   在include/asm/ioctl.h頭文件中,定義一些宏用來操作cmd。

//構造無參數的命令編號
#define _IO(type,nr)           _IOC(_IOC_NONE,(type),(nr),0) 

//構造從驅動程序中讀取數據的命令編號
#define _IOR(type,nr,size)     _IOC(_IOC_READ,(type),(nr),sizeof(size)) 

//用於向驅動程序寫入數據命令
#define _IOW(type,nr,size)     _IOC(_IOC_WRITE,(type),(nr),sizeof(size))

//用於雙向傳輸
#define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

//從命令參數中解析出數據方向,即寫進還是讀出
#define _IOC_DIR(nr)          (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)

//從命令參數中解析出幻數type
#define _IOC_TYPE(nr)         (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)

//從命令參數中解析出序數number
#define _IOC_NR(nr)           (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)

//從命令參數中解析出用戶數據大小
#define _IOC_SIZE(nr)         (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

  例如我們可以這樣來來定義一個cmd:

#define HELLO_MAGIC 'k'
#define HELLO_CMD1    _IO(HELLO_MAGIC,0x1a)
#define HELLO_CMD2    _IO(HELLO_MAGIC,0x1b)
/*
    對幻數的編號千萬不能重複定義,如ioctl-number.txt已經說明‘k'的編號已經被佔用的範圍爲:
'k'    00-0F    linux/spi/spidev.h    conflict!
'k'    00-05    video/kyro.h        conflict!
    所以我們在這裏分別編號爲0x1a和0x1b.
*/

  在我看來,內核對cmd的這種約定,只是一種規範而已,最多隻能避免我們定義的cmd不和內核定義的衝突,根本無法避免在同一個系統上兩個互不相關的驅動程序員不會使用相同的幻數。但是能避免不和內核衝突已經夠了,只要應用程序調用ioctl時,不把設備描述符傳錯應該就不會有什麼太大的問題。

  2.2.2 實現初始化函數,註冊字符設備

  首先我們要向內核申請設備號,然後將設備號和設備綁定,還要將設備和文件描述結構體綁定。具體流程看第三節的函數介紹和第四節的例子。

  2.2.3 實現銷燬函數,釋放字符設備

  驅動卸載時需要釋放資源,否則第二次insmod驅動時會出錯。具體流程看第三節的函數介紹和第四節的例子。

 2.3 創建設備文件節點

  1、手動創建
  mknod /dev/node_name c major minor
  例如:
  mknod /dev/hello c 250 0
  2、自動創建
  可以在模塊註冊函數裏調用class_create和device_create。
  例如:
  p_hello_class = class_create(THIS_MODULE, HELLO_NAME);
  p_hello_device = device_create(p_hello_class, NULL, devno, NULL, HELLO_NAME);
  用class_create函數來創建一個名字爲HELLO_NAME的類,這個類存放於sys/class下面。一旦創建好了這個類,再調用 device_create函數。這樣,加載模塊的時候,用戶空間中的udev或mdev會自動響應 device_create函數,去sys/class下尋找對應的類從而在dev目錄下創建相應的設備節點。

3、相關函數介紹

(1)靜態申請設備號
    int register_chrdev_region(dev_t first, unsigned int count, char *name);
    頭文件:#include <linux/fs.h>
    返回值:成功返回0,失敗返回一個負的錯誤碼。
    參數:
        first:分配的起始設備編號。first 的次編號部分常常是 0, 但是沒有要求是那個效果。
        count:所請求的連續設備編號的個數。
        name:是應當連接到這個編號範圍的設備的名字。
(2)動態申請設備號
    int  alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
    頭文件:#include <linux/fs.h>
    返回值:成功返回0,失敗返回一個負的錯誤碼。
    參數:
        dev:返回的設備號
        firstminor:被請求的第一個次設備號
        count:請求的個數
        name:是應當連接到這個編號範圍的設備的名字。
(3)釋放設備號
    void  unregister_chrdev_region(dev_t first, unsigned int count);
(4)添加設備
    int cdev_add(struct cdev *, dev_t, unsigned);
    把申請的設備號與設備綁定,然後向系統添加一個cdev,至此,系統通過設備號就能找到這個設備。完成字符設備的註冊,通常在模塊加載函數中調用
(5)刪除設備
    void cdev_del(struct cdev *);
    分別向系統刪除一個cdev,完成字符設備的註銷,通常在模塊的卸載函數中調用
(6)字符設備初始化
    void cdev_init( struct cdev *, struc t file_operations *);
    用於初始化cdev的成員,並建立cdev和file_operations之間的連接
注意點:
1、在調用cdv_add()函數向系統註冊字符設備之前,應先調用register_chrdev_region()函數或alloc_chrdev_region函數向系統申請設備號。
2、相反地,在調用cdev_del()函數從系統註銷字符設備後,unregister_chrdev_region函數應該被調用以釋放原先申請的設備號。  

4、例子

源文件:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/major.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/uaccess.h>


#define HELLO_NAME "myhello"


static struct cdev hello_cdev;
static int hello_major = 0;
static int hello_minor = 0;

static struct class * p_myhello_class = NULL;           
static struct device *p_myhello_device = NULL;          


static int hello_open(struct inode *inode,struct file *file)
{
    printk("myhello open\n");
    return 0;
}

static int hello_release(struct inode *inode,struct file *file)
{
    printk("myhell release\n");
    return 0;
}

static int hello_read(struct file *filp,char __user *buf,size_t count,loff_t *offp)
{
    printk("myhello read\n");
    return 0;
}

static int hello_write(struct file *flip,const char __user *buf,size_t count,loff_t *offp)
{
    printk("myhello write\n");
    return 0;
}

static const struct file_operations hello_fops = {
    .owner = THIS_MODULE,
    .open = hello_open,
    .release = hello_release,
    .read = hello_read,
    .write = hello_write,
    //.ioctl = hello_ioctl,
};

static int hello_setup_cdev(struct cdev *cdev,dev_t devno)
{
    int ret = 0;

    cdev_init(cdev,&hello_fops);
    cdev->owner = THIS_MODULE;
    ret = cdev_add(cdev,devno,1);

    return ret;
}

static int __init hello_init(void)
{
    int ret = 0;
    dev_t devno;

    printk("hello_init...\n");

    if(hello_major) {
        devno = MKDEV(hello_major,hello_minor);
        ret = register_chrdev_region(devno,1,HELLO_NAME);
    } else {
        ret = alloc_chrdev_region(&devno,hello_minor,1,HELLO_NAME);
        hello_major = MAJOR(devno);
    }

    if(ret < 0) {
        printk("register cdev failed! ret = %d\n", ret);
        return ret;
    }

    ret = hello_setup_cdev(&hello_cdev,devno);
    if(ret) {
        printk("setup cdev failed! ret = %d\n", ret);
        goto cdev_add_fail;
    }

    p_myhello_class = class_create(THIS_MODULE, HELLO_NAME);
    ret = IS_ERR(p_myhello_class);
    if(ret) {
        printk("myhello class_create failed!\n");
        goto class_create_fail;
    }

    //p_myhello_device = class_device_create(p_hello_class, NULL, devno, NULL, HELLO_NAME);
    p_myhello_device = device_create(p_myhello_class, NULL, devno, NULL, HELLO_NAME);
    ret = IS_ERR(p_myhello_device);
    if (ret) {
        printk(KERN_WARNING "myhello device_create failed, ret = %ld", PTR_ERR(p_myhello_device));
        goto device_create_fail;
    }

    return 0;

device_create_fail:
    class_destroy(p_myhello_class);
class_create_fail:
    cdev_del(&hello_cdev);  
cdev_add_fail:
    unregister_chrdev_region(devno,1);
    return ret;
}

static void __exit hello_exit(void)
{
    dev_t devno;

    printk("hello_exit...\n");

    devno = MKDEV(hello_major,hello_minor);

    device_destroy(p_myhello_class, devno);
    class_destroy(p_myhello_class);
    cdev_del(&hello_cdev);
    unregister_chrdev_region(devno,1);
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("hello driver");
MODULE_LICENSE("GPL");

Makefile文件:

ifneq ($(KERNELRELEASE),)
obj-m := myhello.o
else
KERNELDIR ?= /home/share/linux-2.6.32-devkit8500
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

endif

install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order
發佈了57 篇原創文章 · 獲贊 80 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章