WDM(Win32 Driver Model),即Windows驱动程序模型,是Microsoft力推的全新驱动程序模式,旨在通过提供一种灵活的方式来简化驱动程序的开发,在实现对新硬件支持的基础上减少并降低所必须开发的驱动程序的数量和复杂性。WDM用于开发Windows 98和Windows 2000设备驱动程序。
WDM驱动程序开发工具通常用NuMega DriverStudio。因为它可以集成到VC 开发环境中,这样就可以像生成其他工程一样生成驱动程序框架,省去了大量的编写代码的时间。(后面要介绍的功能实现都是在用VC6.0集成环境生成的驱动程序框架中编写的)
编写一个设备驱动程序,除了要掌握驱动程序模型之外,还要了解相应设备的硬件知识。本文要介绍的是并行口EPP模式的WDM编程。主要介绍并行口EPP模式,并用实际的例子介绍并行口EPP模式驱动程序的一些功能实现:如何在驱动程序中实现并行口EPP的基本I/O;如何利用控制码实现上层应用程序对并行口的通讯;如何截获中断和挂接中断服务例程;如何通过在驱动程序中设置信号来激活上层应用程序定义的事件,以实现驱动程序对上层应用程序的通讯。
一、 并行口EPP模式介绍
高级并行接口EPP可以进行高速的双向数据传输。EPP可以区分两种类型的信息,他们通常分别
被定义为数据和地址。由于这种接口可以实现快速的方向转换,因此他特别适用于进行较小数据块的传输、需要经常交换数据传输方向的设备。
1. 并行口EPP模式的I/O地址及其特性定义:
1) 并行口基地址:
378 新系统通用,通常是LPT1,也可以是LPT2,通常使用中断IRQ7
278 通常是LPT2,也可以是LPT1,LPT3(只能用此基地址),通常使用中断IRQ5
(注1:下面都是针对378基地址LPT1介绍)
2) 并行口EPP模式的寄存器表:(表1)
寄存器 地址 读/写
数据寄存器(Data) [Base+0] 378 读/写
状态寄存器(Status) [Base+1] 379 读
控制寄存器(Control) [Base+2] 37A 写
地址寄存器(Address) [Base+3] 37B 读/写
扩展数据寄存器(Ext_Data) [Base+4] 37C 读/写
未定义 [Base+5] 37D
未定义 [Base+6] 37E
未定义 [Base+7] 37F
表1. 并行口EPP模式的寄存器
3) 状态寄存器(379)和控制寄存器(37A)的定义:(表2)
D7 D6 D5 D4 D3 D2 D1 D0
状态寄存器(379) S7 S6 S5 S4 S3 S2 S1 S0
Wait(Busy) Int(Ack) 用户定义(引脚12、13、15) Timeout超时
控制寄存器 (37A) C7 C6 C5 C4 C3 C2 C1 C0
双向控制1出0入 中断允许1允许 地址选通nAStrb0有效 init0有效 数据选通nDStrb0有效 write0有效
表2. 状态寄存器(379)和控制寄存器(37A)的定义
4) 并行口25芯引脚信号表:(表3)
Pin SPP Signal EPP Signal IN/OUT Status/Control Register
1 Strobe Write Out /C0
2-9 Data 0-7 Data 0-7 In-Out
10 Ack Interrupt In S6
11 Busy Wait In /S7
12 Paper Out / End Spare In S5
13 Select Spare In S4
14 Auto Linefeed Data Strobe Out /C3
15 Error / Fault Spare In S3
16 Initialize Reset Out /C2
17 Select Printer Address Strobe Out /C3
18-25 Ground Ground GND Ground
表3. 并行口25芯引脚信号表.
2. 并行口EPP模式寄存器的读写:
1) 先对控制寄存器(Control)初始化
如果禁止中断用out(37A,0x80),如果使用中断用out(37A,0x90)
2) 写一个寄存器的两条基本指令:
out(37B,addr); // 将addr写入用户设备地址寄存器
写:out(37C,data); // 将数据data写入addr指向的用户设备空间单元
读:in(37C); // 从addr指向的用户设备空间单元中读取数据
二、 并行口EPP模式驱动程序的功能实现
下面是并行口EPP模式驱动程序接口框图:(图1)
图1. 并行口EPP模式驱动程序接口框图
1. 并行口EPP的基本I/O
Ÿ 首先定义类KIoRange的一个实例,以对应EPP
KIoRange m_ParPortIos;
status = m_ParPortIos.Initialize(
0x378, // LPT1 Bus address
TRUE, //InCpuIoSpace
8, // Device size
TRUE // Map to system space
);
Ÿ 下面就可以用类KIoRange的成员函数来访问EPP的寄存器了
// EPP的寄存器相对于EPP基址的偏移量
#define CONTROL 2 //对应EPP的控制寄存器(37A)
#define ADDRESS 3 //对应EPP的地址寄存器(37B)
#define EDATA 4 //对应EPP的扩展数据寄存器(37C)
//设置控制寄存器
m_ParPortIos.outb(CONTROL,0x80);
// EPP读
m_ParPortIos.outb(ADDRESS,addr); //addr是实际编程中你要访问的设备单元的地址
UCHAR data = m_DeviceIos.inb(EDATA)
// EPP写
m_ParPortIos.outb(ADDRESS,addr); //addr是实际编程中要访问的设备单元的地址
m_DeviceIos.outb(EDATA,data); //data是实际要写入的数据
2. 利用控制码实现上层应用程序对并行口的通讯
在上层应用程序中,要对设备进行访问,首先到创建设备连接对象。通过调用函数CreateFile,如下,其中ParPortDevice0是设备的符号连接名:
char *sLinkName = ".//ParPortDevice0";
m_hParPort = CreateFile(sLinkName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
创建成功后就可使用该句柄来对设备进行读写访问,如:
ReadFile(m_hParPort,…);
WriteFile(m_hParPort,…);
另外还有一个重要的功能就是通过定义一些特殊的控制码来对设备进行特殊的交互,如
DeviceIoControl(m_hParPort,IOCTLCODE,….);
其中IOCTLCODE是在上层应用程序中根据要访问的设备和对设备的访问方式定义的控制码。
驱动程序在DeviceControl(KIrp I)中作相应的处理。
I/O控制码的定义原型如下:
#define CTL_CODE( DeviceType, Function, Method, Access ) ( /
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) /
)
DeviceType表示设备类型,不同的设备有不同的值,并行口的DeviceType 值为0x00000016
Access 表示允许对设备进行的访问:
#define FILE_ANY_ACCESS 0
#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS)
#define FILE_READ_ACCESS ( 0x0001 )
#define FILE_WRITE_ACCESS ( 0x0002 )
Method表示对设备访问的方式:
#define METHOD_BUFFERED 0
#define METHOD_IN_DIRECT 1
#define METHOD_OUT_DIRECT 2
#define METHOD_NEITHER 3
Function用来在DeviceType,Access,Method都相同的情况下,区分不同的控制码。
其中DeviceType,Function,Method,Access都是4字节16进制常量,关于这几个常量的定义请详见wdm.h或ntddk.h,“DeviceType<<16”表示将常量DeviceType左移16位。
具体通讯方式如下:
首先,在上层应用程序中定义I/O控制码:
#define CPUTESTDEVICE_IOCTL_MEM_READ 0x00160000
0x00160000的含义是允许对并行口进行带有缓冲的任何访问。
调用函数DeviceIoControl(m_hParPort, CPUTESTDEVICE_IOCTL_MEM_READ,…);
然后,在驱动程序中响应I/O控制码:
#define CPUTESTDEVICE_IOCTL_MEM_READ 0x00160000
在DeviceControl(Kirp I)例程中作如下处理:
NTSTATUS ParPortDevice::DeviceControl(KIrp I)
{
……
ParPortDevice *pDev = (ParPortDevice *) KDevicePTR(I.DeviceObject());
switch (I.IoctlCode())
{
……
case CPUTESTDEVICE_IOCTL_MEM_READ:
pDev->MemRead(I); //MemRead(KIrp I)为自定义的成员函数
break;
……
}
……
}
3. 截获中断和挂接中断服务例程
许多驱动程序都要处理硬件中断。当用户设备要通知上层应用程序读数据时,它可以发一个硬件中断,驱动程序截获此中断,再通过在驱动程序中设置信号来通知上层应用程序。但在处理硬件中断之前,一定要使硬件的中断允许有效,对于EPP,是使用下面这条语句实现的:
m_ParPortIos.outb(Control,0x90);
实现中断的截获和挂接方法如下:
a) 定义类KInterrupt的一个实例
KInterrupt m_TheInterrupt;
b) 在设备类中声明一个成员函数TheIsr作为中断服务例程ISR。
class ParPortDevice : public KPnpDevice
{
……
public:
MEMBER_ISR (HelloDevice, TheIsr);
……
#ifdef _COMMENT_ONLY
BOOLEAN TheIsr(void);
#endif
……
}
c) 在OnStartDevice例程中获取包括中断的设备资源并初始化中断和挂接ISR
ParPortDevice::OnStartDevice(KIrp I)
{
……
m_ParPortIos.outb(Control,0x90); //置中断允许位
PCM_RESOURCE_LIST pResList = I.TranslatedResources(); //获取设备资源
//初始化中断并挂接中断服务例程TheIsr
status = m_TheInterrupt.InitializeAndConnect(
pResList,
LinkTo(TheIsr),
this
);
……
}
4. 通过在驱动程序中设置信号来激活上层应用程序定义的事件
驱动程序有时需要主动通知上层应用程序来执行某一个操作,以实现硬件响应的实时性。
这可以通过在上层应用程序中创建一个事件,将该事件句柄放入输入缓冲区中通过函数DeviceIoControl传给驱动程序,并创建一个线程来守候该事件被激活。具体实现如下:
§ 在应用程序中:
#define CPUTESTDEVICE_IOCTL_SETUP_SIGNAL 0x00160004
声明一个类的成员:
public:
HANDLE m_hDeviceEvent;
BOOL CApptestDlg::OnInitDialog()
{
……
m_hDeviceEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
DWORD count;
::DeviceIoControl(m_hDevice,CPUTESTDEVICE_IOCTL_SETUP_SIGNAL,
&m_hDeviceEvent,4,NULL,0,&count,NULL);
……
}
//守候中断的线程
UINT ThreadProc( LPVOID pParam )
{
HANDLE * pm_hEvent = (HANDLE *) pParam;
if (pm_hEvent == NULL )
return 1; // if pm_hEvent is not valid
WaitForSingleObject(*pm_hEvent, INFINITE);
……
return 0; // thread completed successfully
}
§ 在驱动程序中:
#define CPUTESTDEVICE_IOCTL_SETUP_SIGNAL 0x00160004
NTSTATUS ParPortDevice::DeviceControl(KIrp I)
{
……
switch (I.IoctlCode())
{
……
case CPUTESTDEVICE_IOCTL_SETUP_SIGNAL:
Kevent *m_pEventToSignal;
HANDLE hEvent;
hEvent = *(HANDLE*)I.IoctlBuffer(); //从输入缓冲区中得到事件句柄
//驱动程序在非分页内存中分配事件的指针空间
m_pEventToSignal = new(NonPagedPool) KEvent(hEvent);
status = m_pEventToSignal != NULL?STATUS_SUCCESS : STATUS_INSUFFICIENT_RESOURCES;
m_pEventToSignal->set(); //通过set成员函数来通知上层应用程序
break;
……
}
……
}
更多驱动信息请到: http://www.qudong360.com
WDM驱动程序开发工具通常用NuMega DriverStudio。因为它可以集成到VC 开发环境中,这样就可以像生成其他工程一样生成驱动程序框架,省去了大量的编写代码的时间。(后面要介绍的功能实现都是在用VC6.0集成环境生成的驱动程序框架中编写的)
编写一个设备驱动程序,除了要掌握驱动程序模型之外,还要了解相应设备的硬件知识。本文要介绍的是并行口EPP模式的WDM编程。主要介绍并行口EPP模式,并用实际的例子介绍并行口EPP模式驱动程序的一些功能实现:如何在驱动程序中实现并行口EPP的基本I/O;如何利用控制码实现上层应用程序对并行口的通讯;如何截获中断和挂接中断服务例程;如何通过在驱动程序中设置信号来激活上层应用程序定义的事件,以实现驱动程序对上层应用程序的通讯。
一、 并行口EPP模式介绍
高级并行接口EPP可以进行高速的双向数据传输。EPP可以区分两种类型的信息,他们通常分别
被定义为数据和地址。由于这种接口可以实现快速的方向转换,因此他特别适用于进行较小数据块的传输、需要经常交换数据传输方向的设备。
1. 并行口EPP模式的I/O地址及其特性定义:
1) 并行口基地址:
378 新系统通用,通常是LPT1,也可以是LPT2,通常使用中断IRQ7
278 通常是LPT2,也可以是LPT1,LPT3(只能用此基地址),通常使用中断IRQ5
(注1:下面都是针对378基地址LPT1介绍)
2) 并行口EPP模式的寄存器表:(表1)
寄存器 地址 读/写
数据寄存器(Data) [Base+0] 378 读/写
状态寄存器(Status) [Base+1] 379 读
控制寄存器(Control) [Base+2] 37A 写
地址寄存器(Address) [Base+3] 37B 读/写
扩展数据寄存器(Ext_Data) [Base+4] 37C 读/写
未定义 [Base+5] 37D
未定义 [Base+6] 37E
未定义 [Base+7] 37F
表1. 并行口EPP模式的寄存器
3) 状态寄存器(379)和控制寄存器(37A)的定义:(表2)
D7 D6 D5 D4 D3 D2 D1 D0
状态寄存器(379) S7 S6 S5 S4 S3 S2 S1 S0
Wait(Busy) Int(Ack) 用户定义(引脚12、13、15) Timeout超时
控制寄存器 (37A) C7 C6 C5 C4 C3 C2 C1 C0
双向控制1出0入 中断允许1允许 地址选通nAStrb0有效 init0有效 数据选通nDStrb0有效 write0有效
表2. 状态寄存器(379)和控制寄存器(37A)的定义
4) 并行口25芯引脚信号表:(表3)
Pin SPP Signal EPP Signal IN/OUT Status/Control Register
1 Strobe Write Out /C0
2-9 Data 0-7 Data 0-7 In-Out
10 Ack Interrupt In S6
11 Busy Wait In /S7
12 Paper Out / End Spare In S5
13 Select Spare In S4
14 Auto Linefeed Data Strobe Out /C3
15 Error / Fault Spare In S3
16 Initialize Reset Out /C2
17 Select Printer Address Strobe Out /C3
18-25 Ground Ground GND Ground
表3. 并行口25芯引脚信号表.
2. 并行口EPP模式寄存器的读写:
1) 先对控制寄存器(Control)初始化
如果禁止中断用out(37A,0x80),如果使用中断用out(37A,0x90)
2) 写一个寄存器的两条基本指令:
out(37B,addr); // 将addr写入用户设备地址寄存器
写:out(37C,data); // 将数据data写入addr指向的用户设备空间单元
读:in(37C); // 从addr指向的用户设备空间单元中读取数据
二、 并行口EPP模式驱动程序的功能实现
下面是并行口EPP模式驱动程序接口框图:(图1)
图1. 并行口EPP模式驱动程序接口框图
1. 并行口EPP的基本I/O
Ÿ 首先定义类KIoRange的一个实例,以对应EPP
KIoRange m_ParPortIos;
status = m_ParPortIos.Initialize(
0x378, // LPT1 Bus address
TRUE, //InCpuIoSpace
8, // Device size
TRUE // Map to system space
);
Ÿ 下面就可以用类KIoRange的成员函数来访问EPP的寄存器了
// EPP的寄存器相对于EPP基址的偏移量
#define CONTROL 2 //对应EPP的控制寄存器(37A)
#define ADDRESS 3 //对应EPP的地址寄存器(37B)
#define EDATA 4 //对应EPP的扩展数据寄存器(37C)
//设置控制寄存器
m_ParPortIos.outb(CONTROL,0x80);
// EPP读
m_ParPortIos.outb(ADDRESS,addr); //addr是实际编程中你要访问的设备单元的地址
UCHAR data = m_DeviceIos.inb(EDATA)
// EPP写
m_ParPortIos.outb(ADDRESS,addr); //addr是实际编程中要访问的设备单元的地址
m_DeviceIos.outb(EDATA,data); //data是实际要写入的数据
2. 利用控制码实现上层应用程序对并行口的通讯
在上层应用程序中,要对设备进行访问,首先到创建设备连接对象。通过调用函数CreateFile,如下,其中ParPortDevice0是设备的符号连接名:
char *sLinkName = ".//ParPortDevice0";
m_hParPort = CreateFile(sLinkName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
创建成功后就可使用该句柄来对设备进行读写访问,如:
ReadFile(m_hParPort,…);
WriteFile(m_hParPort,…);
另外还有一个重要的功能就是通过定义一些特殊的控制码来对设备进行特殊的交互,如
DeviceIoControl(m_hParPort,IOCTLCODE,….);
其中IOCTLCODE是在上层应用程序中根据要访问的设备和对设备的访问方式定义的控制码。
驱动程序在DeviceControl(KIrp I)中作相应的处理。
I/O控制码的定义原型如下:
#define CTL_CODE( DeviceType, Function, Method, Access ) ( /
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) /
)
DeviceType表示设备类型,不同的设备有不同的值,并行口的DeviceType 值为0x00000016
Access 表示允许对设备进行的访问:
#define FILE_ANY_ACCESS 0
#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS)
#define FILE_READ_ACCESS ( 0x0001 )
#define FILE_WRITE_ACCESS ( 0x0002 )
Method表示对设备访问的方式:
#define METHOD_BUFFERED 0
#define METHOD_IN_DIRECT 1
#define METHOD_OUT_DIRECT 2
#define METHOD_NEITHER 3
Function用来在DeviceType,Access,Method都相同的情况下,区分不同的控制码。
其中DeviceType,Function,Method,Access都是4字节16进制常量,关于这几个常量的定义请详见wdm.h或ntddk.h,“DeviceType<<16”表示将常量DeviceType左移16位。
具体通讯方式如下:
首先,在上层应用程序中定义I/O控制码:
#define CPUTESTDEVICE_IOCTL_MEM_READ 0x00160000
0x00160000的含义是允许对并行口进行带有缓冲的任何访问。
调用函数DeviceIoControl(m_hParPort, CPUTESTDEVICE_IOCTL_MEM_READ,…);
然后,在驱动程序中响应I/O控制码:
#define CPUTESTDEVICE_IOCTL_MEM_READ 0x00160000
在DeviceControl(Kirp I)例程中作如下处理:
NTSTATUS ParPortDevice::DeviceControl(KIrp I)
{
……
ParPortDevice *pDev = (ParPortDevice *) KDevicePTR(I.DeviceObject());
switch (I.IoctlCode())
{
……
case CPUTESTDEVICE_IOCTL_MEM_READ:
pDev->MemRead(I); //MemRead(KIrp I)为自定义的成员函数
break;
……
}
……
}
3. 截获中断和挂接中断服务例程
许多驱动程序都要处理硬件中断。当用户设备要通知上层应用程序读数据时,它可以发一个硬件中断,驱动程序截获此中断,再通过在驱动程序中设置信号来通知上层应用程序。但在处理硬件中断之前,一定要使硬件的中断允许有效,对于EPP,是使用下面这条语句实现的:
m_ParPortIos.outb(Control,0x90);
实现中断的截获和挂接方法如下:
a) 定义类KInterrupt的一个实例
KInterrupt m_TheInterrupt;
b) 在设备类中声明一个成员函数TheIsr作为中断服务例程ISR。
class ParPortDevice : public KPnpDevice
{
……
public:
MEMBER_ISR (HelloDevice, TheIsr);
……
#ifdef _COMMENT_ONLY
BOOLEAN TheIsr(void);
#endif
……
}
c) 在OnStartDevice例程中获取包括中断的设备资源并初始化中断和挂接ISR
ParPortDevice::OnStartDevice(KIrp I)
{
……
m_ParPortIos.outb(Control,0x90); //置中断允许位
PCM_RESOURCE_LIST pResList = I.TranslatedResources(); //获取设备资源
//初始化中断并挂接中断服务例程TheIsr
status = m_TheInterrupt.InitializeAndConnect(
pResList,
LinkTo(TheIsr),
this
);
……
}
4. 通过在驱动程序中设置信号来激活上层应用程序定义的事件
驱动程序有时需要主动通知上层应用程序来执行某一个操作,以实现硬件响应的实时性。
这可以通过在上层应用程序中创建一个事件,将该事件句柄放入输入缓冲区中通过函数DeviceIoControl传给驱动程序,并创建一个线程来守候该事件被激活。具体实现如下:
§ 在应用程序中:
#define CPUTESTDEVICE_IOCTL_SETUP_SIGNAL 0x00160004
声明一个类的成员:
public:
HANDLE m_hDeviceEvent;
BOOL CApptestDlg::OnInitDialog()
{
……
m_hDeviceEvent = ::CreateEvent(NULL,FALSE,FALSE,NULL);
DWORD count;
::DeviceIoControl(m_hDevice,CPUTESTDEVICE_IOCTL_SETUP_SIGNAL,
&m_hDeviceEvent,4,NULL,0,&count,NULL);
……
}
//守候中断的线程
UINT ThreadProc( LPVOID pParam )
{
HANDLE * pm_hEvent = (HANDLE *) pParam;
if (pm_hEvent == NULL )
return 1; // if pm_hEvent is not valid
WaitForSingleObject(*pm_hEvent, INFINITE);
……
return 0; // thread completed successfully
}
§ 在驱动程序中:
#define CPUTESTDEVICE_IOCTL_SETUP_SIGNAL 0x00160004
NTSTATUS ParPortDevice::DeviceControl(KIrp I)
{
……
switch (I.IoctlCode())
{
……
case CPUTESTDEVICE_IOCTL_SETUP_SIGNAL:
Kevent *m_pEventToSignal;
HANDLE hEvent;
hEvent = *(HANDLE*)I.IoctlBuffer(); //从输入缓冲区中得到事件句柄
//驱动程序在非分页内存中分配事件的指针空间
m_pEventToSignal = new(NonPagedPool) KEvent(hEvent);
status = m_pEventToSignal != NULL?STATUS_SUCCESS : STATUS_INSUFFICIENT_RESOURCES;
m_pEventToSignal->set(); //通过set成员函数来通知上层应用程序
break;
……
}
……
}
更多驱动信息请到: http://www.qudong360.com