十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Windows XP PCI驱动开发全解析:配置空间、中断与DMA实现

Windows XP PCI驱动开发全解析:配置空间、中断与DMA实现 简介面向Windows XP平台PCI设备驱动开发的完整资源包聚焦内核模式驱动设计与WDM驱动模型适合驱动开发初学者、系统程序员以及需要了解硬件驱动原理的嵌入式开发者。内容围绕PCI设备接口、设备枚举与配置、IRP请求处理、中断服务例程、DMA传输等关键环节展开通过实际工程代码演示如何匹配设备ID与供应商ID、注册调度例程并调用IoCallDriver传递请求。资源共100个文件压缩包约1.53MB包含18个头文件、15个C源程序、3个INF安装配置文件、34个bin二进制数据以及Visual C工程文件dsp/dsw、编译中间文件和可执行程序源码与工程结构完整便于直接编译、调试和对照学习。已有418人学习使用。借助该资源可系统梳理驱动开发中的PnP事件处理、电源管理和调试方法同时理解INF文件编写与驱动签名流程为后续开发其他硬件驱动打下扎实基础。1. 为什么在 Windows XP 上开发 PCI 驱动依然是个硬门槛在 2020 年之后依然坚持维护 Windows XP 的场合多半是工控机、医疗设备、税控终端或者某条产线的专用测试工装。这些机器上的板卡往往是十年前出厂的 PCI 接口而系统的安装盘里只带了主板芯片组和网卡驱动给板卡重装驱动的任务最终会落到开发或 IT 运维手里。PCI 设备驱动程序开发不是简单的“写一个 .inf 指向 .sys”它要求你直接面对总线协议、配置空间、中断和内存映射整个逻辑放在内核态一个越界就能让系统直接蓝屏。正因如此即便 XP 已经停止补丁支持其驱动模型仍然值得学习因为 WDM 在 PCI 资源处理上非常直白反而是快速理解现代 Windows 驱动的地基。这篇文章面向的是有 C 语言基础、被分配了“让这块 PCI 卡跑起来”任务的技术人员。2. 从配置空间到实现PCI 驱动开发的理论基础2.1 先读懂 PCI 配置空间里的关键偏移PCI 总线有个和内存、I/O 空间独立的三维空间每个设备在 0~255 编号的访问周期中暴露 256 字节的配置数据。操作系统启动时已经枚举完总线驱动拿到的是已分配好的资源但你必须自己知道这些资源放在哪。最需要理解的是下面这张表偏移长度字段作用0x002Vendor ID厂商编号如 1234 表示测试卡0x022Device ID设备型号配合厂商 ID 形成唯一硬件 ID0x082Class Code设备类别用于总线驱动自动分类0x10166 个 BAR每个 4 字节声明设备需要的内存/IO 地址范围0x3C1Interrupt LineBIOS 或总线驱动写入的中断向量0x3D1Interrupt Pin物理引脚 INTA#/INTB#/INTC#/INTD#0x341Capabilities Pointer指向能力链表用来识别 PCI-X、PM、MSI 等扩展BAR 展开后最小 16 字节每个 BAR 的低位表示区域类型bit01 是 I/O 空间bit1~2 决定 32 位还是 64 位内存映射。系统通过向 BAR 写入全 1 再读回就能算出这个区域需要多少地址空间。如果你的硬件手册里写“BAR0 占用 4KB 内存”对应的就是这块卡的本地寄存器窗口。驱动开发的第一步并不是写代码而是确定你需要的资源配置方式。先看板卡 ID再判断它是简单寄存器卡还是带 DMA 引擎的采集卡最后才决定驱动里用READ_PORT_ULONG还是直接映射到系统虚拟地址。2.2 用 IRP_MN_READ_CONFIG 读取配置空间的惯用写法配置空间本来由 pci.sys 总线驱动管理功能和设备驱动要读配置空间不能直接访问总线端口必须向 PnP 管理器发送一个IRP_MN_READ_CONFIG请求。这是一个非常常见的操作很多入门开发者不知道如何取得这个 IRP下面是最容易理解的封装NTSTATUS PciReadConfig( PDEVICE_OBJECT DeviceObject, ULONG Offset, // 要读取的配置空间偏移例如 0x00 ULONG Length, // 读取长度一般为 2 或 4 PVOID Buffer // 输出缓冲区 ) { KEVENT event; PIRP irp; IO_STACK_LOCATION *stack; NTSTATUS status; KeInitializeEvent(event, NotificationEvent, FALSE); // IoBuildSynchronousFsdRequest 构造一个 PnP 请求 irp IoBuildSynchronousFsdRequest( IRP_MJ_PNP, DeviceObject, Buffer, Length, NULL, event, status); if (!irp) return STATUS_INSUFFICIENT_RESOURCES; // 设置子功能号为 READ_CONFIG stack IoGetNextIrpStackLocation(irp); stack-MajorFunction IRP_MJ_PNP; stack-MinorFunction IRP_MN_READ_CONFIG; stack-Parameters.ReadWriteConfig.WhichSpace PCI_WHICHSPACE_CONFIG; stack-Parameters.ReadWriteConfig.Offset Offset; status IoCallDriver(DeviceObject, irp); if (status STATUS_PENDING) { KeWaitForSingleObject(event, Executive, KernelMode, FALSE, NULL); status irp-IoStatus.Status; } return status; }代码逻辑分三步先把请求包装成同步 IRP然后在 IO 堆栈位置上填入IRP_MN_READ_CONFIG和WhichSpace PCI_WHICHSPACE_CONFIG最后调用IoCallDriver把请求发给总线驱动。Offset就是配置空间的字节偏移比如读 Vendor ID 时填 0Length受总线驱动实现限制一次读 4 字节最安全。注意irp-IoStatus.Status才是真正结果IoCallDriver返回 STATUS_PENDING 时可能还没完成所以必须等待事件。如果你使用的是 KMDF 而不是 WDM则不需要手写这段 IRP。调用WdfDeviceGetEjectionRelations那套 API 不行正确路径是通过WdfFdoInitWdmGetPhysicalDevice拿到 WDM 设备指针再走上述代码。KMDF 框架没有直接封装配置空间读取所以这个函数在 XP 驱动里是常备工具。2.3 INF 文件里的硬件 ID 匹配规则配置空间里的 Vendor ID 和 Device ID 最终要落到 INF 文件的PCI\VEN_1234DEV_5678字符串上。INF 文件不负责写驱动逻辑它只告诉即插即用管理器当系统枚举到某个 PCI 设备时应该加载哪个.sys文件。常见的错误是把系统自带的 usb.inf 里的结构照抄过来忘了Class必须写System或Unknown导致安装界面出现黄色感叹号。一个最低可用的 INF 片段是这样的[Version] Signature$WINDOWS NT$ ClassSystem ClassGuid{4d36e97d-e325-11ce-bfc1-08002be10318} ProviderMyCompany [Manufacturer] MyCompanyDevList,NT.5.1 [DevList.NT.5.1] MyDeviceDescInstallDevice, PCI\VEN_1234DEV_5678 [InstallDevice.NT] CopyFilesDriverFiles AddRegDriverReg [SourceDisksFiles] mypci.sys1 [DriverFiles] mypci.sys [DriverReg] HKLM, SYSTEM\CurrentControlSet\Services\mypci, Type, 0x00010001, 1 HKLM, SYSTEM\CurrentControlSet\Services\mypci, Start, 0x00010001, 3 HKLM, SYSTEM\CurrentControlSet\Services\mypci, ErrorControl, 0x00010001, 1NT.5.1里的5.1是 Windows XP 的版本号如果写NT则对所有系统生效但 XP 安装程序要求显式标注版本才能正确识别。AddReg部分把驱动服务注册成Type1内核驱动、Start3手动/即插即用加载。如果设备没有出现在设备管理器中先用!pci命令枚举看硬件 ID 是否与 INF 完全一致包括大小写和斜杠。3. 搭建 XP 驱动编译环境并跑通第一个 PCI 驱动3.1 编译环境WDK 7600 与 build 命令的配合Windows XP 的驱动开发环境一般用 Windows Driver Kit 7.1.0即 WDK 7600它是最后一个完整支持 XP 的官方工具包。安装时不需要额外安装 Visual Studio因为内核驱动用命令行工具build.exe编译而不是 MSBuild。你可以在一台 Windows XP 虚拟机或物理机上安装也可以用 Win2000 兼容模式运行。整个环境的目录结构通常如下C:\WinDDK\7600.16385.1\bin\setenv.bat C:\WinDDK\7600.16385.1\lib\win7\i386 C:\WinDDK\7600.16385.1\inc\ddk打开 SDK 命令提示符后执行setenv.bat XP然后进入源码目录运行build -cZ。其中-c表示仅编译-Z表示不增量编译、强制全部重建。得到的mypci.sys在objfre_wxp_x86\i386目录下。这里有个容易踩的坑XP 的驱动必须使用 WDK 自带的libcrt库不要链接 VC 运行库否则启动时会报INIT_TABLE错误。操作上只需要在sources文件里设置USE_LIBCRT1并包含NTDDK.h。sources文件是 DDK 时代的关键配置文件一个典型内容如下TARGETNAMEmypci TARGETPATHobj TARGETTYPEDRIVER SOURCESmypci.c mypci.rc INCLUDES..TARGETTYPEDRIVER告诉构建系统生成内核驱动而不是 EXE 或 DLL。如果你使用 KMDF需要额外指定MSC_WARNING_LEVEL/W4和KMDF_VERSION_MAJOR0x0109但 WDM 不需要。编译完成后用Inf2Cat给 INF 生成目录文件不过 XP 驱动通常不需要数字签名除非机器开启了强制签名。3.2 可以运行起来的最小 WDM 驱动骨架下面的代码是一个能加载但完全不处理硬件请求的 PCI WDM 驱动。它展示了DriverEntry、AddDevice和DriverUnload三个必须函数足以让设备管理器识别并启动它#include ntddk.h NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject); VOID DriverUnload(PDRIVER_OBJECT DriverObject); NTSTATUS DefaultPassThrough(PDEVICE_OBJECT DeviceObject, PIRP Irp); NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject-DriverUnload DriverUnload; DriverObject-DriverExtension-AddDevice AddDevice; // 所有 IRP 都走同一处理函数 DriverObject-MajorFunction[IRP_MJ_CREATE] DefaultPassThrough; DriverObject-MajorFunction[IRP_MJ_CLOSE] DefaultPassThrough; DriverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] DefaultPassThrough; DriverObject-MajorFunction[IRP_MJ_PNP] DefaultPassThrough; DriverObject-MajorFunction[IRP_MJ_POWER] DefaultPassThrough; return STATUS_SUCCESS; } NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject) { PDEVICE_OBJECT fdo; NTSTATUS status IoCreateDevice( DriverObject, 0, NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, fdo ); if (!NT_SUCCESS(status)) return status; // 将功能设备对象绑定到总线设备 fdo-Flags | DO_BUFFERED_IO; fdo-Flags | DO_POWER_PAGABLE; IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject); IoSetDeviceInterfaceState(fdo-DeviceName, TRUE); return STATUS_SUCCESS; } NTSTATUS DefaultPassThrough(PDEVICE_OBJECT DeviceObject, PIRP Irp) { IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(((PDEVICE_OBJECT)DeviceObject-DeviceExtension), Irp); } VOID DriverUnload(PDRIVER_OBJECT DriverObject) { // 卸载时不需要特殊清理 }IoCreateDevice的FALSE参数表示不创建符号链接PCI 驱动通常不暴露应用层接口而是靠内核模式访问所以这里是合理的。AddDevice里IoAttachDeviceToDeviceStack将功能设备对象挂到设备栈上返回值是底层设备对象保存到DeviceExtension后后续的 IRP 转发都需要它。DefaultPassThrough把所有 IRP 原样传给下一层因此还可以直接做“空驱动”测试看 PCI 设备能否正常分配资源。3.3 安装、日志与常见失败点安装驱动时先用管理员账户打开设备管理器右键更新驱动手动选择你的 INF 文件。最省事的做法是写个.cmd脚本调用rundll32 syssetup.dll,SetupInfObjectInstallAction DefaultInstall 128 安装 mypci.inf但 XP 下经常因为签名问题弹出警告直接点“仍然安装”。安装后出现以下现象时按对应方向排错现象原因处理方式设备管理器显示黄叹号代码 10驱动未成功启动查看Setupapi.log和Ntbtlog.txt中的错误码代码 39sys 文件损坏或路径不对检查sources的 TARGETNAME 与 INF CopyFiles 名称一致加载后立即重启DriverEntry 或 AddDevice 崩溃用/basevideo安全模式替换驱动并抓 MinidumpINF 安装提示“设备类型不能设置”ClassGuid 或 Class 段写错改用System类和{4d36e97d-e325-11ce-bfc1-08002be10318}的对应 GUID其中一个特别隐蔽的问题是IoCreateDevice时设置了FILE_DEVICE_UNKNOWN但 INF 里没有包含%DeviceDesc% CopyFilesSection, PCI\VEN...的复制节导致即使 sys 文件存在当系统从 INF 安装时也无法复制到system32\drivers。解决办法是在 INF 里单独写一个[CopyFiles]节用1,,mypci.sys指定硬编码名称。4. 处理 PCI 设备的中断和 DMA从理论到实战参数4.1 先决定用 I/O 端口还是内存映射寄存器PCI 设备的寄存器访问通常有两种方式配置空间的 BAR 会指定使用哪种。I/O 端口访问要用READ_PORT_UCHAR、WRITE_PORT_ULONG等函数内存映射则用MmMapIoSpace将物理地址转成非分页内存的虚拟地址再直接解引用指针。I/O 端口的代码简单但每次读取都有总线协议开销内存映射方式更快且更适合大量数据传输。在 XP 下内存映射时要注意MmMapIoSpace的NumberOfBytes必须按页对齐不析构会导致地址对不上。一个稳定读取 BAR0 内存映射地址的初始化代码如下PHYSICAL_ADDRESS bar0Phys; PVOID bar0Mapped; ULONG bar0Len; // 读配置空间偏移 0x10获取 BAR0 的物理地址 PciReadConfig(fdo, 0x10, 4, bar0Phys); // 低 4 位清掉得到真正地址 bar0Phys.QuadPart ~0xF; // 根据硬件手册假设 BAR0 长度为 4KB bar0Len 0x1000; bar0Mapped MmMapIoSpace(bar0Phys, bar0Len, MmNonCached); if (bar0Mapped NULL) { // 映射失败通常是 BAR 地址被系统判定为非法 return STATUS_DEVICE_CONFIGURATION_ERROR; }MmNonCached是缓存模式对大多数 PCI 设备是安全选择如果你确定设备带 DMA 一致性可以用MmCached但会引入内存序问题。映射后bar0Mapped就可以当普通指针用但必须保证所有访问在中断级DISPATCH_LEVEL以下发生否则要改用READ_REGISTER_ULONG系列函数。4.2 IoConnectInterrupt 的中断参数怎么填中断处理是 PCI 驱动最容易蓝屏的地方。连接中断使用IoConnectInterrupt需要从配置空间读取中断向量和亲和性。下面的例子连接 PCI INTA# 中断并配置一个 DPC 执行底半部PKINTERRUPT kInterrupt; ULONG irql; KAFFINITY affinity; // 中断向量和 IRQL 由总线分配驱动从配置空间读取 PciReadConfig(fdo, 0x3C, 1, irql); // Interrupt Line PciReadConfig(fdo, 0x3D, 1, pin); // Interrupt Pin KeQueryActiveProcessors(affinity); // 用当前所有处理器作为亲和性 IoConnectInterrupt( kInterrupt, myIsr, pContext, NULL, tgtDevice, irql, irql, LevelSensitive, TRUE, affinity, TRUE );参数里irql是设备中断的同步级别通常是 PCI 设备对应的 DIRQL 数值不能随便填。LevelSensitive表示电平触发PCI 规范要求电平触发所以必须用LevelSensitive而不是Latched。最后一个TRUE表示共享中断PCI 设备可能共享同一根 IRQ 线不声明共享会导致中断无法工作。ISR 本身要快速判断是否来自本设备读状态寄存器如果不是就返回FALSE让共享链上的下一个 ISR 处理。在 ISR 里不能访问分页内存不能调用长时间阻塞的函数。需要延后处理的工作丢给 DPC通过KeSetDpc和KeInsertQueueDpc调度。4.3 DMA 适配器与 MAP_REGISTER 参数带 DMA 的 PCI 驱动必须获取适配器对象IoGetDmaAdapter的Description参数里最容易出错的是MaximumLength和MapRegisterBase。下面是常见配置DEVICE_DESCRIPTION desc; PADAPTER_OBJECT adapter; ULONG mapRegs; RtlZeroMemory(desc, sizeof(desc)); desc.Version DEVICE_DESCRIPTION_VERSION; desc.Master TRUE; desc.ScatterGather TRUE; desc.Dma32BitAddresses TRUE; desc.MaximumLength 0x10000; adapter IoGetDmaAdapter(NULL, desc, mapRegs); if (adapter NULL) return STATUS_INSUFFICIENT_RESOURCES;MapRegisterBase的mapRegs返回系统能提供的映射寄存器数量如果硬件需要 32 个Scatter/Gather表项而mapRegs小于需求DMA 传输就会失败。常见板卡手册要求MaximumLength是 64KB 的倍数这是不对的它只是单次 DMA 的最大字节数不是必须对齐只是总线驱动会按页取整。分配 DMA 缓冲区用AllocateCommonBuffer这个函数要求内存连续且非分页只要调用时机在PASSIVE_LEVEL即可。DMA 完成后的中断处理里必须调用FlushAdapterBuffers和FreeMapRegisters释放映射寄存器。如果忘记了第二次传输时会卡在等待分配寄存器上。比较好的调试手法是在释放寄存器前打印adapterObj-DmaOperations的返回值或者用KeSetEvent做信号量同步。5. 在 XP 上验证驱动稳定性的几个具体技巧XP 没有自带 WDF 验证程序但可以通过注册表启用传统的Driver Verifier。在命令行里执行verifier /standard /driver mypci.sys然后重启。/standard会开启Special Pool,IRQL checks,Low Resources等七项最基本检查对 PCI 驱动最有用的是Special Pool它把驱动程序分配的内存块前后页都标记为无效任何越界写都会立即触发蓝屏而不是过几小时才出现数据错误。开发期间建议同时在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\mypci\Parameters建一个DebugLevel的 DWORD驱动里用KdPrint按位输出调试信息。双机调试是排查中断死锁最直接的办法。用一根串口线连接目标机和主机在 boot.ini 里给 XP 内核添加/debug /debugportCOM1 /baudrate57600主机端用 WinDbg 连接。加载驱动后在 WinDbg 里执行!devobj FDO地址能查看设备栈!irp 地址能列出挂起的 IRP。如果驱动卡在中断等待最常见的是!ioapic或!pci显示的 IRQ 数值和IoConnectInterrupt传入的irql不一致。用!pci 0 0 0可以显示 00:00.0 设备的完整配置空间和硬件手册比对 BAR 与中断引脚。未使用 DMA 时建议在主线上配合性能监视器看PCI Bus计数器的Read Operations/sec如果你只测中断不传数据这个值会接近 0说明设备寄存器读写有问题。另一个小技巧是在AddDevice里用IoInvalidateDeviceRelations强制重新枚举设备这能验证驱动是否正确处理了IRP_MN_START_DEVICE因为很多入门驱动在第一次启动时资源分配正常重新枚举后资源变化立刻导致STATUS_CONFLICTING_ADDRESSES。最后XP 下 PID 泄漏问题很难用工具发现最简单的方法是让驱动在卸载时使用MmUnmapIoSpace和IoDisconnectInterrupt并确保所有 IRP 在PASSIVE_LEVEL下同步释放。你可以在 DriverUnload 中断设置一个 2 秒钟的计时器用KeQueryTickCount检查是否有未完成 I/O若计数非零则输出警告。本文还有配套的精品资源点击获取
返回列表