消息(图2-2),而Control Message是指Windows3.1和Windows95都支持的消息。
中。
3.4VxD访问硬件设备
计算机的硬件可以被定位于I/O空间和内存空间,这两种空间是完全独立的而且I/O地址空间比内存空间小的多,对于80386后的处理器而言前者只有64KB而后者有4GB。映射到I/O地址空间的硬件设备只能通过专门的I/O指令如IN、OUT命令(C语言中为inp和outp)来访问。映射到内存空间范围的硬件可以象访问内存一样进行访问,一般使用MOV、ADD、OR等指令(C语言中则通过指针来访问)。
在实际访问过程中VxD访问I/O映射硬件可以直接使用I/O指令访问,但对于内存映射硬件则复杂的多。因为尽管VxD运行在Ring0级,但它所见的是平面内存,只能用线性地址,因此在每次访问一个硬件前必须先配置页表,保证设备的物理地址与一个线性地址相联系,而这个线性地址可以作为一个指针来由我们进行内存访问,就好像我们在直接对硬件操作一样。由于我们的PCI插卡是动态配置的内存映射设备,它每次分配的物理地址可能会改变的,因此在进行地址转换时采用的也是动态转换。
3.5VxD处理硬件中断
在Windows环境下,将所有的IDT(中断描述符表)指向VMM的一个例程,由VMM来确定它是做为异常事件处理还是中断的结果被调用。VMM自己只管理异常事件,所有的中断由VPICD (虚拟可编程中断处理器)处理。如果一个VxD为中断进行了注册,VPICD就将这个中断交给它处理,否则VPICD将中断交给VM由它来处理。
VxD通过调用VPICD的服务VPICD_Virtualize_IRQ()来为硬件中断注册 ,并将会调例程的地址交给VPICD。一旦VxD进行了注册,它就可以处理硬件中断。
2..3VxD的其它开发工具
DDK
DDK(Device Developer Kit)是专业软件开发人员常用的Microsoft公司出品的设备驱动程序开发工具包。它有Windows98 DDK和Windows2000 DDK两个版本。前者能够开发Windows95/98/Me/NT下的VxD、KMD和WDM驱动程序,后者可以开发Windows98/Me/NT/2000下的KMD和WDM驱动程序。
DDK虽然功能强大,但它要求设计者必须对Windows的体系结构、设备驱动程序的结构、虚拟机管理器(VMM)以及Intel CPU体系结构有深入的了解,而且需要保护模式的汇编语言编程经验, 因此,在实际中DDK一般不常被人使用。
WinDriver
WinDriver是美国KRFTech公司出品的用于编写驱动程序的另一种工具包。它包括1个类似于QuickVxD的代码生成器WinDriver Wizard、1个WinDriver发行包、2个公用程序。
与VToolsD一样,WinDriver工具包的优点在于可以使编程人员用C或C++语言来编写设备驱动程序,而不是将大量精力放在编写那些复杂的、难于调试的内核模式代码。
以上介绍的几种工具各有优点。DDK功能强大,编程灵活,适用范围广,可应用于各类硬件驱动程序的编写,但对编程人员的要求较高,编程难度较大。VToolsD主要工作环境是在Windows 98/95下,它具有较强的开发能力和较高的开发效率,是编程人员常用的工具。WinDriver的适用面比前二者窄,它主要针对ISA/PCI插卡,而对其他类硬件的技术支持较少,但它编写的程序可同时工作在Windows95/98/NT操作系统。
第四章PCI2040芯片驱动程序的开发
PCI(peripheral component interconnect)总线主要为Pentium微处理器的开发使用而设计的,它定义了32位数据总线,且可扩展为64位,使用33MHz时钟频率,最大数据传输率为132~264Mb/s,支持无限读写猝发操作,支持即插即用,支持并发工作方式,即多组外围设备可与CPU并发工作。PCI总线以其优良性能和适应性已成为Pentium以上微机的主流总线。要开发基于PCI接口的设备就需要选择PCI总线接口芯片,目前常用的有S5933,AN3042,PLX9030等,但是它们不便于和DSP连接。
4.1 PCI2040控制芯片
TI公司推出的PCI2040 PCI桥控制器(内部结构如图4-1)可以很方便的通过主机接口实现多达四片C54x或C6x系列DSP与PCI总线的连接。同时它提供了两种数据传输方式:HPI方式和GPBUS总线传输方式,其中GPBUS总线传输方式比较简单但是传输的速度有所限制,因此我们采用了HPI传输方式。在HPI方式下,主机可以通过三个HPI寄存器实现对DSP片内DRAM的访问。这三个寄存器分别是:
(1)HPI地址寄存器(HPIA):由主机直接访问。寄存器中保存了当前要访问的DSP片内存储器地址。
(2)HPI数据寄存器(HPID):由主机对其直接访问。如果当前操作是读,则将数据从HPIA当前所指向的存储器中读入该寄存器;如果当前操作是写,则将该寄存器内容写入HPIA当前所指向的存储器中。当进行较大的数据块读写操作时,可以通过地址自增方式对HPID进行读写。
(3)HPI控制寄存器(HPIC):被主机和C54x直接访问。它映象在C54x数据存储器的地址002ch处,该寄存器中保存了HPI的控制和状态位。在进行第一个数据或地址寄存器操作前必须先初始化HPIC的BOB位(若为零表示传输的第一个字节是高字节,否则表示传输的第一个字节是低字节)。这是因为主机接口总是传送8位字节,而HPIC寄存器(通常是主机首先要寻址的寄存器)又是一个16位的寄存器,在主机这边就以相同内容的高字节与低字节来管理HPIC寄存器。
图4-1
PCI总线规范要求任何PCI设备必须提供256字节的配置空间,它用来实现无需用户干预的安装、配置和引导;完全的设备再定位,由与设备无关的软件进行系统地址映射。
PCI2040的配置空间如下:
图4-2
其中设备ID用以标识特定的设备,具体的代码由供应商来分配;供应商ID用以表明设备的制造者,这二者是识别PCI设备的唯一标识。对于PCI2040来讲,当读Vendor-device ID时,返回值为AC60104C。
4.3 即插即用体系
即插即用(PnP)是微软公司为了使新硬件设备的安装和配置更加容易而采取的一种策略。PnP设备能够自身识别,自动提出资源需求,并且可以在运行时接收资源分配,较新类型的扩展总线如PCI、EISE、PCMCIA等,其任一设备都定义为即插即用的,这些总线也都满足即插即用的要求。
配置管理器是即插即用体系结构的核心元件。它由四个软件元件来工作:
枚举器
仲裁器
设备安装器
设备驱动器
其中,枚举器生成设备结点树并参与总线中的设备配置。枚举过程发生在计算机开机时,枚举结束后设备结点树包含的信息有:可用的系统资源、需要的设备驱动程序和资源的要求。
当用户将卡插上时,系统的枚举器将自动识别新设备然后查找所有的已知的.INF文件看是否有与设备ID号相匹配的,如果有则在新生成的注册表项内复制相应文件,否则提示用户插入一张安装盘。
设备信息安装文件(.INF)包含了驱动程序的名称,驱动程序应该复制到的目录,以及在驱动程序安装时必须生成和修改的注册表入口。
在编写INF文件时,我使用了VtoolsD开发包中的INF Editor工具,以下是我所编写的PCI2040安装信息文件。
[Version]
Signature=$CHICAGO$
Class=PCI Bridge //设备类型为PCI桥设备
Provider=%String0% //供应商的名称
[ClassInstall]
[DestinationDirs]
DefaultDestDir=11 //将驱动程序安装在C:/Windows/System目录下
[Manufacturer]
%String1%=SECTION_0
[SECTION_0]
%String2%=sevenstar,PCI/VEN_104C&DEV_AC60
[sevenstar]
CopyFiles=CopyFiles_sevenstar
AddReg=AddReg_sevenstar
[CopyFiles_sevenstar]
pci2040.vxd //驱动程序的名称为PCI2040.vxd
[AddReg_sevenstar]
HKR,,DevLoader,0,pci2040.vxd //在注册表中添加相关信息
[sevenstar_LogConfig]
ConfigPriority=NORMAL
IRQConfig=3,7,9,10,15 //指定PCI2040可选的中断口
[ControlFlags]
[SourceDisksNames]
1=pci2040驱动盘,,0000-0000
[SourceDisksFiles]
pci2040.vxd=1
[Strings]
String0="南京七星公司"
String1="sevenstar"
String2="PCI总线控制器"
4.4PCI2040驱动程序的开发
首先利用QuickVxD工具写出PCI2040.VxD程序的框架,其中包括将要使用的消息如SYS_DYNAMIC_DEVICE_INIT,SYS_DYNAMIC_DEVICE_EXIT,PNP_NEW_DEVNODE
W32_DEVICEIOCONTROL等,设置创建方式为动态创建,然后利用VC6.0++软件打开程序框架后进行编写。图4-3是VxD的处理过程:
硬件中断
图4-3
4.41设备初始化
在PCI设备驱动程序的初始化过程中,可以向CONFIG_ADDRESS(0CF8h)寄存器写入一个配置地址,再对CONFIG_DATA(0CFCh)寄存器进行读就会启动PCI配置周期,就读出了设备的配置空间的寄存器中的值。
3130 2423 16 15 1110 8 7 2 1 0
图4-4
其中第32位决定是否进行PCI的配置访问。1表示使能,0表示禁止。理论上只要知道了总线号,设备号,功能号就可以直接取得系统对PCI2040芯片配置以后配置空间的所有寄存器的内容。但是由于我们没有直接找到这几个ID,因此第一次必须自己通过枚举方式来获得这几个ID。 在枚举过程中:
1.依次向CONFIG_ADDRESS中写入所有可能的总线号,设备号和功能号,将寄存器号固定为0即第一个寄存器
2.读出CONFIG_DATA寄存器中的值,它对应于配置空间首寄存器中的值即Vendor-device ID
3.将读出的值和PCI2040芯片的Vendor-device ID(AC60104C)比较,直到二者相等此时写入的配置地址即为它的配置空间首寄存器的地址,在此基础上加上偏移量即可得到配置空间其他寄存器的地址。
下面就是获得PCI2040芯片各个ID的枚举过程,由于这些ID是不变的,因此执行一次后可以直接使用。
for(int i=0x80000000;i<0x80ffff00+1;i=i+0x100)
{ _outpd(0x0cf8,i);
Testid=_inpd(0x0cfc);
if(Testid==0xac60104c)
{
printf("The bus line ID and device ID is %08lx/n", i);
break;
}
}
由结果得知总线号是00000000,设备号是01011,功能号是000,以后可以直接以之为基准访问任意寄存器。
4.42 PCI2040的内存映射访问
对于PCI2040的映射的内存访问应该分为两步:获取PCI2040被分配的物理地址和将物理地址转换成线性地址。
获取物理地址
方法一:直接利用Windows95/98的系统信息项获得,打开我的电脑的属性,找到PCI总线控制器,选中它并看其资源项可以直接得到系统分给它的内存资源和IRQ。这种方法一般我们不采用,因为资源随时会变而我们的程序写好后就不变了,因此它只是用来对我们的结果进行校对。
方法二:在Win32程序中获得,因为对于PCI2040的HPI传输模式我们要获得两个物理地址,控制空间基址和通用总线基址,它们分别对应于配置空间偏移量为14H和18H。而在上一步骤我们得到了Vendor-device ID寄存器地址,再加上14H和18H就可以得到控制空间基址寄存器的地址,然后从中取出控制空间的基址,同样再获得通用总线基址。
方法三:利用VxD来获得,这也是我在编写过程中所采用的方法。因为一旦即插即用的设备有什么增加或删减,VxD就获得一个消息即PNP_NEW_DEVNODE,在处理函数中判断发生了什么情况,如果是它所对应设备的注册和装载,VxD就会返回CM_Register_Device_Driver函数,而它又会调用另一个配置管理函数CM_Get_ALLoc_Log_Conf来获得系统分配给它的资源。该结构如下:
Struct Config_Buffer_s
{
WORD wNumMemWindows;
DWORD dMemBase[MAX_MEM_REGISTERS];
DWORD dMemLength[MAX_MEM_REGISTERS];
WORD wMemAttrib[MAX_MEM_REGISTERS];
WORD wNumIOPorts;
WORD wIOPortBase[MAX_IO_PORTS];
WORD wIOPortLength[MAX_IO_PORTS];
WORD wNumIRQs;
BYTE bIRQRegisters[MAX_IRQS];
BYTE bIRQAttrib[MAX_IRQS];
WORD wNumDMAs;
BYTE bDMALst[MAX_DMA_CHANNELS];
WORD wDMAAttrib[MAX_DMA_CHANNELS];
BYTE bReserved[3];
};
该结构可以分为四组:内存资源描述,I/O资源描述,IRQ资源描述和DMA资源。每组的第一段表明该类作了多少分配,后面的域为相关的参数项。对于PCI2040.VXD我们得到了两个内存资源即控制空间基址和通用总线基址,一个IRQ资源是05号中断口。
值得注意的是各项资源(如控制空间基址,通用总线基址和IRQ)不固定,在不同的计算机上一般不相同,即使在同一台计算机上当硬件环境变了后一般也会变。
物理地址转换成线性地址
对于即插即用的设备,我们必须进行动态地址映射。VxD一般要经过以下几步:
(1) 调用PageReserve()函数,分配一块线性地址空间。此空间并不和具体的物理地址相关联,仅是线性地址的页表进入项。
(2) 调用PageCommitPhys()函数,将线性地址映射到设备的物理地址。
(3) 调用LinPageLock()函数,阻止虚拟内存管理器将这些页交换到磁盘空间,以保证在硬件中断期间这些地址也有效。
下面是我的源程序中关于物理地址映射成线性地址的一段程序:
#define PAGENUM(p) (((ULONG)(p))>>12)//除以4k得多少页
#define PAGEOFF(p) (((ULONG)(p))&0xFFF)//偏移地址为低4k位
#define PAGEBASE(p) (((ULONG)(p))&~0xFFF)//基址为高位
#define _NPAGES_(p,k) ((PAGENUM((char*)p+(k-1))-PAGENUM(p))+1)
PVOID MapDevice(PVOID PhysAddress, DWORD SizeInBytes)
{
#ifdef USE_MAP_PHYS_TO_LINEAR
return MapPhysToLinear(PhysAddress, SizeInBytes, 0);
#else
PVOID Linear;
ULONG nPages = _NPAGES_(PhysAddress, SizeInBytes);
Linear = PageReserve(
PR_SYSTEM,
nPages,
PR_FIXED
);//分配一块不和具体的物理地址相关联的线性地址空间
PageCommitPhys(
PAGENUM(Linear),
nPages,
PAGENUM(PhysAddress),
PC_INCR | PC_WRITEABLE | PC_USER
);//将线性地址和物理地址联系起来
LinPageLock(PAGENUM(Linear), nPages, 0);
return (PVOID) ((ULONG)Linear+PAGEOFF(PhysAddress));
#endif
}//返回值为线性地址的指针。
由于资源是动态分配的,当程序结束时应该释放资源其中包括锁定的线性页表,否则会出现内存泄漏的错误甚至导致死机。.解除设备的内存映射时,可以调用LinPageUnLock(),
PageDecommit()和PageFree()函数,它们分别是映射函数的逆过程。
下面是关于解除内存映射的一段程序:
VOID UnmapDevice(PVOID LinearAddress, DWORD SizeInBytes)
{
#ifdef USE_MAP_PHYS_TO_LINEAR
// cannot unmap
#else
LinPageUnLock(
PAGENUM(LinearAddress),
_NPAGES_(LinearAddress, SizeInBytes),
0
);
PageDecommit(
PAGENUM(LinearAddress),
_NPAGES_(LinearAddress, SizeInBytes),
0
);
PageFree((MEMHANDLE)LinearAddress,0);
#endif
}
4.43硬件中断编程
VtoolsD提供两种硬件中断类:VHardwareInt类和VsharedHardwareInt类,它们的区别在于后者允许一个IRQ被多个VxD虚拟化,即多个VxD共享一个硬件中断,这也是我们经常它的原因。在VsharedHardwareInt类中我们使用了hook()函数将VxD与分配给我们的IRQ挂起勾来,每次硬件发出中断时激发OnSharedHardwareInt函数,在该函数中进行处理或通知Win32程序让它来处理。整个流程如下:
pMyIRQ=new MyHwInt();//创建一个派生类实例
if(!pMyIRQ||!pMyIRQ->hook())break;//虚拟化系统分给的中断
BOOL MyHwInt::OnSharedHardwareInt(VMHANDLE)
{
VWIN32_QueueUserApc(CallBackApc, (DWORD)&x, TheThread);//通知Win32程序的回调函数
sendPhysicalEOI();//通知VPICD中断处理结束
return FALSE;//必须返回FALSE以让其他VxD捕获该中断
}
第五章应用程序的开发
我的应用程序是用VC6.0++开发的,在应用程序中主要做了两件事情,一是利用从VxD获得的内存映射地址对PCI2040进行初始化设置,二是响应VxD的中断通知读取数据并显示与屏幕。它的流程如下:
图5-1
5.1打开VxD
当应用程序运行后首先要做的是动态打开VxD,我们使用了CreateFile()函数来打开PCI2040.VxD文件,hDevice = CreateFile(".//pci2040.VXD", 0,0,0,CREATE_NEW, FILE_FLAG_DELETE_ON_CLOSE, 0);返回值是VxD的设备句柄,.//PCI2040.VXD表示按照默认路径查找文件PCI2040.VXD,首先是C:/Windows/System目录,然后才是当前目录。如果返回值是INVALID_HANDLE_VALUE表示有错误,可以调用GetLastError()获得错误信息,错误代码若为2,表示找不到要加载的VxD,可能导致原因是没有VxD文件或即使有但它不能动态加载;错误代码若为50,表示虽然VxD存在,但系统不支持DeviceIoControl()函数。
5.2应用程序与VxD通信
在Windows中,Win32应用程序主动对VxD的通信方法只有一种,就是利用设备输入输出控制函数DeviceIoControl(),它的定义如下:
BOOL DeviceIoControl(
HANDLE hDevice, //加载VxD后所获得的句柄
DWORD dwIoControlCode, //应用程序调用VxD的命令代码
LPVOID lpInBuffer, //应用程序传递给VxD数据缓冲地址
DWORD nInBufferSize, //应用程序传递给VxD的数据缓冲字节数
LPVOID lpOutBuffer, //VxD的返回数据所存放的缓冲地址
DWORD nOutBufferSize, //VxD的返回数据所存放的缓冲字节数
LPOVERLAPPED lpOverlapped , //一个指向OVERLAPPED结构的地址,同步 时置为NULL
);
其中dwIoControlCode:应用程序调用VxD的命令代码,它是应用程序和VxD通信的“语言”,通过它VxD知道应用程序要它做什么事情。
在VxD中收到W32_DEVICEIOCONTROL控制消息,由OnW32DeviceIoControl (PIOCTLPARAM pDIOCParams)函数来响应。它的结构定义如下:
参数 pDIOCParams:为指向IOCTLPARAMS结构的指针。
typedef struct tagIOCTLParams
{
PCLIENT_STRUCT dioc_pcrs;
VMHANDLE dioc_hvm;
DWORD dioc_VxdDDB;
DWORD dioc_IOCtlCode; //应用程序调用VxD的命令代码
PVOID dioc_InBuf; //应用程序传递给VxD的数据缓冲地址
DWORD dioc_cbInBuf; //应用程序传递给VxD的数据缓冲字节数
PVOID dioc_OutBuf; //VxD的返回数据所存放的缓冲地址
DWORD dioc_cbOutBuf; //VxD的返回数据所存放的缓冲字节数
PDWORD dioc_bytesret; //VxD实际返回数据的字节数
OVERLAPPED* dioc_ovrlp; //一个OVERLAPPED的结构地址,同步时
//置为NULL
DWORD dioc_hDevice;
DWORD dioc_ppdb;
} IOCTLPARAMS, *PIOCTLPARAMS;
其中dioc_IOCtlCode:为应用程序调用VxD的命令代码。后面的六个参数分别与前面的参数相对应。下面将详细说明我所用的几个命令代码,它们必须在一个头文件中定义,而应用程序和VxD必须包含该头文件。
switch (pDIOCParams->dioc_IOCtlCode)
{
case DIOC_OPEN:――应用程序打开VxD文件的命令通知
dout<<”open”<
break;
case DIOC_CLOSEHANDLE:――应用程序关闭VxD的命令通知
dout<<"close"<
break;
case MDR_SERVICE_UNMAP:――通知VxD解除线性地址锁定
dout<<"unmap"<
break;
case MDR_SERVICE_MAP:――通知VxD进行线性地址转换
dout<<"map"<
break;
case GETMEMBASE0:――通知VxD传递第一个物理地址基址到应用程序
dout<<"getmembase0"<
break;
case GETMEMBASE1: ――通知VxD传递第二个物理地址基址到应用程序
dout<<"getmembase1"<
break;
case GETIOBASE: ――通知VxD传递I/0基址到应用程序
dout<<"GETIOBASE"<
break;
case GETIRQ: ――通知VxD传递中断号到应用程序
dout<<"GETIRQ"<
break;
case ADDRPASS: ――将回调函数的基址送给VxD
dout<<"addrpass"<
break;
default:
return -1;――其他为非法的命令通知
}
5.3VxD与应用程序通信
一般情况我们只需要应用程序对VxD通信,但是由于我们选择了在应用程序中处理中断,而只有VxD才能响应中断,因此VxD必须主动通知应用程序。VxD对Win32应用程序的通信经常使用的方法有两种,一是使用APC(异步调用过程),二是使用Win32事件。二者都依赖VxD唤醒一个Win32应用程序,异步调用过程比较简单,使用Win32事件编程相对复杂一些但它的效率要高于异步调用过程。
Win32事件编程
Win32事件要求应用程序为多线程的,一个线程处于等待状态等待VxD唤醒,当VxD捕捉到特定的事件发生时,给处于等待状态的线程发送消息将其唤醒,而与此同时主线程进行着相应的工作。具体流程如下见图5-2。
异步调用过程
这种方法比较简单,我在编写过程中使用了该通信方式。应用程序首先将VxD要回调的应用程序函数的入口地址传递给VxD,然后应用程序执行SleepEx()使此线程处于警觉等待状态,当VxD捕捉到中断时调用VWIN32_QueueUserApc()函数给Win32应用程序发消息,即可触发Ring3层应用程序的回调函数。
在应用程序中,回调函数的格式如下:
DWORD WINAPI CallBackAPC(PVOID param)
{ ――进行相应处理
return 0;
}
其中param表示VxD传给它的参数的首地址的指针。
在VxD的中断响应函数中使用了VWIN32_QueueUserApc()函数,中断响应函数如下:
BOOL MyHwInt::OnSharedHardwareInt(VMHANDLE)
{
VWIN32_QueueUserApc(CallBackApc, (DWORD)&x, TheThread);
sendPhysicalEOI();
return FALSE;
}
其中VWIN32_QueueUserApc()的三个参数分别对应于回调函数的入口地址,VxD传递给应用程序的数据的入口地址以及回调的应用程序的线性句柄。
NO
YES
图5-2
5.4绘图程序
对于
参考文献
1.《虚拟设备驱动程序开发起步与进步》 彭礼孝 编著 人民邮电出版社
2.《Windows设备驱动程序开发实务》 武安河 周利莉 编著 电子工业出版社3.《Winows设备驱动程序技术内幕》 孙守阁 徐勇 编著 清华大学出版社 4.《Visual C++ 程序设计 提高篇》 乔林 杨志刚 刘文杰编著 中国铁道出版社5.《深入浅出MFC》 候俊杰 编著 华中科技大学出版社
6.《PCI2040 PCI-DSP Bridge Controller Data Manual》 TI公司