4. Android USB设备模式开发:USB Device API介绍(UsbAccessory)、AOA协议(Android Open Accessory)原理、AOA 2.0与音频支持、设备模式下的数据通信。

好,我们继续往下走。上一章我们把USB Host模式聊透了,这一章咱们来聊聊它的“镜像”——USB Device模式。说白了,就是你的Android设备当“从机”,去连电脑或者其他USB主机。

你可能会问:“这有啥难的?手机插电脑上不就是MTP传文件吗?”嗯,那是系统帮你封装好的。但如果你想自己写一个App,让手机变成一个自定义的USB外设——比如一个数据采集器、一个游戏手柄、甚至一个音频输出设备——那就得深入理解我们今天要讲的这套东西了。

核心概念:在Device模式下,Android设备不再主动枚举外设,而是等待主机(如PC)来连接它。通信的桥梁是AOA协议(Android Open Accessory)。

4.1 UsbAccessory:设备端的API入口

在Host模式里,我们用的是UsbDevice。在Device模式里,对应的就是UsbAccessory。名字不一样,但思路很像——它代表了一个连接到Android设备的外部配件(比如一个Arduino板子或者一个定制的USB底座)。

我个人习惯把UsbAccessory理解成一个“虚拟的USB设备描述符”。它里面包含了配件的制造商、型号、版本、序列号等信息。你不需要自己去解析USB描述符,系统帮你干完了。

获取UsbAccessory的方式有两种:

  1. 通过Intent过滤:在Manifest里声明USB_ACCESSORY_ATTACHED的Intent Filter。当配件插入时,系统自动启动你的Activity,并把UsbAccessory对象通过Intent传过来。
  2. 通过UsbManager查询:调用UsbManager.getAccessoryList(),返回当前已连接的配件列表。

我记得第一次做这个的时候,死活拿不到配件列表。后来发现——getAccessoryList()返回的是数组,但大多数情况下只有一个元素。我直接取索引0就对了。嗯,这里要注意:如果没连接配件,返回的是null,不是空数组。

4.2 AOA协议原理:Android Open Accessory

AOA协议是Google搞的一套标准。它的核心思想是:让Android设备在Device模式下,能够被一个外部USB主机识别并通信

你想想看,普通的Android手机插到电脑上,电脑识别的是“Android Composite Device”或者“MTP设备”。但通过AOA协议,手机可以“伪装”成一个自定义的USB配件——比如一个串口设备、一个HID设备。

协议分两个版本:

版本 特点 我的经验
AOA 1.0 基本的数据通信,支持控制传输和批量传输 适合简单的命令交互,比如发个“开灯”、“关灯”
AOA 2.0 增加了音频支持,可以传输音频流 做车载系统或者音频底座时必用

协议握手过程是这样的:

  1. 主机(比如PC)检测到Android设备连接。
  2. 主机发送一个特殊的控制请求(USB_REQ_GET_PROTOCOL),询问设备是否支持AOA。
  3. 设备回复“支持”。
  4. 主机发送配件的描述信息(制造商、型号等)。
  5. 主机发送USB_REQ_START_ACCESSORY,让设备切换到配件模式。
  6. 设备断开USB连接,重新枚举成一个新的USB配件。
  7. 主机再次枚举,发现一个全新的设备——就是你的Android App控制的那个“虚拟外设”。

我曾经在调试这个握手过程时,发现设备总是卡在第五步。后来用USB分析仪抓包一看——原来是主机发送的描述信息里,字符串编码不对。Android要求必须是UTF-16LE编码,我传成了ASCII。嗯,细节决定成败。

小技巧:如果你在PC端开发AOA主机程序,可以用libusb或者android-tools里的aoa工具来模拟主机。我经常用aoa命令行工具快速验证协议握手是否正常。

4.3 AOA 2.0与音频支持

AOA 2.0最大的亮点就是音频支持。它定义了一个标准的音频接口,让Android设备可以通过USB输出音频到外部配件——比如车载音响、外接音箱。

为什么需要这个?你想想看,传统的音频输出要么走3.5mm耳机孔,要么走蓝牙。但蓝牙有延迟,3.5mm孔又逐渐被取消。AOA 2.0提供了一条低延迟、高保真的音频通道。

实现上,AOA 2.0在USB描述符里增加了一个音频接口。这个接口使用标准的USB Audio Class协议。也就是说,你的Android设备在音频模式下,会被电脑识别成一个“USB声卡”。

具体参数:

  • 支持16位或24位音频采样
  • 采样率:48kHz(标准)、96kHz(可选)
  • 通道数:立体声(2通道)
  • 传输方式:等时传输(Isochronous)

我记得有一次做车载项目,客户要求手机通过USB线连接车载中控,播放导航语音。我一开始用AOA 1.0自己传PCM数据,结果延迟大得离谱。后来切到AOA 2.0的音频接口,延迟直接降到10ms以内。嗯,专业的事还是交给专业的接口做。

注意:AOA 2.0的音频接口是单向的——从Android设备到配件。如果你想实现双向音频(比如麦克风输入),需要自己额外实现一个控制通道来传输反向数据。或者等AOA 3.0?目前还没普及。

4.4 设备模式下的数据通信

好了,协议讲完了,咱们来点实际的——怎么在代码里收发数据。

在Device模式下,通信的“管道”是UsbAccessory对象。你通过UsbManager拿到它之后,调用openAccessory()方法得到一个ParcelFileDescriptor。然后,把这个FD包装成FileInputStreamFileOutputStream,就可以读写数据了。

代码示例:

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbAccessory[] accessoryList = usbManager.getAccessoryList();
if (accessoryList != null && accessoryList.length > 0) {
    UsbAccessory accessory = accessoryList[0];
    ParcelFileDescriptor pfd = usbManager.openAccessory(accessory);
    if (pfd != null) {
        FileInputStream inputStream = new FileInputStream(pfd.getFileDescriptor());
        FileOutputStream outputStream = new FileOutputStream(pfd.getFileDescriptor());
        // 现在你可以用inputStream读,用outputStream写了
        // 注意:读写需要在子线程中进行
    }
}

这里有几个坑,我帮你踩过了:

  • 读写必须异步openAccessory()返回的FD是阻塞的。如果你在主线程里读数据,UI会卡死。我习惯用AsyncTask或者HandlerThread来处理。
  • 数据包大小限制:AOA协议底层用的是USB批量传输(Bulk Transfer)。单次传输的最大包大小通常是512字节(高速模式)或64字节(全速模式)。如果你要传大文件,记得自己分包。
  • 关闭资源:用完一定要关掉ParcelFileDescriptor和流。否则下次打开配件时会失败。我曾经因为这个bug被测试追着打。

通信模式上,我建议采用请求-响应模型。Android设备作为从机,等待主机发命令,然后回复数据。这样逻辑清晰,不容易死锁。

举个例子:主机发一个字节0x01表示“请求温度数据”,Android设备收到后,回复4个字节的浮点数。就这么简单。

避坑指南:我曾经遇到过一个诡异的问题——数据偶尔会丢包。后来发现是主机端发送太快,Android设备来不及处理。解决方案是在主机端每发送一个包后,等待设备回复一个ACK(确认字节),再发下一个。虽然牺牲了一点吞吐量,但可靠性大大提升。

4.5 知识体系总览

为了让你对整个章节有个直观的理解,我画了一张图。它把AOA协议、API、通信流程串在了一起。

Android USB Device模式开发知识体系 UsbAccessory API 获取配件对象 打开数据通道 读写FileStream AOA协议原理 AOA 1.0:基本通信 AOA 2.0:音频支持 协议握手流程 数据通信实践 请求-响应模型 分包与ACK机制 异步读写 典型通信流程 主机发送 控制请求 (AOA握手) 设备切换 配件模式 (重新枚举) 批量传输 数据通信 (请求/响应) 注:AOA 2.0在批量传输基础上,增加了等时传输通道用于音频

这张图从左到右展示了三个核心模块:API入口、协议原理、通信实践。底部是典型的通信流程。你可以把它当作一个快速参考。

好了,这一章的内容就到这里。USB Device模式开发,说白了就是让Android设备“扮演”一个USB外设。掌握了UsbAccessory的用法、AOA协议的握手过程、以及数据通信的读写技巧,你就能做出很多有意思的东西了。

记住我踩过的那些坑——编码问题、丢包问题、资源释放问题。嗯,这些都是实战中才会遇到的。理论懂了,动手试试吧。


公众号:蓝海资料掘金营,微信deep3321