Linux設備驅動程序的基本知識

設備驅動程序用作硬件與使用硬件的應用程序(用戶代碼)或內核之間的轉譯器,它將硬件的工作細節隱藏於幕後,從而起到簡化編程的作用。編程人員可以利用一套標準化調用方法(系統調用)編寫高級應用程序代碼,而不必關心它將控制的特定硬件或運行於其上的處理器。藉助定義明確的內部應用程序編程接口(內核API),應用程序代碼便可以通過與軟件上層結構或底層硬件無關的標準方式與設備驅動程序實現接口。

  針對特定處理器平臺,操作系統(OS)處理硬件操作的細節。利用內核(OS)內部硬件抽象層(HAL)和處理器專用外設驅動程序(例如I2C®SPI總線驅動程序),通常的設備驅動程序甚至也能獨立處理器平臺。這種方法允許一個設備驅動程序(例如觸摸屏數字化儀AD7879的驅動程序)可以不加修改地用在任何運行Linux的處理器平臺上,Linux內核之上運行任何圖形用戶界面(GUI)包和適當的應用程序。如果硬件設計人員決定轉而使用觸摸屏控制器AD7877,他(她)將無需軟件團隊提供信息。兩款器件均可用驅動程序;雖然器件不同,連接方式可能不同(AD7877僅提供SPI,AD7879則有SPI或I2C),並且寄存器圖也不相同,但相對於觸摸屏用戶代碼的內核API完全相同。這樣,對硬件的控制權便又回到硬件架構師手中。

  Linux內核中的不同類型設備驅動程序提供不同的抽象層次,傳統上一般將其分爲以下三類。

  字符設備:處理字節流。串行端口或輸入設備驅動程序(鍵盤、鼠標、觸摸屏、遊戲操縱桿等)通常實現字符設備類型。

  塊數據設備:單次操作處理512字節或更多的二次冪塊數據。存儲設備驅動程序通常實現此類塊設備。

  網絡接口:任何網絡事務都通過接口完成,接口指能夠與其他主機交換數據的設備。

  在Linux內核中,各特殊類別都可能有多個獨立的設備核心層,以幫助開發人員實現提供標準用途的驅動程序,如視頻、音頻、網絡、輸入設備或背光處理等。通常,每個子系統在Linux內核源代碼樹中都有其自己的目錄。這種“設備驅動程序核心方法”消除了特定類別所有設備驅動程序的公共代碼,爲上層構建了一個標準接口。每類設備或總線設備核心驅動程序通常會將一個函數集導出至其子類。驅動程序利用這種核心驅動程序註冊,並使用核心驅動程序所導出的API,而不是註冊其自己的字符/塊/網絡驅動程序。這通常包括支持和處理多個實例以及在層間分配數據的方式。絕大部分系統無意瞭解設備如何連接,但需要知道何種設備可用。Linux設備模型也包括一種將設備指派給特定類別的機制,如input(輸入)、RTC(實時時鐘)、net(網絡)或GPIO(通用輸入/輸出)等。這些類名在更高的功能層次描述設備,使之能在用戶空間中被發現。

  特定硬件可能有多個設備驅動程序子系統與之相關。多功能芯片,例如帶I/O擴展器的背光驅動器ADP5520,會同時利用Linux背光、LED、GPIO和輸入子系統來實現其鍵盤功能。

  如前文所述,用戶應用程序不能直接與硬件通信,因爲那將要求擁有對處理器的管理員權限,例如執行特殊指令或處理中斷。使用特定硬件設備的應用程序通常在通過/dev目錄中的節點暴露的內核驅動程序上工作。

  設備節點稱爲僞文件,因爲它們看起來像文件;應用程序也可以打開或關閉它們(open()或close()),但在讀取或寫入這些文件時,數據來自或傳遞至設備節點相關的驅動程序。這個抽象層次由Linux內核中的虛擬文件系統(VFS)處理。除了read()、write()或poll()外,用戶應用程序也可以利用ioctl()(輸入/輸出控制)與設備交互。

  除設備節點外,應用程序也可以利用/sys目錄中的文件條目;這是一個sysfs虛擬文件系統,可將有關設備和驅動程序的信息,包括父子關係或與特定類、總線的關聯,從內核設備模型導出至用戶空間。/sys也頻繁用於設備配置,特別是當相關驅動程序以一個設備驅動程序核心註冊時,此時它只將其標準功能集導出給用戶。

  設備驅動程序可以註冊/sys“鉤子”或“條目”;讀取或寫入鉤子或條目時,將執行設備驅動程序專門註冊的回調函數。這些回調函數(在管理員模式下運行)可以接受參數、發起總線傳輸、調用某種處理、修改特定設備變量,並將整數值或字符串返回給用戶。這就爲實現其他功能創造了條件;例如,用戶空間可以使用觸摸屏數字化儀AD7877的溫度傳感器或輔助ADC。

  設備驅動程序既可以靜態地構建於內核中,也可以在以後作爲可加載模塊動態安裝。Linux內核模塊(LKM)是動態組件,可以在運行時插入和移除。這對於驅動程序開發人員特別有用,因爲更快的編譯速度可以節省時間,而且測試模塊不必重啓系統。讓硬件驅動程序駐留在可以隨時載入內核的模塊中,便可以在特定硬件不用時節約RAM。

  加載模塊時,也可以賦予其配置參數。對於構建於內核中的模塊,參數在內核啓動時傳送給該模塊。例如:

  root:~> insmod ./sample_module.ko argument=1

  root:~> lsmod

  Module Size Used by

  sample_module 1396 0 - Live 0x00653000

  root:~> rmmod sample_module

  驅動程序也可以多次實例化,每次實例化都可以採用不同的設置,目標設備可以有不同的I2C從ID,連接到不同的SPI從選擇,或者映射到不同的物理存儲器地址。所有實例共用同樣的代碼,以便節省存儲器,但具有各自的數據段。

  Linux是一種先佔式多任務、多用戶操作系統,因此幾乎所有設備驅動程序和內核子系統都允許多個進程(可能由不同的用戶所有)同時利用設備。常見的例子有network(網絡)、audio(音頻)或input(輸入)接口。QWERTY鍵盤控制器ADP5588的鍵按下或釋放事件會被加上時間戳、排隊併發送至所有已打開input vent device(輸入事件設備)的進程。這些事件代碼在所有架構上都相同,並且與硬件無關。讀USB鍵盤與從用戶空間讀取ADP5588並無區別。事件類型通過代碼加以區分。鍵盤發送鍵事件(EV_KEY)、鍵識別碼以及代表按下或釋放動作的某種狀態值。觸摸屏發送絕對座標事件(EV_ABS)以及由x、y和觸摸壓力組成的一個三元組,鼠標則發送相對運動事件(EV_REL)。加速度計ADXL346在發送關於加速度的絕對座標事件的同時,可以發送關於單振或雙振的鍵事件。

  某些應用中,加速度計ADXL346產生相對事件或者發送特定鍵代碼(特定應用設置),也很有意義。一般而言,定製驅動程序有兩種方式:運行時或編譯時。

  可能在運行時進行定製的設備特性使用模塊參數和或/sys條目。

  實現特定目標

  使用開源Linux驅動程序—通過定製實現特定目標,對於編譯時配置,將特定板和特定應用配置排除在主驅動程序文件之外是Linux的慣例,一般將其放入board support file(板支持文件)中。

  對於定製板上的設備(這是嵌入式和基於SoC片上系統硬件的典型現象),Linux使用platform_data指向描述設備及其如何連到SoC的特定板結構。這可以包括可用端口、不同芯片版本、首選模式、默認初始化、引腳的其他作用等。這將能縮小板支持包(BSP),並儘量減少驅動程序中板和應用特定的#ifdef。至於哪些可調變量進入platform_data,哪些應當在運行時具有訪問權,則由驅動程序的作者決定。

  數字加速度計特性與應用具有非常密切的關係,不同的板和型號可能具有不同的特性。下例顯示了一組配置選項。這些變量在頭文件adxl34x.h(include/linux/input/adxl34x.h)中有詳細描述。

  Analog Dialogue 44-03, March (2010)

  #include

  static const struct adxl34x_platform_data

  adxl34x_info={

  .x_axis_offset=0,

  .y_axis_offset=0,

  .z_axis_offset=0,

  .tap_threshold=0x31,

  .tap_duration=0x10,

  .tap_latency=0x60,

  .tap_window=0xF0,

  .tap_axis_control=ADXL_TAP_X_EN | ADXL_TAP_

  Y_EN | ADXL_TAP_Z_EN,

  .act_axis_control=0xFF,

  .activity_threshold=5,

  .inactivity_threshold=3,

  .inactivity_time=4,

  .free_fall_threshold=0x7,

  .free_fall_time=0x20,

  .data_rate=0x8,

  .data_range=ADXL_FULL_RES,

  .ev_type=EV_ABS,

  .ev_code_x=ABS_X,/*EV_REL*/

  .ev_code_y=ABS_Y,/*EV_REL*/

  .ev_code_z=ABS_Z,/*EV_REL*/

  .ev_code_tap={BTN_TOUCH,BTN_TOUCH,BTN_TOUCH},/*EV_KEY x,y,z */

  .ev_code_ff=KEY_F,/* EV_KEY */

  .ev_code_act_inactivity=KEY_A,/*EV_KEY*/

  .power_mode=ADXL_AUTO_SLEEP|ADXL_LINK,

  .fifo_mode=ADXL_FIFO_STREAM,

  };

  爲將設備與驅動程序相關聯,“平臺和總線模型”無需設備驅動程序來包含其所控制設備的硬編碼物理地址或總線ID。平臺和總線模型還能防止資源衝突,大大改善便攜性,並與內核的電源管理特性乾淨利落地接口。

  利用平臺和總線模型,設備驅動程序一旦獲得設備的物理位置和中斷線路,便知道如何控制設備。該信息在探測期間作爲一個數據結構傳遞給驅動程序。

  與PCI或USB設備不同,I2C或SPI設備不會在硬件層次上進行枚舉。相反,軟件必須知道每個I2C/SPI總線段上連接了哪些設備,以及這些設備使用什麼地址。因此,內核代碼必須明確實例化I2C/SPI設備。這可以通過多種不同方法實現,具體取決於上下文和要求。不過,最常用的方法是通過總線號碼聲明I2C/SPI設備。

  當I2C/SPI總線是一條系統總線時,這種方法是合適的;許多嵌入式系統正是這種情況,其中每條I2C/SPI總線都有一個事先已知的號碼。因此,可以預先聲明連到該總線的I2C/SPI設備。這可以利用一個結構體i2c_board_info/spi_board_info陣列來完成,該陣列通過調用以下內容註冊i2c_register_board_info()/spi_register_board_info()

  static struct i2c_board_info __initdata bfin_ i2c_board_info[] = {#if defined(CONFIG_TOUCHSCREEN_AD7879_I2C)||defined(CONFIG_TOUCHSCREEN_AD7879_I2C_MODULE)

  {

  I2C_BOARD_INFO("ad7879",0x2F),

  .irq=IRQ_PG5,

  .platform_data=(void*)&bfin_ad7879_ts_info,

  },

  #endif

  #ifdefined(CONFIG_KEYBOARD_ADP5588)||defined(CONFIG_KEYBOARD_ADP5588_MODULE)

  {

  I2C_BOARD_INFO("adp5588-keys",0x34),

  .irq=IRQ_PG0,

  .platform_data=(void*)&adp5588_kpad_data,

  },

  #endif

  #ifdefined(CONFIG_PMIC_ADP5520)||defined(CONFIG_PMIC_ADP5520_MODULE)

  {

  I2C_BOARD_INFO("pmic-adp5520",0x32),

  .irq=IRQ_PG0,

  .platform_data=(void*)&adp5520_pdev_data,

  },

  #endif

  #ifefined(CONFIG_INPUT_ADXL34X_I2C)|| defined(CONFIG_INPUT_ADXL34X_I2C_MODULE)

  {

  I2C_BOARD_INFO("adxl34x", 0x53),

  .irq = IRQ_PG0,

  .platform_data = (void *)&adxl34x_info,

  },

  #endif

  };

  static void __init blackfin_init(void)

  {

  (...)

  i2c_register_board_info(0,bfin_i2c_board_info, ARRAY_SIZE(bfin_i2c_board_info));

  spi_register_board_info(bfin_spi_board_info, ARRAY_SIZE(bfin_spi_board_info));

  (...)

  }

  因此,爲了啓用這樣一個驅動程序,只需要編輯板支持文件,將適當的條目添加至i2c_board_info(spi_board_info)。

  還應注意到,需在內核配置期間選擇驅動程序。驅動程序按照所屬的子系統分類。可在以下位置查找ADXL34x驅動程序:

  Device Drivers--->

  Input device support --->

  [*] Miscellaneous devices --->

  Analog Devices AD714x Capacitance Touch Sensor

  support I2C bus connection

  support SPI bus connection

  <*>Analog Devices ADXL34x Three-Axis

  Digital Accelerometer

  <*>support I2C bus connection

  <*>support SPI bus connection

  一旦用戶開始內核構建過程,就會自動編譯所選的驅動程序。上面的代碼聲明I2C總線0上有四個設備,包括其各自的地址、IRQ以及其驅動程序所需的定製platform_data。註冊相關I2C總線時,i2c-core內核子系統會自動實例化這些I2C設備。

  static struct i2c_driver adxl34x_driver = {

  .driver={

  .name="adxl34x",

  .owner=THIS_MODULE,

  },

  .probe=adxl34x_i2c_probe,

  .remove=__devexit_p(adxl34x_i2c_remove),

  .suspend=adxl34x_suspend,

  .resume=adxl34x_resume,

  .id_table=adxl34x_id,

  };

  static int __init adxl34x_i2c_init(void)

  {

  return i2c_add_driver(&adxl34x_driver);

  }

  module_init(adxl34x_i2c_init);

  在內核啓動的某一時間點,或者在其後的任何時候,一個名爲adxl34x的設備驅動程序可以利用struct i2c_driver註冊自己——通過調用i2c_add_driver()進行註冊。struct i2c_driver的成員利用指向ADXL34x驅動程序函數的指針進行設置,將驅動程序與其總線主控內核相連接。(宏module_init()定義模塊插入時調用哪個函數(adxl34x_ i2c_init())。)

  如果存檔的驅動程序名稱與宏I2C_BOARD_INFO所提供的名稱相符,i2c-core總線模型實現方法將調用驅動程序的probe()函數,將相關的platform_data和irq從板支持文件傳遞到驅動程序。這僅會在沒有追索衝突的情況下發生,例如前一實例化設備使用同一I2C從地址時。

  adxl34x_i2c_probe()函數隨後開始執行其名稱所表示的功能。它通過讀取製造商和設備ID,檢查ADXL345或ADXL346是否存在以及是否正常工作。如果檢查成功,驅動程序的探測函數將分配特定設備數據結構,請求中斷,並初始化加速度計。

  然後利用input_allocate_device()分配新的輸入設備結構,並設置輸入位域。這樣,設備驅動程序就告知輸入系統的其他部分這是何種設備,以及這種新的輸入設備能夠產生何種事件。最後,ADXL34x驅動程序通過調用input_register_device()註冊該輸入設備。

  這將把新的輸入設備結構添加到輸入驅動程序的鏈接列表中,並調用設備處理程序模塊的連接函數,告知其已出現一個新的輸入設備。從此時起,該設備可以產生中斷。一旦執行中斷服務例程,就會從加速度計讀取狀態寄存器和事件FIFO,並利用nput_event()向輸入子系統發回適當的事件。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章