Android AOA协议嵌入式实现:裸机/RTOS兼容的USB配件模式库
1. AndroidAccessory 库概述
AndroidAccessory(AA)库是专为嵌入式微控制器设计的 USB 主机侧协议栈,用于与运行 Android 系统的移动设备建立直接、免驱动的通信通道。该库并非标准 USB 类设备(如 CDC ACM 或 HID),而是基于 Google 在 Android 3.1(Honeycomb MR1)中引入的Android Open Accessory (AOA) 协议实现。AOA 协议的核心思想是:将 Android 设备置于“配件模式”(Accessory Mode),由外部 MCU 作为 USB 主机发起控制,Android 设备则降级为受控的 USB 设备——这一角色反转彻底规避了 Android 端对通用 USB 驱动的依赖,使开发者无需 root、无需定制 ROM、无需安装 APK 即可实现双向数据交换。
本库为原始 Arduino 官方AndroidAccessory库的深度修改版本(Modified AndroidAccessory Library),其工程价值在于:在保留 AOA 协议兼容性的前提下,剥离了 Arduino 框架强耦合,重构为面向裸机(Bare-metal)与 RTOS(如 FreeRTOS)环境的可移植 C 模块;同时强化了错误恢复机制、USB 描述符动态配置能力,并显式支持 STM32F1/F4/H7 等主流 Cortex-M 平台的 HAL/LL 库集成路径。它不提供 Android 端 SDK,仅专注 MCU 侧协议解析与状态机管理,是构建工业手持终端、车载诊断仪、智能外设桥接器等场景的关键中间件。
1.1 AOA 协议工作原理
AOA 协议本质是建立在标准 USB 2.0 架构之上的应用层握手协议。其启动流程严格遵循以下六步时序:
- 初始枚举:MCU 以默认 USB 设备身份接入 Android 手机(VID=0x18D1, PID=0x2D00),Android 内核识别为未知设备;
- AOA 查询:Android 端通过
GET_PROTOCOL控制请求(bRequest=51)查询设备是否支持 AOA,MCU 必须返回协议版本(v1.0 → 0x0100,v2.0 → 0x0200); - 描述符交换:若支持,Android 发送
SEND_IDENTIFER请求(bRequest=52),携带制造商名、型号名、描述、版本、URI、序列号共 6 个 UTF-8 字符串(各≤255 字节); - 模式切换触发:Android 发送
START_ACCESSORY请求(bRequest=53),MCU 收到后必须立即断开 USB 连接(物理复位或软件断连); - 重枚举为配件:MCU 重新上电/复位,以新 VID/PID(0x18D1/0x2D01)和自定义字符串描述符再次枚举,Android 内核识别为合法配件并加载
android_accessory内核模块; - 数据通道激活:Android 为该设备创建
/dev/usb_accessory节点,APP 通过UsbManager.openAccessory()获取文件描述符,后续通信走 Bulk IN/OUT 端点(端点地址由描述符指定,通常为 EP1 IN / EP2 OUT)。
此流程的关键工程约束在于:步骤 4 的断连必须在 100ms 内完成,否则 Android 端超时放弃;且重枚举时的描述符字符串长度必须与步骤 3 中声明的完全一致,否则内核拒绝加载。
1.2 修改版库的核心增强点
原始 Arduino 库存在三大工程缺陷:
- 硬编码字符串:制造商/型号等字段写死于 Flash,无法运行时配置;
- 无错误隔离:USB 总线错误(如 NAK、STALL)导致状态机卡死;
- 阻塞式传输:
read()/write()直接轮询端点状态,占用 CPU 且无法与 RTOS 任务协同。
本修改版通过以下设计解决:
| 增强维度 | 实现方案 | 工程价值 |
|---|---|---|
| 动态描述符 | 提供aa_set_string_descriptor()API,支持 RAM 中维护字符串缓冲区 | 支持设备序列号绑定、固件版本透传、多型号共用 BIN |
| 状态机硬化 | 引入AA_STATE_ERROR独立状态,所有 USB 错误(SETUP STALL、BULK TIMEOUT)触发状态回退 | 避免因手机热插拔、USB 线缆抖动导致永久离线 |
| 非阻塞 I/O | aa_available()/aa_read()/aa_write()仅操作内部环形缓冲区,底层由 USB ISR 填充/清空 | 可安全在 FreeRTOS 任务中调用,CPU 占用率 <5% |
| HAL 解耦 | USB 底层抽象为aa_usb_driver_t接口,含init()/ep_write()/ep_read()等纯虚函数 | 一行代码切换 STM32 HAL_USB_FS / LL_USB / GD32 USBFS |
2. 硬件接口与平台适配
2.1 USB 物理层要求
AOA 协议强制要求USB OTG(On-The-Go)功能,这意味着 MCU 必须具备:
- 双角色 USB PHY(既可作 Device 也可作 Host);
- ID 引脚检测电路(用于判别插入的是 A 型插头(Host)还是 B 型插头(Device));
- VBUS 供电管理(AOA 模式下 MCU 为主机,需向 Android 设备提供 5V@500mA)。
典型合规芯片包括:
- STM32F407VG:内置 USB OTG FS PHY,ID 引脚为 PA9,VBUS 检测引脚为 PA10;
- STM32H743VI:双 USB OTG(FS+HS),支持硬件 OTG 协议栈;
- NXP i.MX RT1064:USB HS PHY + 外置 USB3320 PHY 方案。
⚠️ 注意:STM32F103 等无原生 OTG 的芯片需外挂 USB PHY(如 SMSC USB3317)并自行实现 OTG 协议,本库不提供此类方案支持。
2.2 STM32 HAL 集成示例
以 STM32F407 开发板为例,HAL 层初始化关键代码如下:
// aa_hal_driver.c - USB 驱动适配层 #include "AndroidAccessory.h" #include "usbd_core.h" #include "usbd_desc.h" #include "usbd_ctlreq.h" static USBD_HandleTypeDef hUsbDeviceFS; static uint8_t usb_rx_buffer[64]; static uint8_t usb_tx_buffer[64]; // 实现 aa_usb_driver_t 接口 static const aa_usb_driver_t hal_usb_driver = { .init = hal_usb_init, .ep_write = hal_usb_ep_write, .ep_read = hal_usb_ep_read, .ep_stall = hal_usb_ep_stall, .get_setup_packet = hal_usb_get_setup_packet, }; static int hal_usb_init(void) { // 初始化 USB Device 外设(非 Host!AOA 初始阶段为 Device) __HAL_RCC_USB_OTG_FS_CLK_ENABLE(); hUsbDeviceFS.pData = NULL; hUsbDeviceFS.Instance = USB_OTG_FS; hUsbDeviceFS.Init.dev_endpoints = 4; hUsbDeviceFS.Init.speed = USBD_SPEED_FULL; hUsbDeviceFS.Init.phy_itface = USBD_PHY_EMBEDDED; if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK) { return -1; } USBD_RegisterClass(&hUsbDeviceFS, &USBD_CUSTOM_CLASS); USBD_Start(&hUsbDeviceFS); return 0; } static int hal_usb_ep_write(uint8_t ep_num, uint8_t *buf, uint16_t len) { return USBD_LL_Transmit(&hUsbDeviceFS, ep_num, buf, len); } // ... 其他函数实现在main.c中注册驱动并启动:
int main(void) { HAL_Init(); SystemClock_Config(); // 注册 USB 驱动 aa_set_usb_driver(&hal_usb_driver); // 配置 AOI 字符串(运行时可变) aa_set_string_descriptor(AA_STRING_MANUFACTURER, "Shenzhen Embedded Tech"); aa_set_string_descriptor(AA_STRING_MODEL, "SmartBridge v2.1"); aa_set_string_descriptor(AA_STRING_DESCRIPTION, "Industrial USB-AOA Bridge"); // 启动 AndroidAccessory 状态机 aa_begin(); while (1) { aa_task(); // 必须周期性调用,处理 USB 事件 HAL_Delay(1); // 1ms tick } }2.3 FreeRTOS 任务封装
为适配实时系统,推荐创建专用 USB 任务:
// FreeRTOS 任务函数 void usb_accessory_task(void const * argument) { aa_begin(); for(;;) { // 每毫秒执行一次状态机 aa_task(); // 检查是否有数据到达 if (aa_available() > 0) { uint8_t buffer[64]; int len = aa_read(buffer, sizeof(buffer)); if (len > 0) { // 解析 Android 发来的命令(例如 JSON 格式指令) parse_android_command(buffer, len); } } // 向 Android 发送传感器数据(每 100ms 一次) if (xTaskGetTickCount() % 100 == 0) { uint8_t sensor_data[] = {0x01, 0x23, 0x45, 0x67}; aa_write(sensor_data, sizeof(sensor_data)); } osDelay(1); } } // 创建任务 osThreadDef(usb_acc_task, usb_accessory_task, osPriorityNormal, 0, 256); osThreadCreate(osThread(usb_acc_task), NULL);3. 核心 API 详解
3.1 状态机控制 API
| 函数签名 | 参数说明 | 返回值 | 典型调用时机 |
|---|---|---|---|
void aa_begin(void) | 无参数,初始化内部状态机、缓冲区、USB 驱动 | void | main()中首次调用 |
void aa_task(void) | 无参数,执行单次状态机迭代(处理 SETUP、IN/OUT 事务、错误恢复) | void | 必须在主循环或 RTOS 任务中周期调用 |
aa_state_t aa_get_state(void) | 无参数,返回当前状态(AA_STATE_NONE,AA_STATE_IDLE,AA_STATE_ACCESSORY) | 枚举值 | 调试时检查连接状态 |
int aa_is_connected(void) | 无参数,等价于aa_get_state() == AA_STATE_ACCESSORY | 0/1 | 判断是否已进入配件模式 |
状态流转图(简化):
AA_STATE_NONE ↓ aa_begin() AA_STATE_IDLE → [收到 GET_PROTOCOL] → AA_STATE_WAITING_PROTOCOL ↓ [返回协议版本] AA_STATE_WAITING_IDENT → [收到 SEND_IDENT] → AA_STATE_WAITING_START ↓ [收到 START_ACCESSORY + 断连重枚举] AA_STATE_ACCESSORY → [USB 数据收发] → 持续运行 ↓ [USB 总线错误] AA_STATE_ERROR → [自动尝试恢复] → AA_STATE_IDLE3.2 数据通信 API
| 函数签名 | 参数说明 | 返回值 | 注意事项 |
|---|---|---|---|
int aa_available(void) | 无参数,返回接收缓冲区中待读取字节数 | ≥0 | 非阻塞,始终立即返回 |
int aa_read(uint8_t *buf, int len) | buf: 存储数据的缓冲区;len: 最大读取长度 | 实际读取字节数 | 若len> 缓冲区剩余数据,只读取可用部分 |
int aa_write(const uint8_t *buf, int len) | buf: 待发送数据;len: 数据长度 | 实际写入字节数 | 底层使用 DMA 时需确保buf地址 4 字节对齐 |
void aa_flush(void) | 清空发送缓冲区(丢弃未发出的数据) | void | 用于紧急中断数据流,如设备复位前清理队列 |
缓冲区设计细节:
- 接收缓冲区:1024 字节环形缓冲区,由 USB IN 端点 ISR 自动填充;
- 发送缓冲区:512 字节环形缓冲区,由
aa_write()写入,由 USB OUT 端点 ISR 自动提交; - 缓冲区大小可通过
#define AA_RX_BUFFER_SIZE 2048在AndroidAccessory.h中调整。
3.3 描述符与配置 API
| 函数签名 | 参数说明 | 返回值 | 使用约束 |
|---|---|---|---|
void aa_set_string_descriptor(uint8_t index, const char *str) | index: 字符串索引(AA_STRING_MANUFACTURER=0,MODEL=1...);str: UTF-8 字符串 | void | 必须在aa_begin()前调用,且str生命周期需持续有效 |
void aa_set_protocol_version(uint16_t version) | version: 协议版本(0x0100或0x0200) | void | 默认为0x0100,v2.0 支持音频通道但本库未实现 |
void aa_set_endpoint_config(uint8_t in_ep, uint8_t out_ep, uint16_t max_packet) | in_ep: Bulk IN 端点地址(如0x81);out_ep: Bulk OUT 端点地址(如0x02);max_packet: 最大包长(通常 64) | void | 必须在aa_begin()前调用,决定 USB 描述符内容 |
字符串长度限制:
AOA 协议规定每个字符串最大 255 字节(UTF-8 编码),超出部分被截断。实际工程中建议控制在 64 字节内,避免 Android 端解析异常。
4. 典型应用场景与代码实例
4.1 工业传感器数据透传
场景:STM32H7 采集温湿度传感器(SHT30)数据,通过 AOA 实时上传至 Android APP。
// 伪代码:传感器采集与上报 void sensor_upload_task(void const * argument) { sht30_init(); // 初始化传感器 for(;;) { float temp, humi; if (sht30_read(&temp, &humi) == 0) { // 构造二进制协议帧:[TYPE:1B][TEMP:4B][HUMI:4B][CRC:2B] uint8_t frame[12]; frame[0] = 0x01; // SENSOR_DATA memcpy(&frame[1], &temp, 4); memcpy(&frame[5], &humi, 4); uint16_t crc = calculate_crc16(frame, 9); memcpy(&frame[9], &crc, 2); // 仅在已连接时发送 if (aa_is_connected()) { aa_write(frame, sizeof(frame)); } } osDelay(1000); // 每秒上报一次 } }Android 端 APP 可通过UsbAccessory的getInputStream()读取该二进制流,无需任何权限声明。
4.2 Android 指令控制外设
场景:Android APP 发送 LED 控制指令(JSON 格式),MCU 解析后驱动 GPIO。
// 解析函数(精简版) void parse_android_command(uint8_t *data, int len) { // 查找 JSON 起始位置(跳过可能的乱码) char *json_start = memchr(data, '{', len); if (!json_start) return; // 解析 {"led":"on"} 或 {"led":"off"} cJSON *root = cJSON_Parse(json_start); if (!root) return; cJSON *led_obj = cJSON_GetObjectItem(root, "led"); if (led_obj && led_obj->valuestring) { if (strcmp(led_obj->valuestring, "on") == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (strcmp(led_obj->valuestring, "off") == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } } cJSON_Delete(root); }此方案替代了传统蓝牙串口(BLE UART)方案,规避了 Android 12+ 对BLUETOOTH_CONNECT权限的强制要求。
4.3 固件升级桥接器
利用 AOA 的高带宽(理论 480Mbps,实际约 20MB/s)实现 Android APP 向 MCU 推送固件:
- Android APP 将
.bin文件分片(每片 1024 字节),添加序列号与 CRC; - MCU 接收后缓存至外部 SPI Flash;
- 收到
{"cmd":"upgrade","crc":0xABCD}指令后,校验整包 CRC; - 校验通过则跳转至 Bootloader 执行擦写。
此方案比 DFU(Device Firmware Upgrade)更简单:无需 USB DFU 类描述符,无需特殊 Bootloader,纯应用层协议。
5. 故障排查与性能优化
5.1 常见连接失败原因
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| Android 无任何提示,设备不识别 | MCU 未正确拉低 ID 引脚(OTG 检测失败) | 用万用表测量 ID 引脚电压,确认为 0V(A 型插头) |
| 显示“USB 设备不受支持” | VID/PID 不为0x18D1/0x2D00或0x18D1/0x2D01 | 检查 USB 描述符中的idVendor/idProduct字段 |
| 连接后立即断开,循环重连 | SEND_IDENTIFER中字符串长度与实际不符 | 使用aa_set_string_descriptor()后,用strlen()验证长度 ≤255 |
| 数据接收乱码 | Android 端未调用openAccessory()获取文件描述符 | 确保 APP 在UsbManager.ACTION_USB_ACCESSORY_ATTACHED广播中响应 |
5.2 性能调优关键点
- 中断优先级:USB IRQ 优先级必须高于其他外设(如 UART、SPI),建议设为
NVIC_PRIORITYGROUP_4下的最高级(0); - DMA 配置:启用 USB PMA(Packet Memory Area)的双缓冲模式,避免 IN/OUT 端点冲突;
- 缓冲区对齐:
aa_write()的buf参数地址需 4 字节对齐,否则 HAL 层触发 HardFault; - 时钟精度:USB FS 要求 48MHz 时钟误差 <±0.25%,必须使用 HSE(外部晶振)而非 HSI;
验证方法:在aa_task()中添加计时戳,若单次执行 >500μs,需检查是否有长耗时操作(如未优化的浮点运算)混入。
6. 安全边界与工程约束
6.1 协议层安全限制
AOA 协议本身不提供加密与认证,所有数据明文传输。在工业场景中必须额外实现:
- 应用层 AES-128 加密:Android APP 与 MCU 共享密钥,对
aa_write()数据加密; - 指令白名单机制:MCU 端硬编码允许的指令集(如仅接受
{"cmd":"led"}),忽略所有未知 JSON key; - 速率限制:在
aa_read()后添加if (xTaskGetTickCount() - last_cmd_time < 100) return;防止指令洪泛攻击。
6.2 硬件资源占用实测(STM32F407)
| 资源类型 | 占用量 | 说明 |
|---|---|---|
| Flash | 12.4 KB | 含 USB 协议栈、状态机、环形缓冲区管理 |
| RAM | 2.1 KB | 1024B RX + 512B TX + 状态变量 + 栈空间 |
| CPU(168MHz) | 峰值 8.2%(连续传输) | aa_task()单次耗时 14μs,1ms 周期下占比极低 |
| USB 端点 | 4 个端点(EP0~EP3) | EP0(控制)、EP1(IN)、EP2(OUT)、EP3(可选中断) |
此资源占用证明:该库可在 256KB Flash / 64KB RAM 的低成本 MCU 上稳定运行,满足绝大多数嵌入式需求。
