1. 程式人生 > >Windows CE下驅動程式開發基礎

Windows CE下驅動程式開發基礎

我想即使讀者看過微軟的關於驅動開發的培訓教材和CE幫助文件中的驅動部分,頭腦中仍然一片茫然。要想真正瞭解驅動程式必須結合一些驅動程式原始碼,在此我以串列埠驅動程式(COM16550)中初始化過程為線索簡單講一講驅動開發的基礎知識。

   WindowsCE下的串列埠驅動程式能夠處理所有I/O行為類似串列埠的裝置,包括基於16450、16550 UART(通用非同步收發晶片)的裝置和一些採用DMA的裝置,常見的有9針串列埠、紅外I/O口、Modem等。在%_WINCEROOT%/Public /Common/OAK/Drivers/Serial目錄下,COM_MDD2子目錄包含新的串列埠驅動MDD層函式程式碼。COM16550子目錄包含串 口驅動PDD層程式碼。SER16550子目錄包含的一系列函式專用於控制與16550相容的UART,這樣PDD層的主要工作就是呼叫SER16550中 的函式。還有一個ISR16550子目錄包含的是串列埠驅動程式專用的可安裝ISR(中斷服務例程),而很多硬體裝置驅動程式採用CE預設的可安裝ISR giisr.dll。一般串列埠裝置相應的登錄檔設定例子及意義如下:

以下是引用片段:
  [HKEY_LOCAL_MACHINE/Drivers/BuiltIn/Serial_1]
  "SysIntr"=dword:13 串列埠1的中斷ID為十進位制13
  "IoBase"=dword:02F8 串列埠1的IO空間首地址為十六進位制2F8
  "IoLen"=dword:8 串列埠1的IO空間長度為8個位元組
  "DeviceArrayIndex"=dword:0 串列埠1的索引,是1的由來
  "Order"=dword:0 串列埠1驅動的載入順序
  "DeviceType"=dword:0 串列埠1的裝置型別
  "DevConfig"=hex: 10,00 .... 串列埠1在與Modem裝置通訊時的配置,如波特率、奇偶校檢等
  "FriendlyName"="COM1:" 串列埠1在撥號程式中顯示的名字
  "Tsp"="Unimodem.dll" 串列埠1 被用於與Modem裝置通訊的時候要載入的TSP(TAPI Service provider)DLL
  "Prefix"="COM" 串列埠1的流介面的字首
  "Dll"="com16550.Dll" 串列埠1的驅動程式DLL


   SysIntr由CE在檔案Nkintr.h中預定義,用於唯一標識中斷裝置。OEM可以在檔案Oalintr.h中定義自己的SysIntr。常見的 預定義SysIntr有SYSINTR_NOP(中斷只由ISR處理,IST不再處理),SYSINTR_RESCHED(重新排程執行緒), SYSINTR_DEVICES(由CE預定義的裝置中斷ID的基值),SYSINTR_PROFILE、SYSINTR_TIMING、 SYSINTR_FIRMWARE等都是基於SYSINTR_DEVICES定義的。IoBase是串列埠1的IO地址空間的首地址,IoLen是IO空間 的大小。IO地址空間只存在於x86平臺,如果在其它平臺硬體暫存器必須對映到實體地址空間,那子鍵的名稱為MemBase和MemLen。在x86平臺 更多硬體的暫存器由於IO空間的侷限也對映到實體地址空間。DeviceArrayIndex是裝置的索引,用於區分同類型的裝置。Prefix是流驅動 程式的字首,當應用程式呼叫CreateFile函式傳遞COM1:引數時,檔案系統負責與串列埠驅動程式通訊,串列埠驅動程式是在CE啟動時由 device.exe載入的。

  下面從MDD層函式COM_Init開始探索串列埠驅動的初始化過程。COM_Init是在串列埠裝置 被檢測後由裝置管理器device.exe呼叫的,主要的作用是初始化裝置,它的唯一引數Identifier是由device.exe傳遞的,其型別是 一個字串指標,字串的內容是HLM/Drivers/Active/xx,xx是一個十進位制數(device.exe會跟蹤系統中每個驅動程式,把加 載的驅動程式記錄在Active鍵下)。

  COM_Init先分配一個HW_INDEP_INFO結構體,這個結構體是獨立於串列埠 硬體的頭資訊(MDD、PDD、SER16550都包含自己獨特的結構體,具體的結構體定義請參見串列埠驅動原始碼),分配之後再初始化結構體中每個成員,初 始化結構體後呼叫 OpenDeviceKey((LPCTSTR)Identifier)開啟HLM/Drivers/Active/xx/Key包含的登錄檔路徑,在這 里路徑一般為HLM/Drivers/BuiltIn/Serial,即串列埠的驅動程式資訊在登錄檔中所處的位置。COM_Init接著在HLM/ Drivers/BuiltIn/Serial下查詢DeviceArrayIndex、Priority256的值,Priority256指定了驅動 程式的優先順序,如果沒有就用預設的優先順序。接下來呼叫GetSerialObject(DeviceArrayIndex),這個函式由PDD層定義,返 回HWOBJ結構體,這個結構體主要包含PDD層和SER16550定義的函式的指標。

  也就是說MDD通過呼叫這個函式才能呼叫 底層實現的函式。接下來的大多數工作都是呼叫底層函式實現初始化。第一個呼叫的底層函式SerInit主要設定由使用者設定的硬體配置,例如線路控制、波特 率。它呼叫Ser_GetRegistryData函式得到儲存在登錄檔中的硬體資訊,Ser_GetRegistryData在內部呼叫系統提供的 DDKReg_GetIsrInfoDDK和DDKReg_GetWindowInfo函式得到在HLM/Drivers/BuiltIn/Serial 下儲存的IRQ、SysIntr、IsrDll、IsrHandler、IoBase、IoLen。IRQ是邏輯中斷號,IsrDll表示當前驅動程式的 可安裝ISR所在的DLL名稱,IsrHandler 表示可安裝ISR的函式名稱。

  在這裡順便提一下可安裝ISR,讀者在我以 前發表的關於OAL的文章中可以瞭解到OEM在OEMInit函式中關聯IRQ和SysIntr,當硬體裝置發生中斷時,ISR會禁止同級和低階中斷,然 後根據IRQ返回關聯的SysIntr,核心根據ISR返回的SysIntr喚醒相應的IST(SysIntr與IST建立的Event關聯),IST處 理中斷之後呼叫InterruptDone解除中斷禁止。在OEMInit中關聯的缺點是一旦編譯了CE核心後就無法新增這種關聯了,而一些硬體裝置會隨 時插拔或者共享中斷,要關聯這樣的硬體裝置解決方法就是可安裝ISR,可安裝ISR專用於處理指定的硬體裝置發出的中斷,所以如果硬體裝置需要可安裝 ISR必須在登錄檔中新增IsrDll、IsrHandler。多數硬體裝置採用CE預設的可安裝ISR giisr.dll,格式如下:

以下是引用片段:
  "IsrDll"="giisr.dll"
  "IsrHandler"="ISRHandler"


  如果一個硬體驅動程式需要可安裝ISR而開發者又不想自己寫一個,那麼可以利用giisr.dll來實現。除了在登錄檔中新增如上所示外,還要在驅動程式中呼叫相關函式註冊可安裝ISR。虛擬碼如下:

以下是引用片段:
  g_IsrHandle = LoadIntChainHandler(IsrDll, IsrHandler, (BYTE)Irq);
  GIISR_INFO Info;
  PHYSICAL_ADDRESS PortAddress = {PhysAddr, 0};
  TransBusAddrToStatic(BusType, dwBusNumber, PortAddress, dwAddrLen, &dwIOSpace, &(PVOID)PhysAddr)
  Info.SysIntr = dwSysIntr;
  Info.CheckPort = TRUE;
  Info.PortIsIO = (dwIOSpace) ? TRUE : FALSE;
  Info.UseMaskReg = TRUE;
  Info.PortAddr = PhysAddr + 0x0C;
  Info.PortSize = sizeof(DWORD);
  Info.MaskAddr = PhysAddr + 0x10;
  KernelLibIoControl(g_IsrHandle, IOCTL_GIISR_INFO, &Info, sizeof(Info), NULL, 0, NULL);


   LoadIntChainHandler函式負責註冊可安裝ISR,引數1為DLL名稱,引數2為ISR函式名稱,引數3為IRQ。 TransBusAddrToStatic函式在後面講。如果要利用giisr.dll作為可安裝ISR,必須先填充GIISR_INFO結構體, CheckPort=TRUE表示giisr要檢測指定的暫存器來確定當前發出中斷的是否是這個裝置。PortIsIO表示暫存器地址屬於哪個地址空間, FALSE表示是內定空間,TRUE表示IO空間。UseMaskReg=TRUE表示裝置有一個掩碼暫存器,專用於指定當前裝置是否是中斷源,也就是發 出中斷,而MaskAddr表示掩碼暫存器的地址。如果對Info.Mask賦值,那麼PortAddr表示一個特殊的暫存器地址,這個暫存器的值與 Mask的值&運算的結果如果為真,則證明當前裝置是中斷源,否則返回SYSINTR_CHAIN(表示當前ISR沒有處理中斷,核心將呼叫 ISR鏈中下一個ISR),如果UseMaskReg=TRUE,那麼MaskReg暫存器的值與PortAddr指定的暫存器的值&運算的結果 如果為真,則證明當前裝置是中斷源。

  函式SerInit接著呼叫函式 Ser_InternalMapRegisterAddresses轉換IO地址並且對映地址, Ser_InternalMapRegisterAddresses在內部呼叫系統提供的HalTranslateBusAddress(Isa, 0, ioPhysicalBase, &inIoSpace, &ioPhysicalBase)函式將與匯流排相關的地址轉換為系統地址,引數1為匯流排型別,引數2為匯流排號,引數3為要轉換的地址 (PHYSICAL_ADDRESS型別,實際是LARGE_INTEGER型),引數4指定暫存器地址屬於IO地址空間還是實體地址空間,引數5返回轉 換後的實體地址。觀察HalTranslateBusAddress的原始碼得知如果是在x86平臺,這個函式除了把引數3賦給了引數5其餘什麼都沒有做, 而非x86平臺將inIoSpace的值置為0,表示一定是實體地址。在呼叫HalTranslateBusAddress前要確定從登錄檔中得到的寄存 器地址到底是屬於哪個地址空間的,例如:

以下是引用片段:
  ULONG inIoSpace = 1; ///1表示是IO空間
  PHYSICAL_ADDRESS ioPhysicalBase = {iobase, 0}; ///相當於ioPhysicalBase.LowPart = iobase


  在地址轉換後就要將轉換後的地址對映到驅動程式(一般IST和應用程式一樣執行在使用者模式)能夠訪問的虛擬地址空間(0x80000000以下)和ISR能夠訪問的靜態虛擬地址空間中(0x80000000以上)。例如:

以下是引用片段:
  ////如果地址屬於實體地址空間
  ioPortBase = (PUCHAR)MmMapIoSpace(ioPhysicalBase, Size, FALSE);
  TransBusAddrToStatic(Isa, 0, ioPhysicalBase, Size, &inIoSpace, ppStaticAddress);
  MmMapIoSpace函式負責將實體地址對映到驅動程式能夠訪問的虛擬地址空間中,通過原始碼分析MmMapIoSpace在內部分別呼叫:
  pVirtualAddress =VirtualAlloc(0, SourceSize, MEM_RESERVE, PAGE_NOACCESS);
  VirtualCopy(pVirtualAddress, (PVOID)(SourcePhys >>8), SourceSize, PAGE_PHYSICAL | PAGE_READWRITE |
  (CacheEnable ? 0 : PAGE_NOCACHE));


   VirtualAlloc分配一塊和MemLen一樣大小的虛擬地址空間,因為引數1為0,所以核心自動分配。一般MemLen小於2MB,所以會在應 用程式的地址空間中分配。VirtualCopy負責將硬體裝置暫存器的實體地址與VirtualAlloc分配的虛擬地址做一個對映關係,這樣驅動程式 訪問PvirtualAddress實際上就是訪問第一個暫存器。因為硬體裝置暫存器的實體地址一定是在512MB(CE支援RAM的最大值)以上,所以 除了最後的引數要加PAGE_PHYSICAL外,第二個引數實體地址也要右移8位(或者除以256)。

  對映硬體暫存器當然 PAGE_NOCACHE是必須加的。TransBusAddrToStatic函式負責將實體地址對映到ISR能夠訪問的靜態虛擬地址空間中,當出現中 斷共享時,ISR要負責訪問硬體裝置的某一個暫存器來判斷中斷源,所以將暫存器的實體地址對映到靜態虛擬地址空間中是必要的(ISR只能訪問靜態的虛擬地 址空間)。所謂靜態虛擬地址空間是指在OEMAddressTable中定義的虛擬地址空間(當然是0x80000000以上)。在x86平臺一般這個表 只定義RAM的實體地址與虛擬地址對應關係,而硬體裝置的暫存器地址並不在該表中定義,所以如果要建立一塊靜態的虛擬地址空間供ISR訪問,必須在此之前 呼叫CreateStaticMapping函式在0xC4000000到0xE0000000虛擬地址空間中分配。 TransBusAddrToStatic函式在內部就是呼叫了CreateStaticMapping函式。注:硬體裝置的暫存器地址也可以在 OEMAddressTable中定義。

以下是引用片段:
  ////如果地址屬於IO空間
  ioPortBase = (PUCHAR)ioPhysicalBase.LowPart;
  *ppStaticAddress=ioPortBase


  這種情況只屬於x86平臺,是IO空間就可以直接訪問,即使是使用者模式。

   SerInit函式接著初始化SER_INFO結構體成員,之後呼叫SL_Init函式,這個函式在ser16550中定義,負責初始化 SER16550_INFO結構體,在這個結構體中儲存串列埠8個暫存器的地址。SerInit函式執行完畢後COM_Init函式建立接收緩衝區,然後調 用StartDispatchThread函式初始化中斷並且建立IST。StartDispatchThread函式在內部呼叫 InterruptInitialize函式關聯SysIntr和Event,然後呼叫InterruptDone函式告訴核心當前串列埠可以中斷處理,接 著呼叫CreateThread函式建立IST執行緒。(over吧,再往下說就和串列埠硬體有關了 )

//*******************************************************************************************************************
來源:http://www.gd-emb.org/detail/id-33933.html