Linux驅動系列--3.新字符型設備驅動

新字符型設備驅動原理

分配和釋放設備號

在使用register_chrdev 函數註冊字符設備的時候只需要給定一個主設備號即可,但是這樣會帶來兩個問題:
①、需要我們事先確定好哪些主設備號沒有使用。
②、會將一個主設備號下的所有次設備號都使用掉,比如現在設置 LED 這個主設備號爲200, 那麼 0~1048575(2^20-1)這個區間的次設備號就全部都被 LED 一個設備分走了。 這樣太浪費次設備號了!一個 LED 設備肯定只能有一個主設備號,一個次設備號。
解決這兩個問題最好的方法就是要使用設備號的時候向 Linux 內核申請,需要幾個就申請幾個,由 Linux 內核分配設備可以使用的設備號。如果沒有指定設備號的話就使用如下函數來申請設備號:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

如果給定了設備的主設備號和次設備號就使用如下所示函數來註冊設備號即可:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

註銷字符設備之後要釋放掉設備號 ,不管是通過 alloc_chrdev_region 函數還是register_chrdev_region 函數申請的設備號,統一使用如下釋放函數:

void unregister_chrdev_region(dev_t from, unsigned count)

新字符型設備註冊

字符設備結構

在 Linux 中使用 cdev 結構體表示一個字符設備, cdev 結構體在 include/linux/cdev.h 文件中的定義如下:

	struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

在 cdev 中有兩個重要的成員變量:ops 和 dev,這兩個就是字符設備文件操作函數集合file_operations 以及設備號 dev_t。編寫字符設備驅動之前需要定義一個 cdev 結構體變量,這個
變量就表示一個字符設備,如下所示:

struct cdev test_cdev;

cdev_init函數

定義好 cdev 變量以後就要使用 cdev_init 函數對其進行初始化, cdev_init 函數原型如下:

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

參數 cdev 就是要初始化的 cdev 結構體變量,參數 fops 就是字符設備文件操作函數集合。

cdev_add函數

cdev_add 函數用於向 Linux 系統添加字符設備(cdev 結構體變量),首先使用 cdev_init 函數完成對 cdev 結構體變量的初始化,然後使用 cdev_add 函數向 Linux 系統添加這個字符設備。cdev_add 函數原型如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

參數 p 指向要添加的字符設備(cdev 結構體變量),參數 dev 就是設備所使用的設備號,參
數 count 是要添加的設備數量。

cdev_del函數

卸載驅動的時候一定要使用 cdev_del 函數從 Linux 內核中刪除相應的字符設備, cdev_del
函數原型如下:

void cdev_del(struct cdev *p)

參數 p 就是要刪除的字符設備。如果要刪除字符設備。

自動創建設備節點

創建和刪除類

自動創建設備節點的工作是在驅動程序的入口函數中完成的,一般在 cdev_add 函數後面添加自動創建設備節點相關代碼。首先要創建一個 class 類, class 是個結構體,定義在文件include/linux/device.h 裏面。 class_create 是類創建函數, class_create 是個宏定義,內容如下:

#define class_create(owner, name) \
({ \
	static struct lock_class_key __key; \
	__class_create(owner, name, &__key); \
})
struct class *__class_create(struct module *owner, const char *name,
												struct lock_class_key *key)

根據上述代碼,將宏 class_create 展開以後內容如下:

struct class *class_create (struct module *owner, const char *name)

class_create 一共有兩個參數,參數 owner 一般爲 THIS_MODULE,參數 name 是類名字。返回值是個指向結構體 class 的指針,也就是創建的類。
卸載驅動程序的時候需要刪除掉類,類刪除函數爲 class_destroy,函數原型如下:

void class_destroy(struct class *cls);

參數 cls 就是要刪除的類。

創建與刪除設備

上一小節創建好類以後還不能實現自動創建設備節點,我們還需要在這個類下創建一個設備。使用 device_create 函數在類下面創建設備, device_create 函數原型如下:

struct device *device_create(struct class *class,
							struct device *parent,
							dev_t devt,
							void *drvdata,
							const char *fmt, ...)

device_create 是個可變參數函數,參數 class 就是設備要創建哪個類下面;參數 parent 是父設備,一般爲 NULL,也就是沒有父設備;參數 devt 是設備號;參數 drvdata 是設備可能會使用的一些數據,一般爲 NULL;參數 fmt 是設備名字,如果設置 fmt=xxx 的話,就會生成/dev/xxx這個設備文件。返回值就是創建好的設備。
同樣的,卸載驅動的時候需要刪除掉創建的設備,設備刪除函數爲 device_destroy,函數原型如下:

void device_destroy(struct class *class, dev_t devt)

參數 classs 是要刪除的設備所處的類,參數 devt 是要刪除的設備號。

設置文件私有數據

每個硬件設備都有一些屬性,比如主設備號(dev_t), 類(class)、設備(device)、開關狀態(state)
等等,對於一個設備的所有屬性信息我們最好將其做成一個結構體。編寫驅動 open 函數的時候將設備結構體作爲私有數據添加到設備文件中,如下所示:

/* 設備結構體 */
struct test_dev{
				dev_t devid; /* 設備號 */
				struct cdev cdev; /* cdev */
				struct class *class; /* 類 */
				struct device *device; /* 設備 */
				int major; /* 主設備號 */
				int minor; /* 次設備號 */
};
struct test_dev testdev;
/* open 函數 */
static int test_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &testdev; /* 設置私有數據 */
	return 0;
}

在 open 函數裏面設置好私有數據以後,在 write、 read、 close 等函數中直接讀取 private_data即可得到設備結構體。

代碼編寫

newchrled.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>


#define NEWCHRLED_CNT       1                //設備號個數
#define NEWCHRLED_NAME      "newchrled"      //設備名字



#define LEDOFF      0   //關燈
#define LEDON       1   //開燈

/*  寄存器物理地址  */
#define CCM_CCGR1_BASE              (0x020C406C)
#define SW_MUX_GPIO1_IO04_BASE      (0x020E006C)
#define SW_PAD_GPIO1_IO04_BASE      (0x020E02F8)
#define GPIO1_DR_BASE               (0x0209C000)
#define GPIO1_GDIR_BASE             (0x0209C004)


/*   映射後的寄存器虛擬地址   */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;


//newchrled設備結構體
struct  newchrled_dev
{
    /* data */
    dev_t devid;                //設備號
    struct cdev cdev;           //cdev
    struct class *class;        //類
    struct device *device;      //設備
    int major;                  //主設備號
    int minor;                  //次設備號
};

struct newchrled_dev newchrled;

/*
@description    :   LED打開/關閉
@param - sta    :   LEDON(0)打開LED,LEDOFF(1)關閉LED
@return         :   無
*/
void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON)
    {
        val = readl(GPIO1_DR);
        val &= ~(1<<4);
        writel(val,GPIO1_DR);
    }
    else if(sta == LEDOFF)
    {
        /* code */
        val = readl(GPIO1_DR);
        val |= (1<<4);
        writel(val,GPIO1_DR);
    }
    
}


/*
@description      :   打開設備
@param - inode    :   傳遞給驅動的inode   
@param - filp     :   設備文件 
@return           :   0 成功; 其他 失敗
*/
static int led_open(struct inode *inode, struct file *file)
{
    file->private_data = &newchrled;        //設置私有數據
    return 0;
}


/*
@description      :   從設備讀取數據 
@param - filp     :   要打開的設備文件
@param - buf      :   返回給用戶控件的數據緩衝區
@param - cnt      :   要讀取的數據長度
@param - offt     :   相對於文件首地址的偏移      
@return           :   讀取的字節數,若爲負值,讀取失敗
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt,loff_t *offt)
{
    return 0;
}

/*
@description      :   向設備寫數據 
@param - filp     :   設備文件
@param - buf      :   要寫入設備的數據
@param - cnt      :   要寫入的數據長度
@param - offt     :   相對於文件首地址的偏移      
@return           :   寫入的字節數,若爲負值,讀取失敗
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt,loff_t *offt)
{
    int retvalue;
    unsigned char datebuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(datebuf, buf, cnt);
    if(retvalue < 0)
    {
        printk("kernel write failed!!\r\n");
        return -EFAULT;
    }

    ledstat = datebuf[0];

    if(ledstat  == LEDON)
    {
        led_switch(LEDON);
    }    
    else if (ledstat == LEDOFF)
    {
        led_switch(LEDOFF);
    }
    return 0;

}

/*
@description      :   關閉/釋放設備  
@param - filp     :   要關閉的設備文件 
@return           :   0 成功; 其他 失敗
*/
static int led_release(struct inode *inode, struct file *file)
{
    return 0;
}

/*   設備操作函數集合   */
static struct file_operations newchrled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};


/*
@description      :   驅動入口函數  
@param -          :   無 
@return           :   無
*/
static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    /*初始化LED*/
    /*1.寄存器地址映射*/
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE,4);
    SW_MUX_GPIO1_IO04 = ioremap(SW_MUX_GPIO1_IO04_BASE,4);
    SW_PAD_GPIO1_IO04 = ioremap(SW_PAD_GPIO1_IO04_BASE,4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE,4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE,4);

    /*2.使能GPIO1始終*/
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val,IMX6U_CCM_CCGR1);

    /*3.設置GOIO1_IO04 的複用功能,將其複用爲GPIO1_IO04,最後設置IO屬性*/
    writel(5,SW_MUX_GPIO1_IO04);
    //設置IO屬性
    writel(0x10B0,SW_PAD_GPIO1_IO04);
    
    //設置GPIO1_IO04爲輸出功能
    val = readl(GPIO1_GDIR);
    val &= ~(1<<4);
    val |= (1<<4);
    writel(val,GPIO1_GDIR);

    //默認關閉LED
    val = readl(GPIO1_DR);
    val |= (1<<4);
    writel(val,GPIO1_DR);

    //註冊字符設備驅動

    //1.創建設備號
    if(newchrled.major)
    {
        newchrled.devid = MKDEV(newchrled.major,0);
        register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME);
    }
    else
    {
        alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME);//申請設備號
        newchrled.major = MAJOR(newchrled.devid);//獲取主設備號
        newchrled.minor = MINOR(newchrled.devid);//獲取次設備號
    }

    printk("newchrled major=%d,minor=%d\r\n",newchrled.major,newchrled.minor);

    //2.初始化cdev
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev,&newchrled_fops);

    //3.添加cdev
    cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_CNT);

    //4.創建類
    newchrled.class = class_create(THIS_MODULE,NEWCHRLED_NAME);
    if(IS_ERR(newchrled.class))
    {
        return PTR_ERR(newchrled.class);
    }
    //5.創建設備
    newchrled.device = device_create(newchrled.class,NULL,newchrled.devid,NULL,NEWCHRLED_NAME);
    if(IS_ERR(newchrled.device))
    {
        return PTR_ERR(newchrled.device);
    }
    


    return 0;

}

static void __exit led_exit(void)
{
    //取消映射
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO04);
    iounmap(SW_PAD_GPIO1_IO04);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    //註銷字符驅動
    cdev_del(&newchrled.cdev);//刪除cdev
    unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT);
    device_destroy(newchrled.class,newchrled.devid);
    class_destroy(newchrled.class);
    
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Turing");

編譯測試

同上節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章