949 字
5 分钟
C# 进阶指南:调用 Win32 API 实现自定义 USB (HID) 设备通信
1. 概述:USB 通信的底层逻辑
在嵌入式开发中,将设备定义为 HID (Human Interface Device) 类是实现免驱通信的最简便方法。上位机(PC)无需安装第三方驱动,直接通过调用 Windows 系统内核提供的 DLL 即可实现数据交换。
开发核心流程
实现 C# 与 USB 设备通信的逻辑可以拆解为以下四个关键环:
- 设备枚举 (Enumeration):在设备丛林中通过 GUID 找到 HID 类别。
- 查找匹配 (Matching):根据硬件的 VID (Vendor ID) 和 PID (Product ID) 定位目标。
- 握手连接 (Linking):获取设备路径,创建文件句柄。
- 异步通信 (I/O):开启读写线程,实现数据的非阻塞交互。
2. 引入核心 Win32 库
由于 C# 运行在托管环境,访问底层硬件必须通过 P/Invoke (Platform Invocation) 调用系统原生的 DLL。
| DLL 名称 | 核心职责 |
|---|---|
| hid.dll | 负责 HID 专属操作,如获取 GUID、获取设备属性(VID/PID)。 |
| setupapi.dll | 负责管理 Windows 设备管理器中的设备信息集。 |
| kernel32.dll | 负责最基础的 I/O 操作:CreateFile、ReadFile、WriteFile。 |
3. 核心步骤实现
3.1 识别与过滤设备
首先,我们需要获取 HID 类的全局唯一标识符 (GUID),然后遍历所有已连接的 HID 设备。
// 获取系统 HID 类的 GUID[DllImport("hid.dll")]public static extern void HidD_GetHidGuid(ref Guid HidGuid);
// 获取包含所有已连接 HID 设备的设备信息集合[DllImport("setupapi.dll", SetLastError = true)]public static extern IntPtr SetupDiGetClassDevs(ref Guid ClassGuid, uint Enumerator, IntPtr HwndParent, uint Flags);3.2 建立物理连接
在找到匹配的路径后,使用 CreateFile 打开设备。
注意:USB 通信在系统底层被视为“文件读写”。
// 打开设备句柄[DllImport("kernel32.dll", SetLastError = true)]private static extern IntPtr CreateFile( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
// 常量定义const uint GENERIC_READ = 0x80000000;const uint GENERIC_WRITE = 0x40000000;const uint FILE_SHARE_READ = 0x00000001;const uint FILE_SHARE_WRITE = 0x00000002;const uint OPEN_EXISTING = 3;3.3 数据交互(同步与异步)
对于实时性要求较高的硬件应用,建议使用 ReadFile 的异步模式(Overlapped),避免 UI 界面卡死。
[StructLayout(LayoutKind.Sequential)]public struct OVERLAPPED { public IntPtr Internal; public IntPtr InternalHigh; public int Offset; public int OffsetHigh; public IntPtr hEvent; // 用于存储通知事件的句柄}4. 优化后的完整通信类 (精简版)
以下是一个封装良好的示例类,增强了错误处理机制:
public class HidUsbManager{ private IntPtr _deviceHandle = IntPtr.Zero;
public bool OpenDevice(ushort targetVid, ushort targetPid) { Guid hidGuid = Guid.Empty; HidD_GetHidGuid(ref hidGuid);
IntPtr deviceInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, IntPtr.Zero, 0x00000012); // DIGCF_PRESENT | DIGCF_DEVICEINTERFACE
// 此处应包含 SetupDiEnumDeviceInterfaces 遍历逻辑... // 匹配 VID 和 PID 后获取 devicePathName
_deviceHandle = CreateFile(devicePathName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
return _deviceHandle != (IntPtr)(-1); }
public bool WriteReport(byte[] reportData) { if (_deviceHandle == IntPtr.Zero) return false;
uint bytesWritten = 0; // 注意:HID 报告的第一字节通常是 Report ID,如果不使用则填 0 return WriteFile(_deviceHandle, reportData, (uint)reportData.Length, ref bytesWritten, IntPtr.Zero); }
public void Close() { if (_deviceHandle != IntPtr.Zero) { CloseHandle(_deviceHandle); _deviceHandle = IntPtr.Zero; } }}5. 开发避坑指南
- Report ID 的坑:在调用
WriteFile时,发送数组的第一个字节必须是Report ID。如果你的硬件固件没有定义多个 ID,通常这个字节需要设为0x00,且数组实际长度要比数据长度多 1。 - 管理员权限:访问某些底层 USB 设备可能需要程序以管理员权限运行。
- 垃圾回收 (GC):在使用
OVERLAPPED结构体进行异步操作时,确保该结构体在 I/O 完成前不会被 GC 回收(可以使用GCHandle固定内存)。
6. 总结
使用 C# 开发 USB 上位机,本质上是托管代码与非托管内核对象的博弈。虽然 P/Invoke 看起来繁琐,但它提供了对硬件最精准的控制能力。对于更复杂的应用,建议基于上述逻辑进一步封装成响应式的 Observable 流。
C# 进阶指南:调用 Win32 API 实现自定义 USB (HID) 设备通信
https://hw.rscclub.website/posts/csharpusb/