Android本質上是基於Linux內核的系統,也就是說Android就是一種Linux操作系統。隻不過大多數時候都會運行在ARM架構的設備上,例如,Android手機、平板等。Android驅動實際上就是Linux驅動, 所以驅動可以安裝在Android模擬器、Android手機(需要root)或平板上(這些設備都要使用給予ARM架構的CPU),當然,使用傳統的GCC也可以編譯成X86架構的驅動(並不需要修改代碼),這樣也可以在Ubuntu Linux上安裝Linux驅動。
本文及後面幾篇文章主要介紹如何利用Android模擬器和S3C6410開發板開發給予ARM架構的Linux驅動,當然,測試的環境是Android,而不是我們通常使用的Ubuntu Linux等X86架構的系統。最後會介紹通過多種方式測試這個驅動,測試方法包括命令行、NDK、Android程序(Java代碼)等,當然,在最最後還會介紹如果將驅動嵌入到LInux內核中,這樣Android在啟動是就自動擁有瞭這個驅動。
想學習Android底層開發的童鞋可以通過本文完全掌握開發基於Android的LInux驅動的完整步驟。在《Android深度探索(卷1):HAL與驅動開發》隨書光盤上有完整的實驗環境(VMWare Ubuntu Linux12.04LTS),如果嫌自己配置麻煩,可以從光盤中復制該虛擬環境,虛擬文件太大(3.6G),傳不上去,隻能發文章瞭!
一、Linux驅動到底是個什麼東西
對於從未接觸過驅動開發的程序員可能會感覺Linux驅動很神秘。感覺開發起來會很復雜。其實這完全是誤解。實際上Linux驅動和普通的LinuxAPI沒有本質的區別。隻是使用Linux驅動的方式與使用Linux API的方式不同而已。
在學習Linux驅動之前我們先來介紹一下Linux驅動的工作方式。如果讀者以前接觸過Windows或其他非Unix體系的操作系統,最好將它們的工作方式暫時忘掉,因為這些記憶會幹擾我們理解Linux底層的一些細節。
Linux驅動的工作和訪問方式是Linux的亮點之一,同時受到瞭業界的廣泛好評。Linux系統將每一個驅動都映射成一個文件。這些文件稱為設備文件或驅動文件,都保存在/dev目錄中。這種設計理念使得與Linux驅動進行交互就像與普通文件進行交互一樣容易。當然,也比訪問LinuxAPI更容易。由於大多數Linux驅動都有與其對應的設備文件,因此與Linux驅動交換數據就變成瞭與設備文件交換數據。例如,向Linux打印機驅動發送一個打印命令,可以直接使用C語言函數open打開設備文件,再使用C語言函數ioctl向該驅動的設備文件發送打印命令。
當然,要編寫Linux驅動程序還需要更高級的功能。如向打印機驅動寫入數據時,對於打印機驅動來說,需要接收這些被寫入的數據,並將它們通過PC的並口、USB等端口發送給打印機。要實現這一過程就需要Linux驅動可以響應應用程序傳遞過來的數據。這就是Linux驅動的事件,雖然在C語言裡沒有事件的概念,但卻有與事件類似的概念,這就是回調(callback)函數。因此,編寫Linux驅動最重要的一步就是編寫回調函數,否則與設備文件交互的數據將無法得到處理。圖6-1是應用軟件、設備文件、驅動程序、硬件之間的關系。
二、編寫Linux驅動程序的步驟
Linux驅動程序與其他類型的Linux程序一樣,也有自己的規則。對於剛開始接觸Linux驅動開發的讀者可能對如何開發一個LInux驅動程序還不是太瞭解。為瞭解決這部分讀者的困惑,本節給出瞭編寫一個基本的Linux驅動的一般步驟。讀者可以按著這些步驟循序漸進地學習Linux驅動開發。
第1步:建立Linux驅動骨架(裝載和卸載Linux驅動)
任何類型的程序都有一個基本的結構,例如,C語言需要有一個入口函數main。Linux驅動程序也不例外。Linux內核在使用驅動時首先需要裝載驅動。在裝載過程中需要進行一些初始化工作,例如,建立設備文件,分配內存地址空間等。當Linux系統退出時需要卸載Linux驅動,在卸載的過程中需要釋放由Linux驅動占用的資源,例如,刪除設備文件、釋放內存地址空間等。在Linux驅動程序中需要提供兩個函數來分別處理驅動初始化和退出的工作。這兩個函數分別用module_init和module_exit宏指定。Linux驅動程序一般都都需要指定這兩個函數,因此包含這兩個函數以及指定這兩個函數的兩個宏的C程序文件也可看作是Linux驅動的骨架。
第2步:註冊和註銷設備文件
任何一個Linux驅動都需要有一個設備文件。否則應用程序將無法與驅動程序交互。建立設備文件的工作一般在第1步編寫的處理Linux初始化工作的函數中完成。刪除設備文件一般在第1步編寫的處理Linux退出工作的函數中完成。可以分別使用misc_register和misc_deregister函數創建和移除設備文件。
第3步:指定與驅動相關的信息
驅動程序是自描述的。例如,可以通過modinfo命令獲取驅動程序的作者姓名、使用的開源協議、別名、驅動描述等信息。這些信息都需要在驅動源代碼中指定。通過MODULE_AUTHOR、MODULE_LICENSE 、MODULE_ALIAS 、MODULE_DESCRIPTION等宏可以指定與驅動相關的信息。
第4步:指定回調函數
Linux驅動包含瞭多種動作,也可稱為事件。例如,向設備文件寫入數據時會觸發“寫”事件,Linux系統會調用對應驅動程序的write回調函數,從設備文件讀數據時會觸發“讀”事件,Linux系統會調用對應驅動程序的read回調函數。一個驅動程序並不一定要指定所有的回調函數。回調函數會通過相關機制進行註冊。例如,與設備文件相關的回調函數會通過misc_register函數進行註冊。
第5步:編寫業務邏輯
這一步是Linux驅動的核心部分。光有骨架和回調函數的Linux驅動是沒有任何意義的。任何一個完整的Linux驅動都會做一些與其功能相關的工作,如打印機驅動會向打印機發送打印指令。COM驅動會根據傳輸數率進行數據交互。具體的業務邏輯與驅動的功能有關。業務邏輯可能有多個函數、多個文件甚至是多個Linux驅動模塊組成。具體的實現讀者可以根據實際情況而定。
第6步:編寫Makefile文件
Linux內核源代碼的編譯規則是通過Makefile文件定義的。因此編寫一個新的Linux驅動程序必須要有一個Makefile文件。
第7步:編譯Linux驅動程序
Linux驅動程序可以直接編譯進內核,也可以作為模塊單獨編譯。
第8步:安裝和卸載Linux驅動
如果將Linux驅動編譯進內核,隻要Linux使用該內核,驅動程序就會自動裝載。如果Linux驅動程序以模塊單獨存在,需要使用insmod或modprobe命令裝載Linux驅動模塊,使用rmmod命令卸載Linux驅動模塊。
上面8步中的前5步是關於如何編寫Linux驅動程序的,通過後3步可以使Linux驅動正常工作。
三、編寫Linux驅動程序前的準備工作
本例的Linux驅動源代碼並未與linux內核源代碼放在一起,而是單獨放在一個目錄。首先使用下面的命令建立存放Linux驅動程序的目錄。
# mkdir –p /root/drivers/ch06/word_count
# cd /root/drivers/ch06/word_count
然後使用下面的命令建立驅動源代碼文件(word_count.c)
# echo '' > word_count.c
最後編寫一個Makefile文件,實際上這是6.2節介紹的編寫Linux驅動程序的第6步。當熟悉編寫Linux驅動程序的步驟後可以不按6.2節介紹的順序來編寫Linux驅動。
# echo 'obj-m := word_count.o' > Makefile
其中obj-m表示將Linux驅動作為模塊(.ko文件)編譯。如果使用obj-y,則將Linux驅動編譯進Linux內核。obj-m或obj-y需要使用“:=”賦值。如果obj-m或obj-y的值為word_count.o,表示make命令會把Linux驅動源代碼目錄中的word_count.c或word_count.s文件編譯成word_count.o文件。如果使用obj-m,word_count.o會被連接進word_count.ko文件,然後使用insmod或modprobe命令裝載word_count.ko。如果使用obj-y,word_count.o會被連接進built-in.o文件,最終會被連接進內核。其中built-in.o文件是連接同一類程序的.o文件生成的中間目標文件。例如,所有的字符設備驅動程序會最終生成一個built-in.o文件。讀者可以在<Linux內核源代碼目錄>/drivers/char目錄找到一個built-in.o文件。該目標文件包含瞭所有可連接進Linux內核的字符驅動(通過make menuconfig命令可以配置每一個驅動及其他內核程序是否允許編譯進內核,關於配置Linux內核的技術詳見4.2.4節介紹)。
如果Linux驅動依賴其他程序,如process.c、data.c。需要按如下方式編寫Makefile文件。
obj-m := word_count.o
word_count-y := process.o data.o
其中依賴文件要使用module-y或module-objs指定。module表示模塊名,如word_count。
四、編寫Linux驅動程序的骨架
現在編寫Linux驅動程序的骨架部分,也就是前面介紹的第1步。骨架部分主要是Linux驅動的初始化和退出函數,代碼如下:
[cpp]
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>
// 初始化Linux驅動
static int word_count_init(void)
{
// 輸出日志信息
printk("word_count_init_success\n");
return 0;
}
// 退出Linux驅動
static void word_count_exit(void)
{
// 輸出日志信息
printk("word_count_init_exit_success\n");
}
// 註冊初始化Linux驅動的函數
module_init(word_count_init);
// 註冊退出Linux驅動的函數
module_exit(word_count_exit);
在上面的代碼中使用瞭printk函數。該函數用於輸出日志信息(關於printk函數的詳細用法將在10.1節詳細介紹)。printk函數與printf函數的用法類似。有的讀者可能會有疑問,為什麼不用printf函數呢?這裡就涉及到一個Linux內核程序可以調用什麼,不可以調用什麼的問題。Linux系統將內存分為瞭用戶空間和內核空間,這兩個空間的程序不能直接訪問。printf函數運行在用戶空間,printk函數運行在內核空間。因此,屬於內核程序的Linux驅動是不能直接訪問printf函數的。就算包含瞭stdio.h頭文件,在編譯Linux驅動時也會拋出stdio.h文件沒找到的錯誤。當然,運行在用戶空間的程序也不能直接調用printk函數。那麼是不是用戶空間和內核空間的程序就無法交互瞭呢?答案是否定的。否則這兩塊內存不就成瞭孤島瞭嗎。運行在這兩塊內存中的程序之間交互的方法很多。其中設備文件就是一種主要的交互方式(在後面的章節還會介紹/proc虛擬文件的交互方式)。如果用戶空間的程序要訪問內核空間,隻要做一個可以訪問內核空間的驅動程序,然後用戶空間的程序通過設備文件與驅動程序進行交互即可。
看到這可能有的讀者疑問更大瞭。Linux驅動程序無法直接訪問運行在用戶空間的程序,那麼很多功能就都得自己實現瞭。例如,在C語言中會經常使用malloc函數動態分配內存空間,該函數在Linux驅動程序中是無法使用的。那麼如何在Linux驅動程序中動態分配內存空間呢?解決類似的問題也很簡單。既然Linux驅動無法直接調用運行在用戶空間的函數,那麼在Linux內核中就必須要提供替代品。讀者可以進入<Linux內核源代碼>/include目錄,該目錄的各個子目錄中包含瞭大量的C語言頭文件。這些頭文件中定義的函數、宏等資源就是運行在用戶空間的程序的替代品。運行在用戶空間的函數庫對應的頭文件在/usr/include目錄中。剛才提到的malloc函數在內核空間的替代品是kmalloc(需要包含slab.h頭文件,#include <linux/slab.h>)。
註意:用戶空間與內核空間完成同樣或類似功能的函數、宏等資源的名稱並不一定相同,有的名稱類似,如malloc和kmalloc,有的完全是兩個不同的名字:如atoi(用戶空間)和simple_strtol(內核空間)、itoa(用戶空間)和snprintf(內核空間)。讀者在使用內核相關資源時要註意在一點。
如果讀者想看看前面編寫的程序的效果,可以使用下面的命令編譯Linux驅動源代碼(X86架構)。
# make -C /usr/src/linux-headers-3.0.0-15-generic M=/root/drivers/ch06/word_count
在測試Linux驅動未必一定在Android設備上完成。因為Android系統和Ubuntu Linux以及其他Linux發行版本都是基於Linux內核的,大多數Linux驅動程序可以在Ubuntu Linux或其他Linux發行版上測試完再重新用交叉編譯器編譯成基於ARM架構的目標文件,然後再安裝到Android上即可正常運行。由於編譯Linux內核源代碼需要使用Linux內核的頭文件。為瞭在Ubuntu Linux上測試驅動程序,需要使用-C命令行參數指定Linux內核頭文件的目錄(/usr/src/linux-headers-3.0.0-15-generic)。其中linux-headers-3.0.0-15-generic目錄是Linux內核源代碼目錄,在該目錄中隻有include子目錄有實際的頭文件,其他目錄隻有Makefile和其他一些配置文件,並不包含Linux內核源代碼。該目錄就是為瞭開發當前Linux內核版本的驅動及其他內核程序而提供的(因為在編譯Linux驅動時生成目標文件隻需要頭文件,在進行目標文件鏈接時隻要有相關的目標文件即可,並不需要源代碼文件)。如果以模塊方式編譯Linux驅動程序,需要使用M指定驅動程序所在的目錄(M= root/drivers/ch06/word_count)。
註意:如果讀者使用的Linux發行版采用瞭其他Linux內核,需要為-C命令行參數設置正確的路徑。
執行上面的命令後,會輸出如圖6-2所示信息。從這些信息可以看出,已經將word_count.c文件編譯成瞭Linux驅動模塊文件word_count.ko。
使用ls命令列出/root/drivers/ch06/word_count目錄中的文件後發現,除瞭多瞭幾個.o和.ko文件,還多瞭一些其他的文件,如圖6-3所示。這些文件是有編譯器自動生成的,一般並不需要管這些文件的內容。
本文編寫的Linux驅動程序雖然什麼實際的功能都沒有,但已經可以作為驅動程序安裝在Linux內核空間瞭。讀者可以使用下面的命令安裝、查看、卸載Linux驅動,也可以查看由驅動程序輸出的日志信息(執行下面命令時需要先進入word_count目錄)。
安裝Linux驅動
# insmod word_count.ko
查看word_count是否成功安裝
# lsmod | grep word_count
卸載Linux驅動
# rmmod word_count
查看由Linux驅動輸出的日志信息
# dmesg | grep word_count | tail –n 2
執行上面的命令後,如果輸出如圖6-4所示的信息說明讀者已成功完成本節的學習,可以繼續看下一節瞭。
dmesg命令實際上是從/var/log/messages(Ubuntu Linux 10.04)或/var/log/syslog(Ubuntu Linux11.10)文件中讀取的日志信息,因此也可以執行下面的命令獲取由Linux驅動輸出的日志信息。
# cat /var/log/syslog | grep word_count | tail –n 2
執行上面的命令後會輸出更多的信息,如圖6-5所示。