linux驅動開發學習筆記1---字符設備驅動開發

1.字符設備驅動簡介

字符設備是linux驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先後順序的,比如我們最常見的點燈、按鍵、IIC、SPI、LCD等等都是字符設備,這些設備的驅動叫做字符設備驅動。

我記得除了字節流之外好像還有一個叫字符流的
沒錯百度了一下真的有,科普一下,順便給自己做個筆記
字節流在操作時本身不會用到緩衝區(內存),是文件本身直接操作的,而字符流在操作時使用了緩衝區,通過緩衝區再操作文件

在學習字符設備驅動架構之前,先簡單瞭解一下linux下的應用程序是如何調用驅動程序的,linux應用程序對驅動程序的調用如下:
在這裏插入圖片描述
在linux中一切都是文件,驅動加載成功之後會在/dev目錄下生成一個相應的文件,比如有一個/dev/led的驅動文件,此文件是led燈的驅動文件。應用程序使用open函數來打開文件/dev/led的驅動文件。使用完成之後使用close函數關閉/dev/led這個文件。如果要點亮或關閉 led,那麼就使用 write 函數來操作,也就是向此驅動寫入數據,這個數據就是要關閉還是要打開 led 的控制參數。如果要獲取led 燈的狀態,就用 read 函數從驅動中讀取相應的狀態。

==用戶空間不能對內核進行操作,所以要使用系統調用的方法來實現從用戶空間陷入到系統空間。==當我們調用open函數的時候,流程圖如下:
在這裏插入圖片描述
每一個系統調用,在驅動中都有與之相對應的一個驅動函數,在Linux內核文件include/linux/fs.h中,有一個叫做file_operations的結構體,就是linux內核驅動操作函數集合。如下:
在這裏插入圖片描述
1589行:owner擁有該結構體的模塊的指針,一般設置爲THIS_MODULE

第 1590 行, llseek 函數用於修改文件當前的讀寫位置。
第 1591 行, read 函數用於讀取設備文件。
第 1592 行, write 函數用於向設備文件寫入(發送)數據。
第 1596 行, poll 是個輪詢函數,用於查詢設備是否可以進行非阻塞的讀寫。
第 1597 行, unlocked_ioctl 函數提供對於設備的控制功能,與應用程序中的 ioctl 函數對應。
第 1598 行, compat_ioctl 函數與 unlocked_ioctl 函數功能一樣,區別在於在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是unlocked_ioctl。
第 1599 行, mmap 函數用於將將設備的內存映射到進程空間中(也就是用戶空間),一般幀緩衝設備會使用此函數,比如 LCD 驅動的顯存,將幀緩衝(LCD 顯存)映射到用戶空間中以後應用程序就可以直接操作顯存了,這樣就不用在用戶空間和內核空間之間來回複製。
第 1601 行, open 函數用於打開設備文件。
第 1603 行, release 函數用於釋放(關閉)設備文件,與應用程序中的 close 函數對應。
第 1604 行, fasync 函數用於刷新待處理的數據,用於將緩衝區中的數據刷新到磁盤中。
第 1605 行, aio_fsync 函數與 fasync 函數的功能類似,只是 aio_fsync 是異步刷新待處理的數據。
在字符設備驅動開發中最常用的就是上面這些函數,關於其他的函數大家可以查閱相關文檔。我們在字符設備驅動開發中最主要的工作就是實現上面這些函數,不一定全部都要實現,但是像 open、 release、 write、 read 等都是需要實現的,當然了,具體需要實現哪些函數還是要看具體的驅動要求

2.字符設備驅動開發步驟

2.1 驅動模塊的加載與卸載

linux驅動有兩種運行方式,第一種就是將驅動編譯進linux內核中,這樣當linux內核啓動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(linux下模塊拓展名爲.ko),在linux內核啓動以後使用insmod命令加載驅動模塊。在調試驅動的時候一般都選擇將其編譯爲模塊。

模塊有加載和卸載兩種操作,我們在編寫驅動的時候需要註冊這兩種操作函數,模塊的加載和卸載註冊函數如下:
module_init(xxx_init); //註冊模塊加載函數
module_exit(xxx_exit); //註冊模塊卸載函數
當使用“insmod”命令加載驅動的時候, xxx_init 這個函數就會被調用。
當使用“rmmod”命令卸載具體驅動的時候 xxx_exit 函數就會被調用。

字符設備驅動模塊加載和卸載模板如下:

/*驅動入口函數*/
static int __init xxx_init(void)		//定義了一個名爲xxx_init的驅動入口函數,用_init來修飾
{
	/*入口函數具體內容*/
	return 0;
}

/*驅動出口函數*/
static void __exit xxx_exit(void)  //定義了個名爲 xxx_exit 的驅動出口函數,並且使用了“__exit”來修飾。
{
	/*出口函數具體內容*/
}

/*將上面兩個函數指定爲驅動的入口和出口函數*/
module_init(xxx_init);  //調用函數 module_init 來聲明 xxx_init 爲驅動入口函數,當加載驅動的時候 xxx_init函數就會被調用。
module_exit(xxx_exit);  //調用函數module_exit來聲明xxx_exit爲驅動出口函數,當卸載驅動的時候xxx_exit函數就會被調用。

驅動編譯完成以後拓展名爲.ko

有兩種命令可以加載驅動模塊:insmod modprobe
insmod用來加載指定的.ko模塊,比如加載drv.ko這個驅動模塊,命令爲insmod drv.ko
但是insmod不能解決模塊的依賴關係,modprobe可以。比如說drv.ko 依賴 first.ko 這個模塊,使用insmod的時候就要先執行insmod first.ko 再執行insmod drv.ko

也有兩種命令卸載驅動模塊,比如要卸載drv.ko可以使用:
rmmod drv.ko
也可以使用
modprobe -r drv.ko
使用 modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊。所以對於模塊的卸載,還是推薦使用 rmmod 命令。

2.2 字符設備註冊與註銷

對於字符設備驅動而言,當驅動模塊加載成功以後需要註冊字符設備,卸載驅動模塊的時候也要註銷掉字符設備。字符設備的註冊和註銷函數原型如下:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函數用於註冊字符設備,此函數一共有三個參數,這三個參數的含義如下:
major: 主設備號, Linux 下每個設備都有一個設備號,設備號分爲主設備號和次設備號兩部分
name: 設備名字,指向一串字符串。
fops: 結構體 file_operations 類型指針,指向設備的操作函數集合變量。

unregister_chrdev 函數用戶註銷字符設備,此函數有兩個參數,這兩個參數含義如下:
major: 要註銷的設備對應的主設備號。
name: 要註銷的設備對應的設備名

一般字符設備的註冊在驅動模塊的入口函數 xxx_init 中進行,字符設備的註銷在驅動模塊的出口函數 xxx_exit 中進行。示例如下:

static struct file_operations test_fops;

/*驅動入口函數*/
static int __init xxx_init(void)
{
	/*入口函數具體內容*/
	int retvalue = 0;
	
	/*註冊字符設備驅動*/
	retvalue = register_chrdev(200,"chrtest",&test_fops);
	if(retvalue < 0)
	{
		/*字符設備註冊失敗,自行處理*/
	}
	return 0;
}

/*驅動出口函數*/
static void __exit xxx_exit(void)
{
/* 註銷字符設備驅動 */
unregister_chrdev(200, "chrtest");
}

/* 將上面兩個函數指定爲驅動的入口和出口函數 */
module_init(xxx_init);
module_exit(xxx_exit);

可能會有疑問,爲什麼知道上面函數register_chrdev中的設備號爲200,其實爲其他的也是可以,只要沒被使用,怎樣看呢?在secureCRT中輸入cat /proc/devices
在這裏插入圖片描述
後面還有一些,只不過沒有列出來,然後發現設備號200沒有被使用,所以就用這個。

2.3 實現設備的具體操作函數

file_operations 結構體就是設備的具體操作函數,在Linux內核文件include/linux/fs.h中,上面有說過哦。在上一個代碼片中,定義了file_operations結構體類型的變量test_fops,但是還沒對其進行初始化,也就是初始化其中的open、release、read和write等jvti的設備操作函數。在初始化test_fops之前我們要分析一下需求,也就是要對chrtest這個設備進行哪些操作。

1、能夠對 chrtest 進行打開和關閉操作
設備打開和關閉是最基本的要求,幾乎所有的設備都得提供打開和關閉的功能。因此我們需要實現 file_operations 中的 open 和 release 這兩個函數。
2、對 chrtest 進行讀寫操作
假設 chrtest 這個設備控制着一段緩衝區(內存),應用程序需要通過 read 和 write 這兩個函數對 chrtest 的緩衝區進行讀寫操作。所以需要實現 file_operations 中的 read 和 write 這兩個函數。

需求清晰之後,整個流程變成了下面這個樣子

/* 打開設備 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
	/* 用戶實現具體功能 */
	return 0;
}
/* 從設備讀取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
	/* 用戶實現具體功能 */
	return 0;
}
/* 向設備寫數據 */
static ssize_t chrtest_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt)
{
	/* 用戶實現具體功能 */
	return 0;
}

/* 關閉/釋放設備 */
static int chrtest_release(struct inode *inode, struct file *fil
{
	/* 用戶實現具體功能 */
	return 0;
}
static struct file_operations test_fops = {
		.owner = THIS_MODULE,
		.open = chrtest_open,
		.read = chrtest_read,
		.write = chrtest_write,
		.release = chrtest_release,
};
/* 驅動入口函數 */
static int __init xxx_init(void)
{
	/* 入口函數具體內容 */
	int retvalue = 0;
	
	/* 註冊字符設備驅動 */
	retvalue = register_chrdev(200, "chrtest", &test_fops);
	if(retvalue < 0){
	/* 字符設備註冊失敗,自行處理 */
	}
	return 0;
}

/* 驅動出口函數 */
static void __exit xxx_exit(void)
{
	/* 註銷字符設備驅動 */
	unregister_chrdev(200, "chrtest");
}

/* 將上面兩個函數指定爲驅動的入口和出口函數 */
module_init(xxx_init);
module_exit(xxx_exit);

其實我發現就是把file_operations結構體裏面的操作函數具體化

2.4 添加LICENSE和作者信息

最後我們需要在驅動中加入 LICENSE 信息和作者信息,其中 LICENSE 是必須添加的,否則的話編譯的時候會報錯,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下兩個函數:

MODULE_LICENSE() //添加模塊 LICENSE 信息
MODULE_AUTHOR() //添加模塊作者信息

這個添加到上面代碼片的最後。

3.linux設備號

3.1 設備號的組成

爲了方面管理,linux中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。linux提供了一個名爲dev_t的數據類型表示設備號,dev_t定義在文件include/linux/types.h中,定義如下:
在這裏插入圖片描述
可以看出 dev_t 是__u32 類型的,而__u32 定義在文件 include/uapi/asm-generic/int-ll64.h
typedef unsigned int __u32;
所以 dev_t就是unsigned int類型,是一個32位的數據類型。這32位數據構成了主設備號和次設備號兩部分,其中高12位爲主設備號,低20位爲次設備號。因此Linux系統中主設備號範圍爲 0~4095

在文件 include/linux/kdev_t.h 中提供了幾個關於設備號的操作函數(本質是宏),如下所示:

#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))

宏 MINORBITS 表示次設備號位數,一共是 20 位。
宏 MINORMASK 表示次設備號掩碼。
宏 MAJOR 用於從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可。
宏 MINOR 用於從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值即可。
宏 MKDEV 用於將給定的主設備號和次設備號的值組合成 dev_t 類型的設備號

3.2 設備號的分配

1、靜態分配設備號
前面講解字符設備驅動的時候說過了,註冊字符設備的時候需要給設備指定一個設備號,這個設備號可以是驅動開發者靜態的指定一個設備號,比如選擇 200 這個主設備號。有一些常用的設備號已經被 Linux 內核開發者給分配掉
了,具體分配的內容可以查看文檔 Documentation/devices.txt。並不是說內核開發者已經分配掉的主設備號我們就不能用了,具體能不能用還得看我們的硬件平臺運行過程中有沒有使用這個主設備號,使用“cat /proc/devices”命令即可查看當前系統中所有已經使用了的設備號。

2、動態分配設備號
靜態分配設備號很容易帶來衝突問題, Linux 社區推薦使用動態分配設備號,在註冊字符設備之前先申請一個設備號,系統會自動給你一個沒有被使用的設備號,這樣就避免了衝突。卸載驅動的時候釋放掉這個設備號即可,設備號的申請函數如下
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函數 alloc_chrdev_region 用於申請設備號,此函數有 4 個參數:
dev:保存申請到的設備號。
baseminor: 次設備號起始地址, alloc_chrdev_region 可以申請一段連續的多個設備號,這些設備號的主設備號一樣,但是次設備號不同,次設備號以 baseminor 爲起始地址地址開始遞增。一般 baseminor 爲 0,也就是說次設備號從 0 開始。
count: 要申請的設備號數量。
name:設備名字

註銷字符設備之後要釋放掉設備號,設備號釋放函數如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from:要釋放的設備號。
count: 表示從 from 開始,要釋放的設備號數量

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