Windows 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。
一般串口設備相應的註冊表設置例子及意義如下: 鍵 意義
"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吧,再往下說就和串口硬件有關了,看多了沒註釋的代碼我也煩!!)

Windows CE 作業系統模型圖


Windows CE 簡介

Windows CE為微軟研發的嵌入式作業系統,可以應用在各種嵌入式系統,或是硬體規格層次較低的電腦系統(例如很少的記憶體,較慢的中央處理器等)。微軟並未定義CE縮寫由來,一般解釋則有Customer Embedded、Compact Edition、Consumer Electronics等等。

概覽
Windows CE可以使用在各式各樣的系統上,最有名的是Pocket PC以及
微軟的SmartPhone. 其他較不為人知的裝置包括微軟的車用電腦、
電視機上盒、生產線上的控制設備、公共場所的資訊站等等,
有些裝置甚至沒有任何人機介面。
Windows CE 並非從桌上型電腦的Windows(NT,98,XP...)修改縮小而來,
而是使用一套完全重新設計的核心,所以它可以在功能非常有限的硬體上執行。
雖然核心不同,但是它卻提供了高度的Win32 API軟體開發介面的相容性,
功能有記憶體管理、檔案操作、多執行緒、網路功能等。
因此,開發桌上型電腦軟體的人可以很容易編寫甚或直接移植軟體到Windows CE上。
一個與其他微軟作業系統的差異是 Windows CE 提供原始碼,
首先已經提供了原始碼給部分廠商,讓廠商能夠依照他們自己的硬體架構修改原始碼,
例如:
在 Windows CE 的開發 IDE 軟體 Platform Builder 中就提供了
許多開放原碼的常用軟體元件,
但是一些與硬體架構的軟體元件仍然以二進制檔案形式來提供。
版本
Windows CE 1.0是最早期的版本,非常不穩定,隨便版本不斷的釋出,使得WinCE日趨穩定。
Windows CE 1.0 (Pegasus)
Windows CE 2.0, 2.11, 2.12 (Mercury)
Windows CE 3.0 (Cedar)
Windows CE .NET (4.0, 4.1, 4.2) (Talisker)
Windows CE 5.0 (Macallan)
Windows Embedded CE 6.0 (Yamazaki)

最新功能
目前最新的Windows CE為Windows CE 6.0,這個版本在核心部分有很大的進步:
所有系統元件都由EXE改為DLL,並移到 kernel space.
全新設計的虛擬記憶體架構
全新的裝置驅動程式架構,同時支援 User Mode 與 Kernel Mode 兩種驅動程式。
突破只能執行 32 個工作元(process)的限制,可以執行 32768 個工作元。
每一工作元的的虛擬記憶體限制由32 M 增加到全系統總虛擬記憶體。
Platform Builder IDE 整合到 Microsoft Visual Studio 2005。
新的安全架構,確保只有被信任的軟體可以在系統中執行。
UDF 2.5 檔案系統。
支援 802.11i (WPA2)及 802.11e (QoS) 等無線規格, 及多重 radio support.
支援 x86, ARM, SH4, MIPS 等各種處理器。
提供新的 Cellcore components 使系統在行動電話網路中更容易建立資料連結及啟動通話。
在開發環境上,微軟也提供相容於.NET Framework的開發元件:.NET Compact Framework,讓正在學習.NET或已擁有.NET程式開發技術的開發人員能迅速而順利的在搭載Windows CE .NET系統的裝置上開發應用程式
用於掌上電腦Pocket PC以及智慧手機Smart Phone上的Windows CE系統稱為Windows Mobile,目前的最新版本為Windows Mobile 6.1
限制
Windows CE 只支援 UNICODE,故char必須改為TCHAR, WCHAR。
Windows CE不支援重疊I/O。
WinCE的許多APIs功能都受限,如:CreateThread 函式在許多參數在Windows CE下都不支援,第1、2、5的參數值必須設為NULL或0。 HThread = CreateThread(NULL, 0, Thread, nParameter, 0, &dwThreadID);

Windows CE.net 系統註冊表

限制:

鍵或鍵值項的名字最多為255個字元 , 資料最大4KB , 鍵嵌套層次最多16層




Windows CE.net 註冊表根鍵 :

HKEY_LOCAL_MACHINE -> 硬體及驅動程式配置資料

HKEY_CURRENT_USER -> 使用者配置資料

HKEY_CLASSES_ROOT -> OLE和檔案類型匹配配置資料

HKEY_USERS -> 適用於所有使用者的儲存資料

KernelIoControl [轉載]

參考 msdn http://msdn2.microsoft.com/en-us/library/ms886729.aspx
argument 依序是
CommandConde
InBuffer Pointer
InBuffer Size
OutBuffer Pointer
OutBuffer Size
Buffer Pointer to Save the returned value (DWORD)就和 Linux 的 IOControl 一樣。
但是這個 IOControl 是對 Kernel ,在source code上來說,也就是 OAL。
CE 的 Driver 都是run 在 User Space (6.0以前),
所以driver 的access 方法不一樣。
driver 的iocontrol 和 linux 的比較類似,
就是 open device,然後用iocontrol function。
但是 kernel 不是 driver,所以就不用open 了,
直接呼叫 KernelIoControl( )就可以。
自己要加一些 Kernel IoControl command的話,要
先define IOControl code ,
用 CTL_CODE 宣告,
有很多argument和限制,
參考 http://msdn2.microsoft.com/en-us/library/ms904001.aspx
在oal 中寫好處理的function,
通常會是 OALIoCtlXXX( ),
這個funciton 的argument也是限制的,
要符合iocontrol 的四個argument : in,insize,out,outsize。
在oak_ioctl_tab.h 中建立IOcontrol code和 iocontrol function 的關聯。
在 CTL_CODE的說明頁,
有說明可以指定 cotrol type.(oal 的就是FILE_DEVICE_HAL)。
這樣看來,也可以用 FILE_DEVICE_BATTERY 來指定由battery 來handle ?

Little endian & Big endian是何意思?[轉載]

Endian是什麼意思呢?還是讓我們先來看看下面的情況,
這是記憶體中一個WORD值中的內容,
那麼這個WORD中的值是0x1234呢,還是0x3412 ?

low byte high byte
0x12 0x34

熟悉x86彙編的人立刻就知道這個值應為0x3412,
很對,
但在一些情況下,
比如說你在SGI的機器上看到這種情況,
則正好相反,
0x1234才是正確答案,
這與CPU內部處理資料的方式有關。
這兩種處理方式都存在于不同廠商生產的CPU之中,
在上例中若此WORD值為0x3412的,
我們稱之為little-endian,
若為0x1234的,
我們稱之為big-endian,
這是兩種不同的byte orders。
MSDN中有比較精確的定義如下:

Byte Ordering Byte ordering Meaning
big-endian The most significant byte is on the left end of a word.
little-endian The most significant byte is on the right end of a word.

一般來說我們不用關心byte ordering的問題,
但若要涉及跨平臺之間的通信和資源分享,
則不得不考慮這個問題了。
也許你會說,
我永遠不會去用其他非x86的CPU,
也許是這樣,
你甚至可以不必知道我們最常用的Intel,
AMD等生產的x86的byte ordering是little-endian的,
而且按現在的裝機數量來看,
可以說世界上絕大多數CPU是little-endian的,
但多瞭解一些沒有什麼壞處,
也許有用上的一天,
實際若您要涉及到網路編程,
瞭解一些還是有所幫助的,
看完本文後您就應該知道
為何socket編程中為何要用到如 ntohl, htonl, ntohs, htons
這幾個看起來名字似乎怪怪的API了,
也很容易理解這些函數名的意義了。

假設我們要在不同byte ordering的機器之間傳輸和交換資料,
那該怎麼辦呢,
有兩個方法,
一是全部轉換成文本來傳輸(如XML使用的),
另一個方法兩方都按照某一方的byte order,
這時就涉及到了不同byte order之間相互轉換的問題
(網路傳輸標準如TCP/IP採用第二種方法並且由於歷史的原因,
byte ordering是big-endian的)。
兩種之間該如何轉換呢?
方法有很多,我們可以先看看MFC中在
處理serialize的代碼中所用的方法(List),
雖然代碼應該是高效易讀的,
但我個人並不喜歡它,
原因是我覺得這不是一種通用優美的方法.
下面列出的是我自己寫的轉換的代碼:

template F3D_INLINE T ConvertEndian(T t)
{
T tResult = 0;
for (int I = 0; I < tresult =" (t">>= 8;
}
return t Result;
}

原理非常簡單,
交換位元組順序,
我就不多說了,
當然這個寫法並不是快速的,
只是通用的(我沒條件試, 若有不對之處請指出), 若要快速的代碼,
可以在不同platform上用與platform
相關的代碼, 如在PowerPC上有 "load word byte-reversed indexed" (lwbrx)
和 "load halfword byte-reversed indexed" (lhbrx) 指令,
在x86上還可用BSWAP單個彙編指令等,
在類型上專為int16, int32寫的通用的代碼也可以比這快得多.

當然如果在byte ordering相同的情況下,
應該不必用這個轉換函數,
所以我們可以定義一個宏來處理不同的byte ordering,
也可以在運行時測試byte ordering,
下面的代碼給出了一個簡單的測試方法。

// Test for endianness.
F3D_INLINE bool IsLittleEndian(void)
{
DWORD dwTestValue = 0x12345678L;
return (*((BYTE*)&dwTestValue) == 0x78);
}

但是float比較怪,
有可能所涉及到不僅僅是byte order的問題,
因為有些平臺如Alpha不使用IEEE的浮點格式,
還得自己轉換。
當然同上,
其他的方法一是將所用的float用文本方式輸入輸出,
另一個辦法是在某些情況下可將其轉換成定點數再處理,
這裏我不再深入。

如果是讀寫第三方已經指定byte order的檔或資料流程,
比如說讀SGI的點陣圖檔格式,
則可以直接自行按指定的byte order拼起來,
不必考慮host機是何種byte ordering。
下面我給出相應的代碼:

// Read a little-endian TYPE from address
template F3D_INLINE T GetLittleEndian(const BYTE* pBuf)
{
T tResult = 0;
pBuf += sizeof(T) - 1;
for (int I = 0; I < tresult =" *pBuf" tresult =" 0;" i =" 0;" tresult =" *pBuf" i =" 0;">>= 8;
}
}

// Set a big-endian T on a address
template F3D_INLINE void SetBigEndian(BYTE* pBuf, T t)
{
pBuf += sizeof(T) - 1;
for (int I = 0; I <>>= 8;
}
}

從上文可以看出,byte order挺簡單的,
一般應用中可能也用不上,
但若您對寫跨平臺的程式有興趣,
則一定要瞭解的比較清楚才行。
以上代碼都是從實際使用的源碼中取下來的。

附:常見Processor, OS的byte ordering情況

Processor OS Order
x86 (Intel, AMD, … ) All little-endian
DEC Alpha All little-endian
HP-PA NT little-endian
HP-PA UNIX big-endian
SUN SPARC All? big-endian
MIPS NT little-endian
MIPS UNIX big-endian
PowerPC NT little-endian
PowerPC non-NT big-endian
RS/6000 UNIX big-endian
Motorola m68k All big-endian

虹光大成就-密教灌頂(一)