01
嵌入式驅動開發到底學什么
嵌入式大體分為以下四個方向:
一、嵌入式硬件開發:熟悉電路等知識,非常熟悉各種常用元器件,掌握模擬電路和數字電路設計的開發能力。熟練掌握嵌入式硬件知識,熟悉硬件開發模式和設計模式,熟悉 ARM32 位處理器嵌入式硬件平臺開發、并具備產品開發經驗。精通常用的硬件設計工具:Protel/PADS(PowerPCB)/Cadence/OrCad。一般需要有 4~8 層高速 PCB 設計經驗。
二、嵌入式驅動開發:熟練掌握 Linux 操作系統、系統結構、計算機組成原理、數據結構相關知識。熟悉嵌入式 ARM 開發,至少掌握 Linux 字符驅動程序開發。具有單片機、ARM 嵌入式處理器的移植開發能力,理解硬件原理圖,能獨立完成相關硬件驅動調試,具有扎實的硬件知識,能夠根據芯片手冊編寫軟件驅動程序。
三、嵌入式系統開發:掌握 Linux 系統配置,精通處理器體系結構、編程環境、指令集、尋址方式、調試、匯編和混合編程等方面的內容;掌握 Linux 文件系統制作,熟悉各種文件系統格式(YAFFS2、JAFFS2、RAMDISK 等);熟悉嵌入式 Linux 啟動流程,熟悉 Linux 配置文件的修改;掌握內核裁減、內核移植、交叉編譯、內核調試、啟動程序 Bootloader 編寫、根文件系統制作和集成部署 Linux 系統等整個流程;、熟悉搭建 Linux 軟件開發環境(庫文件的交叉編譯及環境配置等);
四、嵌入式軟件開發:精通 Linux 操作系統的概念和安裝方法、Linux 下的基本命令、管理配置和編輯器,包括 VI 編輯器,GCC 編譯器,GDB 調試器和 Make 項目管理工具等知識;精通 C 語言的高級編程知識,包括函數與程序結構、指針、數組、常用算法、庫函數的使用等知識、數據結構的基礎內容,包括鏈表、隊列等;掌握面向對象編程的基本思想,以及 C++語言的基礎內容;精通嵌入式 Linux 下的程序設計,精通嵌入式 Linux 開發環境,包括系統編程、文件 I/O、多進程和多線程、網絡編程、GUI 圖形界面編程、數據庫;熟悉常用的圖形庫的編程,如 QT、GTK、miniGUI、fltk、nano-x 等。
公司的日常活動還是看公司的規模,大一點的一般只是讓你負責一個模塊,這樣你就要精通一點。若是公司比較小的話估計要你什么都做一點。還要了解點硬件的東西。
那么看了這么多,嵌入式和純軟最大的區別在于:
純軟學習的是一門語言,例如 C,C++,java,甚至 Python,語言說到底只是一門工具,就像學會英語法語日語一樣。
但嵌入式學習的是軟件+硬件,通俗的講,它學的是做系統做產品,講究的是除了具體的語言工具,更多的是如何將一個產品分解為具體可實施的軟件和硬件,以及更小的單元。
不少人問,將來就業到底是選驅動還是選應用?只能說憑興趣,并且驅動和應用并不是截然分開的。
▍PART 01
我們說的驅動,其實并不局限于硬件的操作,還有操作系統的原理、進程的休眠喚醒調度等概念。想寫出一個好的應用,想比較好的解決應用碰到的問題,這些知識大家應該都懂。
▍PART 02
做應用的發展路徑個人認為就是業務純熟。比如在通信行業、IPTV 行業、手機行業,行業需求很了解。
▍PART 03
做驅動,其實不能稱為“做驅動”,而是可以稱為“做底層系統”,做好了這是通殺各行業。比如一個人工作幾年,做過手機、IPTV、會議電視,但是這些產品對他毫無差別,因為他只做底層。當應用出現問題,解決不了時,他就可以從內核角度給他們出主意,提供工具。做底層的發展方向,應該是技術專家。
▍PART 04
其實,做底層還是做應用,之間并沒有一個界線,有底層經驗,再去做應用,會感覺很踏實。有了業務經驗,再了解一下底層,很快就可以組成一個團隊。
02
嵌入式 Linux 底層系統包含哪些東西?
嵌入式 LINUX 里含有 bootloader, 內核, 驅動程序、根文件系統這 4 大塊。
一、bootloader
它就是一個稍微復雜的裸板程序。但是要把這裸板程序看懂寫好一點都不容易。Windows 下好用的工具弱化了我們的編程能力。很多人一玩嵌入式就用 ADS、KEIL。能回答這幾個問題嗎?
Q:一上電,CPU 從哪里取指令執行?
A:一般從 Flash 上指令。
Q:但是 Flash 一般是只能讀不能直接寫的,如果用到全局變量,這些全局變量在哪里?
A:全局變量應該在內存里。
Q:那么誰把全局變量放到內存里去?
A:長期用 ADS、KEIL 的朋友,你能回答嗎?這需要“重定位”。在 ADS 或 KEIL 里,重定位的代碼是制作這些工具的公司幫你寫好了。你可曾去閱讀過?
Q:內存那么大,我怎么知道把“原來存在 Flash 上的內容”讀到內存的“哪個地址去”?
A:這個地址用“鏈接腳本”決定,在 ADS 里有 scatter 文件,KEIL 里也有類似的文件。但是,你去研究過嗎?
Q:你說重定位是把程序從 Flash 復制到內存,那么這個程序可以讀 Flash 啊?
A:是的,要能操作 Flash。當然不僅僅是這些,還有設置時鐘讓系統運行得更快等等。
先自問自答到這里吧,對于 bootloader 這一個裸板程序,其實有 3 部分要點:
①對硬件的操作
對硬件的操作,需要看原理圖、芯片手冊。這需要一定的硬件知識,不要求能設計硬件,但是至少能看懂; 不求能看懂模擬電路,但是要能看懂數字電路。這方面的能力在學校里都可以學到,微機原理、數字電路這 2 本書就足夠了。想速成的話,就先放掉這塊吧,不懂就 GOOGLE、發貼。另外,芯片手冊是肯定要讀的,別去找中文的,就看英文的。開始是非常痛苦,以后就會發現那些語法、詞匯一旦熟悉后,讀任何芯片手冊都很容易。
②對 ARM 體系處理器的了解
對 ARM 體系處理器的了解,可以看杜春蕾的《ARM 體系架構與編程》,里面講有匯編指令,有異常模式、MMU 等。也就這 3 塊內容需要了解。
③程序的基本概念:重定位、棧、代碼段數據段 BSS 段等
程序的基本概念,王道當然是去看編譯原理了。可惜,這類書絕對是天書級別的。若非超級天才還是別去看了。可以看韋東山的《嵌入式 Linux 應用開發完全手冊》。
對于 bootloader,可以先看《ARM 體系架構與編程》,然后自己寫程序把各個硬件的實驗都做一遍,比如 GPIO、時鐘、SDRAM、UART、NAND。把它們都弄清楚了,組臺在一起就很容易看懂 u-boot 了 。
總結一下,看懂硬件原理圖、看芯片手冊,這都需要自己去找資料。
二、內核
想速成的人,先跨過內核的學習,直接學習怎么寫驅動。
想成為高手,內核必須深刻了解。注意,是了解,要對里面的調度機制、內存管理機制、文件管理機制等等有所了解。
推薦兩本書:
1. 通讀《linux 內核完全注釋》,請看薄的那本
2. 選讀《Linux 內核情景分析》, 想了解哪一塊就讀哪一節
三、驅動
驅動包含兩部分:硬件本身的操作、驅動程序的框架。
又是硬件,還是要看得懂原理圖、讀得懂芯片手冊,多練吧。
①硬件本身的操作
說到驅動框架,有一些書介紹一下。LDD3,即《Linux 設備驅動》,老外寫的那本,里面介紹了不少概念,值得一讀。但是,它的作用 也就限于介紹概念了。入門之前可以用它來熟悉一下概念。
②驅動程序的框架
驅動方面比較全的介紹,應該是宋寶華的《linux 設備驅動開發詳解》了。要想深入了解某一塊,《Linux 內核情景分析》絕對是超 5 星級推薦。別指望把它讀完,1800 多頁,上下兩冊呢。某一塊不清楚時,就去翻一下它。任何一部分,這書都可以講上 2、3 百頁,非常詳細。并且是以某個目標來帶你分析內核源碼。它以 linux2.4 為例,但是原理相通,同樣適用于其它版本的 linux。
把手上的開發板所涉及的硬件,都去嘗試寫一個驅動吧。有問題就先“痛苦地思考”,思考的過程中會把很多不相關的知識串聯起來,最終貫通。
四、根文件系統
大家有沒有想過這 2 個問題:
Q:對于 Linux 做出來的產品,有些用作監控、有些做手機、有些做平板。那么內核啟動后,掛載根文件系統后,應該啟動哪一個應用程序呢?
A:內核不知道也不管應該啟動哪一個用戶程序。它只啟動 init 這一個應用程序,它對應 /sbin/init。
顯然,這個應用程序就要讀取配置文件,根據配置文件去啟動用戶程序(監控、手冊界面、平板界面等等,這個問題提示我們,文件系統的內容是有一些約定的,比如要有 /sbin/init,要有配置文件 。
Q:你寫的 hello,world 程序,有沒有想過里面用到的 printf 是誰實現的?
A:這個函數不是你實現的,是庫函數實現的。它運行時,得找到庫。
這個問題提示我們,文件系統里還要有庫。
簡單的自問自答到這里,要想深入了解,可以看一下 busybox 的 init.c,就可以知道 init 進程做的事情了。
當然,也可以看《嵌入式 Linux 應用開發完全手冊》里構建根文件系統那章。
03
驅動程序設計的 5 個方法
1. 使用設計模式
設計模式是一個用來處理那些在軟件中會重復出現的問題的解決方案。開發人員可以選擇浪費寶貴的時間和預算從無到有地重新發明一個解決方案,也可以從他的解決方案工具箱中選擇一個最適合解決這個問題的方案。在微處理器出現之初,底層驅動已經很成熟了,那么,為什么不利用現有的成熟的解決方案呢?
驅動程序設計模式大致分屬以下 4 個類別:Bit bang、輪詢、中斷驅動和直接存儲器訪問(DMA)。
Bit bang 模式:當微控制器沒有內外設去執行功能的時候,或者當所有的內外設都已經被使用了,而此時又有一個新的請求,那么開發者就應該選擇 Bit bang 設計模式。Bit bang 模式的解決方案很有效率,但通常需要大量的軟件開銷來確保其實施的能力。Bit bang 模式可以讓開發者手動完成通信協議或外部行為。
輪詢模式用于簡單地監視一個輪詢調度方式中的事件。輪詢模式適用于非常簡單的系統,但許多現代應用程序都需要中斷。
中斷可以讓開發者在事件發生時進行處理,而不用等代碼手動檢查。
DMA(直接存儲器訪問)模式允許其它外圍設備來處理數據傳輸的需求,而不需要驅動的干預。
2. 了解實時行為
一個實時系統是否能滿足實時需求取決于它的驅動程序。寫入能力差的驅動是低效的,并可能使不知情的開發者放棄系統的性能。設計者需要考慮驅動的兩個特點:阻塞和非阻塞。一個阻塞的驅動程序在其完成工作之前會阻止其他任何軟件執行操作。例如,一個 USART 驅動程序可以把一個字符裝入傳輸緩沖區,然后一直等到接收到傳輸結束標志符才繼續執行下一步操作。
另一方面,非阻塞驅動則是一般利用中斷來實現它的功能。中斷的使用可以防止驅動程序在等待一個事件發生時攔截其他軟件的執行操作。USART 的驅動程序可以將一個字符裝入傳輸緩沖區然后等主程序發布下一個指令。傳輸結束標志符的設置會導致中斷結束,讓驅動進行下一步操作。
無論哪種類型,為了保持實時性能,并防止系統中的故障,開發人員必須了解驅動的平均執行時間和最壞情況下的執行時間。一個完整的系統可能會因為一個潛在的風險而造成更大的安全問題。
3. 重用設計
在時間和預算都很緊張的情況下為什么還要再造輪子呢?在驅動程序開發中,重用、便攜性和可維護性都是驅動設計的關鍵要求。這里面的許多特征可以通過硬件抽象層的設計和使用來說明。
硬件抽象層(HAL)為開發人員提供一種方式來創建一個標準接口去控制微控制器的外設。抽象隱藏實現細節,取而代之的是提供了可視化功能,如 Usart_Init 和 Usart_Transmit。這個方法就是讓任何 USART、SPI、PWM 或其他外設具備所有微控制器都支持的共同特點。使用 HAL 隱藏底層、特定設備的細節,讓應用程序開發人員專注于應用的需求,而不是關注底層的硬件是如何工作的。同時 HAL 提供了一個重用的容器。
4. 參考數據手冊
微控制器在過去的幾年里變得越來越復雜。以前想要完全了解一個微控制器需要掌握由一個大約包含 500 頁組成的單一數據手冊。而如今,一個 32 位微控制器通常包含由部分的數據手冊、整個微控制器系列的資料表、每個外設數以百計的資料以及所有的勘誤表組成的數據手冊。開發人員如果想要完全掌握這部分的內容需要了解幾千頁的文件。
不幸的是,所有這些數據手冊都是一個驅動程序能真正合理實現所需要的。開發人員在一開始就要對每個數據手冊中包含的信息進行收集和排序。通常它們中的每一個都需要被訪問以使外設啟動和運行。關鍵信息被分散(或隱藏)在每種類型的數據手冊中。
5. 謹防外設故障
最近我剛好有機會把一系列的微控制器驅動移植到其他的微處理器上。制造商和數據手冊都表明 PWM 外設在這兩個系列的微控制器之間是相同的。然而,實際情況卻是在運行 PWM 驅動器的時候兩者之間有很大的不同。該驅動程序只能在原來的微控制器工作,而在新系列的微控制器上卻無效。
在反復翻看數據手冊之后,我在數據手冊中一個完全不相關的注腳里發現了 PWM 外設上電時會處于故障狀態,需要將一個隱藏在寄存器中的標志位清零。在驅動程序實現的開始,確認外設可能出現的故障并查看其他看似無關的寄存器錯誤。
04
大牛對于嵌入式驅動開發的建議
1) 為了今后的發展,你除了考慮廣度以外,更重要的是注意知識的深度。
譬如,做過網絡驅動,那么是不是只停留在會寫驅動的表層上,有沒有對 Linux 內核的網絡結構,TCP/IP 協議作過深入的了解。
2) 在 Linux 下開發很多時候都要利用現成的東西,沒必要什么都自己搞。關鍵是變成自己的驅動后是否了解原作者編寫時背后的一些東西。你應該不止是簡單的讓它工作。寫驅動的時候就要考慮它的性能問題,并給出測試的方法(當然可以利用現成的許多工具,譬如測試網絡性能的 netperf 等)。
當你寫過 Flash 驅動,可能會知道 Flash 的性能有時候有多重要。
3) C 程序的自我修煉,是否考慮到軟件工程方面的一些東西,程序的可維護性和擴展性,譬如 LCD 驅動,是不是從 Sharp 到 NEC 的只需要集中修改很少的幾個地方?
對于不同品牌的 Flash,如果使得 Flash 的驅動做的更具有靈活性。
4) 如果有時間結余,可以關注 Linux 內核的發展。譬如 LCD 的驅動有沒有考慮到 V4L2 通用架構,譬如網絡驅動用到了 NAPI 了嗎?當然在此之前,假設已經對 LDD3, ULK2 理解的比較熟了。
5) 現在所作的這些驅動還算不得非常核心的東西。如果你想有更好的發展,可以考慮往 audio,video,net 方面發展,你應該多注意真個行業需要什么樣的人才,上述每一項都需要很厚的底蘊,譬如 video,需要了解 MPEG4, H264 等,怎么也要個 1 到 2 年才能算個入行阿,所以我建議不要只顧悶頭做東西,要適當關注目前的一些應用。
6) 對硬件知識的補給,做嵌入式 Linux 這一行不可能不讀硬件的 Spec,如果你對硬件的工作機制理解的比較透,會有助你寫出性能好的驅動程序。
順便提一點,適時的提高你的英語水平,對你的職業生涯絕對有幫助。(不要等需要的時候再補,來不及)
7) 如果有時間,平時注意對 Linux 應用程序編寫的了解 / 積累,也將有助于你寫出很好功能很好的驅動程序。
8) 永遠不能以為自己做了很多東西,就驅動而言,像 TVIN/TVOUT, USB, SDIO 等等,好多未知領域呢。在問題還沒有解決之前很難說清是哪里不對了。
有時候是 datasheet 里面的一句話沒有注意,還有好幾次調不出來最后查到是 PCB 的問題,所以有時候特別暈。
05
嵌入式驅動自學者的感受
經過了多年的嵌入式自學,可謂是不斷在絕望中求生。性格使然,我是一個我也不知這種性格的學名叫什么,就是學習一種東西,非得想要能理解每一處的含義作用為什么,要這樣做沒有其他辦法了嗎等等問題。并且當一個問題找不到讓我能接受的解釋時,那么我的學習路程也就幾乎要停在這里了,大概是因為我討厭一知半解。
可能是小時候被老師教導不要做書呆子的教育有關,小時候,聽話孩子,認真,長輩的教育對孩子的影響真的是非常的大,很多影響如果你不細心的觀察自己,你根本不能察覺這些進入了你骨子的觀念,在我成長過程中,這些長輩的教育除了某些讓我自己經歷到并徹底認識到某個觀念并不正確時,我才會形成自己的觀點,自己的觀念,但這些自己的觀念在所有的價值觀中,猶如滄海一粟。
這種討厭一知半解的性格,在現在這個社會來說,可以說是極端的,因為現在你學習使用的很多東西,他都不是從零開始的,就好比,你編程使用的是高級語言而不是低級語言不是機器碼,所以我的整個學習過程是非常緩慢緩慢地進行著,這么說吧,前面說我經過了半年多的學習,但是到現在為止,我接觸嵌入式已經有兩個年頭了,也就是說,學習期間,我有一年多是在停滯著。
學習嵌入式,或者說學習現代的計算機編程,如果你想學好,有一個比較要求,那就是你能接受它的設定、它的模式。反過來說,當你真正接受它的設定、它的模式,并記住它們時,我認為,你已經學好了。
昨天,我又置之死地而后生了一次。最近一直在搞驅動,一個 LCD 驅動搞得我幾乎要放棄繼續走嵌入式這條路。昨夜,睡不覺,打開嵌入學習視頻,躺在已關燈很久的房間的床上,大概凌晨 3,4 點吧。之前我一直都是學習著驅動自編源碼的教學,是那種幾乎和裸板程序沒多大區別的編程方式,只是多使用了一些向內核注冊的接口函數。
而最近我想換一下,因為很多設備驅動,內核都是自帶的,而且是各種平臺的設備驅動都有,我想如果能熟悉掌握內核自帶驅動的編程,那以后要做某個設備的驅動時,我只需要在自帶驅動中修改一下便好了,通過學習 LCD 平臺設備的驅動,我了解了其編程想法,同時也認同這種想法,甚至讓我疑惑,學習資料中教自編驅動的意義,為何不直接教如果修改內核源碼驅動?
于是,繼續按著書去修改內核驅動源碼,但問題是,書中說他們這種修改,代碼成功運行了,但我這,無論怎么調試都失敗,我反復檢測,我的修改是否與書中一致,檢測了很多遍依然沒發現哪一步不同,不過,有一點發現是,書中的內核源碼和我內核使用的源碼有一點點區別(當然書里并沒有把所有的源碼都貼上,只是修改部分附近會聯帶著一些,這就是發現,這些聯帶的沒需要修改的源碼和我的源碼有點區別,比如,我的源碼中多了一些設置(看似無關緊要的設置))。
與書核對無誤但失敗后,我又與成功運行的自編驅動核對,我陸續發現我修改的內涵源碼中,沒有去啟動設備,也更沒有去點亮背光,而在顯存分配后的寄存器設置似乎也有問題,因為這里的地址使用各種宏定義不同的累加或計算,最后算得地址和我的寄存器地址也不知是否吻合,因為驅動源碼中最后計算得到的是虛擬地址。于是我對比自編驅動,一點點修改嘗試,到睡覺前都沒成功。
我是想學得理直氣壯一點的,最后是能一眼就能找到問題,并迅速輕松解決問題的,我也承認自己確實是有些浮躁。但是經過了昨晚床上的一點絕望的思考掙扎后,我好像想通了:為什么嵌入式學習視頻老師要教自編驅動。
下面我說下自編驅動與內核驅動源碼各自的問題:
自便驅動:
程序簡單簡潔,它只能驅動特定的某個設備。如果設備換了需要支持另一款設備,那么你需要重新修改該驅動;如果需要系統同時支持兩種 LCD,那么它就會變成復雜并且對于內核驅動的簡潔優勢會削弱不少;如果你想驅動支持多種設備,那自編驅動,相對了內核驅動源碼的簡潔優勢會變成了劣勢,因為編程思想的適用范圍不同而產生的結果。
內核自帶驅動源碼:
①從系統層次去考量,變量、宏定義使用多,甚至有些宏定義的值為了方便能讓各種在不同的階段需要不同的值調用,把簡單的一個賦值調用變成了需要進行多次運算才能檢測到該值是否滿足使用要求,因為我們不是該驅動的編碼者,不清楚這樣做的好處,也或許是內核驅動源碼的開發者從整個系統的編程簡潔性去考量,這樣做或許也是為了讓整個系統代碼更少,簡潔的一種做法,因為每個設備你都給它賦具體的值的話,整個系統中有幾百種驅動設備源碼,給所有設備的這個位置參數都賦一個值的話,那各設備關于這個值的代碼就要多了幾百行了,所以還不如,讓各設備根據各種平臺去對某個宏進行各自的計算來得到合適的值,但某些計算中相同的算法的也整合在一起,這樣就減少了系統不少行代碼。所以系統中驅動源碼是系統開發者對系統源碼的整合,是基于系統層的整合。所以,對于我這種對單個設備驅動編碼的人,就會覺得系統源碼有好多不人性化的地方,會覺得簡單的地方也被弄得很復雜。
②內核自帶驅動還有一些代碼是為了兼容以前的版本而添加了,比如以前硬件內存資源稀少,需要使用調色板的方法來減少程序運行時的內存使用量,這也會真假代碼的復雜性,這一步雖不是必要的,但如果沒弄好,那 LCD 驅動也不能正常使用。
③程序復雜,為了適用在多種設備型號,更簡單地添加不同型號的設備驅動,內核對驅動抽象分離,把驅動分為平臺管理部分,驅動代碼部分(與硬件無關碼),和設備代碼部分(硬件相關代碼)。用戶添加新型號設備驅動時,只需要在平臺管理部分檢查添加設備的匹配信息,和提供一個硬件設備相關的代碼(有格式)文件即可。
現在,站在驅動開發者而非系統開發者的角度去衡量。
①自編的驅動,簡潔,要點明確。這個對于驅動開發者的用處就是:無論你使用的是哪個版本的內核,哪個芯片平臺,你可以通過自編碼比較簡單方便地就可以確認硬件設備的情況,是否正常。如果自編碼通過,那可以試用自編碼上使用的參數去與內核進行核對、修改,然后再去測試。如果不成功,對于內核中多余的設置(這些大多可能是提供內核用做基本判斷的變量)可以先屏蔽,編譯出錯了,根據提示,找到出錯的位置修改添加。因為這些多余的設置,設置對了還行,設置錯了,你又不好去定位錯在哪。
自編的驅動在此處的用處,調試時,可以讓你排除多余的失敗可能性問題,在較少的代碼去查出錯誤位置,如果你確定你的設置滿足了該設備的必需設置,還是失敗,你可以比較放心地去懷疑是硬件問題了。如果自編碼成功,那個又可以當做你修改內核驅動的一個標準。
②內核驅動源碼支持管理多種型號的設備的優勢是我用使用它的原因。先了解本版本本平臺的設備驅動結構,如果是添加型號支持,那就根據自編驅動的參數與設置即可,如果是第一次啟動這類設備,你就還需要檢測結構是完整性,如果結構完整,參數無誤依舊錯誤,那就把內核驅動源碼精簡到自編碼的簡單粗暴設置吧。最終就變成了在基于內核驅動架構下的自編驅動。如果還不行,那無疑是結構性問題了。
所以自編驅動,還是有其存在價值的。內核驅動源碼內容會變,平臺會變,但自編驅動是變得最小的一個,也是最容易實現驅動目的的一個。是一碼打天下不可缺少的重要組成部分。
06
如何編寫嵌入式 Linux 設備驅動程序?
一、Linux device driver 的概念
系統調用是操作系統內核和應用程序之間的接口,設備驅動程序是操作系統內核和機器硬件之間的接口。設備驅動程序為應用程序屏蔽了硬件的細節,這樣在應用程序看來,硬件設備只是一個設備文件,應用程序可以象操作普通文件一樣對硬件設備進行操作。設備驅動程序是內核的一部分,它完成以下的功能:
1、對設備初始化和釋放;
2、把數據從內核傳送到硬件和從硬件讀取數據;
3、讀取應用程序傳送給設備文件的數據和回送應用程序請求的數據;
4、檢測和處理設備出現的錯誤。
在 linux 操作系統下有三類主要的設備文件類型,一是字符設備,二是塊設備,三是網絡設備。字符設備和塊設備的主要區別是:在對字符設備發出讀 / 寫請求時,實際的硬件 I/O 一般就緊接著發生了,塊設備則不然,它利用一塊系統內存作緩沖區,當用戶進程對設備請求能滿足用戶的要求,就返回請求的數據,如果不能,就調用請求函數來進行實際的 I/O 操作。塊設備是主要針對磁盤等慢速設備設計的,以免耗費過多的 CPU 時間來等待。
已經提到,用戶進程是通過設備文件來與實際的硬件打交道。每個設備文件都都有其文件屬性(c/b),表示是字符設備還是塊設備?另外每個文件都有兩個設備號,第一個是主設備號,標識驅動程序,第二個是從設備號,標識使用同一個設備驅動程序的不同的硬件設備,比如有兩個軟盤,就可以用從設備號來區分他們。設備文件的的主設備號必須與設備驅動程序在登記時申請的主設備號一致,否則用戶進程將無法訪問到驅動程序。
最后必須提到的是,在用戶進程調用驅動程序時,系統進入核心態,這時不再是搶先式調度。也就是說,系統必須在你的驅動程序的子函數返回后才能進行其他的工作。如果你的驅動程序陷入死循環,不幸的是你只有重新啟動機器了,然后就是漫長的 fsck。
二、實例剖析
我們來寫一個最簡單的字符設備驅動程序。雖然它什么也不做,但是通過它可以了解 Linux 的設備驅動程序的工作原理。把下面的 C 代碼輸入機器,你就會獲得一個真正的設備驅動程序。
由于用戶進程是通過設備文件同硬件打交道,對設備文件的操作方式不外乎就是一些系統調用,如 open,read,write,close…, 注意,不是 fopen, fread,但是如何把系統調用和驅動程序關聯起來呢?這需要了解一個非常關鍵的數據結構:
struct file_operations {
int (*seek) (struct inode * ,struct file *, off_t ,int);
int (*read) (struct inode * ,struct file *, char ,int);
int (*write) (struct inode * ,struct file *, off_t ,int);
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
int (*select) (struct inode * ,struct file *, int ,select_table *);
int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
int (*open) (struct inode * ,struct file *);
int (*release) (struct inode * ,struct file *);
int (*fsync) (struct inode * ,struct file *);
int (*fasync) (struct inode * ,struct file *,int);
int (*check_media_change) (struct inode * ,struct file *);
int (*revalidate) (dev_t dev);
}
這個結構的每一個成員的名字都對應著一個系統調用。用戶進程利用系統調用在對設備文件進行諸如 read/write 操作時,系統調用通過設備文件的主設備號找到相應的設備驅動程序,然后讀取這個數據結構相應的函數指針,接著把控制權交給該函數。這是 linux 的設備驅動程序工作的基本原理。既然是這樣,則編寫設備驅動程序的主要工作就是編寫子函數,并填充 file_operations 的各個域。
下面就開始寫子程序。
#include 《linux/types.h》 基本的類型定義
#include 《linux/fs.h》 文件系統使用相關的頭文件
#include 《linux/mm.h》
#include 《linux/errno.h》
#include 《asm/segment.h》
unsigned int test_major = 0;
static int read_test(struct inode *inode,struct file *file,char *buf,int count)
{
int left; 用戶空間和內核空間
if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT )
return -EFAULT;
for(left = count ; left 》 0 ; left--)
{
__put_user(1,buf,1);
buf++;
}
return count;
}
這個函數是為 read 調用準備的。當調用 read 時,read_test()被調用,它把用戶的緩沖區全部寫 1。buf 是 read 調用的一個參數。它是用戶進程空間的一個地址。但是在 read_test 被調用時,系統進入核心態。所以不能使用 buf 這個地址,必須用 __put_user(),這是 kernel 提供的一個函數,用于向用戶傳送數據。另外還有很多類似功能的函數。請參考,在向用戶空間拷貝數據之前,必須驗證 buf 是否可用。這就用到函數 verify_area。為了驗證 BUF 是否可以用。
static int write_test(struct inode *inode,struct file *file,const char *buf,int count)
{
return count;
}
static int open_test(struct inode *inode,struct file *file )
{
MOD_INC_USE_COUNT; 模塊計數加以,表示當前內核有個設備加載內核當中去
return 0;
}
static void release_test(struct inode *inode,struct file *file )
{
MOD_DEC_USE_COUNT;
}
這幾個函數都是空操作。實際調用發生時什么也不做,他們僅僅為下面的結構提供函數指針。
struct file_operations test_fops = {?
read_test,
write_test,
open_test,
release_test,
};
設備驅動程序的主體可以說是寫好了。現在要把驅動程序嵌入內核。驅動程序可以按照兩種方式編譯。一種是編譯進 kernel,另一種是編譯成模塊(modules),如果編譯進內核的話,會增加內核的大小,還要改動內核的源文件,而且不能動態的卸載,不利于調試,所以推薦使用模塊方式。
int init_module(void)
{
int result;
result = register_chrdev(0, “test”, &test_fops); 對設備操作的整個接口
if (result 《 0) {
printk(KERN_INFO “test: can‘t get major number\n”);
return result;
}
if (test_major == 0) test_major = result; /* dynamic */
return 0;
}
在用 insmod 命令將編譯好的模塊調入內存時,init_module 函數被調用。在這里,init_module 只做了一件事,就是向系統的字符設備表登記了一個字符設備。register_chrdev 需要三個參數,參數一是希望獲得的設備號,如果是零的話,系統將選擇一個沒有被占用的設備號返回。參數二是設備文件名,參數三用來登記驅動程序實際執行操作的函數的指針。
如果登記成功,返回設備的主設備號,不成功,返回一個負值。
void cleanup_module(void)
{
unregister_chrdev(test_major,“test”);
}
在用 rmmod 卸載模塊時,cleanup_module 函數被調用,它釋放字符設備 test 在系統字符設備表中占有的表項。
一個極其簡單的字符設備可以說寫好了,文件名就叫 test.c 吧。
下面編譯 :
$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c –c 表示輸出制定名,自動生成 .o 文件
得到文件 test.o 就是一個設備驅動程序。
如果設備驅動程序有多個文件,把每個文件按上面的命令行編譯,然后
ld ?-r ?file1.o ?file2.o ?-o ?modulename。
驅動程序已經編譯好了,現在把它安裝到系統中去。
$ insmod ?–f ?test.o
如果安裝成功,在 /proc/devices 文件中就可以看到設備 test,并可以看到它的主設備號。要卸載的話,運行 :
$ rmmod test
下一步要創建設備文件。
mknod /dev/test c major minor
c 是指字符設備,major 是主設備號,就是在 /proc/devices 里看到的。
用 shell 命令
$ cat /proc/devices
就可以獲得主設備號,可以把上面的命令行加入你的 shell script 中去。
minor 是從設備號,設置成 0 就可以了。
我們現在可以通過設備文件來訪問我們的驅動程序。寫一個小小的測試程序。
#include 《stdio.h》
#include 《sys/types.h》
#include 《sys/stat.h》
#include 《fcntl.h》
main()
{
int testdev;
int i;
char buf[10];
testdev = open(“/dev/test”,O_RDWR);
if ( testdev == -1 )
{
printf(“Cann’t open file \n”);
exit(0);
}
read(testdev,buf,10);
for (i = 0; i 《 10;i++)
printf(“%d\n”,buf[i]);
close(testdev);
}
編譯運行,看看是不是打印出全 1 ?
以上只是一個簡單的演示。真正實用的驅動程序要復雜的多,要處理如中斷,DMA,I/O port 等問題。這些才是真正的難點。上述給出了一個簡單的字符設備驅動編寫的框架和原理,更為復雜的編寫需要去認真研究 LINUX 內核的運行機制和具體的設備運行的機制等等。希望大家好好掌握 LINUX 設備驅動程序編寫的方法。
07
嵌入式驅動的結構分析
在 Linux 系統上編寫驅動程序,說簡單也簡單,說難也難。難在于對算法的編寫和設備的控制方面,是比較讓人頭疼的;說它簡單是因為在 Linux 下已經有一套驅動開發的模式,編寫的時候只需要按照這個模式寫就可以了,而這個模式就是它事先定義好的一些結構體,在驅動編寫的時候,只要對這些結構體根據設備的需求進行適當的填充,就實現了驅動的編寫。
首先在 Linux 下,視一切事物皆為文件,它同樣把驅動設備也看成是文件,對于簡單的文件操作,無非就是 open/close/read/write,在 Linux 對于文件的操作有一個關鍵的數據結構:file_operation,它的定義在源碼目錄下的 include/linux/fs.h 中,內容如下:
[cpp] view plain copy
1. struct file_operations {
2. struct module *owner;
3. loff_t (*llseek) (struct file *, loff_t, int);
4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
7. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
8. int (*readdir) (struct file *, void *, filldir_t);
9. unsigned int (*poll) (struct file *, struct poll_table_struct *);
10. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
11. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
12. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
13. int (*mmap) (struct file *, struct vm_area_struct *);
14. int (*open) (struct inode *, struct file *);
15. int (*flush) (struct file *, fl_owner_t id);
16. int (*release) (struct inode *, struct file *);
17. int (*fsync) (struct file *, int datasync);
18. int (*aio_fsync) (struct kiocb *, int datasync);
19. int (*fasync) (int, struct file *, int);
20. int (*lock) (struct file *, int, struct file_lock *);
21. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
22. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
23. int (*check_flags)(int);
24. int (*flock) (struct file *, int, struct file_lock *);
25. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
26. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
27. int (*setlease)(struct file *, long, struct file_lock **);
28. };
對于這個結構體中的元素來說,大家可以看到每個函數名前都有一個“*”,所以它們都是指向函數的指針。目前我們只需要關心
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
這幾條,因為這篇文章就叫簡單驅動。就是讀(read)、寫(write)、控制(ioctl)、打開(open)、卸載(release)。這個結構體在驅動中的作用就是把系統調用和驅動程序關聯起來,它本身就是一系列指針的集合,每一個都對應一個系統調用。
但是畢竟 file_operation 是針對文件定義的一個結構體,所以在寫驅動時,其中有一些元素是用不到的,所以在 2.6 版本引入了一個針對驅動的結構體框架:platform,它是通過結構體 platform_device 來描述設備,用 platform_driver 描述設備驅動,它們都在源代碼目錄下的 include/linux/platform_device.h 中定義,內容如下:
[cpp] view plain copy
1. struct platform_device {
2. const char * name;
3. int id;
4. struct device dev;
5. u32 num_resources;
6. struct resource * resource;
7. const struct platform_device_id *id_entry;
8. /* arch specific additions */
9. struct pdev_archdata archdata;
10. };
11. struct platform_driver {
12. int (*probe)(struct platform_device *);
13. int (*remove)(struct platform_device *);
14. void (*shutdown)(struct platform_device *);
15. int (*suspend)(struct platform_device *, pm_message_t state);
16. int (*resume)(struct platform_device *);
17. struct device_driver driver;
18. const struct platform_device_id *id_table;
19. };
對于第一個結構體來說,它的作用就是給一個設備進行登記作用,相當于設備的身份證,要有姓名,身份證號,還有你的住址,當然其他一些東西就直接從舊身份證上 copy 過來,這就是其中的 struct device dev,這是傳統設備的一個封裝,基本就是 copy 的意思了。對于第二個結構體,因為 Linux 源代碼都是 C 語言編寫的,對于這里它是利用結構體和函數指針,來實現了 C 語言中沒有的“類”這一種結構,使得驅動模型成為一個面向對象的結構。對于其中的 struct device_driver driver,它是描述設備驅動的基本數據結構,它是在源代碼目錄下的 include/linux/device.h 中定義的,內容如下:
[cpp] view plain copy
1. struct device_driver {
2. const char *name;
3. struct bus_type *bus;
4. struct module *owner;
5. const char *mod_name; /* used for built-in modules */
6. bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
7. #if defined(CONFIG_OF)
8. const struct of_device_id *of_match_table;
9. #endif
10. int (*probe) (struct device *dev);
11. int (*remove) (struct device *dev);
12. void (*shutdown) (struct device *dev);
13. int (*suspend) (struct device *dev, pm_message_t state);
14. int (*resume) (struct device *dev);
15. const struct attribute_group **groups;
16. const struct dev_pm_ops *pm;
17. struct driver_private *p;
18. };
依然全部都是以指針的形式定義的所有元素,對于驅動這一塊來說,每一項肯定都是需要一個函數來實現的,如果不把它們集合起來,是很難管理的,而且很容易找不到,而且對于不同的驅動設備,它的每一個功能的函數名必定是不一樣的,那么我們在開發的時候,需要用到這些函數的時候,就會很不方便,不可能在使用的時候去查找對應的源代碼吧,所以就要進行一個封裝,對于函數的封裝,在 C 語言中一個對好的辦法就是在結構體中使用指向函數的指針,這種方法其實我們在平時的程序開發中也可以使用,原則就是體現出一個“類”的感覺,就是面向對象的思想。
在 Linux 系統中,設備可以大致分為 3 類:字符設備、塊設備和網絡設備,而每種設備中又分為不同的子系統,由于具有自身的一些特殊性質,所以有不能歸到某個已經存在的子類中,所以可以說是便于管理,也可以說是為了達到同一種定義模式,所以 linux 系統把這些子系統歸為一個新類:misc ,以結構體 miscdevice 描述,在源代碼目錄下的 include/linux/miscdevice.h 中定義,內容如下:
[cpp] view plain copy
1. struct miscdevice {
2. int minor;
3. const char *name;
4. const struct file_operations *fops;
5. struct list_head list;
6. struct device *parent;
7. struct device *this_device;
8. const char *nodename;
9. mode_t mode;
10. };
對于這些設備,它們都擁有一個共同主設備號 10,所以它們是以次設備號來區分的,對于它里面的元素,大應該很眼熟吧,而且還有一個我們更熟悉的 list_head 的元素,這里也可以應證我之前說的 list_head 就是一個橋梁的說法了。
其實對于上面介紹的結構體,里面的元素的作用基本可以見名思意了,所以不用贅述了。其實寫一個驅動模塊就是填充上述的結構體,根據設備的功能和用途寫相應的函數,然后對應到結構體中的指針,然后再寫一個入口一個出口(就是模塊編程中的 init 和 exit)就可以了,一般情況下入口程序就是在注冊 platform_device 和 platform_driver(當然,這樣說是針對以 platform 模式編寫驅動程序)。
08
嵌入式書籍推薦
1. 硬件方面的書: 微機原理、數字電路,高校里的教材。
2. Linux 方面的書:
《ARM 體系架構與編程》
《嵌入式 Linux 應用開發完全手冊》
《Linux 設備驅動》,老外寫的那本
《linux 設備驅動開發詳解》
《linux 內核完全注釋》
《Linux 內核情景分析》
在做驅動的時候,肯定會用到與內核相關的東西,或者需要和內核中的某些模塊配合,這樣你也要理解內核的某些部分是如何實現的,最后,你應該可以很好的掌握 linux 的內核整體框架是什么。
這些都是進步,都是在你一次又一次的開發中需要總結的東西,如果你不總結,永遠都是從頭開始(或者說永遠都是還沒看懂別人代碼為什么這么做的時候,就去改它,然后可以工作了),就完事了,這樣你永遠也不可能提高,最后你就有了現在的這種感覺,覺得自己什么都不是,什么都不懂。
還有一點要說明的,現在有許多人搞 linux 開發,卻不去用 linux 系統做為自己工作的平臺,在這種情況下,你很難理解 linux 內核的實現機制,以及為什么要采用這種方式實現。
你都沒用過 linux 系統,就想去實現一個與 linux 運行機理相符合的項目,這是不可能的。就是你這個項目成功了,它也肯定不是最優的,或者是不符合 linux 的使用習慣的(包括內核的擴展和應用程序的實現)。
所以,最后想說的是,你一定要定期總結,總結你這段時間做了什么,你從中得到了什么,為了你以后可以更好的做好類似的工作,你應該去看些其它的什么東西;二是你一定至少要在工作的開發環境中使用 linux 作為你的平時工作平臺,而不要使用虛擬機和服務期,因為你只有完全了解了 linux 的使用,你才可以為它開發符合它規則的項目。
評論
查看更多