Linux设备驱动开发基础
1.驅動概述和開發環境搭建
??????? 1.1驅動設備的作用
??????????????? 對設備驅動最通俗的解釋就是“驅動硬件設備行動”。驅動與底層硬件直接打交道,按照硬件設備的具體工作方式,讀寫設備的寄存器,完成設備的輪訓、中斷處理、DMA通信,進行物理內存向虛擬內存的映射等,最終讓通信設備能收發數據,讓顯示設備能顯示文字和畫面,讓存儲設備能記錄文件和數據。
??????????????? 由此可見,設備驅動充當了硬件和應用軟件之間的紐帶,他使得應用軟件只需要調用系統軟件的應用編程接口(API)就可讓硬件去完成要求的工作。在系統中沒有操作系統的情況下,工程師可以根據硬件設備的特點自行定義接口,如串口定義SerialSend()、SerialRecv(),對LED定義LightOn()、LightOff(),對Flash定義FlashWrite()、FlashRead()等。而在有操作系統的情況下,驅動的架構則由相應的操作系統定義,驅動工程師必須按照相應的架構設計設計驅動,這樣,驅動才能良好地整合操作系統的內核。
??????? 1.2無操作系統時的設備驅動
??????????????? 并不是任何一個計算機系統都一定要運行操作系統,在許多情況下,操作系統都不必存在,對于功能比較單一、控制并不復雜的系統,并不需要多任務調度、文件系統、內存管理等復雜功能,用單任務架構完全可以良好地支持他們工作。一個無限循環中夾雜設備中斷的檢測或者對設備的輪訓是這種系統中軟件的典型架構。
??????????????? 在這樣的系統中,雖然不存在操作系統,但是設備驅動則無論如何都必須存在,一般情況下,每一種設備驅動都會定義為一個軟件模塊,包含.h文件和.c文件,前者定義該設備驅動的數據結構并聲明外部函數,后者進行驅動的具體實現。
??????????????? 其他模塊想要使用這個設備的時候,只需要包含設備驅動的頭文件。然后調用其中的外部接口函數。
??????????????? 驅動如軟硬件的關系如下圖:
???????????????????????
??????????????? 在沒有操作系統的情況下,設備驅動的接口被直接提交給了應用軟件工程師,應用軟件工程師沒有跨越任何層次就直接訪問了設備驅動的接口。驅動包含的接口函數也與硬件的功能直接吻合,沒有任何附加功能。
??????? 1.3有操作系統時的設備驅動
??????????????? 首先,無操作系統時設備驅動的硬件操作工作仍然是必不可少的,沒有這一部分,驅動不可能與硬件打交道。
??????????????? 其次,我們還需要將驅動融入內核。為了實現這種融合,必須在所有設備的驅動中設計面向操作系統內核的接口,這樣的接口由操作系統規定,對一類設備而言結構一致,獨立于具體的設備。
??????????????? 由此可見,當系統中存在操作系統時,驅動變成了連接硬件和內核的橋梁。操作系統的存在勢必要求設備驅動俯加更多的代碼和功能,把單一的“驅動硬件設備行動”變成了操作系統與硬件交互的模塊,他對外呈現為操作系統的API。不再給應用軟件工程師直接提供接口。
???????????????????????
??????????????? 簡而言之,操作系統通過給驅動制造麻煩來達到給上層應用提供便利的目的。當驅動都按照操作系統給出的獨立于設備的接口而言,那么,應用程序將可使用統一的系統調用接口來訪問各種設備。當應用程序通過write()、read()等函數讀寫文件就可以訪問字符設備和塊設備,而不論設備的具體類型和工作方式。
??????? 1.4 Linux設備驅動
??????????????? 1.4.1設備的分類和特點
??????????????????????? 計算機系統的硬件主要有CPU、存儲器和外設組成。
??????????????????????? 驅動針對的對象是存儲器和外設(包括CPU內部集成的存儲器和外設),而不是針對CPU核。Linux將存儲器和外設分為3個基礎大類:字符設備、塊設備、網絡設備。
??????????????????????? 字符設備指那些必須以串行順序依次進行訪問的設備,如觸摸屏、磁盤驅動器、鼠標等。
??????????????????????? 塊設備可以任意順序進行訪問,以塊為單位進行操作,如硬盤、軟驅等。
??????????????????????? 網絡設備面向數據包的接收和發送而設計,它并不對應于文件系統的節點。內核與網絡設備的通信與內核和字符設備、網絡設備的通信方式完全不同。
??????????????????????? 字符設備不經過系統的快速緩沖,而塊設備經過系統的快速緩沖,但是,字符設備和塊設備并沒有明顯的界限,如對于Flash設備,符合塊設備的特點,但是我們仍然把他作為一個字符設備來訪問。
??????????????????????? 字符設備和塊設備的驅動設計呈現很大的差異,但是對于用戶而言,他們都使用系統的接口操作open()、close()、read()、write()等進行訪問。
?????????????????????? 另外一種設備分類方法中所稱的IC驅動、USB驅動、PCI驅動、LCD驅動等本身可歸納入3個基礎大類,但是對于這些復雜的設備,Linux也定義了獨特的驅動體系結構。
??????????????? 1.4.2 Linux設備驅動與整個軟硬件系統的關系
??????????????????????? 除了網絡設備外,字符設備與塊設備都被映射到Linux文件系統的文件和目錄,通過文件系統的系統接口open()、write()、read()、close()等即可訪問字符設備和塊設備。所有的字符設備和塊設備都被統一地呈現給用戶。塊設備比字符設備復雜,在它上面會首先建立一個磁盤/Flash文件系統,如FAT、EXT3、YAFFS2、JFFS2、UBIFS等。FAT、EXT3、YAFFS2、JFFS2、UBIFS定義了文件和目錄存儲介質上的組織。
???????????????????????????????
??????????????????????? 應用程序可以使用Linux的系統調用接口變成,但也可使用C庫函數,出于代碼可一致性的目的,后者更值得推薦。C庫函數本身也通過系統調用接口而實現,如C庫函數fopen()、fwrite()、fread()、fclose()分別會調用操作系統的API open()、write()、read()、close()。
??????????????? 1.4.3 Linux設備驅動的重點、難點
??????????????????????? 編寫Linux設備驅動要求工程師非常好的硬件基礎,懂得SRAM、Flash、SDRAM、磁盤的讀寫方式,UART、IC、USB等設備的接口以及輪訓、中斷、DMA的原理,PCI總線的工作方式以及CPU的內存管理單元(MMU、)等。
??????????????????????? 編寫Linux設備驅動要求工程師有非常好的C語言基礎,能靈活地運用C語言的結構體、指針、函數指針及內存動態申請和釋放等。
??????????????????????? 編寫Linux設備驅動要求工程師有一定的Linux內核基礎,雖然并不要求工程師對內核各個部分有深入的研究,但至少要明白驅動與內核的接口。尤其是對于塊設備、網絡設別、Flash設別、串口設備等復雜設備,內核定義的驅動體系架構本身就非常復雜。
??????????????????????? 編寫Linux設備驅動要求工程師有非常好的多任務并發控制和同步的基礎,因為在驅動中會大量使用自旋鎖、互斥、信號量、等待隊列等并發與同步機制。
??????? 1.5 Linux設備驅動開發環境搭建
??????????????? 1.5.1 PC上的Linux環境
??????????????????????? 安裝虛擬機,然后在虛擬機上安裝Linux系統。
??????????????? 1.5.2 LDD6410開發板
??????????????? 1.5.3 工具鏈安裝
??????????????????????? (1)下載交叉編譯包,例如http://dll6410.googlecode.com/files/cross-4.2.2-eabi.tar.bz2,并解壓到user/local/arm目錄下。
??????????????????????? (2)設置環境變量
???????????????????????????????? 編輯/etc/profile文件,在文件末尾添加:PATH=“$PATH:/usr/local/arm/4.2.2-ebi/usr/bin”?? export PATH 設置環境變量。使環境變量生效 source /etc/profile命令。
???????????????????????????????? 也可以通過修改home目錄的.bashrc來將/usr/local/arm/4.2.2-eabi/usr/bin添加到PATH:export PATH=/usr/local/arm/4.2.2-eabi/usr/bin/:$PATH
??????????????????????? (3)測試環境變量是否設置成功
???????????????????????????????? 在終端輸入:echo $PATH,如果輸出的路徑包含添加的內容,說明環境變量設置成功。
??????????????????????? (4)測試交叉編譯工具鏈
???????????????????????????????? 在終端輸入“arm-linux-gcc-v”查看交叉編譯工具鏈是否安裝成功。調試工具包含了strace、gdbserver和arm-linux-gdb,其中strace、gdbserver用于目標板文件系統,arm-linux-gdb運行于主機端,對目標板上的內核、內核模塊應用程序進行調試。
???????????????????????????????? 將arm-linux-gdb放入主機上arm-linux-gcc所在的目錄/usr/local/arm/4.2.2-ebi/usr/bin/。
???????????????????????????????? 而strace、gdbserver則可根據需要放入目標機根文件系統的/usr/sbin目錄
??????????????? 1.5.4 主機端nfs和tftp服務安裝
???????????????????????? LDD6410可使用tftp或nfs文件系統與主機通過網口交互交互。
???????????????????????? 主機端安裝tftp服務的方法:sudo apt-get install tftpd-hpa
???????????????????????? 開啟tftp服務的方法:sudo /etc/init.d/tftpdhpa start??????????????????????????????????? Starting HPA's tftpd:in.tftpd
???????????????????????? 對于Ubuntu或Debian用戶而言,在主機端可通過如下方法安裝nfs服務:
???????????????????????????????? apt-get install nfs-kernel-server
???????????????????????????????? sudo mkdir /home/nfs
???????????????????????????????? sudo chmod 777 /home/nfs
?????????????????????????運行“sudo vim /etc/exports”或“sudo gedit /etc/exports”,修改該文件內容為:?? /home/nfs *(sync, rw)
???????????????????????? 運行exportfs rv開啟NFS服務:/etc/init.d/nfs-kernel-server restart
??????????????? 1.5.5 源代碼閱讀和編輯
???????????????????????? windows上用Source Insight閱讀和編譯源代碼。
???????????????????????? Linux上閱讀和編譯Linux源碼的常用方式是vim+cscope或者vim+ctags,vim是一個文本編譯器,而cscops和ctags則可建立代碼索引。
??????? 1.6 設備驅動Hello World:LED驅動
??????????????? 1.6.1 無操作系統時的LED驅動
???????????????????????? 在嵌入式系統的設計中,LED一般直接由CPU的GPIO(通用可編程I/O口)控制。GPIO一般由兩種寄存器控制,即一組控制寄存器和一組數據寄存器。控制寄存器可設置GPIO口的工作方式為輸入或輸出。當引腳被設置為輸出時,向數據寄存器的對應位寫入1或0會分別在引腳上產生高電平和低電平;當引腳設置為輸入時,讀取數據寄存器的對應位可獲得引腳上的電平為高或低。
??????????????? 1.6.2 Linux下的LED驅動
???????????????????????? 在Linux下,可以使用字符設備驅動的框架來編寫對應的LED設備驅動,接口函數遵循Linux編程的命名規范,這些函數將被LED設備驅動中獨立于設備的針對內核的接口進行調用。
2.驅動設計的硬件基礎
??????? 2.1 處理器
??????????????? 2.1.1 通用處理器
??????????????????????? 通用處理器(GPP)并不針對特定的應用領域進行體系結構和指令集的優化,他們具有一般化的通用體系結構和指令集,以求支持復雜的運算并易于添加新開發的功能。一般而言,在嵌入式控制器(MCU)和微處理器(MPU)中會包含一個通用處理器核。
??????????????????????? MPU通常代表一個CPU(中央處理器),而MCU則強調把中央處理器、存儲器和外圍電路集成在一個芯片中。
??????????????? 2.1.2 數字信號處理器
??????????????????????? 數字信號處理器(DSP)針對通信、圖像、語音和視頻處理等領域的算法而設計。它包含獨立的硬件乘法器。DSP的乘法指令一般在單周期內完成,且優化了卷積、數字濾波、FFT(快速傅里葉變換)、相關、矩陣運算等算法中的大量重復乘法。
?????????????????????? DSP一般采用改進的哈佛架構,它具有獨立的地址總線和數據總線,兩條總線由程序存儲器和數據存儲器分時共用。
?????????????????????? DSP分為兩類,一類是定點DSP,一類是浮點DSP。浮點DSP的浮點運算用硬件來實現,可以在單周期內完成,因而其浮點運算處理速度高于定點DSP。而定點DSP只能用定點運算模擬浮點運算。
??????????????? 處理器分類:
???????????????????????
??????? 2.2 存儲器
??????????????? 存儲器主要可分類為只讀儲存器(ROM)、閃存(Flash)、隨機存取存儲器(RAM)、光、磁介質存儲器。
??????????????? ROM還可分為不可編程ROM、可編程ROM(PROM)、可檫除可編程ROM(EPROM)和不可檫除可編程ROM(EEPROM),EEPROM完全可以用軟件來插寫,已經非常方便了。
??????????????? 存儲器分類:
???????????????????????
??????? 2.3 接口與總線
??????????????? 2.3.1 串口
??????????????? 2.3.2 IC
??????????????? 2.3.3 USB
??????????????????????? USB提供了4種傳輸方式以適應各種設備的需要,具體說明如下:
??????????????????????? 控制(Control)傳輸方式:是雙向傳輸,數據量通常較小,主要用來進行查詢、配置和給USB設備發送通用的命令。
??????????????????????? 同步(Synchronization)傳輸方式:提供了確定的寬帶和間隔時間,它被用于時間嚴格并具有較強容錯性的流數據傳輸,或者用于要求恒定的數據傳送率的即時應用。例如進行語音業務傳輸時,使用同步傳輸方式是很好的選擇。
??????????????????????? 中斷(Interrupt)傳輸方式:是單向的,對于USB主機而言,只有輸入。中斷傳輸方式主要用于定時查詢設備是否中斷數據要傳送,該傳輸方式應用在少量的、分散的、不可預測的數據傳輸場合,鍵盤、游戲桿和鼠標屬于這一類型。
??????????????????????? 批量(Buld)傳輸方式:批量傳輸方式應用在沒有寬帶和間隔時間要求的批量數據的傳送和接收,他要求保證傳輸。打印機和掃描儀屬于這類型。
??????????????? 2.3.4 以太網接口
??????????????????????? 以太網接口由MAC(以太網媒體接入控制器)和PHY(物理接口接口收發器)組成。
??????????????? 2.3.5 ISA
??????????????? 2.3.6 PCI和cPCI
3.linux內核及內核編程
??????? 3.1 Linux內核的發展與演變
??????????????? Linux操作系統時UNIX操作系統的一種克隆系統。
??????? 3.2 Linux 2.6內核的特點
??????????????? Linux 2.6相對于Linux 2.4有相當大的改進,主要體現在如下幾個方面:
??????????????????????? 新的調度器:Linux內核使用了新的進程調度算法,它在高負載的情況下執行得極其出色,并且當有很多處理器時也可以很好地擴展。
??????????????????????? 內核搶占:一個內核任務可以被搶占,從而提高系統的實時性,這樣做最主要的優勢在于,可以極大地增強系統的用戶交互性,用戶將會覺得鼠標單擊和擊鍵的事件得到了更快速的相應。
??????????????????????? 改進的線程模型:Linux中線程操作速度得以提高,可以處理任意數目的線程,最大可以到20億。
????????????????????????虛擬內存的變化:從虛擬內存的角度看來,新內核融合了r-map(反向映射)技術,顯著改善虛擬內存在一定程度負載下的性能。
??????????????????????? 文件系統:增加了對日志文件系統功能的支持,在文件系統上的關鍵變化還包括擴展屬性及Posix標準訪問控制的支持。et2/et3作為大多數Linux系統缺省安裝的文件系統,增加了對擴展屬性的支持,可以給指定的文件在文件系統中嵌入元數據。
?????????????????????? 音頻:新的Linux音頻體系結構ALSA(Advanced Linux Sound Architecture)取代了缺陷很多的舊的OSS(Open Sound System)。新的聲音體系結構支持USB音頻和MIDI設備,并支持全雙工重放等功能。
?????????????????????? 總線:SCSI/IDE子系統經過大幅度的重寫,解決和改善了以前的一些問題。可以直接通過IDE驅動程序來支持IDE CD/RW設備,而不必像以前一樣要使用一個特別的SCSI模擬驅動程序。
???????????????????????電源管理:支持ACPI(高級電源配置管理界面),用于調增CPU在不同的負載下工作于不同的時鐘頻率以降低功耗。
?????????????????????? 聯網和IPSec:內核中加入了對IPSec的支持,刪除了原來內核內置的HTTP服務器khttpd,加入了對新的NFSv4(網絡文件系統)客戶機/服務器的支持,并改進了對IPv6的支持。
?????????????????????? 用戶界面層:內核重寫了幀緩沖/控制臺層,人機界面層還加入了對近乎所有接口設備的支持(從觸摸屏到盲人用的設備和各種各樣的鼠標)。
?????????????????????? 在設備驅動程序方面,也有較大的改動,主要表現在內核API中增加了不少新功能(例如內存池)、sysfs文件系統、內核模塊從.o變為.ko、驅動模塊編譯方式、模塊使用計數、模塊加載和卸載函數的定義等方面。
??????? 3.3 Linux內核的組成
??????????????? 3.3.1 Linux內核源代碼目錄結構
??????????????????????? arch:包含和硬件體系結構相關的代碼,每種平臺占一個相應的目錄,如i386、arm、powerpc、mips等。
??????????????????????? block:塊設備驅動程序I/O調度。
??????????????????????? crypto:常用加密和散列算法(如AES、SHA等),還有一些壓縮和CRC校驗算法。
??????????????????????? Documentation:內核各部分的通用解釋和注釋。
??????????????????????? drivers:設備驅動程序,每個不同的驅動占用一個子目錄,如char、block、met、mtd、i2c等。
??????????????????????? fs:支持的各種文件系統,如EXT、FAT、NTFS、JFFS2等。
??????????????????????? include:頭文件,與系統相關的頭文件被放置在include/linux子目錄下。
??????????????????????? init:內核初始化代碼。
??????????????????????? ipc:進程間通信的代碼。
??????????????????????? kernel:內核的最核心部分,包括進程調度、定時器等,而平臺相關的一部分代碼放在arch/*/kernel目錄下。
??????????????????????? lib:庫文件代碼。
??????????????????????? mm:內存管理代碼,和平臺相關的一部分代碼放在arch/*/mm目錄下。
??????????????????????? net:網絡相關代碼,實現了各種常見的網絡協議。
??????????????????????? scripts:用于配置內核的腳本文件。
??????????????????????? security:主要是一個SELinux的模塊。
??????????????????????? sound:ALSA、OSS音頻設備的驅動核心代碼和常用設備驅動。
??????????????????????? usr:實現了用于打包和壓縮的cpio等。
??????????????? 3.3.2 Linux內核的組成部分
??????????????????????? Linux內核主要由進程調度(SCHED)、內存管理(MM)、虛擬文件系統(VFS)、網絡接口(NET)和進程間通信(IPC)5個子系統組成。
???????????????????????????????
??????????????????????? (1)進程調度
??????????????????????????????? 進程調度控制系統中的多個進程對CPU的訪問,使得多個進程能在CPU中“微觀串行,宏觀并行”地執行。進程調度處于系統的中心位置,內核中其他的子系統都依賴他,因為每個子系統都需要掛起或恢復進程。
???????????????????????????????????????
??????????????????????????????? 當請求的資源不能得到滿足時,驅動一般會調度其他進程執行,并使本進程進入睡眠狀態,直到它請求的資源被釋放,才會被喚醒而進入就緒態。睡眠分成可被打斷的睡眠和不可被打斷的睡眠,兩者的區別在于被打斷的睡眠在收到信號的時候會醒。
??????????????????????????????? 設備驅動中,如果需要幾個并發執行得任務,可以啟動內核線程,啟動內核線程的函數為:pid_t kernek_thread(int (*fn)(void *), void *arg, unsigned long flags);
??????????????????????? (2)內存管理
??????????????????????????????? 內存管理的主要作用是控制多個進程安全地共享主內存區域。當CPU提供內存管理單元(MMU’)時,Linux內存管理完成為每個進程進行虛擬內存到物理內存的轉換。
???????????????????????????????????????
??????????????????????????????? 一般而言,Linux的每個進程享有4GB的內存空間,0~3GB屬于用戶空間,3~4GB屬于內核空間,內核空間對常規內存、I/O設備內存以及高端內存存在不同的處理方式。
??????????????????????? (3)虛擬文件系統
???????????????????????????????
???????????????????????????????? Linux虛擬文件系統(VFS)隱藏各種硬件的具體細節,為所有的設備提供了統一的接口。而且,它獨立于各個具體的文件系統,是對各種文件系統的一個抽象,它使用超級塊super bolck存放文件系統相關信息,使用索引節點inode存放文件的物理信息,使用目錄項dentry存放文件的邏輯信息。
??????????????????????? (4)網絡接口
???????????????????????????????
??????????????????????????????? 網絡接口提供了對各種網絡標準的存取和各種網絡硬件的支持。在Linux中網絡接口可分為網絡協議和網絡驅動程序,網絡協議部分負責實現每一種可能的網絡傳輸協議,網絡設備驅動程序負責與硬件設備同行,每一種可能的硬件設備都有相應的設備驅動程序。
??????????????????????? (5)進程通信
??????????????????????????????? 進程通信支持提供進程之間的通信,Linux支持進程間的多種通信機制,包含信號量、共享內存、管道等,這些機制可協助多個進程、多個資源的互斥訪問、進程間的同步和消息傳遞。
??????????????????????? Linux內核的5個組成部分之間的依賴關系如下:
??????????????????????????????? 進程調度與內存管理之間的關系:這兩個子系統互相依賴。在多道程序環境下,程序要運行必須為之創建進程,而創建進程的第一件事情就是將程序和數據裝入內存。
??????????????????????????????? 進程間通信與內存管理的關系:進程間通信子系統要依賴內存管理支持共享內存通信機制,這種機制允許兩個進程除了擁有自己的私有空間,還可以存取共同的內存區域。
??????????????????????????????? 虛擬文件系統與網絡接口之間的關系:虛擬文件系統利用網絡接口支持支持網絡文件系統(NFS),也利用內存管理支持RAMDISK設備。
??????????????????????????????? 內存管理與虛擬文件系統之間的關系:內存管理利用虛擬文件系統支持交換,交換進程(swapd)定期由調度程序調度,這也是內存管理依賴于進程調度的唯一原因。當一個進程存取的內存映射被換出時,內存管理向文件系統發出請求,同時,掛起當前正在運行的進程。
??????????????????????? 除了這些依賴關系外,內核中的所有子系統還要依賴于一些共同的資源。這些資源包括所有子系統都用到的例程,如分配和釋放內存空間的函數、打印警告或錯誤信息的函數及系統提供的調試例程等。
??????????????? 3.3.3 Linux內核空間與用戶空間
??????????????????????? 現代CPU內部往往實現了不同的操作模式(級別),不同的模式有不同的功能,高層程序往往不能訪問低級功能,而必須以某種方式切換到低級模式。
??????????????????????? ARM處理器分為7種工作模式:
??????????????????????????????? 用戶模式(usr):大多數的應用程序運行在用戶模式下,當處理器運行在用戶模式下時,某些被保護的系統資源是不能被訪問的。
??????????????????????????????? 快速中斷模式(fiq):用于高速數據傳輸或通道處理。
??????????????????????????????? 外部中斷模式(irq):用于通用的中斷處理。
??????????????????????????????? 管理模式(svc):操作系統使用的保護模式。
??????????????????????????????? 數據訪問終止模式(abt):當數據或指令預取終止時進入該模式,可用于虛擬存儲及存儲保護。
??????????????????????????????? 系統模式(sys):運行具有特權的操作系統任務。
??????????????????????????????? 未定義指令中止模式(und):當未定義的指令執行時進入該模式,可以用于支持硬件協處理器的軟件仿真。
??????????????????????????????? ARM Linux的系統調用實現原理是采用swi軟中斷從用戶態usr模式陷入內核態svc模式。
??????????????????????? X86處理器包含4個不同的特權級,稱為Ring0~Ring3。Ring0下可以執行特權級指令,對任何I/O設備都有訪問權等,而Ring3則被限制很多操作。
??????????????????????? Linux系統中,內核可進行任何操作,而應用程序則被禁止對硬件的直接訪問和對內存的未授權訪問。
??????????????????????? 內核空間和用戶空間這兩個名詞被用來區分程序執行的這兩種不同狀態,他們使用不同的地址空間。Linux只能通過系統調用和硬件中斷完成從用戶空間到內核空間的控制轉移。
??????? 3.4 Linux內核的編譯及加載
??????????????? 3.4.1 Linux內核的編譯
??????????????????????? Linux驅動工程師需要牢固地掌握Linux內核的編譯方法以為嵌入式系統構建可運行的Linux操作系統映像。
??????????????????????? 配置內核的方法:
?????????????????????????????? #make config(基于文本的最為傳統的配置界面,不推薦使用)
?????????????????????????????? #make menuconfig(基于文本菜單的配置界面,最值得推薦,不依賴與QT或GTK+,且非常直觀)
?????????????????????????????? #make xconfig(要求QT被安裝)
?????????????????????????????? #make gconfig(要求GTK+被安裝)
??????????????????????? 編譯內核和模塊的方法:
??????????????????????????????? make zImage
??????????????????????????????? make modules
??????????????????????? 執行完上述命令后,在源代碼的跟目錄下會得到未壓縮的內核映像vmlinux和內核符號表文件System.map,在arch/arm/boot/目錄會得到壓縮的內核映像zImage,在內核個對應目錄得到選中的內核模塊。
?????????????????????? Linux2.6內核的配置系統由以下3個部分組成:
?????????????????????????????? Makefile:分布在Linux內核源碼中的Makefile,定義Linux內核的編譯規則。
?????????????????????????????? 配置文件(Kconfig):給用戶提供配置選擇的功能。
?????????????????????????????? 配置工具:包括配置命令解釋器(對配置腳本中使用的配置命令進行解釋)和配置用戶界面(提供基于字符界面和圖形界面)。這些配置工具都是使用腳本語言,如Tcl/TK、Perl等編寫。
?????????????????????? 使用make config、make menuconfig等命令后,會生成一個.config配置文件,記錄那些部分被編譯入內核、那些部分被編譯為內核模塊。
??????????????? 3.4.2 Kconfig和Makefile
???????????????????????在Linux內核中增加程序需要完成以下3項工作:
?????????????????????????????? 將編寫的源代碼拷入Linux內核源代碼的相應目錄。
?????????????????????????????? 在目錄的Kconfig文件中增加關于新源代碼對應項目的編譯配置選項。
?????????????????????????????? 在目錄的Makefile文件中增加對新源代碼的編譯條目。
??????????????? 3.4.3 Linux內核的引導
??????????????????????? 引導Linux系統的過程包括很多階段,這里將以引導X86 PC為例來進行講解。引導X86 PC上的LInux的過程和引導嵌入式系統上的Linux的過程基本類似。不過在X86 PC上有一個從BISO(基本輸入/輸出系統)轉移到Bootloader的過程,而嵌入式系統往往復位后就直接運行Bootloader。
??????????????????????? 下圖為X86 PC從上電/復位到運行Linux用戶空間初始進程的流程。
???????????????????????????????
??????????????????????? 在進入與Linux相關代碼之間會經歷如下階段:
??????????????????????????????? (1)當系統上電或復位時,CPU會將PC指針賦值為一個特定的地址0xFFFF0并執行該地址處的指令。在PC機上,該地址位于BIOS中,它保存在主板的ROM或Flash中。
????????????????????????????????(2)BIOS運行時按照CMOS的設置定義的啟動設備順序來搜索處于活動狀態并且可以引導的設備。若從硬盤啟動,BIOS會將硬盤MBR(主引導記錄)中的內容加載到RAM。MBR是個512字節大小的扇區,位于磁盤上的第一個扇區中(0道0柱面1扇區)。當MBR被加載到RAM中之后,BIOS就會將控制權交給MBR。
??????????????????????????????? (3)主引導加載程序查找并加載次引導加載程序,它在分區表中查找活動分區,當找到一個活動分區時,掃描分區表中的其他分區,以確保它們都不是活動的。當這個過程驗證完成后,就將活動分區的引導記錄從這個設備中讀入RAM中并執行它。
??????????????????????????????? (4)次引導加載程序加載Linux內核和可選的初始RAM磁盤,將控制權交給Linux內核源代碼。
??????????????????????????????? (5)運行被加載的內核,并啟動用戶空間應用程序。
?????????????????????????嵌入式系統中Linux的引導過程與之類似,但一般更加簡潔。不論具體以怎樣的方式實現,只要具備如下特征就可以稱其為Bootloader。
???????????????????????????????? 可以在系統上電或復位的時候以某種方式執行,這些方式包括被BIOS引導執行、直接在NOR Flash中執行、NAND Flash中的代碼被MCU自動拷入內部或外部RAM執行等。
???????????????????????????????? 能將U盤、磁盤、光盤、NOR/NAND Flash、ROM、SD卡等存儲介質,甚或網口、串口中的操作系統加載到RAM并把控制權交給操作系統源代碼執行。
??????????????????????? 完成上述功能的Bootloader的實現方式非常多樣化,甚至本身也可以是一個簡化版的操作系統。著名的Linux Bootloader包括應用于PC的LILO和GRUB,應用嵌入式系統的U-Boot、RedBoot等。
??????????????????????? 相比較于LILO,GRUB本身能理解EXT2、EXT3文件系統,因此可在文件系統中加載Linux,而LILO只能識別“裸扇區”。
??????????????????????? U-Boot的定位為“Universal Bootloader”,其功能比較強大,涵蓋了包括PowerPC、ARM、MIPS和X86在內的絕大部分處理器架構,提供網卡,串口、Flash等外設驅動,提供必要的網絡協議(BOOTP、DHCP、TFTP),能識別多種文件系統(cramfs、fat、jffs2和registerfs等),并附帶了調試、腳本、引導等工具,應用十分廣泛。
??????????????????????? Redboot是Redhat公司隨eCos發布的Bootloader開源項目,除了包含U-Boot類似的強大功能外,它還包含GDB stub(插樁),因此能通過串口或網口與GDB進行通信,調試GCC產生的任何程序(包括內核)。
??????????????????????? 我們有必要對上述流程的第5個階段進行更詳細的分析,它完成啟動內核并運行用戶空間的init進程。
??????????????????????? 當內核映像被加載到RAM之后,Bootloader的控制權被釋放,內核階段就開發了。內核映像并不是完全可直接執行的目標代碼,而是一個壓縮過的zimage(小內核)或bzimage(打內核)。
??????????????????????? 但是,并非zimage和bzimage映像中的一切都被壓縮了,否則Bootloader把控制權交給這個內核映像他就“傻”了,實際山,映像中包含違背壓縮的部分,這部分中包含解壓縮程序,解壓縮程序會解壓映像中的被壓縮的部分,zImage和bzImage都是用gzip壓縮的,它們不僅是一個壓縮文件,而且在這兩個文件的開頭部分內嵌有gzip解壓縮代碼。
???????????????????????????????
??????????????????????? 如上圖所示,當bzImage(用于i386映像)被調用時,它從/arch/i386/boot/head.S的start匯編例程開始執行。這個程序執行一些基本的硬件設置,并調用/arch/i386/boot/compressed/head.S中的startup_32例程。startup_32程序設置一些基本的運行環境(如堆棧)后,清除BSS段,調用/arch/i386/boot/compressed/misc.c中decompress_kernel() C函數解壓內核。內核被解壓到內存中之后,會調用/arch/i386/kernel/head.S文件中的startup_32例程,這個新的startup_32例程(稱為清除程序和進程0)會初始化頁表,并啟動內存分頁機制,接著為任何可選的浮點單元(FPU)檢測CPU的類型,并將其存儲起來供以后使用。這些都做完之后,/init/main.c中的start_kernel()函數被調用,進入與體系結構無關的Linux內核部分。
??????????????????????? start_kernel()會調用一系列初始化函數來設置中斷,執行進一步的內存配置。之后,/arch/i386/kernel/process.c中kernel_thread()被調用以啟動第一個核心線程,該線程執行init()函數,而原執行序列會調用cpu_idile()等待調度。
??????????????????????? 作為核心線程的init()函數完成外設及其驅動程序的加載和初始化,掛接根文件系統。init()打開/dev/console設備,重定向stdin、stdout和stderr到控制臺。之后,它搜索文件系統中的init程序(也可以由“init=“命令行參數指定init程序),并使用execve()系統調用執行init程序,搜索init程序的順序為:/sbin/init、/etc/init、/bin/init和/bin/sh。在嵌入式系統中,多數情況下,可以給內核傳入一個簡單的shell腳本來啟動必須的嵌入式應用程序。
??????????????????????? 至此,漫長的Linux內核引導和啟動過程就此結束,而init()對應的這個由start_kernel創建的第一個線程也進入用戶模式。
??????? 3.5 Linux下的C編程特點
??????????????? 3.5.1 Linux編碼分格
??????????????????????? Linux程序的命令習慣和Windows程序的命名習慣及著名的匈牙利命名法有很大的不同。
??????????????????????? 在Windows程序中習慣以如下方式命名宏、變量和函數:
??????????????????????????????? #define PI 3.1415926? /*用大寫字母代表宏*/
??????????????????????????????? int minValue, maxValue;??? /*變量:第一個單詞全寫,其中其后的單詞第一個字母大寫*/
??????????????????????????????? void SendData(void)?? /*函數:所有單詞第一個字母都大寫定義*/
??????????????????????? 在Linux程序習慣如下方式命名宏、變量、函數:
??????????????????????????????? #define PI 3.1415926
??????????????????????????????? int min_value, max_value;
??????????????????????????????? void send_data(void)
??????????????????????? Linux的代碼縮進使用”TAB“(8個字符)。
??????????????????????? Linux的代碼括號”{"和”}“的使用原則如下:
??????????????????????????????? 對于結構體、if/for/while/switch語句,”{“不另起一行。
??????????????????????????????? 如果if、for循環后只有一行,不要加”{“和"}"。
??????????????????????????????? if和else混用的情況下,else語句不另起一行。
??????????????????????????????? 對于函數,”{“另起一行。
???????????????????????在switch/case語句方面,Linux建議switch和case對其。
?????????????????????? 內核下的Documentation/CodingStyle描述了Linux內核對編碼風格的要求,內核下的scripts/checkpatch.p1提供了1個檢查代碼風格的腳本。
??????????????? 3.5.2 GUN C與ANSI C
??????????????????????? Linux上可用的C編譯器是GUN C編譯器,它建立在自由軟件基金會的編程許可證的基礎上,因此可以自由發布,GUN C對標準C進行一系列擴展,以增強標準C的功能。
??????????????????????? 1.零長度和變量長度數組
???????????????????????????????? GUN C允許使用零長度數組,在定義變長對象的頭結構時,這個特性非常有用。例如:
???????????????????????????????????????? struct var_data{
???????????????????????????????????????????????? int len;
???????????????????????????????????????????????? char data[0];
?????????????????????????????????????????};
?????????????????????????????????char data[0]僅僅意味著程序中通過var_data結構體實例的data[index]成員可以訪問len之后的第index個地址,它并沒有為data[]數組分配內存,因此sizeof(struct var_data)=sizeof(int)。
?????????????????????????????????GUN C中也可以使用1個變量定義數組,例如可以定義”double x[n]“。
?????????????????????????2.case范圍
???????????????????????????????? GUN C支持case x...y這樣的語法,區間[x,y]的數都會滿足這個case的條件。
?????????????????????????3.語句表達式
???????????????????????????????? GUN C把包含在括號中的復合語句看做事一個表達式,稱為語句表達式,它可以出現在任何允許表達式的地方。我們可以在語句表達式中使用原本只能在符合語句中使用的循環、局部變量等。例如:
??????????????????????????????????????? #define min_t(type, x, y) ({type _x=(x); type _y=(y); _x<_y?_x:_y})
???????????????????????????????? 因為重新定義了_x和_y這兩個局部變量,所以以上述方式定義的宏將不會有副作用。在標準C中,對應的如下宏則會產生副作用:
???????????????????????????????????????? #define min(x,y)?((x)<(y)?(x)?(y))
?????????????????????????4.typeof關鍵字
???????????????????????????????? typeof(x)語句可以獲得x的類型,因此,我們可以借助typeof重新定義min這個宏:
???????????????????????????????????????? #define min(x, y) ({const typeof(x) _x=(x); const typeof(y) _y=(y); (void) (&_x==&_y); _x<_y?_x:_y})
?????????????????????????5.可變參數宏
???????????????????????????????? 標準C就支持可變參數函數,意味著函數的參數是不固定的,例如printf()函數的原型為:
???????????????????????????????????????? int printf(const char *format [, argument]...);
?????????????????????????????????而在GUN C中,宏也可以接受可變數目的參數,例如:
????????????????????????????????????????? #define pr_debug(fmt, arg...) printk(fmt, ##arg)
???????????????????????????????? 使用”##“的原因是處理arg不代表任何參數的情況,這時候,前面的逗號就變得多余了。使用”##“之后,GUN C預處理器會丟棄前面的逗號。
??????????????????????? 6.標號元素
???????????????????????????????? 標準C要求數組或結構體的初始化值必須以固定的順序出現,在GUN C中,通過制定索引或結構體成員名,允許初始化值以任意順序出現。
??????????????????????????????? 指定數組索引的方法是在初始化值前添加”[INDEX]=“,當然也可以用”[FIRST...LAST]=“的形式指定一個范圍。
????????????????????????7.當前函數名
??????????????????????????????? GUN C預定義了兩個標志符保存當前函數的名字,__FUNCTION__保存函數的源碼中的名字,__PRETTY_FUNCTION__保存帶語言特色的名字。在C函數中,著這兩個名字是相同的。C99已經支持__func__宏,因此建議在Linux編程中不再使用__FUNCTION__,而轉而使用__func__。
????????????????????????8.特殊屬性聲明
??????????????????????????????? GUN C運行聲明函數、變量和類型的特殊屬性,以便進行手工的代碼優化和定制代碼檢查的方法,要制定一個聲明的屬性,只需要在聲明后添加__attribute__((ATTRIBUTE))。其中ATTRIBUTE為屬性說明,如果存在多個屬性,則以逗號分隔。GUN C支持noreturn、format、section、aligned、packed等十多個屬性。
??????????????????????????????? noreturn屬性作用于函數,表示該函數從不返回。這讓編譯器優化代碼,并消除不必要的警告信息。
??????????????????????????????? format屬性也用于函數,表示該函數使用printf、scanf或strftime風格的參數,指定format屬性可以讓編譯器根據格式串檢查參數類型。
??????????????????????????????? unused屬性作用于函數和變量,表示該函數或變量可能不會被用到,這個屬性可以避免編譯器產生警告信息。
??????????????????????????????? aligned屬性用于變量、結構體或聯合體,制定變量、結構體或聯合體的對界方式,以字節為單位。
??????????????????????????????? packed屬性作用于變量和類型,用語變量或結構體成員時表示使用最小可能的對界,用于枚舉、結構體或聯合體類型時表示該類型使用最小的內存。
??????????????????????????????? 編譯器對結構體成員及變量對界的目的是為了更快地訪問結構體成員及變量占據的內存。
???????????????????????9.內建函數
?????????????????????????????? GUN C提供了大量的內建函數,其中大部分是標準C庫函數的GNU C編譯器內建版本。不屬于庫函數的其他內建函數的命名通常以__builtin開始。如下所示。
???????????????????????????????內建函數__builtin_return_address(LEVEL)返回當前函數或其調用者的返回地址,參數LEVEL指定調用棧的級數,如0表示當前函數的返回地址,1表示當前函數的調用者的返回地址。
?????????????????????????????? 內建函數__builtin_constant_p(EXP)用于判斷一個值是否為編譯時常數,如果參數EXP的值是常數,函數返回1,否則返回0。
?????????????????????????????? 內建函數__builtin_expect(EXP, C)用于編譯器提供分支預測信息,其返回值是整數表達式EXP的值,C的值必須是編譯時常數。
?????????????????????????????? 在使用gcc編譯C程序的時候,如果使用”-ansi -pedantic“編譯選項,則會告訴編譯器不使用GNU擴展語法。
??????????????? 3.5.3 do{}while(0)
??????????????????????? 在Linux內核中,經常看到do{}while(0)這樣的語句,許多人開始都會疑惑,認為do{}while(0)毫無意義,因為它只會執行一次,加不加do{}while(0)效果是完全一樣的,其實do{}while(0)用法主要用于宏定義中。
??????????????????????? 這里用一個簡單點的宏來演示:
??????????????????????????????? #define SAFE_FREE(p) do{free(p); p=NULL;}while(0)
??????????????????????? 假設這里去掉do{}while(0):
??????????????????????????????? #define SAFE_FREE(p) free(p); p=NULL;
??????????????????????? 展開的代碼中存在兩個問題:
??????????????????????????????? 因為if分支后又兩個語句,導致else分支沒有對應的if,編譯失敗。
??????????????????????????????? 假設沒有else分支,則SAFE_FREE中的第二個語句無論if測試是否通過,都會執行。的確,將SAFE_FREE的定義加上{}就可以解決上述問題了。但是,在C程序中,每個語句后面加分號是一種約定俗成的習慣,這樣,else分支就又沒有對應的if了,編譯將無法通過。假設用了do{}while(0),就沒有問題了,不會再出現編譯問題。do{}while(0)的使用完全是為了保證宏定義的使用者能無編譯錯誤地使用宏,它不對其使用者做任何假設。
??????????????? 3.5.4 goto
??????????????????????? 用不用goto一直是一個著名的爭議話題,Linux內核源代碼中對goto的應用非常廣泛,但是一般只限于錯誤處理中。
????????????????????????這種goto用語處理處理的用法實在是簡單而高效,只需保證在錯誤處理時,注銷、資源釋放等于正常的注冊、資源申請順序相反。
總結
以上是生活随笔為你收集整理的Linux设备驱动开发基础的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2019年 阿里巴巴Python 面试
- 下一篇: linux 其他常用命令