多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

WDM驱动实战:PCIe设备BAR映射、中断与DMA开发指南

WDM驱动实战:PCIe设备BAR映射、中断与DMA开发指南 简介面向Windows平台从事底层硬件驱动开发的工程师和学员这份基于WDM模型的PCI与PCIe驱动开发资料包以完整工程示例演示了从驱动框架搭建到设备交互的完整过程。压缩包内共有二十个文件除了C源码与头文件还包含Visual Studio工程配置、旧式工程文件以及INF安装脚本等整体体积约一百一十KB结构紧凑适合快速定位学习。资料内容紧扣WDM驱动开发的一系列核心机制即插即用下设备动态识别与资源分配、电源管理中的挂起和恢复、IRP请求包的派遣与处理、设备对象与驱动对象的创建和挂接、PCI配置空间的读取解析以及PDO与FDO在设备栈中的协作关系。MyDriver模块是驱动主体Test模块对应测试程序通过源码可以观察设备枚举、中断注册、I/O操作的具体写法也能理解硬件资源如何上报给操作系统。目前已有三百三十一人学习下载。对于希望进入驱动开发领域、深入理解PCI设备工作原理的开发者这份资料提供了可编译的参考工程和清晰的目录结构能帮助将WDM理论落到实际代码并为后续扩展网卡、声卡等PCIe设备驱动打下基础。1. WDM 和 PCI 绑定在一起之后为什么今天还要在 Windows 下写 WDM 驱动做采集卡、运动控制卡、仪器仪表或者工控视觉的团队迟早会碰到一块只给寄存器手册、不给 Windows 驱动的 PCIe 板卡。这时候最稳的做法不是去找现成的驱动装上而是自己用 WDK 写一个 WDM 驱动把硬件资源直接接管过来。WDMWindows Driver Model是 Windows 下最底层的驱动模型之一它不依赖微软高层框架替你兜底BAR 映射、中断注册、DMA 缓冲区全部由你说了算。相比 KMDF 或 UMDFWDM 的代码量更大、出错面更宽但它能覆盖从老 XP 到 Win11 的完整兼容区间也方便你精确控制硬件资源。这篇文章不聊虚的就沿着“WDM 驱动如何注册 PCI 设备、如何映射 BAR、如何做中断与 DMA、出问题怎么定位”这条线给出一套可以照着落地的东西。适合正在写 PCI 驱动、或者准备评估 PC 平台上自研驱动方案的工程师。2. WDM 框架下的 PCI 设备PNP 注册、IRP 分发与 BAR 空间映射2.1 PCIe 设备是怎么把自己“报”给操作系统的PCI 设备接入系统后由 PCI 枚举器pci.sys完成配置空间的扫描与资源分配。每个 PCIe 设备都有 256 字节扩展配置空间是 4KB的配置空间其中包含了 Vendor ID、Device ID、Class Code、中断引脚、BAR 地址等信息。系统启动时PCI 总线驱动读取这些信息结合 ACPI 表做资源仲裁然后把设备的资源需求上报给 PnP 管理器。PnP 管理器收到新设备通知后会匹配 INF 文件里的硬件 ID 与你驱动中的 PNP 回调最终调用你的 DriverEntry 和 AddDevice。这一步的常见误区是很多人把“驱动加载”理解成“驱动一装就拥有全部资源”。实际上 WDM 驱动此时拿到的只是 PnP 管理器分发下来的资源列表Translation 后的物理地址、中断向量而不是你直接去读配置空间的结果。BAR 空间要在你收到 PnP 的 StartDevice 请求后调用 MmMapIoSpace 把物理地址映射到系统虚拟地址空间然后才可以在驱动代码里直接读写寄存器。// 伪结构示意设备扩展中保存资源映射后的基地址 typedef struct _DEVICE_EXTENSION { PDEVICE_OBJECT DeviceObject; PHYSICAL_ADDRESS BarPhysicalAddr; PUCHAR BarVirtualAddr; ULONG BarLength; } DEVICE_EXTENSION, *PDEVICE_EXTENSION;这段结构除了保存设备对象指针主要是为后续 StartDevice 里 MmMapIoSpace 的结果做准备。你在写识别函数时不要一上来就做寄存器验证而是先确认资源配置到第几号 BAR。参数说明BarPhysicalAddr 是 CmResourceList 里被翻译过的物理地址BarVirtualAddr 才是你后续代码中访问寄存器用的基址BarLength 建议取系统分配给你的资源长度不要去猜固定值有些卡在 BIOS 里分配的窗口长度和芯片手册并不一致。2.2 WDM 的分发原则IRP 和 IOCTL 各管什么WDM 驱动里最核心的就是 IRP 分发函数。你需要在 DriverEntry 里为 IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_DEVICE_CONTROL、IRP_MJ_PNP、IRP_MJ_POWER 分别设置处理函数。Create 和 Close 管的是用户态打开的句柄DeviceControl 管的是用户态调用 DeviceIoControl 下发的命令PNP 管的是设备的启动、停止、移除Power 管的是系统睡眠、休眠时的电源状态。你可能会问为什么不能全部塞到一个回调里处理因为 WDM 的 IRP 分发是有次序的。比如系统休眠时电源 IRP 会先于设备移除 IRP 到达驱动在 S1/S2/S3 状态的响应方式直接决定了设备是否会出现“睡死”问题。NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { DriverObject-MajorFunction[IRP_MJ_CREATE] DispatchCreate; DriverObject-MajorFunction[IRP_MJ_CLOSE] DispatchClose; DriverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] DispatchIoctl; DriverObject-MajorFunction[IRP_MJ_PNP] DispatchPnp; DriverObject-DriverUnload DriverUnload; return STATUS_SUCCESS; }逻辑说明这五行的任务是绑定分发入口除此之外 DriverEntry 里不要做任何硬件访问。很多 Windows 驱动开发新手习惯在 DriverEntry 里直接 MmMapIoSpace这在 XP 时代偶尔能跑到了 Win10 以上大概率导致蓝屏或设备无法启动。原因是 DriverEntry 执行时设备资源还没有被 PnP 管理器派发下来。参数说明每个 MajorFunction 函数接收的 IRP 都带一个 IO_STACK_LOCATION你需要通过 IoGetCurrentIrpStackLocation 拿到当前栈位置再根据 Parameters.DeviceIoControl.IoControlCode 判断具体命令。一个驱动处理几十个 IOCTL 时推荐用 switch 分支而不是 if-else 铺开。2.3 到底选 WDM 还是 KMDF一张选型决定表这几年一提到 Windows 驱动开发微软的默认推荐是 KMDF。KMDF 把 PNP、电源、队列都封装成了框架对象代码量确实少很多。但你做 PCI 设备自制驱动时KMDF 的封装未必是好事。KMDF 对非标准 BAR 空间、手动映射的寄存器访问、以及直接操作 MSI 中断的灵活性不如 WDM。下面这张表我每次选型都会先过一遍对比项WDMKMDF代码量多PNP/电源都要自理少框架接管调试自由度高资源自己控中框架有隐式行为对老平台支持XP/Win7 都能编老平台依赖 KB 补丁麻烦中断处理灵活性完全自定义 ISR框架接管回调受限出问题可读性蓝屏堆栈直观对象模型抽象定位偏绕实际项目里如果目标系统是 Win10 以上、硬件逻辑比较标准我会选 KMDF但如果这块卡的寄存器访问时序很敏感、中断行为非标准或者客户现场还在跑 Win7 甚至更老的系统我会直接选 WDM。不要被“微软推 KMDF”带偏硬件驱动归根结底是让设备工作不是让框架落地。3. 用 WDK 建 WDM 的 PCI 最小工程sources 文件到读取配置空间3.1 WDK 环境下 WDM 驱动工程怎么搭以 Visual Studio WDK 为工具链新建项目时选择 “Kernel Mode Driver, Empty (KMDF)” 会把框架默认勾上你需要手动把项目改造成 WDM 工程。更直接的方式是新建一个空 C 工程然后在源码目录手动添加 sources 文件和 makefile用命令行 “wdkbuild” 工具来编译。WDK 安装完成后环境变量会包含 NTDDK_VERSION 等定义命令行构建一般是这样的set WINDOWS_RDDK10 build /zc:wchar_t /M 4 /w实际上新版 WDK 更推荐用 MSBuild 工程但我个人的血泪经验是老项目、老硬件文档里给的源码包往往还是基于 sources 这一套所以保留手工构建能力仍然有实际价值。工程结构只需要四个文件驱动源码 .c、sources、makefile、INF。TARGETNAMEWdmPciSample TARGETTYPEDRIVER TARGETPATHobj SOURCESwdmpci.c逻辑说明TARGETNAME 决定编译产物 .sys 的名称TARGETTYPEDRIVER 告诉构建系统这是一个内核驱动SOURCES 列出你所有要参与编译的 .c 文件。如果工程里包含 .rc 资源文件或者 .inf也要追加到这里。构建出来的驱动文件会放在 obj 目录下。参数说明如果设备需要支持 64 位环境需要在 sources 里加上TARGETENTRYDriverEntry否则默认入口对不上。还有一个常见参数MSC_WARNING_LEVEL/W3建议保留WDM 代码里告警往往对应资源泄漏。3.2 在 AddDevice 里创建设备对象并建立符号链接PCI 驱动的 AddDevice 函数是硬件接入系统的入口。PnP 管理器会调用这个函数要求驱动为设备创建一个功能设备对象FDO。这里的核心动作有四个IoCreateDevice 创建设备对象、初始化设备扩展、为设备创建符号链接名、把设备对象标志设置为 DO_DIRECT_IO这样后续 DeviceIoControl 的缓冲区使用方式才是 METHOD_IN_DIRECT 或 METHOD_OUT_DIRECT 适用。NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo NULL; NTSTATUS status IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN, 0, FALSE, fdo); if (!NT_SUCCESS(status)) return status; PDEVICE_EXTENSION devExt (PDEVICE_EXTENSION)fdo-DeviceExtension; devExt-DeviceObject fdo; UNICODE_STRING symLink RTL_CONSTANT_STRING(L\\??\\WdmPciSample); status IoCreateSymbolicLink(symLink, fdo-DeviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(fdo); return status; } fdo-Flags | DO_DIRECT_IO; fdo-Flags ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }逻辑说明IoCreateDevice 的 DeviceType 参数填 FILE_DEVICE_UNKNOWN 即可不用在意具体的类型编码Exclusive 参数设为 FALSE因为一个采集卡可能要被多个应用进程同时打开独占反而制造麻烦。符号链接名用 ??\ 前缀是用户态 CreateFile 可以直接访问的名字否则用户态程序只能通过设备实例 ID 找路径调试起来很别扭。参数说明DO_DIRECT_IO 标志决定了 IRP_MJ_DEVICE_CONTROL 的缓冲区访问方式是 METHOD_IN_DIRECT 还是 METHOD_OUT_DIRECT。如果你希望用 METHOD_BUFFERED 方式需要把标志位设成 DO_BUFFERED_IO。对于 PCI 驱动我强烈建议用 DIRECT_IO原因后面 IOCTL 参数里会讲。3.3 DeviceIoControl 读配置空间一个完整的 IRP 处理示例用户态取 PCI 配置空间的方式不止一种Windows 也提供了获取配置空间的 API但如果你要读的是设备私有的 Configuration Space 扩展或者需要读特定 BAR 内的寄存器最灵活的还是自己用 DeviceIoControl 下发命令。下面是一个处理读配置空间请求的 WDM 分发函数NTSTATUS DispatchIoctl(PDEVICE_OBJECT fdo, PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); ULONG ioctl irpSp-Parameters.DeviceIoControl.IoControlCode; PDEVICE_EXTENSION devExt (PDEVICE_EXTENSION)fdo-DeviceExtension; NTSTATUS status STATUS_SUCCESS; switch (ioctl) { case IOCTL_PCI_READ_CONFIG: { if (irpSp-Parameters.DeviceIoControl.InputBufferLength sizeof(ULONG)) { status STATUS_BUFFER_TOO_SMALL; break; } ULONG* input (ULONG*)Irp-AssociatedIrp.SystemBuffer; ULONG offset input[0]; if (offset 0x0FFC) { // PCI 配置空间 256 字节扩展空间另说 status STATUS_INVALID_PARAMETER; break; } ULONG value 0; // 读取配置空间需要访问 PCI 总线的 IO 端口这里示意为读取扩展空间的物理映射 value READ_REGISTER_ULONG((PULONG)(devExt-ConfigVirtualAddr offset)); if (irpSp-Parameters.DeviceIoControl.OutputBufferLength sizeof(ULONG)) { status STATUS_BUFFER_TOO_SMALL; break; } *(ULONG*)Irp-UserBuffer value; Irp-IoStatus.Information sizeof(ULONG); status STATUS_SUCCESS; break; } default: status STATUS_INVALID_DEVICE_REQUEST; break; } Irp-IoStatus.Status status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }逻辑说明这个方法的关键在于你对配置空间读取的合法性判断。PCI 配置空间前 256 字节是标准部分扩展空间在 PCIe 下需要通过 ECAM 机制访问简单直接把配置空间虚拟地址加上偏移是有风险的。对于标准配置空间我一般直接操作映射地址对于扩展 Capability 结构建议走总线驱动封装好的 Capability 列表不要自己拿偏移硬算。参数说明InputBufferLength 和 OutputBufferLength 是用户态调用 DeviceIoControl 时传入的缓冲区长度。WDM 在 DIRECT_IO 模式下IRP 的 AssociatedIrp.SystemBuffer 指向输入参数缓冲UserBuffer 指向输出缓冲。注意UserBuffer 必须用 ProbeForWrite 检查后才能写入否则蓝屏概率极大。这个检查我放在 DispatchIoctl 的入口处做一次统一过滤。4. 中断与 DMA从 MSI/MSI-X 注册到一次真实数据搬运4.1 先搞清硬件手册里的中断拓扑Legacy INTx 与 MSI 的区别做 PCI 驱动开发中断配置是第一个分水岭。老式 PCI 卡用的是 INTx 中断线每个设备可能共享一条 IRQ系统把中断拉到 CPU 时驱动需要先识别是否是自己的设备。PCIe 设备则大量使用 MSI/MSI-X 中断消息设备直接往特定地址写入一个中断消息CPU 直接触发中断绕过 IRQ 共享的麻烦。这个区别直接反映到代码上你用 WDM 处理 MSI 时需要注册两个入口一个负责中断初始化一个负责中断释放。而且 MSI-X 支持多个中断向量这意味着一个物理设备可以被拆成多个功能队列每个队列绑定独立的 CPU。做 GPU 驱动开发或者网卡驱动的同事对这块很熟WDM 下的 PCIe 驱动开发逻辑和它们一致只是没有框架替你整理多向量和 CPU 亲缘性。// 注册 MSI 中断的示意使用 IoConnectInterruptEx IO_CONNECT_INTERRUPT_PARAMETERS params; RTL_ZERO(¶ms); params.Version CONNECT_LINE_BASED; params.LineBased.InterruptObject devExt-InterruptObject; params.LineBased.ServiceRoutine PciIsr; params.LineBased.ServiceContext devExt; params.LineBased.SpinLock devExt-InterruptSpinLock; params.LineBased.SynchronizeIrql DIRQL; params.LineBased.Vector resourceInterrupt-u.LineInterrupt.Vector; params.LineBased.Irql (KIRQL)resourceInterrupt-u.LineInterrupt.Irql; status IoConnectInterruptEx(¶ms);逻辑说明这里演示的是连接 Legacy 中断的方式因为你不知道目标设备到底支持 MSI 还是只有 INTx先按传统方式做一轮等设备启动后再查询 PCI 配置空间里的 Capability 列表找到 MSI-X Capability 块再做升级。ServiceRoutine 是你的回调函数它被调用时系统已经为你拿下了自旋锁回调里不要做耗时操作。参数说明SynchronizeIrql 表示中断回调被调用时的 IRQL 级别一般用 DIRQL对应硬件中断级别SpinLock 是中断服务例程和驱动其他部分共享的自旋锁用来保护设备扩展结构中的共享状态。如果设备的 MSI 向量被多 CPU 同时触发这个自旋锁就是唯一的安全屏障省略不得。4.2 中断回调里该做什么、不该做什么很多第一次写 WDM PCI 驱动的工程师在 ISR 里做大量工作读 FIFO、搬运数据、更新环形队列、通知应用层。实际上 ISR 运行在 DISPATCH_LEVEL 及以上在这层做分页内存访问会直接引发系统崩溃。ISR 的标准做法是读中断状态寄存器确认是否是你的设备触发如果是读取关键状态到设备扩展然后关中断或屏蔽该中断接着调用 KeInsertDpcQueue 把剩余工作推给 DPC。BOOLEAN PciIsr(PKINTERRUPT InterruptObject, PDEVICE_EXTENSION devExt) { ULONG statusReg READ_REGISTER_ULONG(devExt-Regs-IntStatus); if (!(statusReg devExt-InterruptMask)) { return FALSE; // 不是本设备的中断 } // 快速收集硬件状态清中断源 WRITE_REGISTER_ULONG(devExt-Regs-IntClear, statusReg); // 把剩下的数据搬运工作交给 DPC 处理 KeInsertQueueDpc(devExt-DpcObj, devExt, NULL); return TRUE; }逻辑说明函数返回 TRUE 表示这个中断已经被处理返回 FALSE 表示不是你的设备系统会继续找其他驱动。很多人忽略这个返回值在共享中断线的情况下导致中断风暴。另外READ_REGISTER_ULONG 和 WRITE_REGISTER_ULONG 是保证访问 volatile 内存映射寄存器的正确方式不要用普通指针解引用。参数说明IntStatus 和 IntClear 是硬件手册中定义的两个寄存器IntStatus 用于读取中断原因IntClear 用于写 1 清除对应中断位。不同硬件的清除方式不同有的写 0 清有的写 1 清务必对照手册确认这是中断调试里最常见的翻车点。4.3 DMA 数据搬运的内存锁页与地址重读PCI 设备做 DMA 时驱动负责把用户态缓冲区锁定在物理内存中并把物理地址告诉设备。WDM 下做这件事的标准路径是分配 MDL调用 MmProbeAndLockPages 锁定用户缓冲区然后通过 MmGetPhysicalAddress 获取每个页面的物理地址最后把这些地址写入设备的 DMA 描述符表。PMDL mdl IoAllocateMdl(userBuffer, length, FALSE, FALSE, NULL); if (!mdl) return STATUS_INSUFFICIENT_RESOURCES; MmProbeAndLockPages(mdl, UserMode, IoReadAccess); PHYSICAL_ADDRESS physAddr MmGetPhysicalAddress(mdl-StartVa); // 将 physAddr 转成设备 DMA 可用的地址写入设备寄存器 WRITE_REGISTER_ULONG(devExt-Regs-DmaLoAddr, (ULONG)physAddr.LowPart); WRITE_REGISTER_ULONG(devExt-Regs-DmaHiAddr, (ULONG)physAddr.HighPart);逻辑说明用户态缓冲区默认是可换页的驱动在 DMA 过程中如果访问已经换出的页面会让系统崩溃。MmProbeAndLockPages 会把这段用户内存锁到物理内存不换出。设备拿到的是物理地址不是虚拟地址因此必须用 MmGetPhysicalAddress 转换。需要注意的是锁页后的 MDL 只能用于 DMA不能被当作普通内核缓冲区直接 memcpy。参数说明IoReadAccess 表示设备只会读取这块内存相当于从设备到内存的 DMA 传输方向如果你的设备要把数据从内存写到设备这里要传 IoWriteAccess。第三个参数是 ChargeQuota设为 FALSE 即可不涉及用户配额计数。MmBuildMdlForNonPagedPool 是另一条路径用于驱动自己分配的非分页池缓冲区锁页逻辑更简单但用户态无法直接访问这个缓冲区跨进程会话会比较麻烦。// 传输完成后解除锁页 MmUnlockPages(mdl); IoFreeMdl(mdl);逻辑说明传输完成并确认设备已经停止 DMA 后才能解除锁页并释放 MDL。顺序反了会陷入“设备还在搬数据缓冲区却被释放”的悬空状态。实践中一个采集卡驱动会在 DMA 完成后通过中断回调发通知驱动在那个上下文里做 MmUnlockPages 是最安全的。5. 找坑细节PCI 驱动开发里容易翻车的五类场景5.1 设备管理器显示“代码 10”而驱动的 AddDevice 根本没执行现象INF 安装完成后设备管理器里出现设备但有一个黄色感叹号错误代码 10驱动无法启动。原因设备没有被 PnP 管理器成功匹配到你的驱动。多数情况下是 INF 里的硬件 ID 和你的 PCI Vendor ID/Device ID 不一致或者 INF 文件中没有正确声明 WDF 协同安装器如果工程里不小心混用了 KMDF 库。少数情况是系统里存在另一个驱动已经抢先占用了该设备。解决先用 pnputil /enum-devices 查到系统的设备实例路径和硬件 ID看 INF 是否出现完全匹配项。我一般会在 DriverEntry 第一行加一个 DbgPrint确认驱动有没有被系统加载。连 DriverEntry 都没进的话问题就锁定在 INF 的匹配逻辑上不要往代码深处查。5.2 WRITE_REGISTER_ULONG 写入后硬件没有反应寄存器读回全 FF现象驱动映射完 BAR 后往某个控制寄存器写入值再回读发现全部是 0xFF或者在调试器里读到的值恒为 FF。原因BAR 物理地址映射错误或映射长度不足。PCI 设备在 BIOS 阶段可能没有正确分配 BAR 窗口或者设备扩展中保存的 BarVirtualAddr 对应到了 BAR0而硬件手册里的控制寄存器实际在 BAR2 空间。解决启动驱动后在 WinDbg 里做两步确认第一步调用 !devobj 查看设备对象的资源列表第二步用 !cm_res_list 检查设备分到的内存窗口数量和起始地址。比对资源列表里的物理地址和硬件手册里的 BAR 定义如果系统分配的地址与手册不一致不要强行改驱动代码去猜先确认 ACPI 资源有没有被 BIOS 正确上报。5.3 中断处理导致 DPC 风暴DPC 队列持续堆积现象驱动加载后 CPU 占用率飙升到接近 100%任务管理器里看到中断占用很高系统交互变卡顿。原因设备处于中断全开的状态但驱动 ISR 里没有正确清除硬件中断标志导致每次设备产生中断都会重复触发同一根中断线。另一种常见情况是硬件上有多个中断源驱动只清除了一个其他中断源继续产生中断。解决硬件手册中一般有一个 Interrupt Status 寄存器ISR 里要先读这个寄存器再根据位图写对应的 Clear 寄存器。确认清中断后再返回 TRUE。我遇到过一块视频采集卡采集引擎状态变化和 DMA 完成共用同一个 INTX 线只清 DMA 完成标志以为完事了结果引擎还在源源不断发中断直到所有中断源都被清除才消停。5.4 MSI 中断模式下驱动无法接收正确中断向量现象注册 MSI 中断后硬件触发中断但驱动收不到或者系统在设备启动时直接蓝屏。原因MSI 中断需要先向设备写入 Message Data 和 Message Address这部分内容由 PCI 总线驱动负责分配WDM 的 IoConnectInterruptEx 在 MSI 模式下只负责连接但设备如果不用标准模式而是需要驱动自己配置 MSI Capability就需要你额外操作 Capability 结构里的 Message Control 寄存器完成 MSI Enable 置位。解决查询配置空间 MSI Capability 块确认 Message Control 寄存器的 MSI Enable 位是否已经置 1。如果总线和驱动代码都设置过这个位而中断依然不触发检查中断亲缘性设置和系统是否屏蔽了 MSI 向量。调试的后悔药是在 DriverEntry 里用 !agp 类似的扩展去检查分配到的 MSI IRQ 是否有效而不是盲改代码。5.5 DMA 未完就释放锁页内存导致内核进程蓝屏现象采集数据过程中不定期蓝屏崩溃堆栈指向 MmUnlockPages 或 MmFreeContiguousMemory。原因驱动在设备 DMA 还在进行时就调用了 MmUnlockPages此时设备还在往已解锁的缓冲区写入数据内存管理器在换页时发现了非法写入。另一个隐含原因是同步问题DMA 完成中断被驱动忽略导致完成信号丢失主线程提前进入清理逻辑。解决DMA 的释放必须与硬件传输完成状态强同步只有中断回调里确认设备 DMA 传输完成标志置位后才能解锁内存。如果中断信号经常丢失需要检查 ISR 返回值和中断状态寄存器读取是否过于急躁。常见做法是加一个 pending request 队列提交 DMA 时把 MDL 挂在队列里完成中断里摘掉主线程循环检查队列是否为空再继续清理。6. 最后一步用 WinDbg 验证 PCI 驱动的资源分配与寄存器访问结果到这一步代码已经能编译驱动的加载顺序、设备枚举、配置空间读取也不要再靠猜直接上 WinDbg 在真实机器上核对。目标是把“驱动认为自己在访问哪个寄存器”和“硬件实际得到的地址”对上。先在 WinDbg 里加载驱动和符号断在 DriverEntry 的起始行。然后执行如下命令确认设备资源!devobj \Device\WdmPciSample !cm_res_list 0xffff8a0a12345678 ; 设备对象地址从上面取第一个命令输出设备对象信息第二个命令会列出系统分配给该设备的所有内存窗口、中断向量和 DMA 通道。你要核对的是内存窗口的数量和地址是否与你代码里 MmMapIoSpace 的参数一致。实际项目里我见过一次资源列表里有两个内存窗口驱动只映射了第一个结果第二个窗口上的中断状态寄存器一直读不回正确数据。这个问题在静态代码里很难发现只有对着资源列表逐项核对才暴露出来。核对完配置空间映射后用用户态测试程序去读一个已知的寄存器验证驱动代理逻辑正确。下面这个函数是用户态验证 IOCTL 的最小示例HANDLE hDevice CreateFile(L\\\\.\\WdmPciSample, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hDevice INVALID_HANDLE_VALUE) return GetLastError(); DWORD input 0x88; // 要读取的配置空间偏移 DWORD output 0; DWORD bytesReturned 0; BOOL ok DeviceIoControl(hDevice, IOCTL_PCI_READ_CONFIG, input, sizeof(input), output, sizeof(output), bytesReturned, NULL); if (ok) { printf(Config[0x%02X] 0x%08X\n, input, output); } else { printf(IOCTL failed, error%d\n, GetLastError()); } CloseHandle(hDevice);逻辑说明CreateFile 打开的是你在 AddDevice 里创建的符号链接 ??\WdmPciSample用户态看到的路径是 \.\WdmPciSample。IOCTL 下发后驱动在 DispatchIoctl 里按参数读配置空间并返回。如果 output 里的值明显不对比如全部 FF通常不是 IOCTL 逻辑问题而是驱动中映射的 BAR 基址与硬件实际使用的窗口不一致。参数说明IOCTL 的缓冲区方式是由驱动设备对象的 DO_DIRECT_IO 标志决定的所以这里的 input 和 output 缓冲区不需要你对齐或设置特殊尺寸。DeviceIoControl 的返回值与 bytesReturned 配合可以判断驱动是否设置好了 IoStatus.Information。这个验证过程能把驱动内部状态和用户态调用串起来一边调试一边确认硬件寄存器是否真的可访问。我最早写 WDM 的 PCI 驱动是在一块只有寄存器手册、没有任何现成样例的采集卡上前两周反复翻车后来才发现问题不在驱动逻辑而在自己对 BAR 窗口的理解少了一段。直到把系统资源列表和硬件手册对齐这块卡的采集才真正稳定下来。从那以后我每做一个 PCI 驱动都会先把配置空间读取验证跑通再去碰中断和 DMA。希望帮到你。本文还有配套的精品资源点击获取
返回列表