我想即使讀者看過微軟的關於驅動開發的培訓教材和CE協助文檔中的驅動部分,頭腦中仍然一片茫然。要想真正瞭解驅動程式必須結合一些驅動程式源碼,在此我以串口驅動程式(COM16550)中初始化過程為線索簡單講一講驅動開發的基礎知識。
Windows CE下的串口驅動程式能夠處理所有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吧,再往下說就和串口硬體有關了,看多了沒注釋的代碼我也煩!!)