摘要:搞嵌入式有兩個方向,一個是嵌入式軟件開發(MCU方向),另一個是嵌入式軟件開發(Linux方向)。其中MCU方向基本是裸機開發和RTOS開發。而Linux開發方向又分為驅動開發和應用開發。其中應用開發相比于驅動開發來說簡單一些,因為搞驅動你要和Linux內核打交道。而我們普通的單片機開發就是應用開發,和Linux開發沒多大區別,單片機你去調別人寫好的庫,Linux應用你也是調別人的驅動程序。
很多人學習的路線是:單片機到RTOS,再到Linux,這個路線其實是非常好,循序漸進。因為你學了單片機,所以你對RTOS的學習會很容易理解,單片機+RTOS在市面上也可以找到一個很好的工作。因為你學了RTOS,你會發現Linux驅動開發其實和RT-Thread的驅動程序非常像,其實RT-Thread驅動大概率可能是仿Linux驅動而寫的。所以如果你現在在學RT-Thread,那么你后面去搞Linux驅動也是非常容易上手。
當然做驅動去之前你還是要學習一下ubuntu操作系統、ARM裸機和linux系統移植,其目的就是為學習嵌入式linux驅動開發做準備。
話不多說先來一個hello驅動程序。
在Linux中,驅動分為三大類:
字符設備驅動
字符設備驅動是占用篇幅最大的一類驅動,因為字符設備最多,從最簡單的點燈到 I2C、SPI、音頻等都屬于字符設備驅動的類型。
塊設備驅動
塊設備和網絡設備驅動要比字符設備驅動復雜,就是因為其復雜所以半導體廠商一般都給我們編寫好了,大多數情況下都是直接可以使用的。
所謂的塊設備驅動就是存儲器設備的驅動,比如 EMMC、NAND、SD 卡和 U 盤等存儲設備,因為這些存儲設備的特點是以存儲塊為基礎,因此叫做塊設備。
網絡設備驅動
網絡設備驅動很好理解,不管是有線的還是無線的,都屬于網絡設備驅動的范疇。一個設備可以屬于多種設備驅動類型,比如 USB WIFI,其使用 USB 接口,所以屬于字符設備,但是其又能上網,所以也屬于網絡設備驅動。
我使用的Linux內核版本為 4.1.15,其支持設備樹Device tree。開發板是正點原子送的Linux-MINI板,你用其他家的板子也是一樣的,沒有任何影響。
一、字符設備驅動簡介
字符設備是Linux驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。比如我們最常見的點燈、按鍵、IIC、SPI,LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
那么在Linux下的應用程序是如何調用驅動程序的呢?Linux 應用程序對驅動程序的調用如圖所示:
Linux應用程序對驅動程序的調用流程
在Linux 中一切皆為文件,驅動加載成功以后會在/dev目錄下生成一個相應的文件,應用程序通過對這個名為/dev/xxx(xxx是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。
寫驅動的人必須要懂linux內核,因為驅動程序就是根據內核的函數去寫的,寫應用的人不需要懂linux內核,只需要熟悉驅動函數就可以了。
比如現在有個叫做/dev/led的驅動文件,是led燈的驅動文件。應用程序使用open函數來打開文件/dev/led,使用完成以后使用close函數關閉/dev/led 這個文件。open和 close 就是打開和關閉led驅動的函數,如果要點亮或關閉led,那么就使用write 函數來操作,也就是向此驅動寫入數據,這個數據就是要關閉還是要打開led的控制參數。如果要獲取led 燈的狀態,就用 read 函數從驅動中讀取相應的狀態。
應用程序運行在用戶空間,而Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。當我們在用戶空間想要實現對內核的操作,比如使用open函數打開/dev/led這個驅動,因為用戶空間不能直接對內核進行操作,因此必須使用一個叫做“系統調用”的方法來實現從用戶空間“陷入”到內核空間,這樣才能實現對底層驅動的操作。
open、close、write 和read等這些函數是由C庫提供的,在Linux系統中,系統調用作為C庫的一部分。當我們調用 open 函數的時候流程如圖所示:
open函數調用流程
其中關于C庫以及如何通過系統調用“陷入”到內核空間這個我們不用去管,我們關注的是應用程序和具體的驅動,應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了open這個函數,那么在驅動程序中也得有一個名為open的函數。每一個系統調用,在驅動中都有與之對應的一個驅動函數。
在Linux內核文件include/linux/fs.h中有個叫做file_operations的結構體,此結構體就是Linux內核驅動操作函數集合,我們可以將linux內核文件下載下來,然后用source insight打開看看。內容如下所示:
點擊此處下載linux內核源碼
Linux內核
struct?file_operations?{ ?struct?module?*owner; ?loff_t?(*llseek)?(struct?file?*,?loff_t,?int); ?ssize_t?(*read)?(struct?file?*,?char?__user?*,?size_t,?loff_t?*); ?ssize_t?(*write)?(struct?file?*,?const?char?__user?*,?size_t,?loff_t?*); ?ssize_t?(*read_iter)?(struct?kiocb?*,?struct?iov_iter?*); ?ssize_t?(*write_iter)?(struct?kiocb?*,?struct?iov_iter?*); ?int?(*iterate)?(struct?file?*,?struct?dir_context?*); ?unsigned?int?(*poll)?(struct?file?*,?struct?poll_table_struct?*); ?long?(*unlocked_ioctl)?(struct?file?*,?unsigned?int,?unsigned?long); ?long?(*compat_ioctl)?(struct?file?*,?unsigned?int,?unsigned?long); ?int?(*mmap)?(struct?file?*,?struct?vm_area_struct?*); ?int?(*mremap)(struct?file?*,?struct?vm_area_struct?*); ?int?(*open)?(struct?inode?*,?struct?file?*); ?int?(*flush)?(struct?file?*,?fl_owner_t?id); ?int?(*release)?(struct?inode?*,?struct?file?*); ?int?(*fsync)?(struct?file?*,?loff_t,?loff_t,?int?datasync); ?int?(*aio_fsync)?(struct?kiocb?*,?int?datasync); ?int?(*fasync)?(int,?struct?file?*,?int); ?int?(*lock)?(struct?file?*,?int,?struct?file_lock?*); ?ssize_t?(*sendpage)?(struct?file?*,?struct?page?*,?int,?size_t,?loff_t?*,?int); ?unsigned?long?(*get_unmapped_area)(struct?file?*,?unsigned?long,?unsigned?long,?unsigned?long,?unsigned?long); ?int?(*check_flags)(int); ?int?(*flock)?(struct?file?*,?int,?struct?file_lock?*); ?ssize_t?(*splice_write)(struct?pipe_inode_info?*,?struct?file?*,?loff_t?*,?size_t,?unsigned?int); ?ssize_t?(*splice_read)(struct?file?*,?loff_t?*,?struct?pipe_inode_info?*,?size_t,?unsigned?int); ?int?(*setlease)(struct?file?*,?long,?struct?file_lock?**,?void?**); ?long?(*fallocate)(struct?file?*file,?int?mode,?loff_t?offset, ?????loff_t?len); ?void?(*show_fdinfo)(struct?seq_file?*m,?struct?file?*f); #ifndef?CONFIG_MMU ?unsigned?(*mmap_capabilities)(struct?file?*); #endif };
?
第 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是異步刷新待處理的數據。
二、字符設備驅動開發
學習裸機或者STM32的時候關于驅動的開發就是初始化相應的外設寄存器,在Linux驅動開發中肯定也是要初始化相應的外設寄存器,這個是毫無疑問的。只是在Linux驅動開發中我們需要按照其規定的框架來編寫驅動,所以說學Linux驅動開發重點是學習其驅動框架。
2.1 APP打開的文件在內核中如何表示
APP使用open函數打開文件時,可以得到一個整數,這個整數被稱為文件句柄。對于APP的每一個文件句柄,在內核里面都有一個struct file與之對應。
struct file
我們使用open打開文件時,傳入的 flags、mode等參數會被記錄在內核中對應的struct file結構體里(f_flags、f_mode):
?
int?open(const?char?*pathname,?int?flags,?mode_t?mode);
?
去讀寫文件時,文件的當前偏移地址也會保存在struct file結構體的f_pos成員里。
open->struct file
打開字符設備節點時,內核中也有對應的struct file注意這個結構體中的結構體:struct file_operations *f_op,這是由驅動程序提供的。
驅動程序的 struct file
驅動程序的 open/read/write
結構體struct file_operations的定義如下,上面也講過了。
2.2 編寫驅動程序的步驟
1、確定主設備號,也可以讓內核分配。
2、定義自己的file_operations結構體。
3、實現對應的drv_open/drv_read/drv_write等函數,填入file_operations結構體。
4、把file_operations結構體告訴內核:register_chrdev。
5、誰來注冊驅動程序啊?得有一個入口函數:安裝驅動程序時,就會去調用這個入口函數。
6、有入口函數就應該有出口函數:卸載驅動程序時,出口函數調用unregister_chrdev。
7、其他完善:提供設備信息,自動創建設備節點:class_create,device_create。
2.3 試驗程序編寫
應用程序調用open函數打開hello_drv這個設備,打開以后可以使用write 函數向hello_drv的寫緩沖區writebuf中寫入數據(不超過 100 個字節),也可以使用read函數讀取讀緩沖區readbuf中的數據操作,操作完成以后應用程序使用close函數關閉chrdevbase設備。
hello_drv.c
?
#include?#include? #include? #include? #include? #include? #include? #include? #include? #include? #include? #include? #include? #include? #include? /*?1.?確定主設備號*/ static?int?major?=?200; static?char?kernel_buf[1024]; static?struct?class?*hello_class; #define?MIN(a,?b)?(a? ?
2.4 測試程序編寫
驅動編寫好以后是需要測試的,一般編寫一個簡單的測試APP,測試APP運行在用戶空間。測試APP很簡單通過輸入相應的指令來對hello_drv設備執行讀或者寫操作。
hello_drv_test.c
?
#include?#include? #include? #include? #include? #include? /* app測試 ./hello_drv_test?-w?www.zhiguoxin.cn ./hello_drv_test?-r */ int?main(int?argc,?char?**argv) { ?int?fd; ?char?buf[1024]; ?int?len; ? ?/*?1.?判斷參數?*/ ?if?(argc?2)? ?{ ??printf("Usage:?%s?-w? ",?argv[0]); ??printf("???????%s?-r ",?argv[0]); ??return?-1; ?} ?/*?2.?打開文件?*/ ?fd?=?open("/dev/hello",?O_RDWR); ?if?(fd?==?-1) ?{ ??printf("can?not?open?file?/dev/hello "); ??return?-1; ?} ?/*?3.?寫文件或讀文件?*/ ?if?((0?==?strcmp(argv[1],?"-w"))?&&?(argc?==?3)) ?{ ??len?=?strlen(argv[2])?+?1; ??len?=?len?1024???len?:?1024; ??write(fd,?argv[2],?len); ?} ?else ?{ ??len?=?read(fd,?buf,?1024);?? ??buf[1023]?=?''; ??printf("APP?read?:?%s ",?buf); ?} ?close(fd); ?return?0; } ?
這里的代碼很簡單就不用再說了,這是linux應用開發的知識。
2.5 編寫Makefile
?
KERNELDIR?:=?/home/zhiguoxin/linux/IMX6ULL/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek CURRENT_PATH?:=?$(shell?pwd) obj-m?:=?hello_drv.o build:?kernel_modules kernel_modules: ?$(MAKE)?-C?$(KERNELDIR)?M=$(CURRENT_PATH)?modules ?$(CROSS_COMPILE)arm-linux-gnueabihf-gcc?-o?hello_drv_test?hello_drv_test.c? clean: ?$(MAKE)?-C?$(KERNELDIR)?M=$(CURRENT_PATH)?clean?
第1行,KERNELDIR表示開發板所使用的Linux內核源碼目錄,使用絕對路徑,大家根據自己的實際情況填寫。
第2行,CURRENT_PATH表示當前路徑,直接通過運行pwd命令來獲取當前所處路徑。
第3行,obj-m表示將hello_drv.c這個文件編譯為hello_drv.ko模塊。
第8行,具體的編譯命令,后面的modules表示編譯模塊,-C表示將當前的工作目錄切換到指定目錄中,也就是KERNERLDIR目錄。M表示模塊源碼目錄,make modules命令中加入M=dir以后程序會自動到指定的 dir 目錄中讀取模塊的源碼并將其編譯為.ko 文件。
第9行,使用交叉編譯工具鏈將hello_drv_test.c編譯成可以在arm板子上運行的hello_drv_test可執行文件。
Makefile 編寫好以后輸入make命令編譯驅動模塊,編譯過程如圖所示
有時候你可能遇到下面的錯誤
這個錯誤的原因是ubuntu中的linux源碼沒有編譯導致的,使用下面的命令將源碼編譯一遍就好了。
?
make?ARCH=arm?CROSS_COMPILE=arm-linux-gnueabihf-?distclean make?ARCH=arm?CROSS_COMPILE=arm-linux-gnueabihf-?imx_v7_defconfig make?ARCH=arm?CROSS_COMPILE=arm-linux-gnueabihf-?all?-j16?
編譯成功以后就會生成一個叫做hello_drv.ko的文件,此文件就是hello_drv設備的驅動模塊。至此,hello_drv設備的驅動就編譯成功。
2.6 運行測試
2.6.1 上傳程序到開發板執行
開發板啟動后通過NFS掛載Ubuntu目錄的方式,將相應的文件拷貝到開發板上。簡單來說,就是通過NFS在開發板上通過網絡直接訪問ubuntu虛擬機上的文件,并且就相當于自己本地的文件一樣。
因為我的代碼都放在/home/zhiguoxin/myproject/alientek_drv_development_source這個目錄下,所以我們將這個目錄作為NFS共享文件夾。
Ubuntu IP為192.168.10.100,一般都是掛載在開發板的mnt目錄下,這個目錄是專門用來給我們作為臨時掛載的目錄。
文件系統目錄簡介
然后使用MobaXterm軟件通過SSH訪問開發板。
?
ubuntu?ip:192.168.10.100 windows?ip:192.168.10.200 開發板ip:192.168.10.50?
在開發板上執行以下命令就可以實現掛載了:
?
mount?-t?nfs?-o?nolock,vers=3?192.168.10.100:/home/zhiguoxin/myproject/alientek_drv_development_source?/mnt?
就將ARM板的mnt目錄掛載在ubuntu的/home/zhiguoxin/myproject/alientek_drv_development_source目錄下了。這樣我們就可以在Ubuntu下修改文件,然后可以直接在開發板上執行可執行文件了。當然我這里的/home/zhiguoxin/myproject/和windows之間是一個共享目錄,我也可以直接在windows上面修改文件,然后ubuntu和開發板直接進行文件同步了。
2.6.2 加載驅動模塊
驅動模塊hello_drv.ko和hello_drv_test可執行文件都已經準備好了,接下來就是運行測試。這里我是用掛載的方式將服務端的項目文件夾掛載到arm板的mnt目錄,進入到/mnt/01_hello_drv目錄輸入如下命令加載hello_drv.ko驅動文件:
?
insmod?hello_drv.ko?
如果模塊加載成功,不會有任何提示,如果失敗會有提示,可能會出錯的是你的模塊版本和你的arm板內版本不一致。
輸入lsmod命令即可查看當前系統中存在的模塊
?
lsmod?
當前系統只有hello_drv這一個模塊。輸入如下命令查看當前系統中有沒有hello_drv這個設備:
?
cat?/proc/devices?
可以看出,當前系統存在hello_drv這個設備,主設備號為200,跟我們設置的主設備號一致。
2.7 創建設備節點文件
驅動加載成功需要在/dev目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作。輸入如下命令創建/dev/hello_drv這個設備節點文件:
?
mknod?/dev/hello_drv?c?200?0?
其中mknod是創建節點命令,/dev/hello_drv 是要創建的節點文件,c表示這是個字符設備,200是設備的主設備號,0是設備的次設備號。創建完成以后就會存在/dev/hello_drv 這個文件,可以使用ls /dev/chrdevbase -l命令查看
?
ls?/dev/hello_drv?-l?
如果hello_drv_test想要讀寫hello_drv設備,直接對/dev/hello_drv進行讀寫操作即可。相當于/dev/hello_drv這個文件是hello_drv設備在用戶空間中的實現。Linux下一切皆文件,包括設備也是文件,現在大家應該是有這個概念了吧?
2.8 hello_drv設備操作測試
一切準備就緒。使用hello_drv_test軟件操作hello_drv這個設備,看看讀寫是否正常,首先進行寫操作,將字符串輸入www.zhiguoxin.cn寫入到內核中
?
./hello_drv_test?-w?www.zhiguoxin.cn?
然后再從內核中將剛寫入的字符串讀出來
?
./hello_drv_test?-r?
可以看到讀寫正常,說明我們編寫的hello_drv驅動是沒有問題的。
2.9 卸載驅動模塊
如果不再使用某個設備的話可以將其驅動卸載掉,比如輸入如下命令卸載掉 hello_drv這個設備:
?
rmmod?hello_drv.ko?
卸載以后使用lsmod命令查看hello_drv這個模塊還存不存在:
可以看出,此時系統已經沒有任何模塊了,hello_drv這個模塊也不存在了,說明模塊卸載成功。而且系統中也沒有了hello_drv這個設備。
至此,hello_drv這個設備的整個驅動就驗證完成了,驅動工作正常。以后的字符設備驅動實驗基本都可以此為模板進行編寫。
總結
上面就是Linux中的字符驅動,可能初學者看起來還有點難,這里我并沒有講解代碼,因為沒有什么好講的,就是我前面在單片機開發中的常說的面向對象編程和指針函數的實際運用,所以做嵌入式還是要把C語言的基礎打牢,尤其是結構體、指針和鏈表,如果這三個你能很好的理解那么Linux驅動編程就非常容易,因為驅動開發就=軟件架構+硬件操作。而軟件架構就需要你要非常熟悉C語言,硬件操作就是你單片機的那幾個寄存器操作。當然如果你學了RT-Thread那么學習Linux驅動也是非常容易的,因為RT-Thread是內核+驅動,而FreeRTOS僅僅只是一個內核而已。
審核編輯:湯梓紅
評論
查看更多