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 协议

核心流程

  1. 设备枚举(通过 GUID 遍历所有 HID)
  2. 匹配 VID/PID + 获取设备路径
  3. CreateFile 打开句柄
  4. ReadFile / WriteFile(支持 Overlapped 异步)
  5. 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);
}
}

五、开发避坑指南(血泪经验)#

  1. Report ID 处理:发送数组第 0 字节必须是 Report ID(无多 ID 时填 0),长度 = wMaxOutputReportSize(用 HidD_GetPreparsedData + HidP_GetCaps 获取)。
  2. 异步必须 Overlapped:同步 ReadFile 会阻塞 UI,使用 NativeOverlapped + ManualResetEvent。
  3. 句柄释放:一定要 CloseHandle,否则设备重插后无法重新打开。
  4. 管理员权限:部分 Win10/11 需要以管理员运行,或添加 manifest。
  5. GC 与固定内存:Overlapped 结构体用 GCHandle.Alloc 固定。

六、2026 年现代替代方案推荐#

Win32 P/Invoke 适合极致性能或教学场景,但日常开发强烈建议使用成熟 NuGet 库:

方案代码量异步支持跨平台推荐指数适用场景
纯 Win32 P/Invoke手动Windows★★★☆☆极致性能、自定义协议
HidLibrary极低原生 async/awaitWindows★★★★★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/
作者
杨月昌
发布于
2017-11-18
许可协议
CC BY-NC-SA 4.0