Linux驅動開發(3)------- 字符設備驅動高級


一,註冊字符設備驅動新接口


1、新接口與老接口
(1)老接口:register_chrdev
(2)新接口:register_chrdev_region/alloc_chrdev_region + cdev

2、cdev介紹
(1)結構體

struct cdev {
   struct kobject kobj;          // 內嵌的內核對象,每個 cdev 都是一個 kobject
   struct module *owner;       // 指向實現驅動的模塊
   const struct file_operations *ops;   // 操縱這個字符設備文件的方法
   struct list_head list;       // 與 cdev 對應的字符設備文件的 inode->i_devices 的鏈表頭,用來將已經向內核註冊的所有字符設備形成鏈表
   dev_t dev;                   // 字符設備的設備號,由主設備號和次設備號構成
   unsigned int count;       // 隸屬於同一主設備號的次設備號的個數.
};

(2)模塊加載函數通過 register_chrdev_region( ) 或 alloc_chrdev_region( )來靜態或者動態獲取設備號;

通過 cdev_init( ) 建立cdev與 file_operations之間的連接,通過 cdev_add( ) 向系統添加一個cdev以完成註冊;

模塊卸載函數通過cdev_del( )來註銷cdev,通過 unregister_chrdev_region( )來釋放設備號;

3、設備號
(1)設備號 = 主設備號 + 次設備號
(2)dev_t類型
(3)MKDEV(MAJOR, MINOR);
說明: 獲取設備在設備表中的位置。
MAJOR 主設備號
MINOR 次設備號

4、編程實踐
(1)使用register_chrdev_region + cdev_init + cdev_add進行字符設備驅動註冊

全局變量

#define MYMAJOR		200
#define MYCNT		1
#define MYNAME		"testchar"

static dev_t mydev;
static struct cdev test_cdev;

註冊驅動

	// 新的接口註冊字符設備驅動需要2步
	
	// 第1步:靜態註冊/分配主次設備號
	int retval;
	mydev = MKDEV(MYMAJOR, 0);
	retval = register_chrdev_region(mydev, MYCNT, MYNAME);
	if (retval)
	{
		printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev_region success\n");
	
	// 第2步:註冊字符設備驅動
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) 
	{
		printk(KERN_ERR "Unable to cdev_add\n");
		return -EINVAL;
	}
	printk(KERN_INFO "cdev_add success\n");

註銷驅動

// 使用新的接口來註銷字符設備驅動
	// 註銷分2步:
	// 第一步真正註銷字符設備驅動用cdev_del
	cdev_del(&test_cdev);
	// 第二步去註銷申請的主次設備號
	unregister_chrdev_region(mydev, MYCNT);

5、使用alloc_chrdev_region自動分配設備號
(1)register_chrdev_region是在事先知道要使用的主、次設備號時使用的;要先查看cat /proc/devices去查看沒有使用的。
(2)更簡便、更智能的方法是讓內核給我們自動分配一個主設備號,使用alloc_chrdev_region就可以自動分配了
(3)自動分配的設備號,我們必須去知道他的主次設備號,否則後面沒法去mknod創建他對應的設備文件
(4)使用MAJOR宏和MINOR宏從dev_t得到major和minor
(5)反過來使用MKDEV宏從major和minor得到dev_t。
(6)使用這些宏的代碼具有可移植性

	#define MYCNT		1
	#define MYNAME		"testchar"
	static dev_t mydev;
	static struct cdev test_cdev;


	//自動分配主次設備號
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}
	printk(KERN_INFO "alloc_chrdev_region success\n");
	printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));  //獲取我們的主次設備號,用於創建設備文件
	

6、中途出錯的倒影式錯誤處理方法
(1)內核中很多函數中包含了很多個操作,這些操作每一步都有可能出錯,而且出錯後後面的步驟就沒有進行下去的必要性了。所以就有了倒影式處理錯誤的方法。
舉例代碼

// 第1步:分配主次設備號
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}

// 第2步:註冊字符設備驅動
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		goto flag2;
	}

// 第3步:使用動態映射的方式來操作寄存器
	if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
		goto flag3;

// 如果第3步纔出錯跳轉到這裏來	
flag3:
	release_mem_region(GPJ0CON_PA, 4);
	
// 如果第2步纔出錯跳轉到這裏來
flag2:
	cdev_del(&test_cdev);

// 如果第1步纔出錯跳轉到這裏來
flag1:
	// 在這裏把第1步做成功的東西給註銷掉
	unregister_chrdev_region(mydev, MYCNT);

還需要注意的:使用cdev_alloc,cdev_init的替代

在這裏插入圖片描述


二,自動創建字符設備驅動的設備文件


1、解決方案:udev(嵌入式中用的是mdev)
(1)什麼是udev?應用層的一個應用程序
(2)內核驅動和應用層udev之間有一套信息傳輸機制(netlink協議)
(3)應用層啓用udev,內核驅動中使用相應接口
(4)驅動註冊和註銷時信息會被傳給udev,由udev在應用層進行設備文件的創建和刪除

2、內核驅動設備類相關函數
(1)class_create
(2)device_create
(3)device_destroy
(4)class_destroy

3、編程實踐

#include <linux/device.h>  //相關的函數包含在這個頭文件裏面

static dev_t mydev;
static struct class *test_class;

	// 註冊字符設備驅動完成後,添加設備類的操作,以讓內核幫我們發信息
	// 給udev,讓udev自動創建和刪除設備文件
	test_class = class_create(THIS_MODULE, "aston_class");
	if (IS_ERR(test_class))
		return -EINVAL;
		
	// 最後1個參數字符串,就是我們將來要在/dev目錄下創建的設備文件的名字
	// 所以我們這裏要的文件名是/dev/test111
	device_create(test_class, NULL, mydev, NULL, "test111");

	//在註銷設備驅動之前
	device_destroy(test_class, mydev);
	class_destroy(test_class);

在這裏插入圖片描述


三,設備類相關代碼分析


1、因爲udev需要sysfs文件系統的支持(sysfs文件系統只在linux-2.6內核中才有),所以它存在於Linux-2.6版本之後的內核。udev藉助於netlink協議在內核驅動和應用層之間傳遞信息。當內核中的驅動完成註冊和註銷時,信息會被傳送給應用層的udev,udev便會自動地完成設備文件的創建和刪除。

內核中定義了struct class結構體,顧名思義,一個struct class結構體類型變量對應一個類, 內核同時提供了class_create(…)函數,可以用它來創建一個類,這個類存放於sysfs下面,一旦創建好了這個類,再調用 device_create(…)函數來在/dev目錄下創建相應的設備節點。這樣,加載模塊的時候,用戶空間中的udev會自動響應 device_create(…)函數,去/sysfs下尋找對應的類從而創建設備節點。

在這裏插入圖片描述

2、
(1)class_creat樹形調用的主要的函數

class_create();
	__class_create();
		__class_register();
			kset_register();
				kobject_uevent();

(2)device_createt樹形調用的主要的函數

	device_create_vargs();
		kobject_set_name_vargs();
		device_register();
			device_add();
				kobject_add();
					device_create_file();
					device_create_sys_dev_entry();
					devtmpfs_create_node();
					device_add_class_symlinks();
					device_add_attrs();
					device_pm_add();
					kobject_uevent();

四,靜態映射表建立過程分析


1、建立映射表的三個關鍵部分
(1)映射表具體物理地址和虛擬地址的值相關的宏定義

在這裏插入圖片描述

(2)映射表建立函數。該函數負責由(1)中的映射表來建立linux內核的頁表映射關係。
kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函數

smdkc110_map_io();
	s5p_init_io();
		iotable_init();

結論:經過分析,真正的內核移植時給定的靜態映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本質是一個結構體數組,數組中每一個元素就是一個映射,這個映射描述了一段物理地址到虛擬地址之間的映射。這個結構體數組所記錄的幾個映射關係被iotable_init所使用,該函數負責將這個結構體數組格式的表建立成MMU所能識別的頁表映射關係,這樣在開機後可以直接使用相對應的虛擬地址來訪問對應的物理地址。

在這裏插入圖片描述

(3)開機時調用映射表建立函數
問題:開機時(kernel啓動時)smdkc110_map_io怎麼被調用的?
函數調用層級

start_kernel();
	setup_arch();
		paging_init();
			devicemaps_init();
			
if (mdesc->map_io)
		mdesc->map_io();


五,動態映射結構體方式操作寄存器


(1)仿效真實驅動中,用結構體封裝的方式來進行單次多寄存器的地址映射。

實驗代碼

typedef struct GPJ0REG
{
	volatile unsigned int gpj0con;
	volatile unsigned int gpj0dat;
}gpj0_reg_t;

#define GPJ0_REGBASE	0xe0200240    //物理地址
gpj0_reg_t *pGPJ0REG;

// 使用動態映射的方式來操作寄存器
	if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
		return -EINVAL;
	pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
	// 映射之後用指向結構體的指針來進行操作
	// 指針使用->結構體內元素的方式來操作各個寄存器
	pGPJ0REG->gpj0con = 0x11111111;
	pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));		// 亮
	
// 解除映射
	iounmap(pGPJ0REG);
	release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));

使用內核提供的讀寫寄存器接口

1、內核提供的寄存器讀寫接口
(1)writel和readl
(2)iowrite32和ioread32

2、代碼實踐

#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT

#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)

#define GPJ0CON_PA	0xe0200240
#define GPJ0DAT_PA 	0xe0200244

#define S5P_GPJ0REG(x)		(x)
#define S5P_GPJ0CON			S5P_GPJ0REG(0)
#define S5P_GPJ0DAT			S5P_GPJ0REG(4)

unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;

static void __iomem *baseaddr;			// 寄存器的虛擬地址的基地址


// 使用動態映射的方式來操作寄存器
	if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
		return -EINVAL;
	if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
		return -EINVAL;
	
	pGPJ0CON = ioremap(GPJ0CON_PA, 4);
	pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

/************/  原始的用解引用指針的方法  /***********/
	*pGPJ0CON = 0x11111111;         
	*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮

/***********/ 使用內部讀寫接口的方法  /***********/

 測試1:用2次ioremap得到的動態映射虛擬地址來操作,測試成功
writel(0x11111111, pGPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
	
測試2:用靜態映射的虛擬地址來操作,測試成功
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
	
測試3:用1次ioremap映射多個寄存器得到虛擬地址,測試成功
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
		return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
	
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章