Android UsbHost与PC libusb双向通信实现字符与文件传输 📅 发布时间:2026/9/10 4:09:32 👁 浏览次数: 简介面向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:androidhttp://schemas.android.com/apk/res/android uses-feature android:nameandroid.hardware.usb.host android:requiredtrue / application activity android:name.UsbBridgeActivity intent-filter action android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED / /intent-filter meta-data android:nameandroid.hardware.usb.action.USB_DEVICE_ATTACHED android:resourcexml/device_filter / /activity /application /manifestandroid:requiredtrue表示应用必须运行在具备 USB 主机能力的设备上一般接 OTG 的板子或手机都满足如果应用还需要适配不支持 USB Host 的低端设备应改为false并在代码里用PackageManager.hasSystemFeature判断。device_filter.xml是 USB 设备插入时启动 Activity 的匹配条件resources usb-device vendor-id1027 product-id24577 / usb-device class2 subclass2 protocol1 / /resources这里vendor-id和product-id必须是十进制1027 对应 0x0403是 FTDI 的 VID24577 对应 0x6001是 FT232R/FT231X 常用 PIDclass2 subclass2对应 CDC 通信设备类。注意写错进制是新手最常见的问题Android Studio 里看 USB 设备信息时可以顺手把十六进制转成十进制再填进去。2.2 UsbManager 动态枚举与 VID/PID 过滤如果是应用已经运行、USB 设备才插入的场景不能只依赖device_filter.xml拉起 Activity还要在代码里动态获取设备列表。UsbManager.getDeviceList()返回的是一个HashMapString, UsbDevicekey 是设备的系统访问名value 里包含 VID、PID、接口数等信息UsbManager usbManager (UsbManager) getSystemService(Context.USB_SERVICE); HashMapString, 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 控制器会持续 NAKAndroid 侧表现为写入超时这时不要立刻判定设备故障应该通过状态机区分“发送队列满”和“链路断开”。返回值-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 设备连接到 PCPC 侧就需要用 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 uart或ft232r 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(idVendor0x0403, idProduct0x6001) 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_matchlambda e: usb.util.endpoint_direction(e.bEndpointAddress) usb.util.ENDPOINT_IN ) ep_out usb.util.find_descriptor( intf, custom_matchlambda e: usb.util.endpoint_direction(e.bEndpointAddress) usb.util.ENDPOINT_OUT ) # 向 Android 端发送一帧字符 ep_out.write(bPING, timeout1000) # 读取 Android 端回包最大 4096 字节 resp dev.read(ep_in.bEndpointAddress, 4096, timeout3000) 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 加 UsbpcapLinux 上直接用usbmon抓内核的 USB 协议事件。抓包时要重点看三个东西控制传输阶段的SET_CONFIGURATION、SET_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.bEndpointAddress、ep_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 应用代码没有关系。确认好这两条路径文件传输出问题时的定位范围就能从“不知道哪一环坏了”缩小到“要么块逻辑出错要么系统把链路挂起了”。本文还有配套的精品资源点击获取