簡單字符驅動筆記(朱有鵬)

框架

static int __init chrdev init(void)
{
    printk(KERN_DEBUG "chrdev_init");
    return 0;
}

static void __exit chrdev_exit(void)
{
    printk(KERN_DEBUG ""chrdev_exit);
}



module_init(chrdev_init);
module_exit(chrdev_exit);


MODULE_LICENSE("GPL");              // 描述模塊的許可證
MODULE_AUTHOR("aston");				// 描述模塊的作者
MODULE_DESCRIPTION("module test");	// 描述模塊的介紹信息
MODULE_ALIAS("alias xxx");			// 描述模塊的別名信息

insmod與module_init宏。模塊源代碼中用module_init宏聲明瞭一個函數(在我們這個例子裏是chrdev_init函數),作用就是指定chrdev_init這個函數和insmod命令綁定起來,也就是說當我們insmod module_test.ko時,insmod命令內部實際執行的操作就是幫我們調用chrdev_init函數。

printk相比printf來說還多了個:打印級別的設置。printk的打印級別是用來控制printk打印的這條信息是否在終端上顯示的。應用程序中的調試信息要麼全部打開要麼全部關閉,一般用條件編譯來實現(DEBUG宏),但是在內核中,因爲內核非常龐大,打印信息非常多,有時候整體調試內核時打印信息要麼太多找不到想要的要麼一個沒有沒法調試。所以纔有了打印級別這個概念。
操作系統的命令行中也有一個打印信息級別屬性,值爲0-7。當前操作系統中執行printk的時候會去對比printk中的打印級別和我的命令行中設置的打印級別,小於我的命令行設置級別的信息會被放行打印出來,大於的就被攔截的。譬如我的ubuntu中的打印級別默認是4,那麼printk中設置的級別比4小的就能打印出來,比4大的就不能打印出來。ubuntu中這個printk的打印級別控制沒法實踐,ubuntu中不管你把級別怎麼設置都不能直接打印出來,必須dmesg命令去查看。

照此分析,那insmod時就應該能看到chrdev_init中使用printk打印出來的一個chrdev_init字符串,但是實際沒看到。原因是ubuntu中攔截了,要怎麼才能看到呢?在ubuntu中使用dmesg命令就可以看到了。

 

定義一個struct file_operation 結構體

file_operations結構體
(1)元素主要是函數指針,用來掛接實體函數地址,驅動源碼中提供真正的open、read、write、close等函數實體
(2)每個設備驅動都需要一個該結構體類型的變量
(3)設備驅動向內核註冊時提供該結構體類型的變量

 

註冊驅動程序register_chrdev   (#include <linux/fs.h>)

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)

(1)作用,驅動向內核註冊自己的file_operations
(2)參數
(3)inline和static

 

內核如何管理字符設備驅動
(1)內核中有一個數組用來存儲註冊的字符設備驅動
(2)register_chrdev內部將我們要註冊的驅動的信息(主要是 )存儲在數組中相應的位置
(3)cat /proc/devices查看內核中已經註冊過的字符設備驅動(和塊設備驅動)
(4)好好理解主設備號(major)的概念

註冊字符設備驅動
(1)爲何要註冊驅動
(2)誰去負責註冊
(3)向誰註冊
(4)註冊函數從哪裏來
(5)註冊前怎樣?註冊後怎樣?註冊產生什麼結果?

誰來調用驅動,驅動入口函數 first_drv_init()

修飾入口函數module_init(first_drv_init)

 

自定義一個file_operation結構體變量,並且去填充

 

static const struct file_operations test_fops = {
    .ower    = THIS_MODULE,
    .open    = test_chrdev_open,
    .release = test_chrdev_release,
};

原型int (*open) (struct inode *, struct file *);

寫test_chrdev_open函數

static int test_chrdev_open(struct inode *inode,struct file *file)
{
    printk(KERN_INFO "test_chrdev_open\n");

    return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "test_chredev_open\n");

    return 0;
}

在module_init 宏調用的函數中去註冊字符設備驅動register_chrdev

#define MYMAJOR    200

#define MYNAME      "testchar"

原型:static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)

//模塊安裝函數
static int __init chrdev_init(void)
{
    int ret = -1;

    printk(KERN INFO "chrdev init helloworld init\n");
    
    //在module_init宏調用的函數中去註冊字符設備驅動
    ret = register_chrdev(MYMAJOR, MYNAME, &test_fops);
    if(ret)
    {
        printk(KERN_ERR "register_chrdev fail\n");
        return -EINVAL;
    }
    printk(KERN_INFO "register_chrdev success\n");

    return 0;
}

在module exit宏調用的函數中去註銷字符設備驅動

static void __exit chrdev_exit(void)
{
    printk(KERN_INFO "chrdev_exit helloworld exit\n");

    //在module_exit 宏調用的函數中去註銷字符設備驅動

    unregister_chrdev(MYMAJOR,MYNAME);
}

 

驅動設備文件的創建
(1)何爲設備文件
(2)設備文件的關鍵信息是:設備號 = 主設備號 + 次設備號,使用ls -l去查看設備文件,就可以得到這個設備文件對應的主次設備號。
(3)使用mknod創建設備文件:mknod /dev/xxx c 主設備號 次設備號

 

寫應用來測試驅動

#include <stdio.h>

#define FILE "/dev/test"      //剛纔mknod 創建的設備文件名

int main(void)
{
    int fd = -1;
    

    fd = open(FILE, O_RDWR);
    if(fd < 0)
    {
        printf("open %s error.\n",FILE);
        return -1;
    }
    printf("open %s success\n",FILE);

//讀寫文件


//關閉文件
    
    return 0;
}

完整驅動:

 

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>

#define MYMAJOR		200
#define MYNAME		"testchar"

int mymajor;


static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 這個函數中真正應該放置的是打開這個設備的硬件操作代碼部分
	// 但是現在暫時我們寫不了這麼多,所以用一個printk打印個信息來做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	return 0;
}

// 自定義一個file_operations結構體變量,並且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 慣例,直接寫即可
	
	.open		= test_chrdev_open,			// 將來應用open打開這個設備時實際調用的
	.release	= test_chrdev_release,		// 就是這個.open對應的函數
};


// 模塊安裝函數
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏調用的函數中去註冊字符設備驅動
	// major傳0進去表示要讓內核幫我們自動分配一個合適的空白的沒被使用的主設備號
	// 內核如果成功分配就會返回分配的主設備好;如果分配失敗會返回負數
	mymajor = register_chrdev(0, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		printk(KERN_ERR "register_chrdev fail\n");
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);

	return 0;
}

// 模塊下載函數
static void __exit chrdev_exit(void)
{
	printk(KERN_INFO "chrdev_exit helloworld exit\n");
	
	// 在module_exit宏調用的函數中去註銷字符設備驅動
	unregister_chrdev(mymajor, MYNAME);
	
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx這種宏作用是用來添加模塊描述信息
MODULE_LICENSE("GPL");				// 描述模塊的許可證
MODULE_AUTHOR("aston");				// 描述模塊的作者
MODULE_DESCRIPTION("module test");	// 描述模塊的介紹信息
MODULE_ALIAS("alias xxx");			// 描述模塊的別名信息








 

在module_init宏調用的函數中去註冊字符設備驅動
major傳0進去表示要讓內核幫我們自動分配一個合適的空白的沒被使用的主設備號
內核如果成功分配就會返回分配的主設備好;如果分配失敗會返回負數

#include <linux/module.h>        // module_init  module_exit
#include <linux/init.h>            // __init   __exit

 

原型copy_from_user(void *to, const void __user *from, unsigned long n)

copy_from_user函數的返回值定義,和常規有點不同。返回值如果成功複製則返回0,如果 不成功複製則返回尚未成功複製剩下的字節數。

#include <asm/uaccess.h>                  //copy_to_user       copy_from_user

 

驅動中如何操控硬件
還是那個硬件
(1)硬件物理原理不變
(2)硬件操作接口(寄存器)不變
(3)硬件操作代碼不變
哪裏不同了?
(1)寄存器地址不同。原來是直接用物理地址,現在需要用該物理地址在內核虛擬地址空間相對應的虛擬地址。寄存器的物理地址是CPU設計時決定的,從datasheet中查找到的。

(2)編程方法不同。裸機中習慣直接用函數指針操作寄存器地址,而kernel中習慣用封裝好的io讀寫函數來操作寄存器,以實現最大程度可移植性。

 

內核中有2套虛擬地址映射方法:動態和靜態
(1)靜態映射方法的特點:
    內核移植時以代碼的形式硬編碼,如果要更改必須改源代碼後重新編譯內核
    在內核啓動時(開機)建立靜態映射表,到內核關機時銷燬,中間一直有效
    對於移植好的內核,你用不用他都在那裏
(2)動態映射方法的特點:
    驅動程序根據需要隨時動態的建立映射、使用、銷燬映射
    映射是短期臨時的

如何選擇虛擬地址映射方法
(1)2種映射並不排他,可以同時使用
(2)靜態映射類似於C語言中全局變量,動態方式類似於C語言中malloc堆內存
(3)靜態映射的好處是執行效率高,壞處是始終佔用虛擬地址空間;動態映射的好處是按需使用虛擬地址空間,壞處是每次使用前後都需要代碼去建立映射&銷燬映射(還得學會使用那些內核函數的使用)

 

關於靜態映射要說的
(1)不同版本內核中靜態映射表位置、文件名可能不同
(2)不同SoC的靜態映射表位置、文件名可能不同
(3)所謂映射表其實就是頭文件中的宏定義

 

三星版本內核中的靜態映射表
(1)主映射表位於:arch/arm/plat-s5p/include/plat/map-s5p.h
CPU在安排寄存器地址時不是隨意亂序分佈的,而是按照模塊去區分的。每一個模塊內部的很多個寄存器的地址是連續的。所以內核在定義寄存器地址時都是先找到基地址,然後再用基地址+偏移量來尋找具體的一個寄存器。
map-s5p.h中定義的就是要用到的幾個模塊的寄存器基地址。
map-s5p.h中定義的是模塊的寄存器基地址的虛擬地址。
(2)虛擬地址基地址定義在:arch/arm/plat-samsung/include/plat/map-base.h
#define S3C_ADDR_BASE    (0xFD000000)        // 三星移植時確定的靜態映射表的基地址,表中的所有虛擬地址都是以這個地址+偏移量來指定的
(3)GPIO相關的主映射表位於:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各個端口的基地址的定義
(4)GPIO的具體寄存器定義位於:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

 

靜態映射操作LED2
參考裸機中的操作方法添加LED操作代碼
(1)宏定義
(2)在init和exit函數中分別點亮和熄滅LED
實踐測試
(1)insmod和rmmod時觀察LED亮滅變化
(2)打印出寄存器的值和靜態映射表中的分析相對比
5.2.15.3、將代碼移動到open和close函數中去


靜態映射操作LED3
添加驅動中的寫函數
(1)先定義好應用和驅動之間的控制接口,這個是由自己來定義的。譬如定義爲:應用向驅動寫"on"則驅動讓LED亮,應用向驅動寫"off",驅動就讓LED滅
(2)應用和驅動的接口定義做的儘量簡單,譬如用1個字目來表示。譬如定義爲:應用寫"1"表示燈亮,寫"0"表示讓燈滅。
5.2.16.2、寫應用來測試寫函數
5.2.16.3、驅動和應用中來添加讀功能


5.2.17.動態映射操作LED
5.2.17.1、如何建立動態映射
(1)request_mem_region,向內核申請(報告)需要映射的內存資源。
(2)ioremap,真正用來實現映射,傳給他物理地址他給你映射返回一個虛擬地址
5.2.17.2、如何銷燬動態映射
(1)iounmap
(2)release_mem_region
注意:映射建立時,是要先申請再映射;然後使用;使用完要解除映射時要先解除映射再釋放申請。

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