吴成英,樊战友
时统设备的PCI设备驱动程序设计
吴成英1,2,樊战友1
(1. 中国科学院国家授时中心,西安 710600;2. 中国科学院研究生院,北京 100039)
介绍了在Windows系统下用VisualC++、DDK和DriverStudio软件对PCI(peripheral component interconnect)设备驱动程序的编写。阐述了PCI设备驱动程序的分层结构和编写方式。PCI驱动属于内核模式驱动程序中的即插即用驱动,PCI的即插即用功能是本文重点介绍对象。另外介绍了PCI设备驱动通过配置空间获取设备资源的过程与方法,以及驱动程序与操作系统之间的通讯机制。
PCI驱动程序;即插即用;配置空间
随着科学技术的发展,作为基本物理量之一的时间在国民经济、国防等领域的重要性日益显著。在向信息化时代迈步的今天,人们的日常生活正处在时间和频率的“包围”中,各类定时器、数据传输、B码终端以及GPS导航定位都离不开高精度的时间。中国科学院国家授时中心(NTSC)的时间基准系统为国民经济做出了重大贡献,同时国家重大项目和基础建设也对时间工作提出了更高的要求[1]。本文提出的PCI设备驱动程序的设计对NTSC相关的时频产品有很大的帮助,主要是解决时码信号传输速度慢的问题。如今,PCI总线以优异的性能逐步取代了ISA(industry standard architecture)和EISA(extension industry standard architecture)等其他总线,成为当今总线发展中的主流。一方面是因为PCI总线的数据吞吐量大,另一方面是因为该总线与具体的处理器无关。PCI设备驱动程序位于PCI总线的上层,而且PCI驱动程序不会因挂在PCI上的硬件不同而不同。本文将以“通用高速PCI总线”的驱动设计为例,探讨通用PCI设备驱动程序的开发过程。
搭建环境是编写驱动程序的根基。驱动程序不像一般应用程序,运行在用户模式下。驱动程序在内核模式下加载,而且需要操作系统的各个核心组件相互配合。因此驱动程序编译环境搭建不好必然会产生很多致命问题,比如在加载驱动程序时,计算机突然死机、蓝屏。如今,开发设备驱动程序主要是在VC、DDK、Visual Studio 3个综合平台上,安装这3个软件时,必须按照VC→DDK→Visual Studio这一先后顺序来进行。安装完成后首先编译DriverStudio自带的2个工程文件Vdwlibs.dws和NdisWdm.dsw,当这2个工程文件编译无错时,说明DDK软件安装成功。驱动程序用到了很多DDK自带的头文件,而且不同的驱动程序包含不同的头文件。编译PCI驱动程序时,要包含wdm.h头文件,wdm.h头文件是编译WDM式驱动程序所必须的头文件,它是DDK编译环境封装好的文件。为了让VC、DDK、Visual Studio这3个软件配合使用,编译时路径设置必须正确,确保不出错。启动编译环境VC时,按以下顺序启动:开始→所有程序→Compuware DriverStudio→Develop→DDK Build Setting→Launch Program(如图1所示)。启动VC成功后,编译DriverStudio自带的例子HellowWdm.dsw,如果运行成功,开发环境到此都配置成功。
图1 编译环境启动界面
PCI总线是连接计算机各个硬件单元的通用总线,不同总线之间的通讯通过相应的桥芯片来转接。PCI总线是计算机南桥芯片与北桥芯片通讯的桥梁,PCI总线周期转换和地址空间映射是南桥和北桥通讯的关键点。
根据协议,PCI设备占用的内存、I/O、中断等资源是浮动的。PCI总线提供了资源配置机制,描述资源配置信息的寄存器称为配置寄存器,配置寄存器的整体就构成了配置空间。PCI有3个相互独立的物理地址空间:设备存储器地址空间、I/O地址空间和配置空间,这3种空间是并列且相互独立的地址空间,处理器访问内存空间或 I/O 空间时使用对应的内存指令或 I/O 读写指令。访问配置空间则需要通过访问配置地址寄存器和配置数据寄存器来完成,配置空间是最能显示PCI特性的物理空间[2]。
PCI的配置空间是实现PCI即插即用功能的关键因素。配置寄存器是PCI硬件设备与PCI设备初始化软件的信息交接区,使得应用程序能够识别PCI硬件设备并能控制该设备。PCI总线规范定义的配置空间总长度是256个字节,256个字节的配置空间包括头标区(64字节)和设备关联区(192字节)2部分,头标区是每个PCI功能配置空间里都具有的区域,主要的功能是用来识别设备和控制通用的设备;关联区的设置取决于PCI设备本身的需求。
头标区根据PCI设备的不同而拥有不同的类型,本文以常用类型0为例进行解释(如表1所示)。操作系统根据厂商ID判断PCI设备是否存在,结合设备ID找到相应的驱动程序。中断线和中断引脚一起管理着PCI设备的中断,PCI总线一般有24个中断。
由于PCI设备驱动是通过WDM驱动程序完成的,WDM会为PCI总线上的设备提供一个物理设备对象(PDO),当功能设备对象(FDO)挂在PDO上时,就可以将IRP_MN_START_DEVICE传递给底层的PDO去处理。PCI总线的PDO就会得到PCI的配置空间,并从中得到有用的信息,如中断号、设备物理内存以及I/O端口信息。
表1 配置空间布局
如今驱动程序有2类:一类是WDM式;另一类是NT式。它们主要的区别在于是否支持即插即用功能(前者支持,后者不支持),同时编写时所包含的头文件也不同。本文的PCI设备驱动是WDM式驱动,WDM式驱动是基于分层的,完成一个WDM式的驱动程序至少需要物理设备对象和功能设备对象[2]。
当PC机的PCI总线上插入一个设备时,总线驱动程序识别总线上的该设备并为设备产生物理设备对象PDO,此时操作系统会提示用户安装PCI设备的FDO。提示界面和平常插入USB设备时提示“找到新的硬件向导”相同。此时把驱动程序对应的.inf文件添加到硬件向导里面就可以使用新的PCI设备了。当1个FDO附加在PDO上的时候,PDO设备对象的子域AttachedDevice会记录FDO的位置。每个设备对象中有个StackSize子域,表明操作这个设备对象需要几层才能到达最下层的物理设备。FDO和PDO的具体关系如图2所示。
图2 驱动的分层结构
驱动程序将创建的FDO附加到PDO上,是靠IoAttachDeviceToDeviceStack函数实现的。函数的申明如下:
PDEVICE_OBJECT IoAttachDeviceToDeviceStack(IN PDEVICE SourceDevice,IN PDEVICE TargetDevice)。其中SourceDevice表示FDO的地址,TargetDevice表示PDO的地址,返回值是返回附加设备的对象。PCI设备驱动在PDO和FDO之间有很多过滤驱动程序,所以在此返回的是过滤驱动。PDO和FDO之间是要互动的,要让FDO知道它下层的驱动必须通过设备扩展记录FDO下层的设备。设备扩展的定义如下:
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT fdo;//功能设备对象,保存FDO的地址
PDEVICE_OBJECT NextStackDevice;//下层驱动设备
UNICODE_STRING ustrDeviceName;//设备名
UNICODE_STRING ustrSymLinkName;//符号链接
}DEVICE_EXTENSION, *PDEVICE_EXTENSION
设备驱动程序就是控制硬件设备的一组函数。PCI驱动程序的开发,就是取得PCI板卡占用的各种资源(内存、端口、中断和DMA等),并提供给用户一条可以访问这些资源的途径[3-4]。当用户模式程序需要读取设备数据时,它就调用Win32 API函数,如ReadFile。Win32子系统模块(如KERNEL32.DLL)通过调用平台相关的系统服务接口实现该 API(application programming interface,应用程序编程接口),而平台相关的系统服务将调用内核模式支持例程。驱动程序通过NTCreateFile()函数以软中断的方式从用户模式进入到内核模式。NTCreateFile()系统函数的调用通过I/O管理器,创建IRP(I/O request package)并传输到设备驱动程序使得硬件设备能够被识别。
运行在内核模式中的驱动程序通常使用硬件抽象层(HAL)提供的函数访问硬件。如IN、OUT用READ_PORT_BUFFER_UCHAR、WRITE_PORT_BUFFER_UCHAR指令代替。HAL例程执行的操作具有平台相关性。在Intel x86计算机上,HAL使用IN指令访问设备端口。驱动程序完成1个I/O操作后,通过调用一个特殊的内核模式服务例程来完成该IRP。完成该操作是处理IRP的最后动作,它使等待的应用程序恢复运行。
基本上Windows应用程序都有一个入口函数,C语言程序的入口函数是main(),C++程序的入口函数是WinMain(),驱动程序同样有一个入口函数DriverEntry()。形式如下:
Extern〝C〞NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING pRegistryPath)。
用extern〝C〞是因为C++语言和C语言对产生的函数名字处理不一样。C++语言为了支持函数重载机制,在编译时,要对函数的名字做额外处理,而在C编译器中不需要对函数名字进行处理。DDK入口函数的定义是在C环境中编写的,在VC编译器中用这个函数就必须加extern〝C〞;IN表示该参数是1个输入参数;NTSTATUS是32位无符号长整形,常用的NTSTATUS有STATUS_SUCCESS、STATUS_BUFFER_OVERFLOW。几乎所有的驱动程序例程都要返回1个NTSTATUS的值。PDRIVER_OBJECT是驱动对象,是结构体指针变量;PUNICODE_STRING是UNICODE结构体指针,UNICODE是宽字符集;pRegistPath指定了驱动程序在注册表中的路径,一旦驱动程序安装,注册表中会有相应的记录。注册表中的记录是为了帮助PCI设备驱动程序识别和定位该设备使用的资源。
PCI驱动程序与一般的驱动程序不同的就是支持即插即用(PnP)的功能。当PCI总线插入1个PCI设备时,即插即用能够通过操作系统协调自动分配PCI设备的中断号、设备物理内存、I/O地址等。即插即用功能是通过IRP派遣函数(dispatch function)发送给PCI设备驱动程序,然后设备驱动程序发送到底层的驱动程序。不同的情况下会有不同的IRP函数。设备启动时,就会发送IRP_MN_START_DEVICE给WDM驱动程序,而设备被突然拔出后会发送IRP_MN_SURPRISE_REMOVAL给WDM驱动程序。对即插即用派遣函数的处理和对其他派遣函数的处理一样,首先必须在入口函数DriverEntry中注册。形式如下:
Extern〝C〞NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,IN PUNICODE_STRING pRegistryPath)
{
KdPrint((〝Enter DriverEntry 〞));
//驱动对象中有驱动扩展子域,驱动扩展里面有添加设备子域,通过这个子域得到添加设备函数的地址
pDriverObject->DriverExtension->AddDevice = PCIWDMAddDevice;
//MajorFunction域记录的是函数指针数组,数组中的每个成员记录着一个指针,指针指向的是一个
IPR派遣函数
//派遣函数注册
pDriverObject->MajorFunction[IRP_MJ_PNP] = PCIWDMPnp;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = PCIDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = PCIDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_READ] = PCIDispatchRoutine;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = PCIDispatchRoutine;
//指定驱动卸载时回调函数的地址
pDriverObject->DriverUnload = PCIWDMUnload;
KdPrint((〝Leave DriverEntry 〞));
return STATUS_SUCCESS
}
以上是一个对即插即用IRP派遣函数的注册实例,对其他的派遣函数是默认处理。但在实际的编写过程中是要对其他的派遣函数做相应处理的。
派遣函数是Windows驱动程序中的重要函数。驱动程序的主要功能是负责处理I/O请求,I/O请求由操作系统转换为内核数据结构IRP,不同的IRP有不同的派遣函数。上层应用程序与底层驱动程序通讯时,应用程序会发出I/O请求。具体过程如图3所示。
图3 驱动程序与IRP
驱动程序与PCI总线设备之间的通讯主要是通过IRP_MN_START_DEVICE获取设备资源、用WRITE_REGISTER_XX和READ-REGISTER_XX(其中XX可以是UCHAR、USHORT、BUFFER_UCHAR)系列函数向设备物理内存读数据和写数据。
PCI设备驱动程序通过设备接口与Windows应用程序进行通讯。该接口是软件如何访问硬件的说明,它由一个128位的GUID(全局唯一标识符)标识,并能保证所有设备的标识符不冲突。这个标识符可以由Windows自带的创建工具guidgen.exe生成,在运行中输入guidgen.exe打开如图4的界面。Guidgen.exe为用户提供4种产生GUID的方式,它们都是128位的数字,只是输出的形式不同,一般选择图4的第2种方式产生一个新的GUID(Result里面的显示结果),单击Copy按钮,会将产生的GUID复制到内存里,在程序中粘贴即可。通过这个GUID可以在注册表中查到PCI驱动的相关信息。
图4 GUID的获取
VC调试 PCI驱动程序不能用VC自带的调试版本和编译版本,需要增加一个驱动版本并修改编译和链接选项卡。编译驱动程序时会因找不到很多头文件而造成很多错误。解决此问题的方法首先是在DDK安装目录中搜索编译时找不到的头文件。如果DDK安装成功的话,马上就能搜索到编译器要的头文件,然后把该头文件的目录复制到VC编译器的路径中。VC要包含的头文件路径在“Tools→Options→Directories”选项卡中设置。如图5所示是驱动程序所需要包含的一些库文件和头文件。要用标准调用驱动程序,在project setting中的C++选项卡中设置成_stadcall即可。
驱动代码编译完成后,生成设备驱动系统文件(.sys文件)。添加1个新的PCI硬件设备时,即将实验板插入计算机的PCI插槽,启动计算机。如果计算机不能正常启动,则实验板上的PCI芯片焊接有问题或者是计算机的PCI插槽可能有问题。如果能正常启动,系统会提示安装硬件的驱动程序。此时系统需要寻找与.sys驱动程序文件同目录下的.inf文件。.inf文件的基本作用是告诉操作系统设备及其驱动程序的相关信息:要创建或修改的注册表信息以及驱动程序的位置、文件名和需要拷贝到的目标位置等。
安装驱动程序时操作系统通过.inf文件定位驱动程序文件,把它拷贝到指定位置,并在注册表记录驱动信息。系统重新启动的时候,一旦发现这个设备,就从注册表来查找设备的VID(vender identification,又称vender ID)和DID(device identification)对应的驱动程序,然后加载编译好的驱动程序。
完成上述过程后重新启动计算机时操作系统会自动搜索到“其他PCI桥设备”。通过WinDriver也可以检测到PCI芯片的存在以及芯片的配置信息。
图5 头文件配置
本文设计的驱动程序主要是为了用PCI总线传输NTSC高新技术产品B码终端设备的时码信号,这对NTSC高新技术产品提供了更好的应用前景。同时,按照本文的设计再结合各自的需求稍加改动就可以设计出各自需求的PCI板卡驱动程序。
[1] 董邵武, 屈俐俐, 李焕信, 等. NTSC的守时工作进展[J]. 时间频率学报, 2010, 33(1): 1-4.
[2] 李海. PCI设备Windows通用驱动程序设计[J]. 电子技术应用, 2000, 26(1): 19-22.
[3] 张帆, 史彩成. 驱动开发技术详解[M]. 北京: 电子工业出版社, 2008.
[4] 武安河, 邰铭, 于洪涛. Windows 2000/XP WDM设备驱动程序开发[M]. 北京: 电子工业出版社, 2003.
A design of PCI driver program for time synchronization devices
WU Cheng-ying1, 2, FAN Zhan-you1
(1. National Time Service Center, Chinese Academy of Sciences, Xi′an 710600, China; 2. Graduate University of Chinese Academy of Sciences, Beijing 100039, China)
This paper introduces a designing of PCI driver program in the Windows environment using the tools of Visual C++, DDK and DriverStudio. The hierarchical structure and programming method for PCI driver are described.PCI driver is a kernel-mode driver and it has the function of plug-and-play which is expounded emphatically in this paper. The process and method of getting the device resources through configure space for the PCI device driver, as well as the mechanism of communication between the operating system and the driver program are introduced.
PCI driver program; plug-and-play; configure space
P127.1+2
A
1674-0637(2011)02-0117-08
2010-10-19
国家自然科学基金资助项目(10773012)
吴成英,女,硕士研究生,主要从事高精度时间同步终端设计研究。