949 字
5 分钟
C# 进阶指南:调用 Win32 API 实现自定义 USB (HID) 设备通信

1. 概述:USB 通信的底层逻辑#

在嵌入式开发中,将设备定义为 HID (Human Interface Device) 类是实现免驱通信的最简便方法。上位机(PC)无需安装第三方驱动,直接通过调用 Windows 系统内核提供的 DLL 即可实现数据交换。

开发核心流程#

实现 C# 与 USB 设备通信的逻辑可以拆解为以下四个关键环:

  1. 设备枚举 (Enumeration):在设备丛林中通过 GUID 找到 HID 类别。
  2. 查找匹配 (Matching):根据硬件的 VID (Vendor ID)PID (Product ID) 定位目标。
  3. 握手连接 (Linking):获取设备路径,创建文件句柄。
  4. 异步通信 (I/O):开启读写线程,实现数据的非阻塞交互。

2. 引入核心 Win32 库#

由于 C# 运行在托管环境,访问底层硬件必须通过 P/Invoke (Platform Invocation) 调用系统原生的 DLL。

DLL 名称核心职责
hid.dll负责 HID 专属操作,如获取 GUID、获取设备属性(VID/PID)。
setupapi.dll负责管理 Windows 设备管理器中的设备信息集。
kernel32.dll负责最基础的 I/O 操作:CreateFileReadFileWriteFile

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. 开发避坑指南#

  1. Report ID 的坑:在调用 WriteFile 时,发送数组的第一个字节必须是 Report ID。如果你的硬件固件没有定义多个 ID,通常这个字节需要设为 0x00,且数组实际长度要比数据长度多 1。
  2. 管理员权限:访问某些底层 USB 设备可能需要程序以管理员权限运行。
  3. 垃圾回收 (GC):在使用 OVERLAPPED 结构体进行异步操作时,确保该结构体在 I/O 完成前不会被 GC 回收(可以使用 GCHandle 固定内存)。

6. 总结#

使用 C# 开发 USB 上位机,本质上是托管代码与非托管内核对象的博弈。虽然 P/Invoke 看起来繁琐,但它提供了对硬件最精准的控制能力。对于更复杂的应用,建议基于上述逻辑进一步封装成响应式的 Observable 流。

C# 进阶指南:调用 Win32 API 实现自定义 USB (HID) 设备通信
https://hw.rscclub.website/posts/csharpusb/
作者
杨月昌
发布于
2017-11-18
许可协议
CC BY-NC-SA 4.0