news 2026/9/10 4:09:17

Android UsbHost与PC libusb双向通信实现字符与文件传输

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android UsbHost与PC libusb双向通信实现字符与文件传输

简介:面向Android开发者的USB双向通信完整工程资源,解决APP与PC之间通过USB进行字符和文件传输的需求,涵盖USB Host/Device模式原理、权限声明、设备热插拔监听、端点读写等核心环节。压缩包内共819个文件,其中256个JSON配置、270个FLAT构建缓存、85个XML布局与清单、8个Java源码及7个TXT说明文档,同时包含class、jar、dex、apk等编译产物,整体约15.63MB,可直接导入Android Studio分析或二次开发。已有1678人学习浏览。资源提供了USBDemo工程代码,演示从AndroidManifest权限配置、BroadcastReceiver设备监听,到UsbDeviceConnection打开输入输出流、将文件转字节数组发送的完整链路,并附带PC端使用libusb或UsbHostAPI进行对接的思路,帮助开发者快速搭建稳定的USB通信调试环境,避免踩坑。

1. 双向通信为什么要绕过 ADB

我在实际项目里遇到过这样的需求:Android 设备作为数据采集端,需要把采集到的字符指令和文件持续推送给 PC,同时 PC 也要能反向下发参数;直接用 ADB 转发也能传,但每次都要先启守护进程、再维护端口映射,传输大文件时还有明显的吞吐抖动。更麻烦的是,部分定制 ROM 会阉割 adbd 的某些服务,或者用户根本不方便开“开发者选项”。如果业务逻辑和 USB 通道耦合在一起,与其依赖 ADB 这条外部通道,不如自己在应用层直接走 USB 协议,把字符流和文件流都封装成两端可读的帧。这篇内容围绕 Android 的 UsbHost 模式和 PC 端 libusb 对称实现展开,覆盖权限申请、端点读写、PC 驱动绑定与 USB 抓包验证,适合做设备联调、硬件调试或数据同步方案的工程师参考。

2. Android 侧 USB 枚举与权限:UsbHost 模式下的入口

Android 的 USB 协议栈把“谁是主机”这件事分得很清楚。UsbManager 只服务于 UsbHost 模式,也就是 Android 设备作为 USB 主机去枚举外设;当 Android 作为 USB 设备接到 PC 时,系统权限和 API 路径完全不一样。本文以 UsbHost 为主线,PC 端用 libusb/WinUSB 配合 USB 转串口或 CDC 设备接入,这样 Android 通过 OTG 线连接 PC,既能拿到稳定的块传输(Bulk Transfer)端点,又不需要厂商私有协议。

2.1 Manifest 声明与 device_filter 定向匹配

UsbHost 模式要求 AndroidManifest.xml 里显式声明 USB 硬件特性,否则应用在部分国产 ROM 上会因为系统级权限裁剪而拿不到设备列表。常见写法:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-feature android:name="android.hardware.usb.host" android:required="true" /> <application> <activity android:name=".UsbBridgeActivity"> <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity> </application> </manifest>

android:required="true"表示应用必须运行在具备 USB 主机能力的设备上,一般接 OTG 的板子或手机都满足;如果应用还需要适配不支持 USB Host 的低端设备,应改为false并在代码里用PackageManager.hasSystemFeature判断。

device_filter.xml是 USB 设备插入时启动 Activity 的匹配条件:

<resources> <usb-device vendor-id="1027" product-id="24577" /> <usb-device class="2" subclass="2" protocol="1" /> </resources>

这里vendor-idproduct-id必须是十进制,1027 对应 0x0403,是 FTDI 的 VID,24577 对应 0x6001,是 FT232R/FT231X 常用 PID;class="2" subclass="2"对应 CDC 通信设备类。注意写错进制是新手最常见的问题,Android Studio 里看 USB 设备信息时可以顺手把十六进制转成十进制再填进去。

2.2 UsbManager 动态枚举与 VID/PID 过滤

如果是应用已经运行、USB 设备才插入的场景,不能只依赖device_filter.xml拉起 Activity,还要在代码里动态获取设备列表。UsbManager.getDeviceList()返回的是一个HashMap<String, UsbDevice>,key 是设备的系统访问名,value 里包含 VID、PID、接口数等信息:

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList(); if (deviceList.isEmpty()) { // 没有检测到任何 USB 设备 return; } for (UsbDevice device : deviceList.values()) { int vid = device.getVendorId(); int pid = device.getProductId(); if ((vid == 0x0403 && pid == 0x6001) || device.getDeviceClass() == UsbConstants.USB_CLASS_CDC_DATA) { // 匹配到目标设备后做权限申请 requestUsbPermission(device); break; } }

不要对 VID/PID 做死板的完全匹配,CDC 设备经常复用相同的接口类,靠device.getDeviceClass()更容易覆盖多个型号。拿到UsbDevice后要立即请求权限,尤其不能等用户点击某个按钮再去拿,否则部分系统会出现设备被其他应用抢占的问题。

2.3 权限申请:动态广播与授权时机

UsbManager.requestPermission()需要一个PendingIntent,系统会在用户点击授权弹窗后通过广播返回结果。实际项目里我习惯在 Activity 启动阶段就注册一个专用广播接收器,只在权限相关时机生效:

private static final String ACTION_USB_PERMISSION = "com.example.usbbridge.USB_PERMISSION"; private final BroadcastReceiver usbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (!ACTION_USB_PERMISSION.equals(action)) { return; } boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (granted && device != null) { // 授权成功,可以进入打开设备环节 openUsbDevice(device); } else { // 用户拒绝授权,或者设备已经离线 notifyPermissionDenied(); } } };

注册时注意把IntentFilter放在onResume里注册、onPause里注销,避免在权限弹窗还开着时 Activity 被回收导致广播泄漏。有些开发者会直接把PendingIntent.getBroadcast的 action 写成UsbManager.ACTION_USB_DEVICE_ATTACHED,这部分行为和权限回调是两回事,见下表:

触发源Action 常量作用
系统 USB 插入广播UsbManager.ACTION_USB_DEVICE_ATTACHED通知有设备接入
系统 USB 拔出广播UsbManager.ACTION_USB_DEVICE_DETACHED通知设备已断开
开发者自定义权限广播自定义字符串或UsbManager.ACTION_USB_PERMISSION返回用户授权结果

一定要把“设备接入广播”和“权限结果广播”分开,权限结果只能通过PendingIntent带回来,Android 官方并不支持直接同步等待授权结果。授权完成后,USB 设备的接口描述符和端点信息才允许被应用打开,这也是进入下一章读写操作的前提。

3. 端到端读写:端点选择与 bulkTransfer 线程模型

拿到 USB 权限只是第一步,真正决定双向通信质量的是接口(Interface)和端点(Endpoint)的选择。USB 设备在描述符层面由配置(Configuration)、接口、端点三级组成,Android 的 UsbManager 没有直接暴露 Configuration 级别的操作,默认使用第一个配置,这在绝大多数复合同设备够用,但你必须清楚自己操作的是哪个接口。

3.1 接口匹配与端点方向判断

一个 CDC 设备通常包含两个接口:一个用于控制消息的通信类接口,一个用于数据的 CDC Data 接口。很多开发者直接写device.getInterface(0).getEndpoint(0),这在某些国产摄像头或串口设备上会拿到 Control 端点,读写立刻失败。我一般按端点类型和方向分别筛选:

UsbInterface usbInterface = device.getInterface(1); for (int i = 0; i < usbInterface.getEndpointCount(); i++) { UsbEndpoint ep = usbInterface.getEndpoint(i); if (ep.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) { continue; } if (ep.getDirection() == UsbConstants.USB_DIR_IN) { bulkInEndpoint = ep; } else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) { bulkOutEndpoint = ep; } }

getType()过滤的是 USB 传输类型,这里用的是USB_ENDPOINT_XFER_BULK块传输类型,适合文件这种无实时性要求但吞吐量大的数据;如果传输的是鼠标键盘类指令,就要用USB_ENDPOINT_XFER_INT中断传输类型。方向里的USB_DIR_IN是相对于 Android 主机视角说的,指的是设备往 Android 方向传数据,PC 端角度看正好相反,两端联调时最容易在这个语义上绕晕。筛选完成后,用usbManager.openDevice(device)拿到连接对象,再对接口执行claimInterface,否则部分设备不会真正进入数据就绪状态:

usbConnection = usbManager.openDevice(device); if (usbConnection == null) { return; } if (!usbConnection.claimInterface(usbInterface, true)) { // 接口被占用,通常是其他应用未释放 usbConnection.close(); return; }

claimInterface的第二个参数force如果设为true,会强制拿到接口控制权,但可能影响系统其他服务,普通应用不建议开强占;设备驱动未正确 detach 时这里会失败,可以直接看系统日志里有没有Permission denied字样的内核提示。

3.2 bulkTransfer 的参数语义与超时控制

Android 在UsbDeviceConnection上提供了bulkTransfer()controlTransfer()两种同步接口。对于双向文件传输,bulkTransfer()是最直接的路径:

// 发送字符数据或文件块 byte[] outBuf = frameEncode(payload); int sent = usbConnection.bulkTransfer(bulkOutEndpoint, outBuf, outBuf.length, 3000); if (sent < 0) { // 返回负值说明总线错误或设备断连 handleUsbError(); } else if (sent != outBuf.length) { // 部分发送成功,需要把剩余字节移到数组头部继续发 resendRemaining(outBuf, sent); } // 读取 PC 端指令 byte[] inBuf = new byte[4096]; int received = usbConnection.bulkTransfer(bulkInEndpoint, inBuf, inBuf.length, 1000); if (received > 0) { dispatchFrame(Arrays.copyOf(inBuf, received)); }

bulkTransfer的超时参数单位是毫秒,0 表示永久阻塞。读操作我建议不要设太长,1000 毫秒足够覆盖绝大多数交互场景;写操作的超时和缓冲区大小强相关,当 PC 端没有及时读取时,USB 控制器会持续 NAK,Android 侧表现为写入超时,这时不要立刻判定设备故障,应该通过状态机区分“发送队列满”和“链路断开”。返回值-1是标准超时或错误,返回值-2在部分高通平台表示设备已移除,错误码含义见下表:

返回值含义处理方式
非负值实际传输字节数对比期望长度,判断是否重传
-1超时或总线错误重试前先查连接状态
-2设备已断开(部分驱动)关闭连接并触发重新枚举
0传输长度为 0检查是否发送了空帧,正常业务应忽略

3.3 读写线程与 App 生命周期解耦

USB 读写是阻塞调用,绝不能放到主线程。常见做法是建立两个独立线程,分别做读循环和写队列消费,并用一个volatile boolean running通知退出:

Thread readThread = new Thread(() -> { byte[] buffer = new byte[64 * 1024]; while (running) { int len = usbConnection.bulkTransfer(bulkInEndpoint, buffer, buffer.length, 500); if (len > 0) { // 交给业务层解析,避免在 IO 线程里做文件落盘 frameProcessor.post(buffer, len); } } }, "usb-read-thread");

读缓冲区取 64KB 是参考了块传输端点最大包长度(通常是 512 字节)与系统页缓冲大小的折中值。帧解析要放在独立队列里,否则高速传输时 IO 线程会被业务逻辑拖慢,导致底层缓冲区溢出丢帧。写线程建议用LinkedBlockingQueue做异步缓冲,UI 或业务线程只负责入队,发送线程负责出队并执行bulkTransfer,这样即使 PC 端暂时不读,Android 端 UI 也不会卡死。设备拔出时,ACTION_USB_DEVICE_DETACHED广播返回的不再是原来的UsbDeviceConnection,必须设置一个全局锁,把读线程的阻塞调用中断后统一关闭连接。

4. PC 端 libusb 接入与 USB 抓包核对

双向通信的另一半落在 PC 端。PC 端的设备接入方式取决于你用的硬件链路:如果 Android 通过 OTG 接的是一个 USB 转 UART 芯片,PC 侧看到的就是一个 COM 口;如果 Android 通过 USB Gadget 或专用 CDC 设备连接到 PC,PC 侧就需要用 WinUSB 或 libusb 直接操作端点。无论哪种路径,底层都绕不开 USB 的类描述符、端点地址和传输类型这几个要素,这一节用 libusb 生态做说明。

4.1 驱动绑定:WinUSB、libusbK 与 FT231X 的差异

Windows 下 USB 设备默认不暴露给你自己的应用,必须把设备驱动绑定到 WinUSB 或 libusbK 上。FT231X、FT232R 这类芯片官方提供 VCP 驱动,装上后 PC 只会看到虚拟串口,如果你要按自定义协议直接读端点,需要替换驱动栈,几种方案的对比:

驱动方案对应用暴露接口典型场景
厂商 VCP 驱动COM 串口快速联调,兼容串口调试助手
WinUSB 驱动USB 控制/批量端点自定义协议、双端加密数据
libusbK 驱动libusb API多平台迁移、过滤层开发
libusb 自带 WinUSB 绑定libusb API临时测试,不修改系统驱动

实际项目里,PC 端如果也使用 libusb,优先用 Zadig 把设备驱动从ft231x usb uartft232r usb uart切换为 WinUSB,但这会让 COM 口消失,需要考虑系统里其他依赖串口的工具是否受影响。稳定的做法是在驱动层做双后端:串口模式下通过pyserial通信,WinUSB 模式下通过libusb通信,接口统一抽象成open/read/write/close

4.2 用 PyUSB 在 PC 端与 Android 对收数据

Python 生态里 PyUSB 是对 libusb 的封装,可以直接读取端点做双向通信。下面的示例枚举 VID 为 0x0403 的设备,找到批量端点后发送字符并读取响应:

import usb.core import usb.util dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) if dev is None: raise ValueError("USB 设备未找到") # 释放系统自带驱动,Windows 下对应 WinUSB/libusbK dev.set_configuration() cfg = dev.get_active_configuration() intf = cfg[(0, 0)] ep_in = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN ) ep_out = usb.util.find_descriptor( intf, custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT ) # 向 Android 端发送一帧字符 ep_out.write(b"PING", timeout=1000) # 读取 Android 端回包,最大 4096 字节 resp = dev.read(ep_in.bEndpointAddress, 4096, timeout=3000) print(resp.tobytes())

PyUSB 的read返回的是array.array,需要用tobytes()才能拿到原始字节。端点地址的筛选方式和 Android 侧正好镜像,ENDPOINT_IN在 PC 视角指的是从设备读入数据,因此 Android 侧写出来的bulkOutEndpoint对应 PC 侧ep_out写在另一边。注意dev.read的第二个参数不是缓冲区大小上限,而是期望读取的字节数;实际返回可能比请求值小,协议解析时要用实际长度收尾,不能直接按 4096 处理。

4.3 USB 抓包与链路层核对

当两端都出现“能打开但收不到数据”时,不要急着复盘应用代码,先抓 USB 总线上真实跑了什么。Windows 上可以用 Wireshark 加 Usbpcap,Linux 上直接用usbmon抓内核的 USB 协议事件。抓包时要重点看三个东西:控制传输阶段的SET_CONFIGURATIONSET_INTERFACE是否成功,批量传输包的Data Offset是否符合自定义帧结构,以及设备是否持续回 NAK 导致主机端超时。抓包结果里如果 PC 一直在发送长度 0 的 IN 令牌且设备不回 ACK,通常是 Android 端没有claimInterface成功;如果设备回了 STALL,说明端点地址选错,常见于接口有多个时错拿了 Control 端点。这个核对过程比我见过的任何日志分析都有效,尤其是跟 STM32 USB Library 对照调试时,标准化抓包能直接看出是描述符阶段失败还是数据阶段失败。

4.4 两端端点配置的一致性校验

联调前先把两端端点信息打出来逐项比对。Android 侧打印bulkInEndpoint.getAddress()bulkOutEndpoint.getAddress(),PC 侧打印ep_in.bEndpointAddressep_out.bEndpointAddress,两个方向必须形成完整闭合。如果 PC 拿到两个ENDPOINT_IN,说明接口描述符解析顺序有问题,需要遍历接口而不是直接取第一个。每次改动 USB 描述符后,PC 端必须重新枚举设备,Android 端要卸载并重插 OTG,只重启应用不会刷新内核里的设备状态。

5. 文件传输的分块、校验与断开恢复

最后一步把字符流升级为文件流。直接对文件字节做单次bulkTransfer在几 KB 的小文件上没问题,但超过 1MB 后 USB 总线错误、PC 端缓存不及时都会造成整文件失败,必须引入分块和校验。

5.1 文件分块与序号编排

我一般把文件切成长度固定的小块,每块头部加 8 字节自定义头:前 4 字节存放块序号,后 4 字节存放块长度,块净数据上限设为 64KB。发送端伪代码如下:

FileInputStream fis = new FileInputStream(file); byte[] chunk = new byte[64 * 1024]; int seq = 0; int readBytes; while ((readBytes = fis.read(chunk)) > 0) { byte[] frame = new byte[readBytes + 8]; ByteBuffer.wrap(frame).putInt(seq).putInt(readBytes); System.arraycopy(chunk, 0, frame, 8, readBytes); // 写入 USB 输出队列 writeQueue.offer(frame); if (!sendFrameWithAck(frame)) { // 连续失败 3 次则断开 break; } seq++; }

块长度取 64KB 是站在 USB 控制器吞吐和 Android 内存分配之间取的折中值,再大容易触发OutOfMemoryError,再小又浪费帧头开销。收端先读 8 字节头,解析出序号和长度,再按序号写入本地临时文件。发送端必须等待接收端 ACK 才发下一块,否则 PC 端读线程稍慢就会造成内核缓冲堆积,表现为传输越来越慢直到超时。

5.2 校验、断点续传与拔出恢复

每传完一个分块,接收端回一个包含块序号的 ACK,同时在 ACK 里附带这块数据的 CRC32,这样发送端能立刻发现问题块并重发。断点续传的实现并不复杂:发送端先把文件总大小、块大小和已确认序号持久化到本地缓存,下次建链后从最后确认序号继续发送。拔线恢复时不要立刻重连写文件,先等手机端广播ACTION_USB_DEVICE_DETACHED,再重新执行枚举与claimInterface;我自己踩过的一个坑是拔插后UsbDeviceConnection对象虽然未变为 null,但底层句柄已经失效,代码里必须捕获IOException并强制关闭旧连接。

5.3 用系统路径定位文件落盘问题

联调文件传输时,PC 端确认已发送全部块,Android 端却找不到文件,这种问题大多数不是 USB 层问题,而是应用没有正确访问外部存储。检查时可以直接用 adb 追踪文件系统的实际变化:

adb shell sh -c 'ls -l /storage/emulated/0/Android/data/com.example.usbbridge/files/' adb shell sh -c 'cat /proc/interrupts | grep -i usb'

/storage/emulated/0/Android/data/<包名>/files/是应用外部私有目录,不需要额外存储权限,比直接写/sdcard/Download更稳妥;后一条命令可以确认 USB 中断是否还在递增,判断底层是否因为 BIOS 或内核配置把 USB 控制器挂起了。碰到极个别设备在传输大文件时中断计数不增长,检查 BIOS 里 USB 电源管理策略,把 USB 选择性挂起关掉,这是 PC 端能量管理导致的链路休眠,和 Android 应用代码没有关系。确认好这两条路径,文件传输出问题时的定位范围就能从“不知道哪一环坏了”缩小到“要么块逻辑出错,要么系统把链路挂起了”。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:08:46

CANN/ge错误信息获取API

GEGetErrorMsgV2 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlo…

作者头像 李华
网站建设 2026/9/10 4:08:34

交叉验证全解析:从K折到时间序列,彻底搞懂模型评估

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:05:44

475与AMS Trex手操器全面对比:从操作逻辑到现场维护的换代之选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:05:40

GE获取算子属性API文档

GetAllAttrNamesAndTypes 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、T…

作者头像 李华
网站建设 2026/9/10 4:04:22

CANN/GE UDF工作空间示例

目录结构 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华