1.總結
從事嵌入式行業多年,雖然因為工作原因接觸過嵌入式Linux,也參與過相關產品的底層和應用功能開發,但對於嵌入式Linux的內核,驅動,以及上層開發,仍然停留在初級的水平,沒有過系統深入的去總結整理,隨着工作年限的遞增,越來越感受到這種浮躁感帶來的技術面瓶頸。既然發現了問題,自然就要去解決,回想起我踏入嵌入式行業來的經歷,正是對STM32芯片以及網絡部分的學習總結筆記支撐我走到如今的地步,那么沉淀下來,從嵌入式Linux入門開始整理,層層深入,對嵌入式Linux進行系統的總結也是最符合我目前現狀的解決辦法,這也是我下定決心放棄日常娛樂,開始本系列的由來。
嵌入式Linux的掌握學習是很復雜的過程,從最基礎的Linux安裝,shell指令的學習和應用,交叉編譯環境搭建,C語言開發,Linux內核接口,Linux系統接口,在掌握了前面所有知識后,才只是完成了產品開發的基礎構建,這些知識不僅對於學習是難點,對於已經掌握的人來用文字描述清楚,特別是系統/軟件版本引發的編譯,調試問題,如果沒有總結和整理,這部分經驗是文字很難描述的,嵌入式Linux是一門應用開發技術,多練多總結才能積累足夠的知識。另外如果遇到問題,不要着急,要善於使用搜索引擎,嵌入式Linux開發遇到的問題基本都能找到答案,但找到解決方法只是目的之一,如何從這些方法中總結經驗,也是學習中的重要部分,這部分對於開發者更加重要,切記!這是我做嵌入式軟件開發來最重要的經驗。按照正常的預期流程,嵌入式Linux的學習應該是講如何注冊字符型設備,然后按照從易到難的順序在掌握中斷和時鍾,文件系統,塊設備,I2C驅動,LCD驅動,攝像頭驅動,網絡設備驅動,設備樹,然后在講述涉及上層的QT界面,遠程訪問的網絡socket(B/S, C/S框架),以及應用端的Android平台開發,多線程,多進程同步等知識,這也是大部分開發板的例程方案,可從我經驗來看,如果按照上面的流程是可以覆蓋嵌入式Linux的主要工作需求的(可能部分知識是溢出的)。但是對於開發產品來說,這些只是基礎的技術,而不是應用的產品方案,事實上,對於剛入門的來說,如何從學習思維轉變為工程師開發思維這部分更加重要,從更高維的角度了解嵌入式Linux開發,這也是本系列的目的。我們先制定一個產品目標(可能不符合現有的產品模型),所有學習都圍繞着此產品來開發。這個系列將不僅僅講述學習嵌入式,而且也講述我根據工作積累的開發經驗,如何完成項目,也方便未踏入行業的人員什么是嵌入式軟件開發。
題目1:基於串口(RS485/RS232)的局域網管理設備
系統架構
硬件說明
正點原子的I.MX6U-ALPHA開發平台,256MB(DDR3)+256MB/512MB(NAND)核心板。涉及硬件 RS232,GPIO,I2C,SPI, ADC, DAC
學習筆記章節
嵌入式Linux學習筆記(二) 交叉編譯環境和Linux系統編譯、下載
嵌入式Linux學習筆記(三) 字符型設備驅動--LED的驅動開發
嵌入式Linux學習筆記(五) 通訊協議制定和下位機代碼實現
嵌入式Linux學習筆記(六) 上位機QT界面實現和通訊實現代碼路徑
詳細代碼見:https://github.com/zc110747/remote_manage
軟件說明
1.上位機軟件支持串口通訊,雙機通訊需要制定協議(可使用自定義協議或者Modbus),支持界面化管理(目前定義使用QT開發, 與后續的完善計划有關)2.支持文件傳輸,文件傳輸支持斷點重傳(傳輸后文件位於指定文件夾,初步定義為/usr/download)
3.能夠查詢內部的一些數據,除顯示已經列出狀態外,支持后期擴展查詢其他狀態
任務分解
1. uboot,內核和文件系統的編譯,下載和調試,並集成ssh方便傳輸應用文件調試
2. 分模塊完成驅動的開發調試,不過為了方便測試及后期集成,需要同步完成串口驅動,串口通訊協議定義及上位機的軟件框架
3. 后期的綜合性功能調試和應用開發(如協議擴展問題,狀態查詢到界面顯示,考慮到協議數據的復用, 后期該數據可能用於網頁界面的狀態顯示或者QT界面的控制)
參考資料
1. 宋寶華《Linux設備驅動開發詳解 -- 基於最新的Linux4.0內核》第四章 Linux內核模塊
內核模塊初探
本節作為整個系列的起點,重點當然是上面的項目規划和任務分解,不過為了讓文章更豐富,我們可以初步體驗下Linux下的應用和編程,下面代碼將執行在Ubuntu系統,PC端,事實上PC端的Ubuntu可以驗證很多實現,如加載驅動和設備,實現QT界面,進行網絡通訊的應用端測試,所以一定不要忽略這個優勢,本小節的代碼都是在PC端測試完成,用於體驗內核模塊開發的特征。作為內核模塊,可以通關Kernel編譯時加入到內核中,也可以通過insmod/rmmod動態的加載到系統中,為了滿足Linux系統的訪問,內核模塊就需要實現接口用於Linux訪問,開發者只要按照規則用C語言實現這些需要的接口,在按照一定的規則編譯后,就可以使用lsmod/rmmod來加載和移除自定義的模塊,這套規則就是我們掌握內核模塊需要學習的知識,按照功能分為以下接口:
必須模塊
模塊加載函數:module_init(func)
模塊卸載函數: module_exit(func)
模塊許可聲明:MODULE_LICENSE("xxx") 支持的許可有: "GPL", "GPL V2", "GPL and additional right", "Dual BSCD/GPL", "DUAL MPL/GPL", "Proprietary"
可選模塊
模塊參數 -- 模塊加載時傳遞變量 module_param(name, charp, S_IRUGO);
模塊導出符號 --用於將符號導出,用於其它內核模塊使用。
EXPORT_SYSMBOL(func)/EXPORT_SYSMBOL_GPL(func)
注意:Linux內核2.6增加了函數校驗機制,后續模塊需要引入時要在Module.symvers下添加導入函數內核的路徑和symbol。
模塊作者 -- MODULE_AUTHOR("xxx")
模塊描述 -- MODULE_DESCRIPTION("xxx")
模塊版本 -- MODULE_VERSION("xxx")
模塊別名 -- MODULE_ALIAS("xxx")
模塊設備表 -- MODULE_DEVICE_TABLE, 對於USB或者PCI設備需要支持,表示支持的設備,這部分比較復雜,這里就不在多說,后續如果用到,在詳細去說明。
在了解上述模塊的基礎上,就可以實現如下的模塊代碼:
1 //hello.ko 2 #include <linux/init.h> 3 #include <linux/module.h> 4 5 6 //extern int add_integar(int a, int b); 7 static char *buf = "driver"; 8 module_param(buf, charp, S_IRUGO); //模塊參數 9 10 static int __init hello_init(void) 11 { 12 int dat = 3; //int dat = add_integar(5, 6); 13 printk(KERN_WARNING "hello world enter, %s, %d\n", buf, dat); 14 return 0; 15 } 16 module_init(hello_init); //模塊加載函數 17 18 static void __exit hello_exit(void) 19 { 20 printk(KERN_WARNING "hello world exit\n"); 21 } 22 module_exit(hello_exit); //模塊卸載函數 23 24 MODULE_AUTHOR("ZC"); //模塊作者 25 MODULE_LICENSE("GPL v2"); //模塊許可協議 26 MODULE_DESCRIPTION("a simple hello module"); //模塊許描述 27 MODULE_ALIAS("a simplest module"); //模塊別名
使用Makefile文件如下:
1 ifeq ($(KERNELRELEASE),) 2 KDIR := /lib/modules/$(shell uname -r)/build 3 PWD := $(shell pwd) 4 modules: 5 $(MAKE) -C $(KDIR) M=$(PWD) modules 6 modules_install: 7 $(MAKE) -C $(KDIR) M=$(PWD) modules_install 8 clean: 9 rm -rf *.o *.ko .depend *.mod.o *.mod.c modules.* 10 .PHONY:modules modules_install clean 11 else 12 obj-m :=hello.o 13 endif
保存后,使用Make即可編譯,如果遇到編譯錯誤,請先查看文章最后的備注,未包含問題請搜索或者留言,編譯結果如圖所示。

之后執行指令modinfo hello.ko即可查看當前的模塊信息。

如果無法查看信息,可通過dmesg查看加載信息。

內核模塊的跨模塊調用
上一節可以解決我們遇到的大部分內核實現問題,但某些時候我們可能需要一些公共內核模塊,提供接口給大部分模塊使用,這就涉及到內核模塊的跨模塊調用。
對於跨核模塊調用的實現,對於調用的模塊,主要包含2步:
1、在代碼實現中添加extern int add_integar(int a, int b);
2、在編譯環境下修改Module.symvers, 添加被鏈接模塊的地址,函數校驗值(可通過查看被鏈接模塊編譯環境下的Module.symvers內復制即可)
對於被鏈接的模塊,代碼實現如下:
1 //math.ko 2 #include <linux/init.h> 3 #include <linux/module.h> 4 5 static int __init math_init(void) 6 { 7 printk(KERN_WARNING "math enter\n"); 8 return 0; 9 } 10 module_init(math_init); 11 12 static void __exit math_exit(void) 13 { 14 printk(KERN_WARNING "math exit\n"); 15 } 16 module_exit(math_exit); 17 18 int add_integar(int a, int b) 19 { 20 return a+b; 21 } 22 EXPORT_SYMBOL(add_integar); 23 24 int sub_integar(int a, int b) 25 { 26 return a-b; 27 } 28 EXPORT_SYMBOL(sub_integar); 29 30 MODULE_LICENSE("GPL V2");
編譯Makefile同上,需要將obj-m :=hello.o修改為obj-m :=math.o
執行make編譯完成該文件,並通過insmod加載完模塊后,可通過
grep integar /proc/kallsyms 查看加載在內核中的符號,狀態如下:

然后加載insmod hello.ko, 即可跨文件調用該接口。如此,便初步完成對Linux內核模塊的學習。
備注
1.內核編譯名稱必須為Makefile,否則編譯會出錯
make[2]: *** No rule to make target `/usr/kernel/hello/Makefile'. Stop.
make[1]: *** [_module_/usr/kernel/hello] Error 2
make[1]: Leaving directory `/usr/src/linux-headers-3.5.0-23-generic'
2.Makefile的內容,如果編譯多個文件obj-m :=hello.o test.o
3.Makefile中,指令必須以Tab對齊,否則編譯會異常。
4.printk不打印,一般來說輸出的KERNEL_INFO為超過最大輸出值,可直接通過dmesg,在系統信息內查看。
5.內核跨文件訪問接口
除EXPORT_SYSMBOL外,在編譯時Module.symvers需要包含對應函數的校驗值,路徑
0x13db98c9 sub_integar /usr/kernel/math/math EXPORT_SYMBOL
0xe1626dee add_integar /usr/kernel/math/math EXPORT_SYMBOL
否則編譯時報警告
WARNING: "add_integar" [/usr/kernel/hello/hello.ko] undefined!
安裝模塊時出錯
[ 9091.025357] hello: no symbol version for add_integar
[ 9091.025360] hello: Unknown symbol add_integar (err -22)
