嵌入式linux與物聯網進階之路四:嵌入式驅動開發思路

前言

荔枝派nano這塊板子,從本章開始,將會發揮它最大的價值,藉由它來帶領我們進入嵌入式linux驅動開發的大門。

想必大家在玩linux類型的板子之前應該或多或少的都嘗試過其他類型的板載系統的開發,諸如裸跑C語言程序的51單片機;基於Arduino開發套件的ESP8266,亦或是一直大火的STM32等等,不一而足。雖然說芯片不一樣,開發模式不一樣,但是從中或多或少我們能夠窺見針對不同芯片的驅動開發思路,哪怕是一個小小的LED等,在這些不同芯片上的驅動方式或多或少都有相同或者不同的地方。

而基於linux平臺的嵌入式驅動開發,則和上面幾種驅動開發模式,都不太一樣。

我們知道,在進行linux內核編譯的時候,我們可以看到有很多文件,然後通過設置arm平臺框架和交叉編譯鏈等選項,來使得這些源碼文件構建成鏡像文件。但是假如我們是設備製作商,現在設計了一款基於linux的板子,也針對linux做了定製化設計並提供了一些擴展提供給用戶,其實最簡單的方式,無非就是我寫好了驅動文件,然後放到內核中和內核文件一起編譯,然後燒寫到板子上。這種方式非常簡便,當需求設計不多的時候,這種做法更直接有效,但是一旦需要實現的驅動設計需求較多而且改動頻繁的時候,這種思想顯然是費時費力的。

在軟件架構領域,目前一直在推崇微服務化等思想,其核心目的就是讓服務儘可能的小,儘可能的去中心化,然後來實現各個模塊的解耦。而這種思想,其實對於嵌入式linux驅動開發來說,也具有相當強的影響力。與其將驅動和內核綁定在一起,那麼有沒有什麼機制,讓驅動實現和內核剝離,驅動可以隨意實現,內核只需要加載驅動就行了呢?

答案是肯定的。而且這個設計思想很早就在linux系統上進行了實現,而且保留至今。

在Linux系統中,主要將存儲器和外設分爲三種類型,即:字符設備、塊設備和網絡設備。其中字符設備是指那些必須以串行順序依次進行訪問的設備,比如觸摸屏,鼠標等等。塊設備則可以按照任意順序進行訪問,以塊爲單位進行操作,比如硬盤,eMMC等。字符設備和塊設備的驅動設計有很大的差別,但是對於用戶而言,他們都要使用文件系統的操作接口,比如open(),close(),read(),write()等進行訪問。而網絡設備是面向數據包的傳輸設計的,並不傾向於對應於文件系統的節點。

接下來,針對這三種類型,說下統一的嵌入式開發思路吧。

linux內核模塊結構

一個Linux內核模塊主要由以下幾個部分組成

 

1. 模塊加載函數

當通過insmode或者modprobe命令加載內核模塊時,模塊的加載函數會自動被內核執行,完成本模塊的相關初始化工作。

Linux內核模塊加載函數,一般以__init標記來聲明,典型的模塊加載函數形式如下:

static int __init hello_init(void)
{
        printk(KERN_EMERG "hello, init\n");
        return 0;
}
module_init(hello_init);

模塊加載函數以“module_init(函數名)”的形式被指定。它返回int型。若初始化成功,返回0,初始化失敗時,則返回相應的錯誤編碼。在Linux內核中,錯誤編碼是一個接近於0的負值。

在Linux內核中,可以使用request_module函數加載內核模塊,驅動開發人員可以通過調用如下代碼:

request_module(module_name);

來靈活的加載其他模塊。

在Linux中,所有標識爲__init的函數,如果直接編譯進入內核,成爲內核鏡像的一部分,在連接的時候,都會放在.init.text這個區段內。

 

2. 模塊卸載函數

當通過rmmod命令卸載某模塊時,模塊的卸載函數會自動被內核執行,完成與模塊卸載函數相反的功能。

典型代碼如下:

static void __exit hello_exit(void)
{
        printk(KERN_EMERG "hello, exit\n");
}
module_exit(hello_exit);

模塊卸載函數在模塊卸載的時候執行,而不返回任何職,且必須以"module_exit"的形式來指定。通常來說,模塊卸載函數需要完成與模塊加載函數相反的功能。

我們用__exit來修飾模塊卸載函數,可以告訴內核如果相關的景象被直接編譯進內核(即built-in),則cleanup_function函數會被忽略,直接不鏈接進最後的景象,既然模塊被內置了,就不可能卸載它了,卸載函數也就沒有存在的必要了。

 

3. 模塊許可證聲明

許可證(LICENSE)聲明描述內核模塊的許可權限,如果不生命LICENSE,模塊被加載時,將收到內核被污染(Kenrel Tainted)的警告。在Linux內核模塊領域,可以接受的LICENSE包括"GPL","GPL v2","GPC and additional rights","Dual BSD/GPL","Dual MPL/GPL"和"Proprietary"(關於模塊是否可以採用非GPL許可權,如"Proprietary",這個在學術界和法律界都有爭議)。大多數情況下,內核模塊應該遵循GPC兼容協議許可,Linux內核模塊最常見的是以MODULE_LICENSE(“GPL v2”)語句聲明模塊來表明採用GPL v2許可。

 

4. 模塊參數(可選)

模塊參數是模塊被加載的時候可以傳遞給它的值,它本身對應模塊內部的全局變量。

我們可以用“module_param(參數名,參數類型,參數讀寫權限)”爲模塊定義一個參數,例如下列diamante定義了1個整型參數和1個字符指針參數:

static char * book_name  = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

參數類型可以是byte, short, ushort, int, uint, long, ulong, charp, bool或者invbool(布爾的反),在模塊編譯時,會將module_param中聲明的類型與變量定義的類型進行比較,判斷是否一致。

除此之外,模塊也可以擁有參數數組,形式爲"module_param_array"(數組名,數組類型,數組長,參數讀寫權限).

 

5. 模塊導出符號(可選)

內核模塊可以導出的符號(symbol,對應於函數或者變量),若導出,其他模塊則可以使用本模塊中的變量或者函數。

Linux的"/proc/kallsyms"文件對應着內核符號表,它記錄了符號以及符號所在的內存地址。

模塊可以使用如下宏導出符號到內核符號表中:

EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);

導出的符號可以被其他模塊使用,只需要使用前提前聲明一下即可。

 

6. 模塊作者等信息聲明(可選)

在Linux內核模塊中,我們可以使用MODULE_AUTHOR,MODULEDESCRIPTION,MODULE_VERSION,MODULE_DEVICE_TABLE,MODULE_ALIAS分別標明模塊作者,描述,版本,設備表和別名,例如:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("feixiaoxing");
MODULE_DESCRIPTION("This is just a hello module!\n");

對於USB,PCI等設備驅動,通常會創建一個MODULE_DEVICE_TABLE,以標明該驅動模塊所支持的設備。

模塊編譯

看完了模塊的組成,這裏我們以一個簡單的Hello.c的例子來展開,代碼如下:

#include <linux/init.h>
#include <linux/sched.h>
#include <linux/module.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cxsr");
MODULE_DESCRIPTION("a hello module!\n");
 
static int __init hello_init(void)
{
        printk(KERN_EMERG "hello, init\n");
        return 0;
}
 
static void __exit hello_exit(void)
{
        printk(KERN_EMERG "hello, exit\n");
}
 
module_init(hello_init);
module_exit(hello_exit);

之後我們在源碼同級目錄創建一個Makefile,注意名稱大小寫, 內容如下:

ifneq ($(KERNELRELEASE),)
obj-m := hello.o
 
else
PWD  := $(shell pwd)
KDIR := /home/scy/linux-mi/linux-f1c100s-480272lcd-test/
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux-gnueabi- ARCH=arm
clean:
	rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
endif

需要特別注意的是, all命令和clean命令,下面的命令行,前面一定要TAB縮進,不能是空格縮進,如果是空格縮進,會報如下的錯誤:

Makefile:8: *** missing separator. Stop.

這個時候,我們可以利用如下命令打開Makefile,然後刪掉命令行前面的空格,用TAB替代即可:

nano Makefile

這裏還需要注意的就是,KDIR目錄,一定是我們編譯到板子上的linux內核的目錄,而all命令中的交叉編譯鏈,必須是我們進行linux內核編譯的交叉編譯鏈,否則將會導致模塊不能被加載。

完畢之後,在終端輸入如下命令:

make

此時,就可以看到模塊被製作:

同時,可以看到,原來目錄下有倆文件,現在有了多個:

這裏,我們需要拷貝到板子上的文件就是hello.ko文件了。

 

ko文件拷貝與安裝

文件拷貝,我們可以使用minicom,也可以直接手動拷貝,由於我這裏比較懶,就手動拷貝了。

插上usb插卡器,然後執行如下命令,進行拷貝:

sudo mkdir /mnt/sdb2             //創建一個臨時目錄
sudo mount /dev/sdb2 /mnt/sdb2   //將sdb2掛載到此臨時目錄
sudo cp hello.ko /mnt/sdb2/media //拷貝到sdb2/media目錄下
sudo sync
sudo umount /dev/sdb2

之後,我們就可以拔掉tf卡,插到開發板上,進入media目錄,進行模塊的安裝:

可以看到,我們寫的簡單的字符驅動成功打印出來了。

如果出現如下錯誤:

	 insmod: can't insert 'hello.ko': invalid module format

此種錯誤,則表明內核版本和makefile使用的內核版本不一致,一定要保證板子的linux內核版本和Makefile中的內核版本一致且交叉編譯鏈一致,即可。

參考

《Linux設備驅動開發詳解:基於最新的Linux 4.0內核》

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