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的方式有两种:
- 通过Intent过滤:在Manifest里声明
USB_ACCESSORY_ATTACHED的Intent Filter。当配件插入时,系统自动启动你的Activity,并把UsbAccessory对象通过Intent传过来。 - 通过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 | 增加了音频支持,可以传输音频流 | 做车载系统或者音频底座时必用 |
协议握手过程是这样的:
- 主机(比如PC)检测到Android设备连接。
- 主机发送一个特殊的控制请求(
USB_REQ_GET_PROTOCOL),询问设备是否支持AOA。 - 设备回复“支持”。
- 主机发送配件的描述信息(制造商、型号等)。
- 主机发送
USB_REQ_START_ACCESSORY,让设备切换到配件模式。 - 设备断开USB连接,重新枚举成一个新的USB配件。
- 主机再次枚举,发现一个全新的设备——就是你的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包装成FileInputStream和FileOutputStream,就可以读写数据了。
代码示例:
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、通信流程串在了一起。
这张图从左到右展示了三个核心模块:API入口、协议原理、通信实践。底部是典型的通信流程。你可以把它当作一个快速参考。
好了,这一章的内容就到这里。USB Device模式开发,说白了就是让Android设备“扮演”一个USB外设。掌握了UsbAccessory的用法、AOA协议的握手过程、以及数据通信的读写技巧,你就能做出很多有意思的东西了。
记住我踩过的那些坑——编码问题、丢包问题、资源释放问题。嗯,这些都是实战中才会遇到的。理论懂了,动手试试吧。
公众号:蓝海资料掘金营,微信deep3321