当前位置:

首页 > C#中的USBHID设备通信全解

C#中的USBHID设备通信全解

在C#中与USBHID设备(比如键盘、鼠标、游戏手柄)打交道,本质上是一场与Windows底层API的对话。这个过程绕不开一个关键技术:P/Invoke。它就像一座桥梁,让托管在.NET环境下的C#代码能够调用那些用C语言写成的、直接与操作系统交互的非托管函数。本文将带你走通从枚举设备、建立连接到读

在C#中与USBHID设备(比如键盘、鼠标、游戏手柄)打交道,本质上是一场与Windows底层API的对话。这个过程绕不开一个关键技术:P/Invoke。它就像一座桥梁,让托管在.NET环境下的C#代码能够调用那些用C语言写成的、直接与操作系统交互的非托管函数。本文将带你走通从枚举设备、建立连接到读写数据的完整路径,并提供可直接使用的代码示例。

C#中的USBHID设备通信全解

1. USBHID概述和C#中的实现

USBHID,即人机接口设备,是USB标准中专门为交互设备定义的一类。相比老式的串口通信,它凭借即插即用、高传输速率和低错误率,成为了许多硬件交互的首选方案。

要在C#世界里驾驭USBHID设备,核心在于理解其工作原理和数据流,并熟练运用P/Invoke技术来调用Windows API。这为后续所有操作——发现设备、打开连接、收发数据——奠定了基础。

2. P/Invoke技术与Windows API调用

2.1 P/Invoke技术简介

2.1.1 P/Invoke技术的定义

简单来说,P/Invoke是.NET框架提供的一种机制,允许托管代码(如C#)安全地调用非托管代码库(如Windows系统DLL中的C函数)。这使得开发者能够利用大量成熟且功能强大的Windows API,同时也为旧有Win32应用程序的迁移和维护提供了便利。

2.1.2 P/Invoke技术的工作原理

其核心在于桥接两个世界:受公共语言运行时(CLR)管理的托管代码,和直接在操作系统层面运行的非托管代码。P/Invoke负责处理两者之间的差异,包括内存管理、数据类型转换和异常传递,让调用过程尽可能平滑。

2.2 Windows API在C#中的调用

2.2.1 Windows API的搜索与选择

动手之前,先得找到对的“工具”。你需要明确要完成什么功能,然后去查阅官方文档(如MSDN)来确定具体的API函数、它所在的DLL库、参数类型及顺序。这一步的准确性直接决定了后续代码能否成功运行。

2.2.2 在C#中声明和使用Windows API函数

找到API后,下一步就是在C#中声明它。这主要依靠 DllImport 属性。以下是一个调用系统消息框的经典示例:

using System;
using System.Runtime.InteropServices;

class Program
{
    // 使用DllImport属性指定DLL名和入口点
    [DllImport("user32.dll", SetLastError = true)]
    static extern int MessageBox(int hWnd, String text, String caption, uint type);

    static void Main()
    {
        // 调用MessageBox API
        MessageBox(0, "Hello World!", "My Message Box", 0);
    }
}

这段代码声明了位于 user32.dll 中的 MessageBox 函数,并指定了其参数和返回类型。这样,在C#中就可以像调用普通方法一样使用它了。

2.3 P/Invoke技术的深入分析

2.3.1 数据类型转换与映射

C#和C/C++的数据类型并非总是一一对应。因此,在跨平台调用时,数据类型的正确映射至关重要。P/Invoke通过 StructLayoutMarshalAs 等属性来确保结构体布局和数据类型能够被非托管代码正确识别。

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

[StructLayout(LayoutKind.Sequential)]
struct MEMORY_BASIC_INFORMATION
{
    public IntPtr BaseAddress;
    public IntPtr AllocationBase;
    public uint AllocationProtect;
    public IntPtr RegionSize;
    public StateEnum State;
    public AllocationType Protect;
    public TypeEnum Type;
}

// 使用时
MEMORY_BASIC_INFORMATION mbi = new MEMORY_BASIC_INFORMATION();
// ...调用VirtualAllocEx获取信息...
VirtualQueryEx(hProcess, IntPtr.Zero, out mbi, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)));

2.3.2 异常处理与资源管理

调用非托管代码时,错误可能发生在“墙的另一边”。P/Invoke提供了 Marshal.GetLastWin32Error() 方法来捕获这些错误。同时,必须谨慎管理非托管资源(如文件句柄、内存指针),通常使用 try-finallyusing 模式来确保资源被正确释放,避免内存泄漏。

IntPtr handle = IntPtr.Zero;
try
{
    handle = CreateFile("somefile.txt", GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
    if (handle == INVALID_HANDLE_VALUE)
    {
        int errorCode = Marshal.GetLastWin32Error();
        throw new IOException("Unable to open file", errorCode);
    }
    // ...使用文件...
}
finally
{
    if (handle != IntPtr.Zero && handle != INVALID_HANDLE_VALUE)
    {
        CloseHandle(handle);
    }
}

以上示例确保了无论文件操作是否成功,句柄最终都会被安全关闭。

3. 枚举USBHID设备的步骤

3.1 USBHID设备枚举概念

3.1.1 枚举设备的重要性

枚举,简单说就是“发现并认识”设备的过程。对于USBHID设备,这是通信的第一步。主机通过枚举获取设备的供应商ID、产品ID、设备类等关键信息,没有这一步,后续的打开和读写都无从谈起。

3.1.2 枚举过程中的关键技术点

枚举过程依赖于一系列Windows API,如 SetupDiGetClassDevsSetupDiEnumDeviceInterfaces 等。核心在于使用正确的设备接口GUID(全局唯一标识符)来定位HID设备,并妥善处理可能出现的错误,例如设备未连接或驱动异常。

3.2 枚举USBHID设备的操作步骤

3.2.1 使用Windows API枚举设备

枚举通常从获取设备信息集开始。以下代码展示了如何遍历系统中所有已连接的HID设备:

using System;
using System.Runtime.InteropServices;

class UsbHidEnumeration
{
    const int DIGCF_PRESENT = 0x2;
    const int DIGCF_DEVICEINTERFACE = 0x10;

    [DllImport("setupapi.dll", SetLastError = true)]
    static extern IntPtr SetupDiGetClassDevs(ref Guid classGuid, IntPtr enumerator, IntPtr hwndParent, int flags);

    [DllImport("setupapi.dll", SetLastError = true)]
    static extern bool SetupDiEnumDeviceInterfaces(IntPtr deviceInfoSet, IntPtr deviceInfoData, ref Guid interfaceClassGuid, int memberIndex, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData);

    static Guid HidGuid = new Guid(0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30);

    public static void EnumerateHidDevices()
    {
        IntPtr deviceInfoSet = SetupDiGetClassDevs(ref HidGuid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

        SP_DEVICE_INTERFACE_DATA deviceInterfaceData = new SP_DEVICE_INTERFACE_DATA();
        deviceInterfaceData.cbSize = Marshal.SizeOf(deviceInterfaceData);

        for (int memberIndex = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, IntPtr.Zero, ref HidGuid, memberIndex, ref deviceInterfaceData); memberIndex++)
        {
            // 处理每一个找到的设备接口
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct SP_DEVICE_INTERFACE_DATA
    {
        public int cbSize;
        public Guid InterfaceClassGuid;
        public int Flags;
        public IntPtr Reserved;
    }
}

代码首先通过 SetupDiGetClassDevs 获取HID设备列表,然后循环调用 SetupDiEnumDeviceInterfaces 遍历每个设备接口。

3.2.2 枚举设备时的常见问题及解决方案

枚举失败通常有几个原因:驱动问题、权限不足或物理连接故障。对应的解决思路也很直接:

  • 驱动问题:确保设备驱动程序已正确安装且为最新版本。
  • 权限问题:尝试以管理员身份运行程序。
  • 连接问题:检查USB线缆和端口,尝试重新插拔或更换端口。

在代码层面,务必检查API调用的返回值,并通过 GetLastError 获取详细错误码,这是调试的关键。

if (deviceInfoSet == IntPtr.Zero)
{
    int errorCode = Marshal.GetLastWin32Error();
    throw new Exception("无法获取设备信息集。错误代码: " + errorCode);
}

4. 打开和关闭USBHID设备的方法

成功枚举到设备后,下一步就是与之建立连接(打开)并在完成后妥善断开(关闭)。

4.1 打开USBHID设备的步骤

4.1.1 设备句柄的获取

打开设备的本质是获取一个代表该设备的句柄。在Windows中,这通常通过 CreateFile 函数完成,尽管它的名字看起来像是用于文件操作。

using System;
using System.Runtime.InteropServices;

class UsbHidDevice
{
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr CreateFile(
        string lpFileName,
        uint dwDesiredAccess,
        uint dwShareMode,
        IntPtr lpSecurityAttributes,
        uint dwCreationDisposition,
        uint dwFlagsAndAttributes,
        IntPtr hTemplateFile);
    // USB设备的路径和访问权限等参数
    private static readonly string DevicePath = "\\.\HIDI001";
    private static readonly uint GenericRead = 0x80000000;

    public static IntPtr OpenDevice()
    {
        IntPtr deviceHandle = CreateFile(
            DevicePath,
            GenericRead,
            0,
            IntPtr.Zero,
            0x3, // OPEN_EXISTING
            0,
            IntPtr.Zero);
        if(deviceHandle == IntPtr.Zero || deviceHandle == new IntPtr(-1))
        {
            throw new System.ComponentModel.Win32Exception();
        }

        return deviceHandle;
    }
}

4.1.2 打开设备时的异常处理

打开操作可能因设备被占用、路径错误或权限问题而失败。使用 try-catch 块来捕获并处理这些异常是保证程序健壮性的必要措施。

try
{
    IntPtr deviceHandle = UsbHidDevice.OpenDevice();
    // 使用设备句柄进行后续操作
}
catch (System.ComponentModel.Win32Exception ex)
{
    // 处理异常情况,例如用户没有足够的权限或设备不存在
    Console.WriteLine($"Error opening device: {ex.Message}");
}

4.2 关闭USBHID设备的策略

4.2.1 设备关闭的条件和时机

一个良好的习惯是:在程序结束或不再需要设备时,立即关闭其句柄以释放系统资源。通常放在 finally 块或 Dispose 方法中执行。

// 在程序结束前关闭设备句柄
if(deviceHandle != IntPtr.Zero)
{
    CloseHandle(deviceHandle);
}

4.2.2 关闭设备前的清理工作

关闭前,应确保所有进行中的数据操作(如读写)已经完成或已被妥善取消。这可以避免数据丢失或设备状态异常。记录关闭日志也有助于后续的问题追踪。

// 示例代码,确保设备已断开所有连接
if( // 设备正在传输数据的条件判断)
{
    // 等待数据传输完成或取消传输
}

5. 发送和接收数据的机制

与设备建立连接后,核心任务就是数据交换。理解USBHID的数据包结构和通信模式是关键。

5.1 数据通信的基本概念

5.1.1 数据包的结构和格式

USBHID设备通过“报告”来传输数据,报告的结构由设备固件中的报告描述符定义。主要分为三类:

  • 输入报告:从设备流向主机,例如键盘按下的键值、鼠标移动的坐标。
  • 输出报告:从主机发送到设备,例如设置键盘背光、控制游戏手柄震动。
  • 特征报告:用于双向配置,通常用来获取或设置设备的特定属性。

5.1.2 数据传输的同步和异步方式

数据传输有两种模式:

  • 同步传输:调用会阻塞当前线程,直到数据传输完成。简单直接,但可能影响程序响应。
  • 异步传输:调用后立即返回,数据传输在后台进行。效率更高,但编程模型相对复杂。

5.2 实现数据发送和接收的策略

5.2.1 利用Windows API实现数据通信

在C#中,读写数据主要依靠 ReadFileWriteFile 这两个API。以下是一个简化的类结构展示:

public class HidDevice
{
    public const int HidImports.HidGuid = 0x4d1e55b2, 0xafa6, 0x4fad, 0x9c, 0x85, 0x19, 0xb8, 0x80, 0xee, 0xca, 0x8b;

    [DllImport("hid.dll")]
    private static extern bool HidD_GetAttributes(IntPtr HidDeviceObject, out HidAttributes Attributes);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode,
        IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes,
        IntPtr hTemplateFile);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool ReadFile(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead,
        out uint lpNumberOfBytesRead, IntPtr lpOverlapped);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite,
        out uint lpNumberOfBytesWritten, IntPtr lpOverlapped);
    ...
}

5.2.2 数据传输中的错误检测与处理

ReadFileWriteFile 会返回布尔值指示成功与否。失败时,应立即调用 Marshal.GetLastWin32Error() 获取错误码。常见的错误包括设备意外移除、缓冲区不足或访问被拒绝。健全的错误处理逻辑是确保应用程序稳定性的最后一道防线。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
相关文章 更多
联想推出150W充电头:3个USB-C接口、配三脚插脚,339元
联想推出150W充电头:3个USB-C接口、配三脚插脚,339元

联想推出150W充电头,配备3个USB-C接口,总输出功率150W,单口最高140W。采用三脚插头接地更安心,支持PD3.2、PPS、QC3.0等主流协议,兼容联想及摩托罗拉私有快充。定价339元,附赠7A/240W充电线。

绿联推出 45W“小冰片”单 USB-C 接口轻薄充电头:12.5mm 厚度,109 元
绿联推出 45W“小冰片”单 USB-C 接口轻薄充电头:12.5mm 厚度,109 元

绿联推出“指尖湃45W‘小冰片’”轻薄充电头,单USB-C接口,厚度仅12.5mm,银灰配色,可折叠插脚,最大输出45W,售价109元。极致轻薄便携,适合为笔记本、平板或手机应急快充。

GPD G2 显卡扩展坞预售:MCIO + USB4 双口,内置 800W 电源售价 1800 元
GPD G2 显卡扩展坞预售:MCIO + USB4 双口,内置 800W 电源售价 1800 元

GPDG2显卡扩展坞现已开启预售,售价1800元,7月30日付尾款。其配备MCIO8i与USB42.0双接口,可提供高达256Gbps的全对称双向带宽。内置800W金牌全模组电源,支持ATX3.1标准,当连接RTX4090显卡时性能损耗仅约2%。该产品专门为掌机或迷你主机用户设计。

飞傲 FG3 头戴式游戏耳机明日开售:50mm+16mm 双动圈、USB/3.5mm 双输入接口,399 元
飞傲 FG3 头戴式游戏耳机明日开售:50mm+16mm 双动圈、USB/3.5mm 双输入接口,399 元

飞傲FG3游戏耳机售价399元,明日上午10点开售。采用50mm+16mm同轴双动圈,支持USB/3.5mm双输入。内置DAC耳放,支持虚拟7.1音效及可拆卸降噪麦克风,多平台兼容。

小米新款金沙江磁吸充电宝10000mAh获认证:新增USB-C自带线
小米新款金沙江磁吸充电宝10000mAh获认证:新增USB-C自带线

8月19日消息,就在昨天,小米一款型号为“WPB1025”的移动电源新品顺利通过了国家CCC质量认证,据推测,这极有可能是小米新款金沙江磁吸充电宝10000mAh版。据了解,小米目前售卖的金沙江磁吸充电宝10000mAh版本于今年5月上市,那时就已按照2026年新国标进行设计,并且顺利通过了各项安全

制造商倍魔推出 100W USB-C“AI 语音数据线”,号称可以声控空调
制造商倍魔推出 100W USB-C“AI 语音数据线”,号称可以声控空调

倍魔发布100WUSB-CAI语音数据线,长约1.5米,支持100W快充与USB2.0传输,内置麦克风,可通过唤醒词声控空调等家电,价格约22-29元,兼具充电、数据传输与语音控制功能。

Club3D 推出全球首款无源 9 米长 USB4 线:20Gbps 带宽、支持 100W PD
Club3D 推出全球首款无源 9 米长 USB4 线:20Gbps 带宽、支持 100W PD

Club3D发布全球首款9米无源USB4线缆,无需芯片或供电,实现20Gbps带宽、100WPD充电,支持4K@240Hz或8K@60Hz视频,适用于会议室、VR及扩展坞等长距离高带宽场景。

Club3D推出全球首款无源9米长USB4线:20Gbps带宽
Club3D推出全球首款无源9米长USB4线:20Gbps带宽

Club3D推出全球首款9米无源USB4线缆,无需外部供电即可实现20Gbps带宽,支持100W快充、4K@240Hz及8K@60Hz视频传输,满足长距离高带宽需求。

Win11怎么解决USB传输速度慢
Win11怎么解决USB传输速度慢

在 Win11 里遇到 USB 传文件速度突然掉下来,别急着把问题全算到 U 盘身上。很多时候,症结反而出在系统侧,最常见的就是两条:一条是设备策略还停留在偏保守的模式,另一条是 USB 控制器被节电策略限制了性能。先把这两处排查并处理好,再拿同一批文件做一次回测,通常比上来就重装驱动更能直接看出效

using namespace 使用中遇到的问题怎么解决
using namespace 使用中遇到的问题怎么解决

命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件
macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

Mac软件 更多
灵活计算器
灵活计算器
macOS/iOS/Android

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师
macOS

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

WINDOWS 更多
Windows 10
Windows 10
Windows

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式
Windows/macOS/Linux

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘
Windows/macOS/iOS/Android

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。