要想真正瞭解驅動程式必須結合一些驅動程式源碼,在此我以串口驅動程式(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。一般串口裝置相應的註冊表設定例子及意義如下:
鍵 意義
"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線程。
註冊表操作部分函數介紹:
Windows CE 6.0有4個基本的註冊表索引值, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS。其它的都是這4個註冊表的子鍵。
我們以一個內建的串口驅動為例,它在註冊表檔案Platform.reg中的描述如下: 其中Prefix 和 Dll項是必不可少的,Prefix代表裝置檔案名稱首碼, 與Index合用表示該裝置的名稱,該註冊表子鍵的裝置名稱就是"COM1:",該名稱可以用於CreateFile調用。Dll則是動態連結程式庫名稱。Index為裝置序號。Flags為1表示系統啟動時不載入,需要應用程式自己載入,為0表示該驅動在系統啟動時載入。
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\Serial]
"Prefix"="COM"
"Dll"="$(_TGTPLAT_PFX)_serial.dll"
"Flags"=dword:0
"Index"=dword:1
在%WinCE Dir%Public%Common%OAK%INC%目錄下,檔案cregedit.h中,定義了一個類CRegistryEdit來封裝了註冊表的操作。許多的硬體驅動,比如串口類,也繼承了CRegistryEdit類。
還有一種方法是利用windows CE提供的API進行登錄機碼的操作。
方法一: 利用系統提供的註冊表類CRegistryEdit
類的定義在檔案regedit.h中。
在構建函數中,會取得當前註冊表子鍵的HANDLE控制代碼。有3個建構函式,第一個是用全路徑,調用hKey = OpenDeviceKey(TEXT("HKEY_LOCAL_MACHINE\\Drivers\\BuiltIn\\Serial"))構造, 第二個是如果已知其父註冊表子鍵,調用RegOpenKeyEx( HKEY_LOCAL_MACHINE, TEXT("Drivers\\BuiltIn\\Serial"), 0,0,&hKey). 第三種方法是用RegCreateKeyEx().
解構函式中,調用RegCloseKey(hKey)關閉掉對註冊表子鍵的引用。
讀取登錄機碼可以使用GetRegValue方法,寫入登錄機碼使用RegSetValueEx方法。其方法的實現也是通過windows CE API,具體可以參照下一個Section。
RegGetList和RegSetList方法提供了對類型為REG_MULTI_SZ和DWORD的VALUE的讀寫操作。
其它類方法GetWindowInfo, GetIsrInfo,GetPciInfo擷取註冊表更多的資訊。
方法二: 利用windows CE API
為了擷取一個註冊表索引值,先要調用RegOpenKeyEx。以上面內建串口驅動為例,RegOpenKey( HKEY_LOCAL_MACHINE, TEXT("Drivers\\BuiltIn\\Serial"), 0, 0, &hKey ), 其中hKey就是我們擷取的該註冊表子鍵的HANDLE控制代碼。接下來對註冊表子鍵的操作就是通過hKey來實現.
有了hKey, 就可以對註冊表子鍵的各個內容進行讀寫操作。如果要讀去子鍵的Prefix, 調用函數RegQueryValueEx(hKey, TEXT("Prefix"), NULL, &lpType, &lpData, &lpcbData), lpType, lpData為Prefix傳回型別和值,該例中,lpType = REG_SZ, lpData = TEXT("COM"). lpcbData當調用時,修飾限制lpData的size in bytes,返回時,為lpData傳回值的SIZE。
對應於讀操作,寫操作是一個相反的過程,RegSetValueEx(hKey, TEXT("Prefix"), NULL, REG_SZ, PBYTE(TEXT("TST")), wcslen(TEXT("TST"))*2). 其中要寫入的項為Prefix項,類型為REG_SZ, 值為TEXT("TST"), 最後一個參數為寫入值得SIZE in bytes. 注意第二個參數,如果註冊表中存在該項,則改寫它的值。如果沒有,則建立一個登錄機碼。
某些情況下,我們需要刪除一個登錄機碼,只需調用RegDeleteKey(hKey, TEXT("Index"))就可以刪除Index登錄機碼。
要關閉一個註冊表子鍵,只需要調用RegCloseKey(hKey)就可以完成。
總結:
關於註冊表有很多更加深入的使用,如RegEnumValue. 更多的有關登錄機碼,可以查看MSDN手冊。