为了减少开发难度,缩短开发周期,我们建议选择专用芯片。根据硬件要实现的功能,选择芯片。
以PLX公司的产品为例,PCI9052提供了5个局部地址存储空间、支持中断、支持从模式数据传输;PCI9054和PCI9080在PCI9052的基础上又增加了主模式数据传输、两个独立的DMA通道和八个邮箱寄存器等功能。
驱动程序的模式和开发工具的选择
以上是对PCI设备硬件方面的介绍,为了实现PCI设备与计算机的通信,还需要开发PCI设备驱动程序。驱动程序是用来管理系统资源的可执行二进制代码,与操作系统拥有相同的级别,不同的操作系统支持不同类型的驱动程序。目前在市场上比较流行的操作系统是Windows9x和WindowsNT这两种系列。Windows9x包括Windows95、Windows98、WindowsME;WindowsNT包括WindowsNT4.0、Windows2000。Windows95支持VXD类型的驱动程序,而WindowsNT支持WDM类型的驱动程序,Windows98兼容Windows95的VXD驱动程序,同时它又推出一个新的Win32 Drivers Mode (WDM)驱动类型。这个新的类型实际是在Windows NT的驱动模型的基础上增加了即插即用等内容。WDM驱动也可以用在Windows 2000(先前叫Windows NT5.0)中。一个完善的驱动程序应至少开发Windows 9X和Windows NT两个版本。目前,虽然Windows 2000非常流行,但由于Windows98仍占有相当规模的市场,而且它又兼容Windows95的VXD驱动程序,因此VXD驱动程序仍然实用。本文只介绍基于Windows9x系统下VXD驱动程序的开发。
VXD是虚拟设备驱动程序的简称,x 代表各种设备的名字,如虚拟键盘驱动程序(vkd),虚拟鼠标驱动程序(vmd)等等。开发驱动程序需要对硬件进行操作,由于Intel 80386以上的微处理器有4个优先级别:0级、1级、2级和3级,一般操作系统运行于优先级第0级上,而用户程序运行在第3级上,Windows9x操作系统对系统硬件采取了屏蔽的策略,限制了运行于第3级的应用程序对系统资源(如中断控制器、内存等)的操作。但VXD运行在最高级特权级——第0级,拥有操作系统的特权,可以超越这些屏蔽,直接进行系统硬件的操作。
开发设备驱动采用的主要开发工具是微软为设备开发者提供的软件包Device Driver Kit (DDK)。这个软件包包括有关设备开发的文档、编译需要的头文件和库文件、调试工具和程序范例。在DDK中还定义了一些设备驱动可以调用的系统底层服务,如DMA服务、中断服务、内存管理服务、可安装文件系统服务等等。这些都是编写设备驱动所必须的。但由于Windows 95的DDK主要使用汇编语言描述,代码可读性不强,开发起来比较困难。因此,我们在Windows 9x操作系统中采用了Numega公司的产品VtoolsD。VtoolsD是基于C/C++的,支持Borland C++和Visual C++,代码可读性强,使用和维护都较Windows DDK容易。
驱动程序的设计
编写设备驱动程序的目的是使被驱动的硬件可以管理系统资源,与PC机系统兼容,正常工作,通过设备驱动程序,多个进程可以同时使用这些资源(如内存、I/O、中断源等),实现多进程并行运行。驱动程序是针对具体硬件设计的,不同硬件有不同的驱动程序,下面仅讨论开发驱动程序几个必要的通用的步骤。
1、PCI配置空间简介
每个PCI设备都有自己的配置空间,用于支持即插即用,使之满足现行的系统配置结构。下面对PCI配置空间做一下简要介绍。
配置空间是一容量为256字节并具有特定结构的地址空间。这个空间又分为头标区和设备有关区两部分。头标区的长度是64字节,每个设备都必须配置该区的寄存器。该区中的各个字段用来唯一地识别设备。其余的192字节因设备而异。配置空间的头标区64个字节的使用情况如图1示。
为了实现即插即用,系统可根据硬件资源的使用情况,为PCI设备分配新的资源。因此编写设备驱动程序重点是获得基址寄存器(Base Address)和中断干线寄存器的内容。配置空间共有六个基址寄存器和一个中断干线寄存器,具体用法如下:
PCI Base Address 0寄存器:系统利用此寄存器为PCI接口芯片的配置寄存器分配一段PCI地址空间,通过这段地址我们可以以内存映射的形式访问PCI接口芯片的配置寄存器。
PCI Base Address 1寄存器:系统利用此寄存器为PCI接口芯片的配置寄存器分配一段PCI地址空间,通过这段地址我们可以以I/O的形式访问PCI接口芯片的配置寄存器。
PCI Base Address 2、3、4、5寄存器:系统BIOS利用这些寄存器分配PCI地址空间以支持PCI接口芯片的局部配置寄存器0、1、2、3的访问。
在所有基址寄存器中,第0位均为只读位,表示这段地址映射到存储器空间还是I/O空间,如果是“1”表示映射到I/O空间,如果是“0”则表示映射到存储器空间。
中断干线寄存器(Interrupt Line):用于说明中断线的连接情况,这个寄存器的值与标准8259的IRQ编号(0~15)对应。
设备识别号 供应商识别号
状态寄存器 命令寄存器
分类代码 修改版本
自测试 头标类型 延时计数 Cache
基址寄存器
保留
保留
扩展ROM基址寄存器
保留
保留
Max-Lat Min-Gnt中断引脚 中断干线
图1 配置空间头标区
2、设备初始化
PCI设备驱动程序要完成识别PCI器件、分配PCI硬件资源、响应PCI器件中断等功能,这就需要访问PCI配置空间来获得必需的参数。实现在Windows9x操作系统下访问PCI配置空间可以利用PCI系统BIOS功能调用,通过供应商识别号(VendorID)和设备识别号 (DeviceID)直接访问设备,也可以利用配置管理器(Configuration Manager)封装的功能函数,根据供应商识别号(VendorID)和设备识别号 (DeviceID)搜索设备结点树,查询PCI设备。由于编写PCI系统BIOS功能调用程序更为简捷,所以本文采用这种方法。
PCI系统BIOS功能提供了BIOS的访问与控制的具体方法,所有软件(设备驱动程序、扩展ROM码)将通过标准中断号1AH调用BIOS功能访问特殊部件。在驱动程序中调用VtoolsD系统服务Exec_VxD_Int()来实现PCI系统BIOS的1AH中断。
首先,通过PCI设备的供应商识别号(VendorID)、 设备识别号 (DeviceID)和索引号(Index)查找特定设备所在的总线号(Bus Num)、设备号(Device NUM)、功能号(Function Num)和寄存器号(Register Num)。总线号是从0到255的数值,在一个系统中,可把多达256条的PCI总线用桥连接在一起。由于编号是从0开始的,所以当系统有N条总线时,总线号会达到N-1;设备号是在0到31之间分配的任意值,并不拘于从0开始按顺序分配;功能号分配从0到7的值。代码如下:
ALLREGS* pRegisters; // pRegisters是指向寄存器结构体的指针
pRegisters->REAX =0xb102; // 0xb102是功能号
pRegisters->RECX =0x1001; // 假设Device ID=0x1001
pRegisters->REDX=0x102b; // 假设Vendor ID=0x102b
Exec_VxD_Int(0x1a,pRegisters); // 调用1AH中断
返回值是pRegisters->REBX。BH寄存器是总线号,BL寄存器的高5位是设备号,低3位是功能号。
然后,向配置空间地址寄存器CF8h写入总线号、设备号、功能号、索引号, 从配置空间数据寄存器CFCh读出配置空间的内容。
配置空间地址寄存器(CF8h)格式如下:
Bit31 30-24 23-16 15-11 10-8 7-2 10
使能位 保 留 总线号 设备号 功能号 寄存器号 00
使能位为“1”表示允许访问
配置空间数据寄存器(CFCh)存放要读写的数据。
代码如下:
DWORD d=0;
d=pRegisters->REBX;
(d<<=8)|=0x80000000;
for (short i=0;i<16;i++) // 读取64字节配置空间
{
_outpd(0xcf8,d+4*i); // 按DWORD类型一次读取四个字节
dprintf("%8x",_inpd(0xcfc)); // 打印输出
}
3、内存的读写
Winsows工作在32位保护模式下,保护模式与实模式的根本区别在于CPU寻址方式上的不同,这也是Windows驱动程序设计中需要着重解决的问题。Windows采用了分段、分页机制,这样使应用程序产生一种错觉,好象程序中可以使用非常大的物理存储空间。这样做最大的好处就是一个程序可以很容易地在物理内存容量不一样的、配置范围差别很大的计算机上运行,编程人员使用虚拟存储器可以写出比任何实际配置的物理存储器都大得多的程序。每个虚拟地址由16位的段选择字和32位段偏移量组成。通过分段机制,系统由虚拟地址产生线性地址。再通过分页机制,由线性地址产生物理地址(如图2)。线性地址被分割成页目录(Page Directory)、页表(Page Table)和页偏移(Offset)三个部分。当建立一个新的Win32进程时,操作系统会为它分配一块内存,并建立它自己的页目录、页表,页目录的地址也同时放入进程的现场信息中。当计算一个地址时,系统首先从CPU控制器CR3中读出页目录所在的地址,然后根据页目录得到页表所在的地址,再根据页表得到实际代码/数据页的页帧,最后再根据页偏移访问特定的单元。硬件设备读写的是物理内存,但应用程序读写的是虚拟地址,所以存在着将物理内存地址映射到用户程序线性地址的问题。
15 0 31 0 31 0 31 0
图2 虚拟地址转换为物理地址
从物理地址到线性地址的转换工作是由驱动程序来完成的。驱动程序的内存映射部分主要是调用VxD的系统服务MapPhysToLinear。在VtoolsD中这个函数的定义如下:
PVOID MapPhysToLineag(CONST VOID * PhysAddr,DWORD nBytes,DWORD Flags);
其中第一个参数PhysAddr就是要映射的内存的物理地址的起始位置,这个物理地址可以从PCI配置空间的基址寄存器中获得,nBytes是内存区域的长度,Flags必须设置为0。这个函数返回的就是这段物理地址映射的线性内存地址。如果指定的内存不能存取,函数将返回FFFFFFFFH。
如:PDWORD pBase = (PDWORD)MapPhysToLinear((PVOID)PhysAddress,PhysSize,0);
将pBase传递给调用驱动的用户程序,用户程序就可以像使用指针一样利用pBase访问内存。
4、I/O端口的操作
在PC机上,I/O寻址方式与内存寻址方式不同,所以处理方法也不同。I/O空间是一个64K字节的寻址空间,I/O寻址没有实模式与保护模式之分,在各种模式下寻址方式相同。在Windows9x系统下,运行于第3级的应用程序也可以直接使用I/O指令访问I/O空间。在设备初始化访问配置空间时,已用到了I/O指令,在对硬件进行配置时,也可以根据从配置空间基址寄存器PCI Base Address 1中返回的I/O端口基地址使用I/O指令。
5、响应中断
VTOOLSD提供了VHardwareInt类,虚拟IRQ,处理硬件中断。在Windows9x中,VPICD虚拟了物理可编程中断控制器的端口,从而可以控制物理中断控制器。虚拟IRQ的编程思路:首先从VHardwareInt类中派生出一个类,重载OnHardwareInt函数;然后,动态创建一个派生类对象实例;最后钩挂处理程序,这就是需要编写的中断服务程序。(关于VHardwareInt类可参考VTOOLSD)
四、驱动程序的调用和封装
编写设备驱动并不是最终的目的,需要由用户程序来调用驱动并实现一定的功能。一般调用设备驱动是使用CreateFile函数打开设备文件,得到一个文件句柄。使用如下的语句就可以打开文件。
HANDLE hVxD=CreateFile("\\.\PCIDEVICE.VXD",0,0,0, OPEN_EXISTING,
FILE_FLAG_DELETE_ON_CLOSE,
0);
打开设备文件后,调用DeviceIoControl函数就可以实现应用程序与设备驱动程序的通信。完成硬件操作之后,可以调用函数CloseHandle(hVxD);关闭设备驱动。(关于这三个函数的详细说明请参考MSDN)
至此,我们完成了对驱动程序的初步设计,考虑到有的函数(如DeviceIoControl)调用起来非常复杂,为了提高程序的通用性,要对部分函数进行封装。因为动态链接库(DLL)可以在多数软件开发平台中调用,所以常用DLL封装形式。另外需要注意的是由于驱动程序具有与操作系统相同的特权,并且直接操作硬件,如果程序工作不稳定,会造成死机甚至系统崩溃,所以要对驱动程序进行全面细致的测试。
参考文献
1.李贵山. PCI局部总线开发者指南 西安电子科技大学出版社 1997
2.彭礼孝.虚拟设备驱动程序开发起步与进阶 人民邮电出版社 2000
3.PCI BIOS SPECIFICATION Revision 2.1 1994