1238 字
6 分钟
C# 进阶指南:Win32 API 调用实现自定义 USB HID 设备通信
摘要
HID(Human Interface Device)是 USB 中最常用的免驱动类协议,特别适合自定义传感器、采集卡、控制板等嵌入式设备。本文系统讲解如何在 C# 中通过 P/Invoke 调用 Windows 原生 DLL(hid.dll + setupapi.dll + kernel32.dll)实现完整通信链路,并提供生产级封装建议。
适用场景:需要极致低延迟、自定义 Report 格式、或无法使用第三方库的工业/医疗/仪器上位机。
一、为什么选择 Win32 P/Invoke?
- 免驱动:Windows 自带 HID 类驱动,零安装。
- 控制精细:可直接操作 Report ID、异步重叠 I/O,延迟低至毫秒级。
- 学习价值高:理解后对其他 Win32 硬件接口(串口、PCIe 等)触类旁通。
缺点:代码量较大、错误处理繁琐 → 下文提供完整封装解决。
二、HID 通信核心原理与流程
USB HID 把设备抽象为“文件”,通信本质是 文件读写 + Report 协议。
核心流程:
- 设备枚举(通过 GUID 遍历所有 HID)
- 匹配 VID/PID + 获取设备路径
- CreateFile 打开句柄
- ReadFile / WriteFile(支持 Overlapped 异步)
- CloseHandle 释放
图 1:HID 输入报告处理典型流程(异步通信视角)
图 2:HID Report 结构示例(Report ID + 数据字段)
三、完整 P/Invoke 声明与关键结构体
using System;using System.Runtime.InteropServices;
public class HidApi{ // hid.dll [DllImport("hid.dll")] public static extern void HidD_GetHidGuid(ref Guid HidGuid);
[DllImport("hid.dll", SetLastError = true)] public static extern bool HidD_GetAttributes(IntPtr HidDeviceObject, ref HIDD_ATTRIBUTES Attributes);
// setupapi.dll [DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, string Enumerator, IntPtr hwndParent, uint Flags);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool SetupDiEnumDeviceInterfaces(IntPtr DeviceInfoSet, IntPtr DeviceInfoData, ref Guid InterfaceClassGuid, uint MemberIndex, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr DeviceInfoSet, ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData, IntPtr DeviceInterfaceDetailData, uint DeviceInterfaceDetailDataSize, ref uint RequiredSize, IntPtr DeviceInfoData);
// kernel32.dll [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)] public static extern bool ReadFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToRead, ref uint lpNumberOfBytesRead, ref System.Threading.NativeOverlapped lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, ref uint lpNumberOfBytesWritten, ref System.Threading.NativeOverlapped lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject);
// 结构体定义(关键!) [StructLayout(LayoutKind.Sequential)] public struct HIDD_ATTRIBUTES { public int Size; public ushort VendorID; public ushort ProductID; public ushort VersionNumber; }
[StructLayout(LayoutKind.Sequential)] public struct SP_DEVICE_INTERFACE_DATA { public uint cbSize; public Guid InterfaceClassGuid; public uint Flags; public IntPtr Reserved; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct SP_DEVICE_INTERFACE_DETAIL_DATA { public uint cbSize; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string DevicePath; }}四、实用封装类(可直接使用)
public class HidUsbManager : IDisposable{ private IntPtr _handle = IntPtr.Zero; private const uint GENERIC_READ = 0x80000000; private const uint GENERIC_WRITE = 0x40000000; private const uint OPEN_EXISTING = 3; private const uint FILE_FLAG_OVERLAPPED = 0x40000000;
public bool OpenDevice(ushort vid, ushort pid) { Guid hidGuid = Guid.Empty; HidApi.HidD_GetHidGuid(ref hidGuid);
IntPtr deviceInfoSet = HidApi.SetupDiGetClassDevs(ref hidGuid, null, IntPtr.Zero, 0x12); // DIGCF_PRESENT | DIGCF_DEVICEINTERFACE
var interfaceData = new HidApi.SP_DEVICE_INTERFACE_DATA { cbSize = (uint)Marshal.SizeOf(typeof(HidApi.SP_DEVICE_INTERFACE_DATA)) };
for (uint i = 0; HidApi.SetupDiEnumDeviceInterfaces(deviceInfoSet, IntPtr.Zero, ref hidGuid, i, ref interfaceData); i++) { uint requiredSize = 0; HidApi.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, IntPtr.Zero, 0, ref requiredSize, IntPtr.Zero);
var detail = new HidApi.SP_DEVICE_INTERFACE_DETAIL_DATA { cbSize = IntPtr.Size == 4 ? 4u + 1u : 8u + 1u }; // 32/64位兼容 IntPtr buffer = Marshal.AllocHGlobal((int)requiredSize); Marshal.StructureToPtr(detail, buffer, false);
if (HidApi.SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, buffer, requiredSize, ref requiredSize, IntPtr.Zero)) { detail = Marshal.PtrToStructure<HidApi.SP_DEVICE_INTERFACE_DETAIL_DATA>(buffer); // 此处可进一步用 HidD_GetAttributes 检查 VID/PID IntPtr tempHandle = HidApi.CreateFile(detail.DevicePath, GENERIC_READ | GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, IntPtr.Zero);
if (tempHandle != new IntPtr(-1)) { var attr = new HidApi.HIDD_ATTRIBUTES { Size = Marshal.SizeOf(typeof(HidApi.HIDD_ATTRIBUTES)) }; if (HidApi.HidD_GetAttributes(tempHandle, ref attr) && attr.VendorID == vid && attr.ProductID == pid) { _handle = tempHandle; Marshal.FreeHGlobal(buffer); // 关闭 deviceInfoSet ... return true; } HidApi.CloseHandle(tempHandle); } } Marshal.FreeHGlobal(buffer); } return false; }
public bool WriteReport(byte[] report) // report[0] = Report ID,通常为 0 { if (_handle == IntPtr.Zero) return false; uint written = 0; var overlapped = new System.Threading.NativeOverlapped(); return HidApi.WriteFile(_handle, report, (uint)report.Length, ref written, ref overlapped); }
// 异步 Read 示例可使用 Task + WaitHandle ...
public void Dispose() { if (_handle != IntPtr.Zero) HidApi.CloseHandle(_handle); }}五、开发避坑指南(血泪经验)
- Report ID 处理:发送数组第 0 字节必须是 Report ID(无多 ID 时填 0),长度 = wMaxOutputReportSize(用 HidD_GetPreparsedData + HidP_GetCaps 获取)。
- 异步必须 Overlapped:同步 ReadFile 会阻塞 UI,使用 NativeOverlapped + ManualResetEvent。
- 句柄释放:一定要 CloseHandle,否则设备重插后无法重新打开。
- 管理员权限:部分 Win10/11 需要以管理员运行,或添加 manifest。
- GC 与固定内存:Overlapped 结构体用 GCHandle.Alloc 固定。
六、2026 年现代替代方案推荐
Win32 P/Invoke 适合极致性能或教学场景,但日常开发强烈建议使用成熟 NuGet 库:
| 方案 | 代码量 | 异步支持 | 跨平台 | 推荐指数 | 适用场景 |
|---|---|---|---|---|---|
| 纯 Win32 P/Invoke | 高 | 手动 | Windows | ★★★☆☆ | 极致性能、自定义协议 |
| HidLibrary | 极低 | 原生 async/await | Windows | ★★★★★ | 90% 项目首选 |
| HIDSharp | 低 | 良好 | Windows/Mac/Linux | ★★★★☆ | 需要跨平台 |
| Device.Net | 中 | 优秀 | 多平台 | ★★★★☆ | .NET MAUI / IoT 项目 |
结论:新项目直接用 HidLibrary(NuGet: HidLibrary),3 行代码搞定,性能与 Win32 几乎无差别。
总结与设计核对清单
Win32 P/Invoke 是理解 HID 通信的“内功”,掌握后可轻松切换任何上位机框架。建议先用纯 Win32 写一个最小可运行 Demo,再封装或迁移到 NuGet 库。
核对清单:
- VID/PID 匹配逻辑是否完整?
- Report 数组是否包含正确的 Report ID 且长度匹配?
- 是否使用 Overlapped + 事件完成异步?
- 错误处理(GetLastError)是否覆盖所有失败路径?
- 是否对比过 HidLibrary,确认有必要用纯 Win32?
C# 进阶指南:Win32 API 调用实现自定义 USB HID 设备通信
https://hw.rscclub.website/posts/csharpusb/