跳转到主要内容
杨月昌的硬件博客
返回

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?

缺点:代码量较大、错误处理繁琐 → 下文提供完整封装解决。


二、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 库。

核对清单



上一篇
电源防护设计必读:工业车载级防反接、浪涌抑制与EMI完整指南
下一篇
硬件开发实战:双三极管镜像电路打造“零压降”理想二极管防护方案