简介这份资源围绕Windows平台下的串口驱动过滤技术展开面向具备一定驱动开发基础、希望深入理解串口通信拦截与定制的中高级开发者。内容涵盖串口驱动过滤的基本原理以及上滤驱动与下滤驱动在I/O请求处理、数据流修改和日志记录中的分工并涉及WDM与UWD驱动模型、Visual Studio驱动项目搭建及Windbg调试思路适合用于学习如何在不改动原始驱动和应用代码的前提下干预串口通信。压缩包共5个文件约121KB包含sln解决方案、vcxproj工程文件、cpp源码、dll动态库与exe可执行程序分别对应驱动工程组织、过滤逻辑实现、模块调用与串口通信测试工具便于直接编译、安装和验证过滤效果。目前已有383人学习下载可作为串口过滤驱动入门与实验的参考素材。1. 串口过滤驱动从「谁在偷看我的 COM 口」说起设备插上 USB 转串口线系统里多出一个 COM 口业务程序打开它收发数据。但你可能遇到过这种场景主程序读到的数据莫名其妙少了几帧或者某个后台工具想同时监听同一路串口却打不开因为 Windows 的 COM 口默认是独占访问。这时候就需要在串口驱动栈里插一层「过滤驱动」在不改动业务代码的前提下把流经串口的数据截下来看一眼或者做转发、做记录、做协议预处理。串口过滤驱动干的就是这件事——它挂在功能驱动之上或之下拦截 IRP处理读写请求。本文面向想自己动手写一个简单串口过滤驱动的工程师从 KMDF 框架选型讲到最小可跑代码再到参数配置和血泪踩坑。如果你正在用 CH340、CH341 这类芯片做串口下载或调试这套思路同样适用因为过滤层不关心底层是原生 UART 还是 USB 转串口。2. 串口过滤驱动到底挂在哪设备栈与 KMDF 选型2.1 串口设备栈的分层结构Windows 里一个串口设备比如 COM3背后是一串设备对象叠起来的栈。最底下是总线驱动创建的 PDO物理设备对象往上是功能驱动 FDO负责实际收发再往上是过滤驱动 FiDO最顶上是文件系统驱动和应用程序的句柄。过滤驱动可以插在 FDO 之上上层过滤或之下下层过滤。上层过滤看到的是已经解析过的读写 IRP适合做数据记录和协议分析下层过滤更接近硬件能看到更原始的请求但处理起来更麻烦因为要自己处理很多底层细节。常见做法是写一个上层过滤驱动附加到串口功能设备对象上。这样业务程序打开 COM 口时IRP 会先经过你的过滤驱动你有机会在 IRP_MJ_READ 和 IRP_MJ_WRITE 里做手脚。选上层过滤的另一个原因是 KMDF 对上层过滤支持得比较自然有现成的WdfFdoInitSetFilter可以调用。2.2 为什么选 KMDF 而不是 WDMWDM 写过滤驱动要手动处理 IRP 栈、完成例程、即插即用状态机代码量大且容易出错。KMDF 把这些封装成对象和回调你只需要实现EvtDeviceAdd、EvtIoRead、EvtIoWrite这几个入口。对于「简单串口过滤」这个目标KMDF 能把代码压到两三百行以内。代价是 KMDF 版本要和目标系统匹配Win7 到 Win11 支持的 KMDF 版本不同编译时要在项目属性里指定最低版本。我一般会先确认目标机器是 64 位还是 32 位然后选 KMDF 1.15 或更高因为 1.15 对串口这类老设备的兼容性比较稳。如果你要支持 Win7得降到 1.9 或 1.11但 Win7 现在很少见了除非工业现场还有老机器。2.3 过滤驱动与业务程序的交互方式过滤驱动截到数据后怎么把数据交给用户态程序常见有三种一是写日志文件驱动直接写盘简单但性能差二是用 inverted call用户态发一个 IOCTL 下来驱动把数据挂起等有数据时完成这个 IOCTL用户态就能收到三是共享内存加事件通知适合高吞吐。简单串口过滤一般用第二种就够了代码量小延迟也能接受。提示不要用DbgPrint输出大量数据它只在调试器附加时有效且会严重拖慢串口吞吐实测能掉一半以上。3. 用 KMDF 写一个最小串口过滤驱动代码与参数3.1 工程结构与 INF 文件要点一个最小 KMDF 过滤驱动工程包含驱动源文件.c、INF 文件、项目文件.vcxproj。INF 里关键的是AddReg段要把你的驱动注册为串口设备的上层过滤。写法是在[Manufacturer]段引用一个模型然后在[Install]段里用HKR,,UpperFilters,0x00010000,YourFilterName。注意UpperFilters是 REG_MULTI_SZ多个过滤驱动用逗号分隔。; 串口过滤驱动 INF 关键片段 [Version] Signature$WINDOWS NT$ ClassPorts ClassGuid{4d36e978-e325-11ce-bfc1-08002be10318} [Manufacturer] %MfgName%MfgSection,NTamd64 [MfgSection.NTamd64] %DeviceName%InstallSection,USB\VID_1A86PID_7523 [InstallSection] Includemsports.inf NeedsPorts.NT CopyFilesFilterCopyFiles AddRegFilterAddReg [FilterAddReg] HKR,,UpperFilters,0x00010000,SerialFilter这段 INF 的意思是针对 VID_1A86 PID_7523 这个 CH340 设备在它的设备注册表项里追加一个名为 SerialFilter 的上层过滤驱动。Include和Needs表示复用系统自带的串口安装段避免自己重写整个安装逻辑。参数0x00010000表示 REG_MULTI_SZ 类型字符串就是你的驱动服务名。3.2 驱动入口与设备附加KMDF 驱动入口用DriverEntry里面调WdfDriverCreate然后注册EvtDriverDeviceAdd。在EvtDeviceAdd里因为我们是过滤驱动要调WdfFdoInitSetFilter然后创建默认的 I/O 队列。#include wdf.h NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { WDF_DRIVER_CONFIG config; WDF_DRIVER_CONFIG_INIT(config, EvtDeviceAdd); return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, config, WDF_NO_HANDLE); } NTSTATUS EvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit) { NTSTATUS status; WDFDEVICE device; WDF_IO_QUEUE_CONFIG queueConfig; // 关键声明自己是过滤驱动不创建 FDO WdfFdoInitSetFilter(DeviceInit); status WdfDeviceCreate(DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, device); if (!NT_SUCCESS(status)) return status; // 创建默认队列接收读、写、IOCTL WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(queueConfig, WdfIoQueueDispatchSequential); queueConfig.EvtIoRead EvtIoRead; queueConfig.EvtIoWrite EvtIoWrite; queueConfig.EvtIoDeviceControl EvtIoDeviceControl; return WdfIoQueueCreate(device, queueConfig, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE); }WdfFdoInitSetFilter是过滤驱动的标志不调它 KMDF 会以为你要创建功能设备对象附加就会失败。队列用WdfIoQueueDispatchSequential表示串行处理避免读写并发导致数据错乱。如果你要低延迟可以改成WdfIoQueueDispatchParallel但那样就得自己加锁保护共享缓冲区。3.3 读写回调里怎么截数据EvtIoRead和EvtIoWrite里拿到的是WDFREQUEST里面封装了 IRP。要读数据先调WdfRequestRetrieveOutputMemory读或WdfRequestRetrieveInputMemory写拿到WDFMEMORY再映射成缓冲区。VOID EvtIoWrite(WDFQUEUE Queue, WDFREQUEST Request, size_t Length) { WDFMEMORY memory; PVOID buffer; NTSTATUS status; status WdfRequestRetrieveInputMemory(Request, memory); if (!NT_SUCCESS(status)) { WdfRequestComplete(Request, status); return; } buffer WdfMemoryGetBuffer(memory, NULL); // 在这里处理 buffer 里的 Length 字节数据 // 例如记录到环形缓冲区或做协议解析 SerialFilterLogData(buffer, Length); // 关键把请求转发给下层驱动否则业务程序会卡住 WdfRequestFormatRequestUsingCurrentType(Request); WdfRequestSend(Request, WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue)), WDF_NO_SEND_OPTIONS); }WdfRequestFormatRequestUsingCurrentType把请求原样往下发WdfRequestSend是异步发送发完就返回。注意这里不能直接WdfRequestComplete因为请求还没被下层处理你只是看了一眼。如果你要修改数据得先拿到输入缓冲区写权限改完再发。Length参数是本次请求的字节数串口一次读写可能只有几个字节也可能几百字节取决于业务程序的缓冲区大小。注意在EvtIoWrite里不要做耗时操作比如写文件或等事件。串口中断上下文里阻塞会导致整个设备栈卡死蓝屏不是吓唬人。4. 串口过滤驱动避坑5 个血泪翻车现场4.1 现象装完驱动后 COM 口消失设备管理器带黄色感叹号原因INF 里UpperFilters写成了REG_SZ而不是REG_MULTI_SZ或者服务名和驱动文件里的名字不一致。Windows 加载过滤驱动时找不到对应服务直接把整个设备栈标记为故障。解决检查注册表HKLM\SYSTEM\CurrentControlSet\Enum\USB\VID_xxxxPID_xxxx\...\Device Parameters下的UpperFilters值类型必须是REG_MULTI_SZ。服务名要和DriverEntry所在驱动的服务名完全一致大小写敏感。4.2 现象业务程序打开串口后读不到数据但写能成功原因EvtIoRead里忘了调WdfRequestSend或者调了但没设置完成例程请求被挂起后没人完成。读请求和写请求不一样读请求需要下层驱动填充数据后完成你如果不转发上层永远等不到。解决在EvtIoRead里同样用WdfRequestFormatRequestUsingCurrentType加WdfRequestSend转发。如果你要修改读到的数据得设置完成例程在下层完成后再改缓冲区然后自己完成请求。4.3 现象串口吞吐量骤降从 115200 波特率掉到几千字节每秒原因在读写回调里用了DbgPrint或者同步写日志文件。DbgPrint在没调试器时也会走内核调试通道每次调用开销巨大。同步写文件在中断上下文里会等待 I/O 完成直接拖垮性能。解决把日志改成异步环形缓冲区用户态用 inverted call 来取。或者只在出错时打印正常数据不记录。实测去掉DbgPrint后吞吐能恢复 90% 以上。4.4 现象卸载驱动时蓝屏错误码 DRIVER_UNLOADED_WITHOUT_CANCELLING_PENDING_OPERATIONS原因驱动卸载时还有未完成的 IRP 挂在队列里KMDF 默认队列在卸载时不会自动取消这些请求。如果你用了手动队列或者自己挂起了请求必须在EvtDeviceCleanup或EvtDriverUnload里调WdfIoQueuePurge把队列清空。解决在EvtDeviceContextCleanup回调里对每个队列调WdfIoQueuePurgeSynchronously确保所有请求都被取消或完成。KMDF 的默认队列在设备移除时会自动处理但手动创建的队列不会。4.5 现象CH340 设备插拔后过滤驱动失效必须重启原因USB 转串口设备拔掉再插上系统会重新枚举设备创建新的 PDO 和 FDO。如果你的过滤驱动没有正确处理EvtDeviceSurpriseRemoval和EvtDeviceQueryRemove新设备栈里可能没有你的过滤层。解决在 INF 里用UpperFilters是持久化的重新枚举时会自动附加。但如果你的驱动在 surprise removal 时没有释放资源下次附加会失败。实现EvtDeviceSurpriseRemoval回调在里面停止队列并释放缓冲区。5. 进阶用 inverted call 把过滤数据送到用户态5.1 inverted call 的工作模型inverted call 的核心是用户态程序先发一个 IOCTL 给驱动驱动不立即完成而是把这个请求挂起。当过滤驱动截到数据时取出挂起的请求把数据填进去然后完成它。用户态收到完成通知处理数据再发下一个 IOCTL。这样驱动不需要主动通知用户态避免了复杂的反向回调。实现步骤在EvtIoDeviceControl里识别自定义 IOCTL 码把请求放入一个手动队列WdfIoQueueDispatchManual。在EvtIoWrite截到数据后从手动队列取一个请求调WdfRequestRetrieveOutputMemory拿到用户态缓冲区RtlCopyMemory填入数据再WdfRequestComplete。5.2 关键参数与队列配置手动队列要用WdfIoQueueDispatchManual这样请求进来后不会自动完成而是等你调WdfIoQueueRetrieveNextRequest取出来。队列深度默认是 1如果你要支持多个并发 IOCTL得在WDF_IO_QUEUE_CONFIG里设PowerManaged和AllowZeroLengthRequests。我一般设队列深度为 16够用了。// 在 EvtIoDeviceControl 里挂起请求 if (IoControlCode IOCTL_SERIALFILTER_GET_DATA) { WdfRequestForwardToIoQueue(Request, manualQueue); return; // 不完成等有数据再完成 } // 在 EvtIoWrite 里取请求并完成 WDFREQUEST request; NTSTATUS status WdfIoQueueRetrieveNextRequest(manualQueue, request); if (NT_SUCCESS(status)) { WDFMEMORY outMem; WdfRequestRetrieveOutputMemory(request, outMem); PVOID outBuf WdfMemoryGetBuffer(outMem, NULL); RtlCopyMemory(outBuf, dataBuffer, dataLength); WdfRequestCompleteWithInformation(request, STATUS_SUCCESS, dataLength); }WdfRequestForwardToIoQueue把请求转到手动队列WdfIoQueueRetrieveNextRequest从队列取一个。注意取到请求后要检查status如果队列空会返回STATUS_INVALID_DEVICE_STATE别直接解引用。5.3 验证方法与一个实用技巧验证 inverted call 是否工作可以写一个简单的用户态程序循环发 IOCTL每次收到数据就打印长度。如果驱动没挂起请求用户态会立刻收到ERROR_INVALID_PARAMETER或超时。另一个技巧是在驱动里加一个计数器记录挂起请求数和完成数用DbgPrint在卸载时打印一次这样不影响性能又能确认逻辑。我自己的习惯是先在虚拟机里用 com0com 创建一对虚拟串口一个口发数据另一个口跑过滤驱动和用户态程序。这样不用真实硬件就能调试翻车了直接回滚快照比真机折腾省时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?