简介:面向Windows XP下的PCI设备驱动开发场景,这份资料包完整涵盖了视窗驱动模型、输入输出请求包处理、中断服务例程、直接内存访问传输、即插即用设备枚举与资源分配等核心知识,适合具备C语言基础、正在尝试入门驱动开发的工程技术人员参考学习。压缩包内含一百个文件,主要包含驱动源程序与头文件、编译链接生成的中间文件、安装配置文件以及可执行示例等,整体约一点五三兆字节,结构紧凑,便于对照工程文件快速理解PCI驱动程序的代码组织方式。目前已有四百一十八人学习下载。资料价值在于可直接浏览驱动源码、头文件、工程配置与INF安装信息,配合描述中的知识点梳理,能帮助读者将设备标识匹配、中断处理、直接内存访问映射等抽象概念落实到具体实现上,缩短自行搭建驱动开发环境与调试排错的摸索过程。
1. 在Windows XP上开发PCI驱动:为什么这老东西还值得折腾
搞设备驱动开发的人,看到“Windows XP下的PCI设备驱动程序开发”这种标题多半会愣一下:这年头谁还碰这个?可你要是做过工控机维护、医疗设备维保或实验室老采集卡,就知道现场一堆设备还在跑Windows XP,而且插的板卡大多是PCI接口。系统可以老,板卡的驱动不能没有。手头这份rar装着的就是一套PCI驱动源码,但一套源码解决不了实际问题,你需要的是把“硬件设备”和“XP系统”之间的资源通道打通的方法。下面我来把这套方法拆开讲,从PCI总线的配置空间讲到写一个能加载的WDM驱动,再讲到蓝屏排查。适合想在XP上适配新PCI卡或者给老驱动做二次开发的人,读完能照着搭环境、写骨架、调试验证,少走弯路。
2. 写驱动前先摸透PCI硬件三件事:配置空间、BAR、中断
开发PCI驱动和写那些挂自家总线的驱动不一样。PCI设备不是插上就能直接拿地址操作的,系统要通过总线枚举,分配资源,然后把资源位置告诉驱动。你要是不理解配置空间、BAR和中断这几样东西,写出代码来也是“黑匣子”,跑不跑得通只能靠试。
2.1 PCI配置空间:驱动认识设备的唯一身份卡
每块PCI卡上都有一段256字节的配置空间,系统通过枚举总线时读这段空间来识别设备。前64字节的数据结构是标准固定的,后续的是设备相关的能力列表。你在驱动里做设备匹配、资源分配,全是从配置空间开始的。
常用字段列成一张表,开发时对着查:
| 偏移 | 字段 | 长度 | 作用 |
|---|---|---|---|
| 0x00 | Vendor ID | 2字节 | 厂商ID,比如8086是Intel |
| 0x02 | Device ID | 2字节 | 具体设备型号 |
| 0x04 | Command | 2字节 | 控制设备响应IO/内存访问 |
| 0x06 | Status | 2字节 | 设备状态 |
| 0x08 | Revision ID | 1字节 | 版本修订 |
| 0x09 | Class Code | 3字节 | 设备类别(网卡、显卡、串口等) |
| 0x10-0x27 | BAR0~BAR5 | 每组4字节 | 设备寄存器/内存的物理地址 |
| 0x3C | Interrupt Line | 1字节 | 系统分配的中断向量 |
| 0x3E | Interrupt Pin | 1字节 | 设备使用哪个中断引脚 |
在Windows XP下,你几乎不用直接去裸读PCI总线,DDK提供了HalGetBusDataByOffset和HalSetBusDataByOffset两个接口,它们能直接对指定总线上的设备配置空间做读写。下面这段是老驱动里最常见的封装方式:
// 读PCI配置空间的封装,XP下可用 UCHAR ReadPciConfig(UCHAR bus, UCHAR dev, UCHAR func, ULONG offset, PUCHAR buf, ULONG len) { // PCIConfigSpace: 表示访问PCI配置空间 // SlotNumber 高8位是设备号,低8位是功能号 ULONG slot = (dev << 8) | func; return HalGetBusDataByOffset( PCIConfigSpace, bus, slot, buf, offset, len); }这里要特别强调,HalGetBusDataByOffset的第二个参数是总线号,第三个参数是“槽位号”,槽位号低8位是功能号,高8位是设备号,不是直接把设备号传进去。我第一次写的时候也掉进这个坑,传参顺序反了,导致读出来的全是0xFFFFFFFF。调用时,如果读0x00开始的4个字节,返回的缓冲里前两个字节就是Vendor ID,后两个就是Device ID:
// 以配置空间0x00为例,读出厂商和设备ID UCHAR idbuf[4] = {0}; UCHAR status = ReadPciConfig(bus, dev, func, 0x00, idbuf, 4); USHORT vid = (USHORT)(idbuf[0] | (idbuf[1] << 8)); USHORT did = (USHORT)(idbuf[2] | (idbuf[3] << 8)); DbgPrint("PCI: vid=0x%04X did=0x%04X\n", vid, did);很多老驱动套件里,作者喜欢把这套封装成ReadPCIConfig之类的函数,你拿到rar里的源码时,先看它有没有正确设置SlotNumber参数。凡是读出来的VID为0xFFFF或者和硬件实际标称不符的,九成就是这里的问题。
配置空间里的Class Code也很重要。系统会根据这个字段决定给你加载什么样的系统驱动,如果你设备的Class Code是0x06FF(桥)但你想装自己的驱动,必须在INF里写清楚硬件ID,否则即使用devcon强制安装,也会出现“驱动已加载”但无法启动之类的奇怪状况。这一点后面避坑章节再细说。
2.2 BAR地址空间与映射策略
BAR(Base Address Register)是配置空间0x10到0x27里那6个32位寄存器,每个BAR告诉系统“设备希望把自己的寄存器或内存放在物理地址的哪个位置”。BAR不是硬编码的地址,操作系统在枚举时会向设备写入一个全1值,再从回读出来的值中分析地址范围,然后分配合适的窗口写回去。所以驱动里千万别写“我的设备在0xE0000000”这种硬编码,那是把自己往火坑里推。
BAR的类型由最低几位决定。对于一个BarValue,bit0为0表示Memory空间,为1表示IO空间;Memory空间再用bit1和bit2组合映射类型。判断代码很简单:
// 判断BAR类型 ULONG bar = ReadBarValue(deviceExt, barIndex); // 从配置空间0x10+4*index读 if (bar & 0x1) { // I/O空间, 基地址为 (bar & ~0x3) // 访问用READ_PORT_UCHAR/READ_PORT_ULONG } else { // Memory空间 switch (bar & 0x6) { case 0x0: // 32位映射 break; case 0x4: // 64位映射,高32位来自BARIndex+1 break; } }这段代码的参数逻辑是:BAR的bit0决定IO还是Memory,bit1~bit2决定是32位还是64位映射。如果你的设备手册说寄存器是IO映射的,你却用内存读写去访问,出来的数据全是垃圾。在XP下,没有现代版本里那种更灵活的映射选项,先分清类型再决定访问方式,能省两个小时的抓头时间。
写驱动时的策略是:在AddDevice阶段不要碰BAR,等到StartDevice(IRP_MN_START_DEVICE)拿到系统分配好的资源列表后,再用MmMapIoSpace把物理地址映射到虚拟地址。后面第4章有具体代码。这里先把这个原则立住:资源是系统给你的,不是你自己认地址。
2.3 中断机制:INTx与MSI在XP下的取舍
PCI设备传统上通过4条中断引脚(INTA#到INTD#)申请中断,这些引脚在系统中很可能是共享的。Windows XP里的中断服务例程必须非常快,一般在ISR里只检查设备是否有中断,然后排队一个DPC(延迟过程调用),让DPC去做真正的读写。老PCI网卡和声卡驱动几乎都遵循这个模式。
在XP下连接PCI设备中断时,IoConnectInterrupt的KINTERRUPT_MODE参数应该用LevelSensitive(电平触发),因为PCI的INTx引脚本身是低电平有效的共享中断线。有些示例代码误写成Latched,结果驱动在共享中断的系统上随机死机。这是PCI规范与Windows中断模式对应关系容易搞混的地方,我把它放到避坑章节再展开。
至于MSI(Message Signaled Interrupt),XP SP1之后系统开始在一定程度上支持,但设备支持情况和总线驱动配合经常出问题,很多PCI老设备压根没有MSI能力。实战中我倾向于在XP下直接只用INTx。虽然MSI能避免共享中断,但在XP这个系统版本上,它的实现并不比传统方式好多少,反而容易遇到“装了驱动后中断风暴”的情况。如果你的设备有MSI而你想试,INF里可以通过附加注册表键来控制,但建议先别折腾,把基础功能跑通再优化。
2.4 驱动模型选型:XP下还认WDM
Windows XP时代,主流的内核驱动模型是WDM(Windows Driver Model)。后来有了KMDF(Kernel-Mode Driver Framework),但KMDF在XP上部署需要安装对应版本的WdfCoinstaller,而且很多老工程、老参考资料讲的都是WDM写法。你手头那个rar里的源码大概率是WDM,由DriverEntry、AddDevice、MajorFunction回调这些基本元素组成。所以你重点应该掌握WDM,而不是一上来就去啃KMDF。
XP下的DDK 3790.1830生成的就是WDM驱动,结构简单,依赖少,适合用来做老PCI卡的适配。WDM的PNP回调机制让驱动在设备插入、移除、系统挂起时都能收到通知,这是XP下驱动的主要骨架。接下来的第三章,就把这个骨架从环境到代码完整搭出来。
3. 搭XP驱动开发环境:用DDK编WDM,靠INF让系统认识PCI卡
开发PCI驱动不是打开Visual Studio就能干的,你先要把XP下的编译环境跑起来。很多人拿着源代码不知道怎么进入主题,第一步是照着老办法把环境搭利索。
3.1 环境怎么搭:XP虚拟机、DDK 3790.1830与build命令
我一般建议在虚拟机里装Windows XP SP3作为开发机,装完系统之后,把DDK 3790.1830(对应XP的驱动开发包)装进去。这个版本网上有ISO,老光盘也有,反正在一台干净的32位XP里可以正常安装。安装时勾选“DDK for Windows XP”编译环境。注意,DDK的build工具和现代WDK不同,它靠的是环境变量和根目录下的sources文件。
装好后,别用IDE,直接打开“Windows XP DDK 3790.1830”下的“Free Build Environment”,它实际上是设置好环境变量的cmd窗口。后续所有编译都在这个窗口里敲build命令。你不用为了编译XP驱动让物理机还跑XP,虚拟机就够,但别在开机的同机调试里一边编译一边跑目标机,容易互相干扰。
然后创建源码目录,目录下要有sources和makefile。sources文件长这样:
TARGETNAME=my_pcidriver TARGETTYPE=DRIVER SOURCES=driver.c device.c别小看这个文件,写错了build直接失败。TARGETNAME生成.sys的名字,TARGETTYPE必须是DRIVER,SOURCES列出所有C文件。
还要有makefile文件,内容固定,引用DDK的构建规则:
!INCLUDE $(NTMAKEENV)\makefile.def没有这个文件,build命令会报找不到makefile。实际在DDK安装目录的src模板里都能找到现成的,拷过来把sources写好就行。
3.2 最小WDM骨架:DriverEntry、AddDevice与PNP回调
写WDM驱动,无论设备多复杂,骨架是固定的。下面这段就是驱动入口和主回调,你可以在任何PCI驱动源码里看到类似的结构:
// Minimal WDM driver skeleton for PCI device, XP DDK #include <ntddk.h> typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT pdo; PDEVICE_OBJECT fdo; ULONG barCount; PHYSICAL_ADDRESS barPhys[6]; ULONG barLen[6]; PVOID barVirtual[6]; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; VOID DriverUnload(PDRIVER_OBJECT pDriver) { // 卸载时释放全局资源,这里通常留空 } NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg) { pDriver->DriverUnload = DriverUnload; pDriver->MajorFunction[IRP_MJ_PNP] = DispatchPnp; pDriver->MajorFunction[IRP_MJ_POWER] = DispatchPass; pDriver->MajorFunction[IRP_MJ_CREATE] = DispatchPass; pDriver->MajorFunction[IRP_MJ_CLOSE] = DispatchPass; pDriver->DriverExtension->AddDevice = AddDevice; // 关键 return STATUS_SUCCESS; } NTSTATUS AddDevice(PDRIVER_OBJECT pDriver, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo = NULL; NTSTATUS status = IoCreateDevice(pDriver, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, &fdo); if (!NT_SUCCESS(status)) return status; DEVICE_EXTENSION* pExt = (DEVICE_EXTENSION*)fdo->DeviceExtension; pExt->pdo = pdo; pExt->fdo = fdo; pExt->barCount = 0; // 挂到设备栈上 IoAttachDeviceToDeviceStack(fdo, pdo); fdo->Flags |= DO_POWER_PAGABLE; fdo->Flags &= ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }逻辑说明:DriverEntry是内核入口,设置各种MajorFunction回调,然后通过DriverExtension->AddDevice注册即插即用设备创建函数。AddDevice在PNP管理器发现设备时调用,IoCreateDevice创建功能设备对象(FDO),并把设备上下文挂在DeviceExtension里。注意最后一定要清掉DO_DEVICE_INITIALIZING标记,否则系统认为设备对象初始化不完。
3.3 用INF文件让XP认出PCI卡
驱动编译出.sys之后,还差一个INF文件。INF是Windows的驱动安装脚本,它告诉系统这个驱动匹配哪些硬件ID。下面是PCI WDM驱动的INF骨架:
[Version] Signature="$WINDOWS NT$" Class=System Provider=%MfgName% DriverVer=01/01/2024,1.0.0.0 [Manufacturer] %MfgName%=Standard [Standard.NT.5.1] %DeviceDesc%=DeviceList, PCI\VEN_8086&DEV_7AA4&SUBSYS_7D481462&REV_11 [DeviceList.NT] CopyFiles=CopyFilesList [DestinationDirs] CopyFilesList=12 [DeviceList.NT.Services] AddService = %DeviceDesc%,0x00000002,DriverService [DriverService] ServiceType = 1 StartType = 3 ErrorControl = 1 ServiceBinary = %12%\my_pcidriver.sys [SourceDisksFiles] my_pcidriver.sys=1 [Strings] MfgName="ExampleDev" DeviceDesc="Example PCI Device"这里INF的关键是[Standard.NT.5.1]节里写的硬件ID。Windows XP装驱动时就是拿设备的硬件ID和这个去匹配。注意硬件ID可以不写SUBSYS和REV,但写上更精确,比如上面的完整PCI\VEN_8086&DEV_7AA4&SUBSYS_7D481462&REV_11。实战中,你要是手头设备显示这串ID,但你的INF里只写了VEN_8086&DEV_7AA4,系统也会认。区别在于,完整ID不会让驱动莫名接管其他同型号但不同子系统的板卡。
编译好sources文件后,在Free Build Environment窗口进入源码目录,执行:
build如果编译成功,在objfre_wxp_x86\i386目录下会生成my_pcidriver.sys。把.sys和.inf拷到XP虚拟机,右键INF选择安装;或者用DDK自带的devcon工具:
devcon update my_pcidriver.inf pci\ven_8086&dev_7aa4注意这里devcon后面的硬件ID一般不加SUBSYS。更新完成后可以在设备管理器里看到设备变成“Example PCI Device”,没有感叹号就算成功装上。
4. 读写PCI设备资源:从PNP到配置空间、BAR映射与中断
驱动装进系统,只是万里长征第一步。真正把事情干完的是在StartDevice里拿到资源,在RemoveDevice里释放干净。这一章把四个环节串起来。
4.1 在StartDevice里完成配置空间和资源解析
AddDevice只负责建对象,不要碰硬件。等到系统给你发IRP_MN_START_DEVICE时,说明设备已经上电且资源已经分配好了,此时可以读取配置空间和资源。PNP处理函数收到这个IRP后,你先调用IoGetDeviceProperty获取总线号、设备号、功能号,再据此读配置空间和BAR。
// 在StartDevice里获取PCI总线位置 ULONG bus, dev, func; ULONG len; IoGetDeviceProperty(pdo, DevicePropertyBusNumber, sizeof(ULONG), &bus, &len); IoGetDeviceProperty(pdo, DevicePropertyDeviceNumber, sizeof(ULONG), &dev, &len); IoGetDeviceProperty(pdo, DevicePropertyFunctionNumber, sizeof(ULONG), &func, &len);IoGetDeviceProperty分别取到三个值。XP下PCI设备号是5位,功能号是3位。取到后,你可以把上一个章节的ReadPciConfig拿来读设备ID,确认驱动适配的就是它。
然后资源列表来自当前IRP的Parameters.StartDevice.TranslatedResources。这个数组里每一项描述一个已被系统翻译成物理地址的资源窗口。遍历它:
// 正确用法:从当前IRP的TranslatedResources取物理内存窗口 PCM_RESOURCE_LIST res = irpStack->Parameters.StartDevice.TranslatedResources; ULONG resCount = res->Count; for (ULONG i = 0; i < resCount; ++i) { PCM_PARTIAL_RESOURCE_DESCRIPTOR pd = res->List[i].PartialResourceList.PartialDescriptors; ULONG count = res->List[i].PartialResourceList.Count; for (ULONG j = 0; j < count; ++j, ++pd) { if (pd->Type == CmResourceTypeMemory) { pExt->barPhys[pExt->barCount] = pd->u.Memory.Start; pExt->barLen[pExt->barCount] = pd->u.Memory.Length; pExt->barCount++; } } }这里有个容易错的地方:AllocatedResources和TranslatedResources不是一回事。AllocatedResources是设备逻辑地址,TranslatedResources是总线翻译后的物理地址。你访问BAR时要用TranslatedResources中的值,不要用AllocatedResources。这一步错会导致你能看到资源但映射后访问全错。
翻译后的资源里,如果设备有多个BAR,通常会出现多个CmResourceTypeMemory项,顺序不一定对应BAR顺序。稳妥做法是以配置空间里BAR索引为准,再用翻译资源里的起始地址比对确认。如果板卡比较简单,也可以假设资源顺序与BAR索引一致,但某些BIOS会重新排序,为了不翻车,我建议花五分钟写个循环把BAR索引和物理地址都打出来核对。
4.2 用MmMapIoSpace映射BAR并验证读写
拿到物理地址后,在StartDevice里做映射:
PHYSICAL_ADDRESS pa; pa.QuadPart = pExt->barPhys[barIndex].QuadPart; PVOID virtualAddr = MmMapIoSpace(pa, pExt->barLen[barIndex], MmNonCached); if (!virtualAddr) { // 映射失败,返回设备错误 } pExt->barVirtual[barIndex] = virtualAddr;参数说明:第一个参数是物理地址,第二个是要映射的字节数,第三个是缓存类型。对设备寄存器来说一般用MmNonCached,防止CPU缓存和设备寄存器内容不同步。XP没有更细的缓存选项,MmNonCached就是常规选择。
映射后访问分两种情况。如果是内存映射的设备寄存器,直接解引用指针读取;如果是IO映射,需要用READ_REGISTER_ULONG等函数。代码:
// 内存映射方式读一个32位寄存器 ULONG regValue = READ_REGISTER_ULONG((PULONG)(pExt->barVirtual[0]));注意READ_REGISTER_ULONG建议在XP下使用,它会阻止编译器把设备寄存器访问优化掉。如果设备寄存器是IO映射,就用READ_PORT_ULONG((PULONG)barVirtual),两者操作数类型相同,但底层指令完全不同。
4.3 中断连接与DPC分工
在StartDevice里调用IoConnectInterrupt连接中断,注册ISR。代码示意如下,省略了DDK完整签名中的部分可选参数:
// 连接中断(示意,完整签名请查DDK) NTSTATUS status = IoConnectInterrupt( &pExt->pIntObj, &DeviceIsr, pExt, NULL, pExt->irqVector, // 从资源列表取到的中断向量 pExt->irqLevel, // 中断请求级别(DIRQL) pExt->irqAffinity, // 亲和性 LevelSensitive); // PCI中断用LevelSensitiveISR里只做三件事:检查是不是自己设备的中断、清除设备挂起、排队DPC。
BOOLEAN DeviceIsr(PKINTERRUPT InterruptObject, PVOID Context) { PDEVICE_EXTENSION pExt = (PDEVICE_EXTENSION)Context; if (!DeviceHasPendingInterrupt(pExt)) { return FALSE; // 共享中断,不是本设备必须返回FALSE } // 清除设备中断挂起位,让设备停止请求 ClearDeviceInterrupt(pExt); // 排队DPC,把真实处理放到低优先级 IoRequestDpc(pExt->dpc, NULL, NULL); return TRUE; }这条逻辑的关键:如果ISR返回FALSE,系统会把中断转交给共享中断的下一家;如果ISR不积极清除设备中断挂起位,设备会立刻再次触发ISR,形成中断风暴。DPC里做数据读写和通知:
VOID DeviceDpc(PKDPC Dpc, PVOID ctx, PVOID arg1, PVOID arg2) { PDEVICE_EXTENSION pExt = (PDEVICE_EXTENSION)ctx; // 真正处理数据、唤醒用户线程等耗时操作 }写到这里,一个PCI WDM驱动的基本路径就通了:AddDevice建对象,StartDevice取资源和连接,IRP处理业务,RemoveDevice断开资源。代码量不算多,但每一步都容易出问题。下面这个章节,我把那些摔过的坑集中列出来。
5. PCI驱动开发避坑:5条摔出来的经验教训
把设备驱动程序开发过程中最容易翻车的地方,按“现象 -> 原因 -> 解决”写清楚。别看每条字数不多,每一条都可能让你调一整天。
5.1 设备管理器代码10:驱动已装但设备无法启动
现象:设备管理器里看到“Example PCI Device”,但前面是黄色感叹号,属性显示“设备无法启动 (代码 10)”,事件日志里有0xE0000235错误。
原因:最常见的是读取资源时使用了AllocatedResources而不是TranslatedResources,导致MmMapIoSpace把逻辑地址当成物理地址,映射到不存在的内存。也可能是AddDevice阶段过早访问BAR,设备还没进入D0状态。
解决:把资源读取全部改用IRP_MN_START_DEVICE里Parameters.StartDevice.TranslatedResources,并且保证在电源管理将设备送进D0之后再访问寄存器。把设备对象DO_POWER_PAGABLE置位,避免在唤醒时访问分页错误。
5.2 硬件ID匹配:PCI\VEN_8086&DEV_7AA4 到底该写多精
现象:INF里写了完整ID(包含SUBSYS和REV_11),但devcon update后设备还是认不了,驱动没有绑上。
原因:有些XP驱动安装工具在匹配硬件ID时会把带SUBSYS的ID要求精确匹配,如果设备的子系统ID和驱动包写的不一致,就放弃了。硬件ID其实是一个逐级匹配的名字空间:全ID是PCI\VEN_8086&DEV_7AA4&SUBSYS_7D481462&REV_11,但系统也支持只匹配PCI\VEN_8086&DEV_7AA4这一级。
解决:通用驱动在INF的[Standard.NT.5.1]节里只写前两段,例如%DeviceDesc%=DeviceList, PCI\VEN_8086&DEV_7AA4。要限定某子系统,再追加完整行,用两个条目分别匹配,让设备去命中最精确的行。那次我就是因为设备子系统的编号和INF里的写差一位,结果驱动一个都不认。
5.3 蓝屏IRQL_NOT_LESS_OR_EQUAL:访问BAR时的优先级失控
现象:驱动一初始化就蓝屏,蓝屏代码0x0000000A,栈上指向的是READ_REGISTER_ULONG。
原因:大多数不是地址被换,而是你还没等到资源分配完成就去读BAR空间。MmMapIoSpace返回的虚拟地址是非分页内存,但它要求你当前运行在正确的IRQL下。很多时候驱动在某个AddDevice回调里直接试读BAR,设备资源根本没有映射成功,读一个未映射的地址指针导致蓝屏。
解决:确保BAR访问只能在StartDevice里完成资源映射之后。不要在任何时候试图通过IoGetDeviceProperty的返回值直接构造地址。另外,映射时必须用MmNonCached,如果用MmCached,寄存器值会被CPU缓存住,读出来永远是第一次的值。
5.4 中断共享时的三道坎:返回FALSE、清中断、DPC
现象:驱动装好后,一打开设备系统就慢得像冻住,并伴随随机死机;或者完全没有中断响应,设备累死累活只出几十个数据包。
原因:PCI中断是共享的。ISR拿到中断却返回TRUE,等于抢了别人的中断而不干活,其他设备没人处理;或者设备中断挂起位没清,导致中断源不断触发,系统在ISR里死循环。
解决:ISR第一步查自己设备的pending位,不是自己的立即返回FALSE。清中断要在排队DPC之前完成。DPC里再对设备寄存器做耗时的操作。这样共享中断时,系统和其他设备都不受影响,这是XP下PCI驱动的标准姿势。
5.5 64位BAR在32位XP下的撕裂
现象:设备有64位BAR,被BAR0和BAR1组合描述。32位XP下驱动映射后,读写高地址低地址错乱,或者只有低32位窗口可用。
原因:32位系统使用MmMapIoSpace时传入的PHYSICAL_ADDRESS是一个64位联合体,但如果你只传了低32位,或者把BAR1当成一个新的资源窗口,就撕裂了。
解决:对于64位BAR,在配置空间读BARn和BARn+1,将高32位放到PHYSICAL_ADDRESS.HighPart,低32位放到LowPart。在TranslatedResources中,系统会把64位物理地址作为一个资源项或拆成两个项,你要检查资源长度和起始地址,直接使用物理地址的QuadPart,不要自己拼接。另外,32位内核地址窗口有限,映射稍微大一点就可能失败,需要设备提供可内存窗口让系统调度。
6. 用WinDbg双机调试,把PCI驱动的皮扒开验证
写驱动不能靠printf盲调,XP下有WinDbg足够。这一章给你三个实用技巧,用来验证驱动有没有真正落在底层的PCI协议上。
6.1 双机调试与PCI配置空间验证
在目标机(XP虚拟机)的boot.ini里加/debug /debugport=...,主机用WinDbg连接。连接后,要验证驱动的资源匹配是否正常,可以执行:
kd> !pci 0 0 0这条命令会显示PCI总线上指定槽位的配置空间摘要。你会发现,用扩展命令读出的配置空间,和你驱动里ReadPciConfig读出的内容一致。如果不一样,说明驱动访问的槽位号写错了。
6.2 查看中断连接和ISR注册是否有效
验证ISR有没有被调用,最直接的办法是在DeviceIsr入口下断点:
kd> bp my_pcidriver!DeviceIsr然后去设备上制造一次中断(比如触发一次DMA读取)。如果断点命中,说明中断管线没问题。如果没命中,检查连接中断时使用的irqVector和irqLevel是否来自资源列表。我见过有人直接写死中断号,在XP这种老系统上验一下马上暴露。
6.3 性能验证:中断延迟和DPC队列
在ISR里用KeQueryPerformanceCounter记录时间,在DPC里再记一次,差值就是中断到DPC的延迟。老PCI设备一般容忍几十微秒,如果发现DPC迟迟排不上,检查IoRequestDpc是否正确初始化了DPC对象(KeInitializeDpc),以及ISR的IRQL是否被错误提高了。
我每次写完PCI驱动的BAR映射和中断部分,都会用WinDbg跑一遍完整验证流程,特别是检查资源描述符,看看有没有把IO空间误判成Memory空间来映射。坚持这个习惯后,我被设备管理器“代码10”找上门的次数明显少了。希望这套方法和踩坑记录能帮到正在拿着PCI驱动代码发呆的你。
本文还有配套的精品资源,点击获取