官术网_书友最值得收藏!

第1篇 Linux設備驅動入門

Linux設備驅動概述及開發環境構建

驅動設計的硬件基礎

Linux內核及內核編程

第1章 Linux設備驅動概述及開發環境構建

本章導讀

本章將介紹Linux設備驅動開發的基本概念,并對本書所基于的平臺和開發環境進行講解。

1.1節闡明了設備驅動的概念和作用。

1.2節和1.3節分別講解在無操作系統情況下和有操作系統情況下設備驅動的設計,通過對兩者不同的分析講解設備驅動與硬件和操作系統的關系。

1.4節對 Linux 操作系統的設備驅動進行了概要性的介紹,給出了設備驅動與整個軟硬件系統的關系,分析了Linux設備驅動的重點、難點和學習方法。

1.5節對本書所基于的LDD6410 ARM11開發板和開發環境的安裝進行了介紹。

本章的最后給出了一個設備驅動的“Hello World”實例,即最簡單的LED驅動在無操作系統情況下和Linux操作系統下的實現。

1.1 設備驅動的作用

任何一個計算機系統的運轉都是系統中軟硬件共同努力的結果,沒有硬件的軟件是空中樓閣,而沒有軟件的硬件則只是一堆廢鐵。硬件是底層基礎,是所有軟件得以運行的平臺,代碼最終會落實為硬件上的組合邏輯與時序邏輯。軟件則實現了具體應用,它按照各種不同的業務需求而設計,完成了用戶的最終訴求。硬件較固定,軟件則很靈活,可以適應各種復雜多變的應用。可以說,計算機系統的軟硬件互相成就了對方。

但是,軟硬件之間同樣存在著悖論,那就是軟件和硬件不應該互相滲透入對方的領地。為盡可能快速地完成設計,應用軟件工程師不想也不必關心硬件,而硬件工程師也難有足夠的閑暇和能力來顧及軟件。譬如,應用軟件工程師在調用套接字發送和接收數據包的時候,不必關心網卡上的中斷、寄存器、存儲空間、I/O 端口、片選以及其他任何硬件詞匯;在使用 printf()函數輸出信息的時候,他不用知道底層究竟是怎樣把相應的信息輸出到屏幕或者串口。

也就是說,應用軟件工程師需要看到一個沒有硬件的純粹的軟件世界,硬件必須被透明地呈現給他。誰來實現硬件對應用軟件工程師的隱形?這個光榮而艱巨的任務就落在了驅動工程師的頭上。

對設備驅動最通俗的解釋就是“驅使硬件設備行動”。驅動與底層硬件直接打交道,按照硬件設備的具體工作方式,讀寫設備的寄存器,完成設備的輪詢、中斷處理、DMA 通信,進行物理內存向虛擬內存的映射等,最終讓通信設備能收發數據,讓顯示設備能顯示文字和畫面,讓存儲設備能記錄文件和數據。

由此可見,設備驅動充當了硬件和應用軟件之間的紐帶,它使得應用軟件只需要調用系統軟件的應用編程接口(API)就可讓硬件去完成要求的工作。在系統中沒有操作系統的情況下,工程師可以根據硬件設備的特點自行定義接口,如對串口定義 SerialSend()、SerialRecv(),對 LED定義LightOn()、LightOff(),對Flash定義FlashWrite()、FlashRead()等。而在有操作系統的情況下,驅動的架構則由相應的操作系統定義,驅動工程師必須按照相應的架構設計驅動,這樣,驅動才能良好地整合入操作系統的內核。

驅動程序溝通著硬件和應用軟件,而驅動工程師則溝通著硬件工程師和應用軟件工程師。目前,隨著通信、電子行業的迅速發展,全世界每天都會有大量的新芯片被生產,大量的新電路板被設計,也因此,會有大量設備驅動需要開發。這些驅動,或運行在簡單的單任務環境,或運行在VxWorks、Linux、Windows等多任務操作系統環境,發揮著不可替代的作用。

1.2 無操作系統時的設備驅動

并不是任何一個計算機系統都一定要運行操作系統,在許多情況下,操作系統都不必存在。對于功能比較單一、控制并不復雜的系統,譬如ASIC內部、公交車的刷卡機、電冰箱、微波爐、簡單的手機和小靈通等,并不需要多任務調度、文件系統、內存管理等復雜功能,用單任務架構完全可以良好地支持它們的工作。一個無限循環中夾雜對設備中斷的檢測或者對設備的輪詢是這種系統中軟件的典型架構,如代碼清單1.1。

代碼清單1.1 單任務軟件典型架構

        1  int main(int argc, char* argv[])
        2  {
        3    while (1)
        4    {
        5     if (serialInt == 1)
        6     /*有串口中斷*/
        7     {
        8       ProcessSerialInt(); /*處理串口中斷*/
        9       serialInt = 0; /*中斷標志變量清0*/
        10    }
        11    if (keyInt == 1)
        12    /*有按鍵中斷*/
        13    {
        14      ProcessKeyInt(); /*處理按鍵中斷*/
        15      keyInt = 0; /*中斷標志變量清0*/
        16    }
        17    status = CheckXXX();
        18    switch (status)
        19    {
        20      ...
        21    }
        22    ...
        23   }
        24 }

在這樣的系統中,雖然不存在操作系統,但是設備驅動則無論如何都必須存在。一般情況下,每一種設備驅動都會定義為一個軟件模塊,包含.h文件和.c文件,前者定義該設備驅動的數據結構并聲明外部函數,后者進行驅動的具體實現。譬如,可以如代碼清單1.2那樣定義一個串口的驅動。

代碼清單1.2無操作系統情況下串口的驅動

        1  /**********************
        2   *serial.h文件
        3   **********************/
        4  extern void SerialInit(void);
        5  extern void SerialSend(const char buf*,int count);
        6  extern void SerialRecv(char buf*,int count);
        7
        8  /**********************
        9   *serial.c文件
        10  **********************/
        11 /*初始化串口*/
        12 void SerialInit(void)
        13 {
        14  ...
        15 }
        16 /*串口發送*/
        17 void SerialSend(const char buf*,int count)
        18 {
        19  ...
        20 }
        21 /*串口接收*/
        22 void SerialRecv(char buf*,int count)
        23 {
        24  ...
        25 }
        26 /*串口中斷處理函數*/
        27 void SerialIsr(void)
        28 {
        29  ...
        30  serialInt = 1;
        31 }

其他模塊想要使用這個設備的時候,只需要包含設備驅動的頭文件 serial.h,然后調用其中的外部接口函數。如我們要從串口上發送“Hello World”字符串,使用語句SerialSend(“Hello World”,11)即可。

由此可見,在沒有操作系統的情況下,設備驅動的接口被直接提交給了應用軟件工程師,應用軟件沒有跨越任何層次就直接訪問了設備驅動的接口。驅動包含的接口函數也與硬件的功能直接吻合,沒有任何附加功能。圖1.1所示為無操作系統情況下硬件、驅動與應用軟件的關系。

圖1.1 無操作系統時硬件、驅動和應用軟件的關系

有的工程師把單任務系統設計成了如圖1.2所示的結構,即設備驅動和具體的應用軟件模塊之間平等,驅動中包含了業務層面上的處理,這顯然是不合理的,不符合軟件設計中高內聚、低耦合的要求。

圖1.2 驅動與應用高耦合的不合理設計

另一種不合理的設計是直接在應用中操作硬件的寄存器,而不單獨設計驅動模塊,如圖1.3所示。這種設計意味著系統中不存在或未能充分利用可被重用的驅動代碼。

圖1.3 應用直接訪問硬件的不合理設計

1.3 有操作系統時的設備驅動

1.2節中我們看到一個干凈利落的設備驅動,它直接運行在硬件之上,不與任何操作系統關聯。當系統中包含操作系統后,設備驅動會變得怎樣?

首先,無操作系統時設備驅動的硬件操作工作仍然是必不可少的,沒有這一部分,驅動不可能與硬件打交道。

其次,我們還需要將驅動融入內核。為了實現這種融合,必須在所有設備的驅動中設計面向操作系統內核的接口,這樣的接口由操作系統規定,對一類設備而言結構一致,獨立于具體的設備。

由此可見,當系統中存在操作系統的時候,驅動變成了連接硬件和內核的橋梁。如圖1.4,操作系統的存在勢必要求設備驅動附加更多的代碼和功能,把單一的“驅使硬件設備行動”變成了操作系統內與硬件交互的模塊,它對外呈現為操作系統的API,不再給應用軟件工程師直接提供接口。

圖1.4 硬件、驅動、操作系統和應用程序的關系

那么我們要問,有了操作系統之后,驅動反而變得復雜,那要操作系統干什么?

首先,一個復雜的軟件系統需要處理多個并發的任務,沒有操作系統,想完成多任務并發是很困難的。

其次,操作系統給我們提供內存管理機制。一個典型的例子是,對于多數含MMU的處理器而言, Windows、Linux 等操作系統可以讓每個進程都可以獨立地訪問4GB的內存空間。

上述優點似乎并沒有體現在設備驅動身上,操作系統的存在給設備驅動究竟帶來了什么實質的好處?

簡而言之,操作系統通過給驅動制造麻煩來達到給上層應用提供便利的目的。當驅動都按照操作系統給出的獨立于設備的接口而設計,那么,應用程序將可使用統一的系統調用接口來訪問各種設備。對于類UNIX的VxWorks、Linux等操作系統而言,當應用程序通過write()、read()等函數讀寫文件就可訪問各種字符設備和塊設備,而不論設備的具體類型和工作方式,那將是怎樣的便利?

1.4 Linux設備驅動

1.4.1 設備的分類及特點

計算機系統的硬件主要由 CPU、存儲器和外設組成。隨著 IC制作工藝的發展,目前,芯片的集成度越來越高,往往在 CPU 內部就集成了存儲器和外設適配器。譬如,相當多的ARM、PowerPC、MIPS等處理器都集成了UART、I2C控制器、USB控制器、SDRAM控制器等,有的處理器還集成了片內RAM和Flash。

驅動針對的對象是存儲器和外設(包括CPU內部集成的存儲器和外設),而不是針對CPU核。Linux將存儲器和外設分為3個基礎大類。

● 字符設備。

● 塊設備。

● 網絡設備。

字符設備指那些必須以串行順序依次進行訪問的設備,如觸摸屏、磁帶驅動器、鼠標等。塊設備可以用任意順序進行訪問,以塊為單位進行操作,如硬盤、軟驅等。字符設備不經過系統的快速緩沖,而塊設備經過系統的快速緩沖。但是,字符設備和塊設備并沒有明顯的界限,如對于Flash設備,符合塊設備的特點,但是我們仍然可以把它作為一個字符設備來訪問。

字符設備和塊設備的驅動設計呈現出很大的差異,但是對于用戶而言,他們都使用文件系統的操作接口open()、close()、read()、write()等進行訪問。

在Linux系統中,網絡設備面向數據包的接收和發送而設計,它并不對應于文件系統的節點。內核與網絡設備的通信與內核和字符設備、網絡設備的通信方式完全不同。

另外一種設備分類方法中所稱的I2C驅動、USB驅動、PCI驅動、LCD驅動等本身可歸納入3個基礎大類,但是對于這些復雜的設備,Linux也定義了獨特的驅動體系結構。

1.4.2 Linux設備驅動與整個軟硬件系統的關系

如圖1.5所示,除網絡設備外,字符設備與塊設備都被映射到Linux文件系統的文件和目錄,通過文件系統的系統調用接口 open()、write()、read()、close()等即可訪問字符設備和塊設備。所有的字符設備和塊設備都被統一地呈現給用戶。塊設備比字符設備復雜,在它上面會首先建立一個磁盤/Flash文件系統,如FAT、EXT3、YAFFS2、JFFS2、UBIFS等。FAT、EXT3、YAFFS2、JFFS2、UBIFS定義了文件和目錄在存儲介質上的組織。

圖1.5 Linux設備驅動與整個軟硬件系統的關系

應用程序可以使用Linux的系統調用接口編程,但也可使用C庫函數,出于代碼可移植性的目的,后者更值得推薦。C庫函數本身也通過系統調用接口而實現,如C庫函數fopen()、fwrite()、fread()、fclose()分別會調用操作系統的API open()、write()、read()、close()。

1.4.3 Linux設備驅動的重點、難點

Linux設備驅動的學習是一項浩繁的工程,包含如下的重點、難點。

● 編寫 Linux 設備驅動要求工程師有非常好的硬件基礎,懂得 SRAM、Flash、SDRAM、磁盤的讀寫方式,UART、I2C、USB等設備的接口以及輪詢、中斷、DMA的原理,PCI總線的工作方式以及CPU的內存管理單元(MMU)等。

● 編寫Linux設備驅動要求工程師有非常好的C語言基礎,能靈活地運用C語言的結構體、指針、函數指針及內存動態申請和釋放等。

● 編寫Linux設備驅動要求工程師有一定的Linux內核基礎,雖然并不要求工程師對內核各個部分有深入的研究,但至少要明白驅動與內核的接口。尤其是對于塊設備、網絡設備、Flash設備、串口設備等復雜設備,內核定義的驅動體系架構本身就非常復雜。

● 編寫Linux設備驅動要求工程師有非常好的多任務并發控制和同步的基礎,因為在驅動中會大量使用自旋鎖、互斥、信號量、等待隊列等并發與同步機制。

上述經驗值的獲取并非朝夕之事,因此要求我們有足夠的學習恒心和毅力。對這些重點、難點,本書都會有相應章節進行講解。

動手實踐永遠是學習任何軟件開發的最好方法,學習Linux設備驅動也不例外。因此,本書專門配備了一款基于S3C6410的ARM11開發板LDD6410(全稱Linux Device Drivers 6410,即Linux設備驅動開發6410專用板),本書中的所有實例均可在該電路板上直接執行。

閱讀經典書籍和參與 Linux 社區的討論也是非常好的學習方法。Linux 內核源代碼中包含了一個Documentation 目錄,其中包含了一批內核設計的文檔,全部是文本文件。很遺憾,這些文檔的組織不太好,內容也不夠細致。本書的參考目錄中給出了一些優秀的參考書籍和Linux網站,并進行了簡單的介紹。

學習Linux設備驅動的一個注意事項是要避免管中窺豹、只見樹木不見森林,因為各類Linux設備驅動都從屬于一個Linux設備驅動的架構,單純而片面地學習幾個函數、幾個數據結構是不可能理清驅動中各組成部分之間的關系的。因此,Linux 驅動的分析方法是點面結合,將對函數和數據結構的理解放在整體架構的背景之中。這是本書各章節講解驅動的方法。

1.5 Linux設備驅動開發環境構建

1.5.1 PC上的Linux環境

本書配套光盤提供了一個Ubuntu的VirtualBox虛擬機映像,該虛擬機上安裝了所有本書涉及的源代碼、工具鏈和各種開發工具,讀者無需再安裝和配置任何環境。該虛擬機可運行于Windows等操作系統中,運行方法如下。

(1)解壓縮安裝盤內的虛擬機磁盤映像virtual-disk.rar到本地硬盤得到virtual-disk.vdi(至少需要16GB的空閑磁盤空間)。

(2)安裝安裝盤內的VirtualBox虛擬機軟件。

(3)建立一個虛擬機。

① 單擊“新建”按鈕,指定虛擬機使用Linux Ubuntu系統,如圖1.6所示。

圖1.6 VirtualBox指定使用Ubuntu

② 單擊“下一步”按鈕,如圖1.7所示,使用推薦的內存384MB。

圖1.7 VirtualBox中內存設定

③ 指定虛擬機磁盤映像為第一步解壓縮得到的virtual-disk.vdi,如圖1.8所示。

圖1.8 VirtualBox中磁盤設定

④ 完成設置,如圖1.9所示。

之后就可以啟動虛擬機,賬號和密碼都是“lihacker”。本書配套源代碼都位于lihacker主目錄的develop目錄下,幾個主要項目針對/home/lihacker/develop/的子目錄如下。

LDD6410開發板內核源代碼:svn/ldd6410-2-6-28-read-only/linux-2.6.28-samsung。

LDD6410開發板U-BOOT源代碼:svn/ldd6410-read-only/s3c-u-boot-1.1.6。

圖1.9 VirtualBox中完成設定

LDD6410 開發板文件系統用的busybox、jpegview、mplayer、appweb 等:svn/ldd6410-read-only/utils。

LDD6410開發板及常用Linux用戶空間驅動測試程序:svn/ldd6410-read-only/tests。

書中globalmem、globalfifo等驅動實例:svn/ldd6410-read-only/training/kernel。

Android的源代碼:git/myandroid。

NDK:android-ndk-r3。

eclipse:單擊桌面上的“android-eclipse”圖標,即可運行附帶ADT的eclipse開發工具。

1.5.2 LDD6410開發板

LDD6410是本書專配的一款高端ARM11處理器開發板(其結構如圖1.10所示,實物如圖1.11所示),采用三星公司最新推出S3C6410處理器,芯片擁有強大的內部資源和視頻處理能力,板上集成了豐富的外圍接口,其主要特點如下。

(1)運行于533MHz的ARM11處理器(最高主頻可達到667MHz)。

(2)運行于266MHz的DDR內存,128MB。

(3)1MB NOR Flash。

(4)256MB NAND Flash。

(5)WM9714 AC97聲卡。

(6)VGA輸出接口(可達1024×768@60Hz)。

(7)TV輸出接口。

(8)USB2.0 OTG接口及USB1.1 host接口。

圖1.10 LDD6410的結構圖

圖1.11 LDD6410實物圖

(9)SD/SDIO接口,支持SD卡和SDIO設備。

(10)DM9000百兆網卡。

(11)4.3寸LCD(分辨率為480×272)、觸摸屏。

(12)S3C6410芯片內嵌圖形加速,JPEG、多媒體編解碼。

(13)6個GPIO按鍵。

(14)可擴展Camera、WiFi、3G modem等模塊。

(15)可擴展外部矩陣鍵盤。

配套電路板提供了如下軟件。

(1)工具鏈:提供了arm-linux-gcc、arm-linux-gdb、gdbserver、strace用于Android開發的eclipse (帶ADT插件)、JDK和NDK。

(2)U-BOOT:U-BOOT源代碼包含獨立的LDD6410文件,支持從SD卡、NAND啟動,支持DM9000網卡引導。

(3)Linux內核、BSP和驅動:Linux2.6.28內核、源代碼,包含獨立的LDD6410 BSP和完整的設備驅動。

(4)文件系統:基于新版Busybox1.15.1,文件系統集成jpegview、mplayer、appweb等大量應用,集成了按鍵、鼠標、觸摸屏、LCD等測試程序,作為驅動的用戶應用案例。

(5)Android:提供Android源代碼和文件系統、內核電源管理補丁源代碼、內核Android驅動源代碼。LDD6410的Android系統支持按鍵、觸摸屏和鼠標操作,支持使用LCD和VGA進行顯示。(6)QT:LDD6410支持Qt/Embedded4.5.3,移植了Ts_lib和Tslib, ts_calibration,支持使用觸摸屏進行操作。

LDD6410支持從SD卡或NAND啟動,通過電路板上的SW1可設置LDD6410的啟動模式。從SD卡啟動設備為全ON;從NAND啟動時,將1、2設置為ON,3、4設置為OFF。

LDD6410開發板的詳細使用方法,請見配套光盤中的“LDD6410開發板用戶手冊”。

1.5.3 工具鏈安裝

本書配套光盤的虛擬機映像中已經安裝好了LDD6410的工具鏈,讀者如果想在其他環境中安裝,只需要從 http://ldd6410.googlecode.com/files/cross-4.2.2-eabi.tar.bz2下載。LDD6410 開發板工具鏈為S3C6410X-ToolChain4.2.2-EABI-V0.0-cross-4.2.2-eabi.tar。安裝步驟如下。

(1)解壓上述工具鏈獲得文件夾:4.2.2-eabi/。

(2)在/usr/local/下面創建目錄 arm/(注意,最好是放到這個目錄,不然在以后的編譯過程中可能出現一些錯誤)。

(3)將目錄4.2.2-eabi/移動到/usr/local/arm/下面。

(4)設置環境變量。

編輯/etc/profile 文件,在文件末尾添加:

        PATH="$PATH:/usr/local/arm/4.2.2-eabi/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

(5)測試環境變量是否設置成功。

在終端輸入:echo $PATH,如果輸出的路徑中包含了/usr/local/arm/4.2.2-eabi/usr/bin,則說明環境變量設置成功。

(6)測試交叉編譯工具鏈。

在終端輸入“arm-linux-gcc -v”,顯示如下:

        Using built-in specs.
        Target: arm-unknown-linux-gnueabi
        Configured with:
        /home/scsuh/workplace/coffee/buildroot-20071011/toolchain_build_arm
        /gcc-4.2.2/configure --prefix=/usr --build=i386-pc-linux-gnu --host=i386-pc-linux-gnu
        --target=arm-unknown-linux-gnueabi --enable-languages=c,c++ --with-sysroot=/usr/local
        /arm/4.2.2-eabi/ --with-build-time-tools=/usr/local/arm/4.2.2-eabi//usr/arm-unknown-linux-
        gnueabi/bin --disable-cxa_atexit --enable-target-optspace --with-gnu-ld --enable-shared
        --with-gmp=/usr/local/arm/4.2.2-eabi/gmp --with-mpfr=/usr/local/arm/4.2.2-eabi//mpfr
        --disable-nls --enable-threads --disable-multilib --disable-largefile --with-arch=armv4t
        --with-float=soft --enable-cxx-flags=-msoft-float
        Thread model: posix gcc version 4.2.2

說明交叉編譯工具鏈已經安裝成功。

ldd6410-debug-tools.tar.gz調試工具包包含了strace、gdbserver和arm-linux-gdb,其中 strace、gdbserver用于目標板文件系統,arm-linux-gdb 運行于主機端,對目標板上的內核、內核模塊應用程序進行調試。

下載地址為 http://ldd6410.googlecode.com/files/ldd6410-debug-tools.tar.gz,光盤目錄為 toolchains/ldd6410-debug-tools.tar.gz。

解壓ldd6410-debug-tools.tar.gz,將其中的arm-linux-gdb放入主機上arm-linux-gcc所在的目錄/usr/local/arm/4.2.2-eabi/usr/bin/。

而 strace、gdbserver則可根據需要放入目標機根文件系統的/usr/sbin目錄。

1.5.4 主機端nfs和tftp服務安裝

本書配套光盤的虛擬機映像中已經安裝好了nfs和tftp,LDD6410可使用tftp或nfs文件系統與主機通過網口交互。如果用戶想在其他環境下自行安裝,對于Ubuntu或Debian用戶而言,在主機端可通過如下方法安裝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 源代碼閱讀和編輯

源代碼是學習 Linux 的最權威資料,在 Windows 上閱讀 Linux 源代碼的最佳工具是 Source Insight,在其中建立一個工程,并將 Linux 的所有源代碼加入該工程,同步這個工程之后,我們將可以非常方便地在代碼之間進行關聯閱讀,如圖1.12所示。

圖1.12 在Source Insight中閱讀Linux源代碼

網站http://lxr.linux.no/提供了內核版本2.6.11到最新版Linux源代碼的交叉索引,在其中輸入Linux 內核中的函數、數據結構或變量的名稱就可以直接得到以超鏈接形式給出的定義和引用它的所有位置。還有一些網站也提供了Linux內核中函數、變量和數據結構的搜索能力,在google中搜索“linux identifier search”可得。

在Linux主機上閱讀和編輯Linux源碼的常用方式是vim + cscope或者vim + ctags,vim是一個文本編輯器,而cscope和ctags則可建立代碼索引,建議讀者盡快使用基于文本界面全鍵盤操作的vim編輯器,如圖1.13所示。

圖1.13 vim編輯器

1.6 設備驅動Hello World:LED驅動

1.6.1 無操作系統時的LED驅動

在嵌入式系統的設計中,LED 一般直接由 CPU 的GPIO(通用可編程 I/O 口)控制。GPIO一般由兩組寄存器控制,即一組控制寄存器和一組數據寄存器。控制寄存器可設置GPIO口的工作方式為輸入或是輸出。當引腳被設置為輸出時,向數據寄存器的對應位寫入1和0會分別在引腳上產生高電平和低電平;當引腳設置為輸入時,讀取數據寄存器的對應位可獲得引腳上的電平為高或低。

在本例子中,我們屏蔽具體CPU的差異,假設在GPIO_REG_CTRL物理地址處的控制寄存器處的第n位寫入1可設置GPIO為輸出,在地址GPIO_REG_DATA物理地址處的數據寄存器的第n位寫入1或0可在引腳上產生高或低電平,則無操作系統的情況下,設備驅動為代碼清單1.3。

代碼清單1.3 無操作系統時的LED驅動

        1  #define reg_gpio_ctrl *(volatile int *)(ToVirtual(GPIO_REG_CTRL))
        2  #define reg_gpio_data *(volatile int *)(ToVirtual(GPIO_REG_DATA))
        3  /*初始化LED*/
        4  void LightInit(void)
        5  {
        6    reg_gpio_ctrl |= (1 << n); /*設置GPIO為輸出*/
        7  }
        8
        9  /*點亮LED*/
        10 void LightOn(void)
        11 {
        12   reg_gpio_data |= (1 << n); /*在GPIO上輸出高電平*/
        13 }
        14
        15 /*熄滅LED*/
        16 void LightOff(void)
        17 {
        18   reg_gpio_data &= ~(1 << n); /*在GPIO上輸出低電平*/
        19 }

上述程序中的LightInit()、LightOn()、LightOff()都直接作為驅動提供給應用程序的外部接口函數。程序中ToVirtual()的作用是當系統啟動了硬件MMU之后,根據物理地址和虛擬地址的映射關系,將寄存器的物理地址轉化為虛擬地址。

1.6.2 Linux下的LED驅動

在Linux下,可以使用字符設備驅動的框架來編寫對應于代碼清單1.3的LED設備驅動(這里僅僅是為了講解的方便,到后文我們會發現,內核中實際實現了一個提供sysfs結點的GPIO LED驅動,位于drivers/leds/leds-gpio.c),操作硬件的LightInit()、LightOn()、LightOff()函數仍然需要,但是,遵循 Linux 編程的命名習慣,重新將其命名為 light_init()、light_on()、light_off()。這些函數將被LED設備驅動中獨立于設備的針對內核的接口進行調用,代碼清單1.4給出了Linux下LED的驅動,此時讀者并不需要能讀懂這些代碼。

代碼清單1.4 Linux操作系統下LED的驅動

          1  #include .../*包含內核中的多個頭文件*/
          2  /*設備結構體*/
          3  struct light_dev {
          4      struct cdev cdev; /*字符設備cdev結構體*/
    5      unsigned char vaule; /*LED亮時為1,熄滅時為0,用戶可讀寫此值*/
    6  };
    7  struct light_dev *light_devp;
    8  int light_major = LIGHT_MAJOR;
    9  MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
    10  MODULE_LICENSE("Dual BSD/GPL");
    11  /*打開和關閉函數*/
    12  int light_open(struct inode *inode, struct file *filp)
    13  {
    14      struct light_dev *dev;
    15      /* 獲得設備結構體指針 */
    16      dev = container_of(inode->i_cdev, struct light_dev, cdev);
    17      /* 讓設備結構體作為設備的私有信息 */
    18      filp->private_data = dev;
    19      return 0;
    20  }
    21  int light_release(struct inode *inode, struct file *filp)
    22  {
    23      return 0;
    24  }
    25  /*讀寫設備:可以不需要 */
    26  ssize_t light_read(struct file *filp, char __user *buf, size_t count,
    27      loff_t *f_pos)
    28  {
    29      struct light_dev *dev = filp->private_data; /*獲得設備結構體 */
    30      if (copy_to_user(buf, &(dev->value), 1))
    31           return  -EFAULT;
    32      return 1;
    33  }
    34  ssize_t light_write(struct file *filp, const char __user *buf, size_t count,
    35      loff_t *f_pos)
    36  {
    37      struct light_dev *dev = filp->private_data;
    38      if (copy_from_user(&(dev->value), buf, 1))
    39           return  -EFAULT;
    40      /*根據寫入的值點亮和熄滅LED*/
    41      if (dev->value == 1)
    42           light_on();
    43      else
    44           light_off();
    45      return 1;
    46  }
    47  /* ioctl函數 */
    48  int light_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
    49      unsigned long arg)
    50  {
    51      struct light_dev *dev = filp->private_data;
    52      switch (cmd) {
    53      case LIGHT_ON:
    54           dev->value = 1;
    55           light_on();
    56           break;
    57      case LIGHT_OFF:
    58           dev->value = 0;
    59           light_off();
    60           break;
    61      default:
    62           /* 不能支持的命令 */
    63           return  -ENOTTY;
    64      }
    65      return 0;
    66  }
    67  struct file_operations light_fops = {
    68      .owner = THIS_MODULE,
    69      .read = light_read,
    70      .write = light_write,
    71      .ioctl = light_ioctl,
    72      .open = light_open,
    73      .release = light_release,
    74  };
    75  /*設置字符設備cdev結構體*/
    76  static void light_setup_cdev(struct light_dev *dev, int index)
    77  {
    78      int err, devno = MKDEV(light_major, index);
    79      cdev_init(&dev->cdev, &light_fops);
    80      dev->cdev.owner = THIS_MODULE;
    81      dev->cdev.ops = &light_fops;
    82      err = cdev_add(&dev->cdev, devno, 1);
    83      if (err)
    84           printk(KERN_NOTICE "Error %d adding LED%d", err, index);
    85  }
    86  /*模塊加載函數*/
    87  int light_init(void)
    88  {
    89      int result;
    90      dev_t dev = MKDEV(light_major, 0);
    91      /* 申請字符設備號*/
    92      if (light_major)
    93           result = register_chrdev_region(dev, 1, "LED");
    94      else {
    95           result = alloc_chrdev_region(&dev, 0, 1, "LED");
    96           light_major = MAJOR(dev);
    97      }
    98      if (result < 0)
    99           return result;
   100      /* 分配設備結構體的內存 */
   101      light_devp = kmalloc(sizeof(struct light_dev), GFP_KERNEL);
   102      if (!light_devp) {
   103           result =  -ENOMEM;
   104           goto fail_malloc;
   105      }
   106      memset(light_devp, 0, sizeof(struct light_dev));
   107      light_setup_cdev(light_devp, 0);
   108      light_gpio_init();
   109      return 0;
   110 fail_malloc:
   111      unregister_chrdev_region(dev, light_devp);
   112      return result;
   113 }
   114 /*模塊卸載函數*/
   115 void light_cleanup(void)
   116 {
   117      cdev_del(&light_devp->cdev); /*刪除字符設備結構體*/
   118      kfree(light_devp); /*釋放在light_init中分配的內存*/
   119      unregister_chrdev_region(MKDEV(light_major, 0), 1); /*刪除字符設備*/
   120 }
   121 module_init(light_init);
   122 module_exit(light_cleanup);

上述代碼的行數與代碼清單1.3已經不能比擬,除了代碼清單1.3中的硬件操作函數仍然需要外,代碼清單1.4中還包含了大量對我們暫時陌生的元素,如結構體file_operations、cdev,Linux內核模塊聲明用的MODULE_AUTHOR、MODULE_LICENSE、module_init、module_exit,以及用于字符設備注冊、分配和注銷用的函數register_chrdev_region()、alloc_chrdev_region()、unregister_chrdev_region()等。我們也不能理解為什么驅動中要包含light_init ()、light_cleanup ()、light_read()、light_write()等函數。

此時,我們只需要有一個感性認識,那就是,上述暫時陌生的元素都是Linux內核給字符設備定義的為實現驅動與內核接口而定義的。Linux 對各類設備的驅動都定義了類似的數據結構和函數。

1.7 全書結構

本書第1篇給您打下Linux設備驅動的基礎。第1章簡要地介紹了設備驅動的作用,并從無操作系統的設備驅動引出了Linux操作系統下的設備驅動,介紹了本書所基于的開發環境。第2章系統地講解了一個Linux驅動工程師應該掌握的硬件知識,為工程師打下Linux驅動編程的硬件基礎,講解了各種類型的CPU、存儲器和常見的外設,并闡述了硬件時序分析方法和數據手冊閱讀方法。第3章將Linux設備驅動放在Linux2.6內核背景中進行講解,說明Linux內核的編程方法。由于驅動編程也在內核編程的范疇,因此,這一章實質是為編寫Linux設備驅動打下軟件基礎。

第2篇講解Linux設備驅動編程的基礎理論、字符設備驅動及設備驅動設計中涉及的并發控制、同步等問題。第4、5章分別講解Linux內核模塊和Linux設備文件系統,第6~9章以虛擬設備globalmem和globalfifo為主線,逐步給其添加高級控制功能,第10、11章分別闡述Linux驅動編程中所涉及的中斷和定時器、內核和I/O操作處理方法,本篇的第12章講解了Linux設備驅動工程化的一些問題,屬于承前啟后的一章。

第3篇剖析復雜設備驅動的體系架構,每一章都給出了具體的實例。所涉及的設備包括塊設備、終端設備、I2C適配器與I2C設備、網絡設備、PCI設備、USB設備、LCD設備、Flash設備等。這一部分的講解方法是抽象與具體相結合,先以模板的形式給出各種設備驅動的設計,然后用具體實例設備的驅動填充對應的模板。

第4篇分析了Linux設備驅動的調試和移植方法。由于在Linux設備驅動的設計工作中人們強調多快好省,因此,如果能方便地把現有的其他平臺中的驅動移植到Linux2.6平臺,或者將類似設備的驅動進行簡單修改就運用于新的設備,那將會極大地縮短工程的實施時間。本書的最后幾章對Linux設備驅動移植中涉及的理論以及移植的技巧進行了講解。

主站蜘蛛池模板: 寻甸| 天台县| 景德镇市| 左云县| 台安县| 左云县| 马龙县| 南投县| 安多县| 铜梁县| 栾城县| 蓬安县| 克山县| 和平区| 三原县| 莱芜市| 哈密市| 江山市| 泰顺县| 孙吴县| 八宿县| 滦南县| 巴中市| 米易县| 兴隆县| 呼伦贝尔市| 泸定县| 滦南县| 平安县| 昌宁县| 鲁山县| 彰武县| 古交市| 伊吾县| 夏邑县| 贡山| 马山县| 广河县| 周宁县| 黄梅县| 城固县|