首页 / 资讯中心 / 文章详情

VC++ HID 读写实战:从枚举到报告解析的完整示例

VC++ HID 读写实战:从枚举到报告解析的完整示例 ★ FEATURED ARTICLE
简介这份资源是面向Windows底层开发与嵌入式方向学习者的VC6.0 HID设备读写实例聚焦于通过Win32 API与HID API完成设备枚举、接口选择、句柄打开、报告读写及资源释放的完整流程适合已具备一定C基础、希望切入硬件通信与驱动交互的开发者参考。压缩包共24个文件约73KB以7个h头文件、3个cpp源文件为核心配合2个lib静态库、dsp与dsw工程文件、rc资源脚本及可执行文件构成可直接编译运行的工程结构便于对照代码理解各环节调用关系。目前已有109人学习下载。通过分析其中的设备枚举、DeviceIoControl读写报告与HID报告描述符解析等实现读者可掌握HID通信的关键调用方式并借助工程文件快速搭建调试环境为后续嵌入式系统与设备驱动开发积累可复用的实践思路。1. 从一次枚举失败说起这个 VC HID 读写例子到底能干什么前阵子帮一个做工业采集的朋友排查问题他的上位机用 VC6.0 写插上 HID 设备后CreateFile一直返回INVALID_HANDLE_VALUE折腾两天没找到原因。我让他把设备路径打印出来一看\\\\?\\hid#vid_0483pid_5750#...里少了个#后面的接口序号路径拼错了。这类问题在 HID 上位机开发里太常见了而手头这个VC HID 读写例子恰好就是用来把「枚举设备 → 打开设备 → 读写报告 → 解析数据」这条链路一次性跑通的参考工程。它面向的是还在用 VC6.0 或早期 VS 工具链维护工控上位机、读卡器、自定义 HID 外设的开发者。这类项目往往不能随便升级编译器SetupAPI和hid.dll的调用方式必须按老规矩来。这个例子的价值不在于代码多高级而在于它把HidD_GetHidGuid、SetupDiGetClassDevs、SetupDiEnumDeviceInterfaces、CreateFile这一串容易写错的调用顺序固定下来了你照着改 VID/PID 就能用。下面我按实际复现顺序拆一遍顺带把几个必踩的坑标出来。2. 环境与工程搭建VC6.0 下把 SetupAPI 和 hid.dll 接进来2.1 为什么这个例子坚持用 VC6.0 的工程结构VC6.0 的工程模型和后续 VS 差别不小它没有「附加依赖项」这种可视化配置链接库得手动写进Project → Settings → Link → Object/library modules或者用#pragma comment(lib, ...)。这个 HID 例子通常会把依赖写成后者好处是工程文件干净拷到别的机器上不用重新配。需要链接的库一共三个setupapi.lib、hid.lib、user32.lib。前两个是 HID 枚举和报告读写的核心user32.lib主要给窗口消息循环用如果你的读写是阻塞式的其实可以省掉。头文件方面hidsdi.h和setupapi.h是必须的顺序上建议setupapi.h在前因为hidsdi.h里有些类型依赖它。VC6.0 自带的 Platform SDK 版本较老如果编译时报HIDD_ATTRIBUTES未定义说明 SDK 没装全或者包含路径没指对常见做法是把 Platform SDK 的Include目录加到Tools → Options → Directories的最前面。2.2 一个最小可编译的工程骨架下面这段是工程入口的骨架我把它精简到能直接编译的程度你新建一个 Win32 Console Application 空工程把这段贴进主 cpp 即可。#include windows.h #include setupapi.h #include hidsdi.h #include stdio.h #pragma comment(lib, setupapi.lib) #pragma comment(lib, hid.lib) // 全局保存设备句柄实际工程里建议封装成类 HANDLE g_hDevice INVALID_HANDLE_VALUE; int main() { GUID hidGuid; HidD_GetHidGuid(hidGuid); // 拿到 HID 类的 GUID固定值但必须动态取 // 只枚举当前已连接的设备DIGCF_PRESENT 是关键标志 HDEVINFO hDevInfo SetupDiGetClassDevs( hidGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (hDevInfo INVALID_HANDLE_VALUE) { printf(SetupDiGetClassDevs failed: %lu\n, GetLastError()); return -1; } // 后续枚举接口的代码放在这里见 2.3 SetupDiDestroyDeviceInfoList(hDevInfo); return 0; }逻辑上分三步先取 HID 类 GUID再拿设备信息集句柄最后遍历接口。参数里DIGCF_PRESENT表示只要当前在线的设备DIGCF_DEVICEINTERFACE表示按设备接口枚举而不是按设备节点这两个标志缺一个都会导致枚举结果为空。GetLastError()的返回值要养成打印习惯ERROR_ACCESS_DENIED和ERROR_FILE_NOT_FOUND在 HID 场景里含义完全不同前者多半是权限或设备被独占后者基本是路径拼错。2.3 枚举接口并拼出正确的设备路径枚举接口是整段代码里最容易翻车的地方因为SP_DEVICE_INTERFACE_DATA和SP_DEVICE_INTERFACE_DETAIL_DATA两个结构体的大小计算有讲究。SP_DEVICE_INTERFACE_DATA did; did.cbSize sizeof(SP_DEVICE_INTERFACE_DATA); for (DWORD i 0; SetupDiEnumDeviceInterfaces( hDevInfo, NULL, hidGuid, i, did); i) { DWORD required 0; // 第一次调用只为拿所需缓冲区大小必然返回 FALSE SetupDiGetDeviceInterfaceDetail( hDevInfo, did, NULL, 0, required, NULL); PSP_DEVICE_INTERFACE_DETAIL_DATA pDetail (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(required); // 这个 cbSize 在 32 位下是 5不是 sizeof(结构体)写错必崩 pDetail-cbSize sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); if (SetupDiGetDeviceInterfaceDetail( hDevInfo, did, pDetail, required, NULL, NULL)) { printf(Path: %s\n, pDetail-DevicePath); // 这里可以调用 3.1 的打开函数 } free(pDetail); }SetupDiGetDeviceInterfaceDetail第一次传NULL拿大小是标准套路别觉得它返回 FALSE 就是出错。cbSize那个坑我见过太多人栽在 32 位编译下它固定是 5一个 DWORD 加一个 TCHAR直接写sizeof会得到 6 或更大导致后续调用返回ERROR_INVALID_USER_BUFFER。设备路径拿到后通常还要用HidD_GetAttributes读 VID/PID 做过滤不然你会枚举出一堆键盘鼠标。3. 打开设备与报告读写CreateFile 参数和 ReadFile 阻塞行为3.1 CreateFile 打开 HID 设备的正确姿势拿到设备路径后打开方式决定了后续能不能正常读写。HID 设备必须用FILE_FLAG_OVERLAPPED还是同步方式取决于你的读写模型这个例子一般用同步阻塞简单直接。HANDLE OpenHidDevice(const char* path) { HANDLE h CreateFile( path, GENERIC_READ | GENERIC_WRITE, // 读写权限只读设备可去掉 WRITE FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享模式独占会失败 NULL, OPEN_EXISTING, // HID 必须用 OPEN_EXISTING FILE_FLAG_OVERLAPPED, // 异步标志配合 OVERLAPPED 结构 NULL); if (h INVALID_HANDLE_VALUE) { printf(CreateFile failed: %lu\n, GetLastError()); } return h; }OPEN_EXISTING是硬性要求用CREATE_ALWAYS之类必然失败。共享模式建议读写都开否则其他进程比如系统输入子系统占用时会打不开。FILE_FLAG_OVERLAPPED这个标志要和你后面的读写方式匹配加了它就必须用OVERLAPPED结构不加就是同步阻塞。我一般建议加上因为同步ReadFile在设备没数据时会一直卡住界面直接假死。3.2 读报告Report ID 和缓冲区首字节的关系HID 读写的缓冲区格式是新手最容易搞混的缓冲区第一个字节永远是 Report ID哪怕你的设备只用 Report ID 0。BOOL ReadHidReport(HANDLE h, BYTE* buf, DWORD len) { OVERLAPPED ov {0}; ov.hEvent CreateEvent(NULL, TRUE, FALSE, NULL); DWORD bytesRead 0; BOOL ok ReadFile(h, buf, len, bytesRead, ov); if (!ok GetLastError() ERROR_IO_PENDING) { // 等待最多 1 秒超时说明设备没数据 DWORD wait WaitForSingleObject(ov.hEvent, 1000); if (wait WAIT_TIMEOUT) { CancelIo(h); CloseHandle(ov.hEvent); return FALSE; } GetOverlappedResult(h, ov, bytesRead, FALSE); } CloseHandle(ov.hEvent); return TRUE; }缓冲区长度要按设备的报告长度来常见做法是先用HidD_GetPreparsedData加HidP_GetCaps拿到InputReportByteLength再分配。如果你直接写死 64 字节遇到报告长度 8 的设备ReadFile会返回ERROR_INVALID_PARAMETER。WaitForSingleObject的超时值按业务定采集类设备一般 100 到 500 毫秒太长会导致退出时卡顿。3.3 写报告WriteFile 的字节数必须精确写报告比读简单但字节数必须等于OutputReportByteLength多一个少一个都失败。BOOL WriteHidReport(HANDLE h, BYTE* buf, DWORD len) { DWORD written 0; // 同步写注意 buf[0] 是 Report ID BOOL ok WriteFile(h, buf, len, written, NULL); if (!ok) { printf(WriteFile failed: %lu\n, GetLastError()); } return ok; }如果你的设备不需要 Report IDbuf[0]填 0实际数据从buf[1]开始。这一点和很多串口转 HID 的模块文档写得不一致文档说「发送 8 字节数据」实际你要发 9 字节首字节补 0。判断方法很简单看HidP_GetCaps返回的OutputReportByteLength它包含 Report ID 那一字节。4. 避坑与排查HID 读写里最常见的五个翻车点4.1 枚举不到设备句柄为空现象SetupDiEnumDeviceInterfaces第一次就返回 FALSEGetLastError是ERROR_NO_MORE_ITEMS。原因九成是DIGCF_PRESENT没加或者设备确实没插好、驱动没装成 HID 类。解决先去掉DIGCF_PRESENT看能否枚举出历史设备能枚举说明是设备未在线再检查设备管理器里该设备是否归在「人体学输入设备」下如果归在「其他设备」带黄色感叹号说明驱动没匹配上跟代码无关。4.2 CreateFile 返回 ERROR_ACCESS_DENIED现象路径打印出来完全正确但CreateFile就是拒绝访问。原因设备被其他进程独占或者共享模式没写全。系统键盘鼠标这类设备会被系统占用普通应用打不开。解决共享模式加上FILE_SHARE_READ | FILE_SHARE_WRITE确认没有别的调试工具或上位机还开着如果是自定义设备检查固件里是否设置了独占访问。4.3 ReadFile 一直阻塞不返回现象界面卡死调试发现停在ReadFile。原因用了同步方式打开但设备没有数据上报。解决打开时加FILE_FLAG_OVERLAPPED读的时候用OVERLAPPED加超时等待或者把读操作放到独立线程里主线程只负责界面刷新。这个例子如果没做异步长时间无数据时必然假死。4.4 读到的数据错位一位现象设备明明发的0x01 0x02收到的是0x00 0x01。原因忘了缓冲区首字节是 Report ID。解决解析时从buf[1]开始取有效数据buf[0]只用来判断报告类型。如果设备支持多个 Report ID还要根据buf[0]分支处理。4.5 编译报 hid.lib 找不到或符号未解析现象链接阶段报unresolved external symbol _HidD_GetHidGuid。原因hid.lib没链接或者 Platform SDK 的 Lib 路径没配。解决确认#pragma comment(lib, hid.lib)存在VC6.0 里检查Tools → Options → Directories的 Library files 是否包含 SDK 的 Lib 目录老版本 SDK 里hid.lib可能叫hid.lib但路径不同用dumpbin /symbols确认符号在不在。5. 进阶技巧把读写封装成可复用类并做超时验证走到这里基本链路已经通了。但直接拿例子里的散装函数去接项目维护起来会很痛苦。我一般会把它封装成一个CHidDevice类构造时枚举并匹配 VID/PID析构时关闭句柄读写各留一个带超时参数的方法。这样换设备只改 VID/PID 两个宏不用动逻辑。封装时有个细节值得单独说超时验证不能只看WaitForSingleObject的返回值。我踩过一次坑设备拔出的瞬间WaitForSingleObject返回WAIT_OBJECT_0但GetOverlappedResult拿到的bytesRead是 0如果代码不判断这个 0就会把空缓冲区当有效数据解析后面全是乱码。正确做法是GetOverlappedResult之后再加一层bytesRead 0的判断直接返回失败。另外设备热插拔的检测建议用WM_DEVICECHANGE消息注册DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE收到消息后重新枚举并重连。不要用定时器轮询CreateFile那样在设备频繁插拔时会产生大量无效句柄。下面这个判断片段我每次都会加上// 读完成后必须校验实际字节数 if (bytesRead 0) { // 设备可能已拔出触发重连逻辑 return HID_ERR_DISCONNECTED; } // 再按 Report ID 分发解析 switch (buf[0]) { case 0x01: ParseReport1(buf 1, bytesRead - 1); break; case 0x02: ParseReport2(buf 1, bytesRead - 1); break; default: break; }参数上bytesRead - 1才是有效数据长度这个减一别漏。VID/PID 建议做成配置文件读取现场调试时不用重新编译。从那以后我每次接新的 HID 设备都强制先跑一遍枚举打印路径和HidP_GetCaps的报告长度确认这两个值对了再写业务逻辑能省掉大半返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站