尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#上位机USB通信实战:使用LibUsbDotNet实现设备读写

C#上位机USB通信实战:使用LibUsbDotNet实现设备读写 做上位机的人迟早会撞上USB设备通信这道坎。不管是接一个定制的数据采集器、一个工业读卡器还是某个传感器的调试工具串口不够用、HID太受限的时候就得直接跟USB设备本身对话。C#配合LibUsbDotNet是目前这类需求里上手成本最低的方案之一它把libusb那套C接口封装成了C#能直接调用的类库不需要自己写驱动也不需要啃几百页的USB协议文档只要搞清楚几个核心类就能跑通收发流程。这篇文章我从实际开发的角度把整个流程拆开讲清楚库的选型理由、环境准备、USB基础概念速补、完整代码示例、驱动坑点、多线程收发模型以及我在项目里踩过的典型问题。代码全部是可以在Visual Studio里直接编译运行的完整版本用的NuGet包版本也是当前稳定的。1. 为什么用LibUsbDotNet而不是其他方案1.1 USB通信开发的几条常见路径做USB通信在Windows下其实有好几条路可以走不同路线差别很大。最传统的串口方案本质上走的是USB转串口芯片比如FT232、CH340操作系统把它虚拟成一个COM口你直接用SerialPort类操作就行。这个方案最简单但前提是设备端得有一颗串口转接芯片或者固件本身就是按CDC类实现的。很多定制设备并不是这种架构或者传输速率要求高、需要批量传输这时候串口方案直接出局。HID方案是另一条路。Windows对HID设备支持很友好不需要装驱动C#里用HidLibrary或者Windows.Devices.HumanInterfaceDevice也能做。但HID的传输带宽很小中断传输的包大小被限制在64字节以内全速设备而且很多设备根本不是HID类这条路也走不通。再往上就是WinUSB。微软提供WinUSB.sys作为通用驱动配合WinUSB API可以做控制传输和批量传输但原始API是C风格的C#调用要自己写一堆P/Invoke定义工作量大不说还容易踩内存布局的坑。LibUsbDotNet就是在这个背景下显得特别顺手的。它是对libusb开源库的C#封装屏蔽掉了底层P/Invoke细节API设计也比较贴近直觉。从设备查找到打开、声明接口、执行控制传输、批量收发整个流程的逻辑非常清晰。你要做的只是通过NuGet把包拉进来然后写业务逻辑驱动层面的交互它已经替你处理好了。1.2 LibUsbDotNet和libusb的关系LibUsbDotNet底层有两个后端可以选一个是调用系统自带的WinUSB驱动另一个是使用libusb-win32的驱动程序。这两种方式各有适用场景但绝大多数使用场景下只要设备通过Zadig或者其他驱动工具装好了WinUSB驱动LibUsbDotNet就能直接工作。值得一提的是LibUsbDotNet是跨平台的。它在Windows、Linux、macOS上都可以运行Linux下走的是libusb的Linux实现。不过在实际项目中绝大多数人的目标平台是Windows所以下文主要围绕Windows场景展开。1.3 什么时候不该用LibUsbDotNet任何方案都有边界。如果你的设备已经被系统识别为虚拟串口即设备管理器里能看到COM口那就直接用SerialPort类不要折腾LibUsbDotNet。如果你的设备是标准HID设备比如普通键鼠、HID读卡器也用不上它。只有当设备是自定义USB设备或者厂商SDK不好用、你需要在没有官方SDK的情况下直接和设备通信时LibUsbDotNet才是真正值得上场的主角。另外如果你对USB协议完全没概念建议至少先花半小时了解什么是端点、接口、批量传输否则下面的代码示例你可能看得懂调用但出了问题完全不知道从哪里排查。USB协议本身不复杂但它的分层结构绕不开。2. 环境准备与USB基础概念补课2.1 NuGet安装和项目配置创建项目这一步没什么好纠结的用控制台应用或者WinForms都行。如果是测试代码新建一个.NET Framework或者.NET 6的控制台程序都可以。注意libusbdotnet这个包名在NuGet上有一个比较老的版本还有一个是后来的维护版本LibUsbDotNet.LibUsbDotNet建议直接装后者稳定性和API完整性更好。安装命令Install-Package LibUsbDotNet.LibUsbDotNet装完之后项目里会多出几个核心命名空间using LibUsbDotNet; using LibUsbDotNet.Main; using LibUsbDotNet.Info; using LibUsbDotNet.LibUsb; using LibUsbDotNet.WinUsb;需要特别注意的一点Windows下访问USB设备通常需要管理员权限。如果你用WinForms调试建议给项目添加app.manifest把requestedExecutionLevel设为requireAdministrator否则可能遇到设备打开失败或者枚举不到设备的情况。当然并不是所有设备都需要管理员权限这取决于设备的驱动配置和操作系统的USB权限策略但提前加上可以省掉很多莫名其妙的坑。2.2 必须搞懂的USB四个概念不管怎么写代码USB逻辑结构是绕不开的。这里我用最直白的话把关键概念说清楚。设备描述符Device Descriptor是USB设备的“身份证”里面包含VID、PID、设备类等信息。VID是厂商IDPID是产品ID这两个值组合起来基本可以唯一定位一款设备。代码里查找设备就是靠VID/PID来过滤的。配置描述符Configuration Descriptor描述设备的一种配置模式。大多数设备只有一个配置你要做的就是使用默认配置一般不需要手动切换。接口Interface是设备内部的功能单元。一个设备可能有多个接口比如一个音频设备有音频输入接口还有音频控制接口。通信之前必须先声明Claim对应的接口告诉驱动“我要占用这个接口了”。端点Endpoint是实际数据传输的通道。每个接口下面有若干个端点端点号带方向信息IN端点用于设备到主机OUT端点用于主机到设备。批量传输、中断传输、同步传输都发生在端点上。你在代码里要做的很大一部分工作就是找到正确的端点号然后往里面写数据或者从里面读数据。2.3 驱动问题设备打不开的元凶很多人的LibUsbDotNet代码写得完全正确但运行时报“设备打开失败”或者一直在等待根本原因是驱动不对。默认情况下Windows会把大多数自定义USB设备识别为未知设备或者装上一个不合适的驱动。LibUsbDotNet要和设备通信通常需要设备绑定WinUSB驱动或者libusb-win32驱动。实际项目中最常用的办法是使用Zadig这个驱动安装工具开源、免费网上很容易找到把设备的驱动替换为WinUSB。操作流程是打开Zadig在菜单里选择Options - List All Devices然后在下拉框里找到你的目标设备选择WinUSB作为目标驱动点击Replace Driver或者Install Driver按钮。整个过程大概一分钟。但要注意替换驱动之后系统将不再把它识别为原来的设备类别。如果你的设备之前被识别成虚拟串口换驱动后COM口会消失。这是一个很重要的取舍点后面会详细讲。注意Zadig只是开发阶段的辅助工具如果产品最终要交付给用户使用需要走驱动签名的流程或者使用厂商提供的驱动安装包。小批量内部工具的话Zadig换来WinUSB驱动基本够用。3. 完整代码示例从枚举设备到数据收发3.1 第一步通过VID/PID枚举要操作的设备代码的第一步是找到目标设备。这里用一个最常见的方式通过VID和PID构造UsbDeviceFinder对象然后让UsbDevice.Open方法返回匹配的设备实例。using System; using LibUsbDotNet; using LibUsbDotNet.Main; using LibUsbDotNet.WinUsb; class UsbCommunication { // 替换成目标设备的实际VID和PID private const int VendorId 0x1234; private const int ProductId 0x5678; private UsbDevice _usbDevice; public bool FindAndOpenDevice() { UsbDeviceFinder usbFinder new UsbDeviceFinder(VendorId, ProductId); try { _usbDevice UsbDevice.Open(usbFinder); if (_usbDevice null) { Console.WriteLine(未找到匹配的设备请检查VID/PID是否正确设备是否已连接。); return false; } Console.WriteLine(设备已找到并打开。); return true; } catch (Exception ex) { Console.WriteLine($打开设备时发生异常: {ex.Message}); return false; } } }这段代码的关键是UsbDeviceFinder这个类。它除了支持用VID/PID查找还支持通过设备序列号、设备名称等条件查找。如果一个机器上插了多台同型号设备只靠VID/PID就不够了这时可以把序列号作为筛选条件UsbDeviceFinder usbFinder new UsbDeviceFinder(VendorId, ProductId, A1B2C3);第三个参数就是序列号字符串。这个方法在批量调试多个同型号设备时非常实用。枚举阶段经常遇到的情况是Open之后拿到null。原因通常是权限不够、驱动没装对、或者设备被其他程序独占。排查思路是先用设备管理器确认设备状态是否为正常的再确认驱动是否为WinUSB最后检查当前进程是否以管理员权限运行。3.2 第二步打开设备后获取配置并声明接口设备打开不等于可以收发数据。在真正读写之前还有一关要过声明接口。public bool ClaimInterface() { if (_usbDevice null) return false; // LibUsbDotNet针对WinUSB设备的特殊处理 if (_usbDevice.IsOpen) { // 首先获取设备的配置信息 UsbConfigInfo configInfo _usbDevice.Configs[0]; foreach (UsbInterfaceInfo interfaceInfo in configInfo.InterfaceInfoList) { Console.WriteLine($发现接口: {interfaceInfo.Descriptor.InterfaceID}); } // 声明第一个接口 bool success _usbDevice.ClaimInterface(0); if (success) { Console.WriteLine(接口声明成功。); return true; } Console.WriteLine(接口声明失败。); } return false; }需要注意ClaimInterface的参数是接口编号而不是索引。大多数设备的接口编号从0开始但有些复合设备可能会从1或者更高的数字开始。单纯看代码找不到答案时可以借助UsbTreeView这类USB抓包工具查看设备实际的接口编号。声明接口的意思可以理解成“向系统申请使用权”。一个接口在Windows中同一时间通常只能被一个进程占用。如果你的调试程序打开设备后没有释放接口第二次运行程序时就会失败——这算是写USB上位机时最常见的操作失误之一。3.3 第三步控制传输和批量传输的实现USB的传输方式分好几种最常用的两种是控制传输和批量传输。控制传输用于发送命令、读取设备状态等小数据量操作批量传输用于大数据块的传输比如读取图像数据、传感器采样数据等。LibUsbDotNet里控制传输通过ControlTransfer方法完成。它接收一个UsbSetupPacket参数这个参数里包含了bmRequestType、bRequest、wValue、wIndex、wLength等字段。这些字段的含义在USB协议文档里都有这里直接看代码public bool SendControlCommand(byte request, ushort value, ushort index, byte[] data) { if (_usbDevice null || !_usbDevice.IsOpen) return false; // 构造控制传输包 UsbSetupPacket setupPacket new UsbSetupPacket( 0x40, // bmRequestType: 0x40表示主机到设备方向类型为厂商自定义 request, value, index, (ushort)data.Length); int transferred 0; bool success _usbDevice.ControlTransfer(ref setupPacket, data, data.Length, out transferred); if (success transferred data.Length) { Console.WriteLine(控制命令发送成功。); return true; } Console.WriteLine($控制命令发送失败实际传输字节数: {transferred}); return false; }bmRequestType这个字段是很多新手看不懂的地方。它由三个部分组成传输方向bit70表示主机到设备1表示设备到主机、请求类型bit6-50标准、1类、2厂商、接收方bit4-00设备、1接口、2端点、3其他。0x40拆开来看bit7为0bit6-5为2bit4-0为0含义是“主机向设备发送厂商自定义请求”。这是一个非常常用的组合。批量传输的读写更直接。拿到设备的读写端点然后调用Read和Write方法就行。但端点不是直接通过属性拿的需要从活动配置里找public bool BulkTransfer(byte[] sendBuffer, byte[] receiveBuffer, int timeout 1000) { if (_usbDevice null || !_usbDevice.IsOpen) return false; // 获取活动配置下的读写端点 UsbEndpointReader reader _usbDevice.OpenEndpointReader(ReadEndpointID.Ep01); UsbEndpointWriter writer _usbDevice.OpenEndpointWriter(WriteEndpointID.Ep01); // 设置读写超时 reader.ReadTimeout timeout; writer.WriteTimeout timeout; int bytesWritten 0; int bytesRead 0; // 发送数据 ErrorCode writeError writer.Write(sendBuffer, timeout, out bytesWritten); if (writeError ! ErrorCode.None) { Console.WriteLine($写入失败: {writeError}); return false; } // 接收数据 ErrorCode readError reader.Read(receiveBuffer, timeout, out bytesRead); if (readError ! ErrorCode.None) { Console.WriteLine($读取失败: {readError}); return false; } Console.WriteLine($发送 {bytesWritten} 字节接收 {bytesRead} 字节。); return true; }端点枚举值比如ReadEndpointID.Ep01、WriteEndpointID.Ep01这些不是随便写的。每个设备的端点号都可能不同需要根据设备的接口描述符确定。查找的方法在网上一般都叫“USB设备描述符查看器”或者直接在LibUsbDotNet的库demo程序里跑一下它会打印出设备的所有端点信息。3.4 第四步释放资源的正确姿势USB设备和文件很像用完不关会导致下一次打不开。很多人在调试时遇到过“程序第一次运行正常第二次再不成功”的问题90%都是上一个进程没有正确释放USB资源。public void CloseDevice() { if (_usbDevice ! null) { if (_usbDevice.IsOpen) { // 释放接口 _usbDevice.ReleaseInterface(0); } _usbDevice.Close(); _usbDevice null; Console.WriteLine(USB设备已关闭并释放。); } }这里的顺序是有讲究的先释放接口再关闭设备。如果反过来可能造成驱动层面状态残留。另外如果程序意外崩溃接口可能不会被释放此时需要重新插拔设备或者在设备管理器里禁用再启用设备才能恢复。所以开发阶段最好在finally块里确保调用CloseDeviceUsbCommunication usbComm new UsbCommunication(); try { if (usbComm.FindAndOpenDevice() usbComm.ClaimInterface()) { // 业务逻辑 } } finally { usbComm.CloseDevice(); }这个习惯建议从一开始就养成能省掉很多调试时间。4. 常见问题与排查技巧实录4.1 设备打开失败或返回null这个问题排第一。检查顺序如下先确认VID/PID完全正确这里的大小写不影响但进制转换容易出错。比如某设备标注的PID是十六进制的0x1A2B你不能在代码里写十进制的1A2B——不C#里0x前缀表示十六进制所以要用UsbDeviceFinder(0x1234, 0x1A2B)这个出错的概率很小但确实有人会把十六进制当十进制用。然后确认Zadig已经把驱动换成了WinUSB。设备管理器里如果设备显示为Unknown Device说明驱动没装好如果显示的是一个带USB图标的WinUSB设备那就对了。最后确认你的程序是否以管理员权限运行。这个在上面已经提过不再赘述。4.2 打开设备后读取超时代码里设置了1000毫秒超时但实际跑的时候总是超时。首先要排查的是端点号对不对。很多设备有多个IN端点你打开的是Ep01但设备实际往Ep02发数据那肯定读不到。其次USB批量传输和TCP不一样它没有“分帧”的概念每一次Read读取的数据量完全取决于设备固件发送的数据包大小。如果你的接收buffer是4096字节但设备一次只发32字节Read方法仍然会返回32字节这个行为本身没有问题。反而如果设备发送的数据长度超过了buffer大小超额部分会被丢弃必须调整buffer大小或者循环读取。最后确认设备是否需要先发送一个请求命令才开始往外吐数据。很多设备设计成“主机请求一次设备响应一次”的半双工模式你上来就Read设备当然不搭理你。4.3 关闭USB之后原来的虚拟串口打不开了这个问题很有代表性。有些设备本身就是USB转串口芯片出厂驱动是系统自带的CDC串口驱动在设备管理器里显示为一个COM口。为了用LibUsbDotNet做更底层的通信你通过Zadig把驱动换成了WinUSB。换了之后COM口消失了串口自然打不开——这是预期行为。还有一种情况是你的设备不是一个单纯设备而是复合设备一部分接口是CDC串口接口另一部分是厂商自定义接口。你把整个设备都替换成了WinUSB驱动系统就不会再枚举出COM口了。解决办法是换驱动时只针对特定的接口而不是整个设备但Zadig对接口级别的驱动替换支持有限或者提前通过.GetConfigDescriptor获取设备接口信息确认是不是复合设备。如果出现“LibUsbDotNet关闭USB后USB上的串口还是无法打开”这种报错常见原因反而很简单你的程序在使用LibUsbDotNet打开设备之后没有正确释放或者释放了但系统还在处理驱动交接状态。这时先确认程序退出后设备管理器里设备是否处于正常状态再重新插拔设备让驱动重新加载。注意不要把LibUsbDotNet和SerialPort两个组件同时打开同一个设备。一个设备的接口只能被一种驱动方式绑定交叉访问后会造成驱动冲突表现为无法打开、卡死或者打开后数据全乱。4.4 程序第一次跑没问题第二次就设备打不开这种问题基本可以断定是释放不彻底。排查方式很简单在第二次运行前打开设备管理器看设备的图标上是否有一个向下的小箭头表示该设备被禁用。如果有那就是上一个进程异常退出驱动把设备标记为故障。解决办法除了重新插拔之外也可以通过代码里加上异常处理尽量保证ReleaseInterface被调用。另外一个容易被忽视的点是如果你在调试器中强制停止程序比如在Visual Studio里点停止调试finally块里的代码并不会执行USB资源就漏掉了。调试USB程序时建议把退出代码放到控制台程序的正常退出流程里尽量用控制台窗口输入按回车退出的方式结束进程而不是直接强制停止。4.5 常见问题速查表现象可能原因解决方向Open返回nullVID/PID错误、驱动不对、权限不足检查设备管理器用Zadig换WinUSB驱动管理员运行设备打开了但ClaimInterface失败接口编号不对、接口被其他进程占用用工具查看接口编号关闭占用程序写入返回AccessDenied设备已被其他进程打开关闭其他进程释放设备读取超时端点号错误、设备未进入发送状态、buffer太小确认端点号先发请求命令调大buffer程序第二次运行失败资源未释放确保finally里执行释放必要时重新插拔设备虚拟串口无法打开驱动被换成WinUSB导致串口接口消失检查复合设备结构重新安装CDC驱动5. 多线程收发的实战经验5.1 为什么需要一个独立的接收线程USB通信是阻塞式的。Read方法在没有数据时不会立刻返回它会等待超时时间耗尽。如果直接在UI线程或者主流程里调用Read界面就会卡死。这是很多新手写上位机时遇到的第一个真问题。正确做法是启动一个后台线程专门负责循环读取USB数据解析完数据之后通过事件、队列或者线程安全集合把结果传递给UI线程。private CancellationTokenSource _cts; private Thread _readThread; private readonly object _dataLock new object(); private Queuebyte[] _receivedQueue new Queuebyte[](); public void StartReading() { _cts new CancellationTokenSource(); _readThread new Thread(ReadLoop); _readThread.IsBackground true; _readThread.Start(); } private void ReadLoop() { byte[] buffer new byte[4096]; while (!_cts.IsCancellationRequested) { int bytesRead 0; ErrorCode readError _reader.Read(buffer, 1000, out bytesRead); if (readError ErrorCode.None bytesRead 0) { byte[] data new byte[bytesRead]; Array.Copy(buffer, data, bytesRead); lock (_dataLock) { _receivedQueue.Enqueue(data); } } else if (readError ! ErrorCode.Timeout readError ! ErrorCode.None) { // 出现真正的错误退出发送循环 break; } } } public void StopReading() { _cts.Cancel(); _readThread?.Join(2000); }这个模型的好处是接收循环只负责“拿数据”数据具体怎么处理由业务层决定。队列本身用了lock加锁能保证多线程环境下的读写安全。在真实项目中我通常还会把队列改成有界队列防止设备疯狂发数据时导致内存暴涨。5.2 发送操作也要防止并发冲突USB设备的Write操作并不是线程安全的。如果多个线程同时往同一个端点写数据驱动层面会发生竞争轻则数据错乱重则直接抛异常。实际项目里建议对发送操作加锁或者用一个发送线程配合发送队列来处理。private readonly object _writeLock new object(); public bool WriteData(byte[] data) { lock (_writeLock) { int bytesWritten; ErrorCode writeError _writer.Write(data, 2000, out bytesWritten); return writeError ErrorCode.None bytesWritten data.Length; } }不要小看这个锁。我见过因为多个定时器同时触发发送而导致设备崩溃重启的案例。USB设备并没有你想象中那么健壮主机的并发访问对它们来说是致命的。5.3 C#上位机里与UI交互的注意点如果你用WinForms或WPF做界面后台线程读到的数据不能直接赋值给控件的Text属性会抛跨线程访问异常。需要用到Invoke或者同步上下文。简单的做法是封装一个事件在UI线程订阅这个事件事件内部再更新控件。public event Actionbyte[] DataReceived; private void ReadLoop() { // 在读取到数据后触发事件 OnDataReceived(data); } private void OnDataReceived(byte[] data) { DataReceived?.Invoke(data); }UI那边可以这样写usbComm.DataReceived data { if (this.InvokeRequired) { this.BeginInvoke(new Action(() UpdateTextBox(data))); } else { UpdateTextBox(data); } };这套模式在工作里用了很多年稳定可靠。如果用的还是.NET Framework早期版本InvokeRequired是标准的写法如果用的.NET Core/5可以考虑用SynchronizationContext或者IProgress 后者在语义上更干净。不过Invoke方案仍然是最直观、最容易被接受的实现方式。6. 开发环境调试技巧和扩展思路6.1 用好USB抓包工具写USB上位机光靠调试代码有时不够。尤其是设备返回的数据不符合预期时你根本不知道是设备的问题还是你的代码解析错误。这个时候USB协议分析工具就很有用。Windows下最常用的是USBlyzer和USBPcap两者都有抓包能力。Linux下可以用Wireshark配合usbmon模块。抓包能让你看到URB层面的所有数据交换包括设备实际返回的字节流、每个请求的状态码。遇到玄学问题时抓包是终极大杀器。Phase注意抓包工具不像Zadig那样对设备有侵入性一般是安全的。但运行抓包工具时会增加系统USB栈的负担调试完成后建议退出不影响设备正常使用。6.2 从业务角度设计上位机的通信协议纯粹的USB收发只是通信通道真正决定系统稳定性的是你定义的应用层协议。建议在做任何功能之前先把协议设计好帧头用什么字段、长度字段占几个字节、校验用CRC16还是累加和、超时重发多少次。这些看似琐碎的决策直接决定了后续联调要花多少时间。我的习惯是把一个完整通信帧设计成下面这样帧头2字节固定为0xAA 0x55命令字1字节区分不同功能数据长度2字节小端序表示数据域长度数据域N字节实际业务数据校验码1字节对数据域做累加和保证完整性LibUsbDotNet只负责把这一帧数据原样发出去或者收回来解析和组包的逻辑你需要在C#代码里自己实现。这个设计的好处是协议层和传输层彻底解耦驱动换了、传输方式换了业务代码不用动。6.3 从同步收发到异步收发的演进示例代码里用的是同步读写做简单应用足够了。但如果你的设备流量比较大或者同时有多路数据要处理可以考虑把读写改成异步方式配合async/await使用避免线程阻塞。LibUsbDotNet本身对异步支持比较有限实际项目中我更推荐的做法是维护一收一发两个线程中间用队列解耦。这个模型理解起来简单调优空间也大遇到瓶颈时可以按字节数批量、减少日志打印等方式去优化。如果你准备把这个方案用于生产环境建议把设备打开、接口声明、端点获取、数据收发、异常处理完整地封装成一个单独的类把业务逻辑、界面逻辑和通信逻辑分离清楚。很多同学喜欢把USB操作直接写在窗体代码里刚开始看着方便后面想复用就非常痛苦。6.4 结合C#生态做扩展LibUsbDotNet只是USB通信这一环。在这个基础上你可以很容易地把数据接到其他C#生态组件里比如用Serilog记录通信日志、用NModbus4做Modbus协议层解析、用LiveCharts绘制实时波形、用DBHelper把采集数据落到数据库甚至可以结合MVCamera之类的相机SDK做图像采集。USB通道跑通之后整个上位机系统就活了。我在一个工业项目里就是用它接一个定制的传感器采集盒批量数据直接进数据库UI层用WinForms展示实时曲线。项目跑了两三年除了设备本身固件升级过几版通信层基本没有动过。这说明只要底层方案选对上层的稳定性就有保障。7. 个人实操过程中的一些体会做了这么多年的USB上位机开发说几个最实在的体会。第一先搞定驱动再写代码。很多人一上来就写代码写完发现设备打不开排查半天发现是驱动没换顺序反了。先花几分钟用Zadig把WinUSB驱动装好后面省下的时间以小时计。第二保存好设备的描述符信息。第一次拿到设备时花几分钟写一个小工具把设备的VID、PID、接口列表、端点方向、端点号、端点包大小全部打印出来。这些信息在你写代码时是刚需临到用时再到处翻文档很痛苦。第三异常处理一定要全面。USB通信比串口通信更容易受环境影响——线缆接触不良、设备供电不足、驱动被Windows更新覆盖都会导致通信中断。代码里对所有USB调用做好异常捕获和状态恢复生产环境靠得住。第四注意多线程资源释放。程序退出时要确保接收线程结束、设备接口释放、设备关闭。很多售后问题其实都是资源没释放、第二天设备就联不上了。希望这份手把手的教程能帮你顺利走通C#和LibUsbDotNet的USB设备通信之路。代码都在上面了剩下的就是插上你的设备改掉VID和PID真正跑起来。
返回列表