透传主机方案

概述

USB 通信设备类 (CDC) 下的抽象控制模型 (ACM) 基于 USB 批量传输 (Bulk Transfer) 机制,定义了一套通用的数据交互标准。 在主机 (Host) 模式下,系统利用该协议建立与外部设备的高速数据通道,实现原始数据流 (Raw Data Stream) 的双向透传。

Ameba 基于 USB-IF 官方发布的 CDC ACM 协议标准,实现了完备的 USB CDC ACM 主机协议栈,提供高效的数据透传功能。

../../_images/usb_host_cdc_acm_overview.svg

特性

  • 支持热插拔

  • 自动解析描述符,自适应速度模式

  • 批量传输长度等参数可配置

  • 支持原始数据(文本、二进制及自定义协议等)的透传

  • 支持独立识别并驱动复合设备 (如 ACM + HID) 中的串口功能

应用场景

作为 USB CDC ACM 主机,Ameba 可通过 USB 接口与外部备建立点对点通信链路,并结合无线能力拓展多种数据透传应用,例如:

  • 无线数据透传网桥:Ameba 作为网关挂载外部专有协议 Dongle 或采集模块,将 USB 侧接收的原始数据流透传至 Wi-Fi 或蓝牙网络,实现有线数据流向无线网络的无缝桥接与协议转换。

  • 传统工业设备赋能:Ameba 通过 USB 转串口适配器连接 PLC、数控机床或精密仪表,实现对传统 RS-232/485 设备的协议转换与云端接入,低成本完成旧式产线的数字化与智能化升级。

  • 高速数据采集:Ameba 连接具备 USB 接口的高频传感器或采集卡,利用批量传输机制实时吞吐大流量原始数据,突破传统低速接口的带宽瓶颈,确保高频采样场景下的数据完整性与实时性。

协议简介

CDC (Communication Device Class) 是 USB 规范定义的通用通信设备类标准。在其 PSTN 子类下,最常用的是 ‌ACM‌ (Abstract Control Model)。它定义了一套标准化的命令集,用于控制通信参数,例如:

  • 设置波特率(如 9600、115200)

  • 配置数据位、停止位、校验位

  • 控制 DTR/RTS 等线路状态信号

正是通过 ACM,USB CDC 设备才能被主机识别为一个标准的“虚拟串口”。

协议文档

USB-IF 官方发布了 CDC 类基础协议及 PSTN 子类规范。开发过程中请参考以下核心文档:

规范类型

文档

CDC 1.2 (通信类基础协议)

https://www.usb.org/sites/default/files/CDC1.2_WMC1.1_012011.zip

PSTN 1.2 (PSTN 子类)

包含在上述 CDC 1.2 压缩包中的 PSTN120.pdf。

协议框架

CDC ACM 协议规定设备必须使用 双接口 机制分离控制流与数据流,并通过 联合功能描述符 (Union Functional Descriptor) 将二者逻辑绑定为单一功能单元。

  • 通信类接口 (Communication Class Interface, CCI)

    负责设备的管理控制与信令交互。

    • 控制传输:通过默认控制端点传输类特定请求。

      • 主机主要发送 PSTN 控制命令,核心指令包括配置波特率/数据位的 SetLineCoding 以及控制 RTS/DTR 握手信号的 SetControlLineState。

    • 中断传输:利用中断输入端点实现设备向主机的异步状态通报 (Notification)。

      • 典型应用是通过 SERIAL_STATE 实时报告 DCD、DSR 或 Ring 等硬件信号状态的变化。

  • 数据类接口 (Data Class Interface, DCI)

    负责承载应用层的业务数据流。该接口通常配置为一对批量端点,负责透传数据流,不涉及控制指令的解析与处理。

协议交互示例:

../../_images/usb_cdc_acm_class.svg

描述符结构

CDC ACM 设备除遵循标准的 USB 描述符(如设备描述符、配置描述符、端点描述符)外,还定义了 类特定功能描述符 (Class-Specific Functional Descriptors)来定义抽象控制模型的能力。

CDC ACM 描述符拓扑 (Descriptor Topology)

下面以高速配置为例,展示描述符的拓扑结构:

Device Descriptor
└── Identifies basic device information (Class: 0x02 CDC, SubClass: 0x02 ACM)

Configuration Descriptor
├── Includes total length, power attributes (Self-powered), etc.
│
├── Communication Class Interface Descriptor (Interface 0)
│   ├── Standard Interface Descriptor (Class: 0x02, SubClass: 0x02, Protocol: 0x01 AT Commands)
│   │
│   ├── Class-Specific Functional Descriptors (Defines ACM capabilities)
│   │   ├── Header Functional Descriptor (Declares CDC Spec version)
│   │   ├── Call Management Functional Descriptor (Call handling capabilities)
│   │   ├── ACM Functional Descriptor (Line coding & state capabilities)
│   │   └── Union Functional Descriptor (Binds Interface 0 and Interface 1)
│   │
│   └── Standard Endpoint Descriptor (Interrupt IN)
│       └── Used for Serial State notifications
│
├── Data Class Interface Descriptor (Interface 1)
│   ├── Standard Interface Descriptor (Class: 0x0A Data, SubClass: 0x00, Protocol: 0x00)
│   │
│   ├── Standard Endpoint Descriptor (Bulk OUT)
│   │   └── Host -> Device Data Stream
│   │
│   └── Standard Endpoint Descriptor (Bulk IN)
│       └── Device -> Host Data Stream
│
├── Device Qualifier Descriptor
│   └── Device information for other speed modes
│
└── Other Speed Configuration Descriptor
    └── Configuration information for Full Speed mode

功能描述符 (Functional Descriptor)

在通信接口中,CDC 必须包含以下特殊的“功能描述符”头部:

  • Header Functional Descriptor: 指明 CDC 版本。

  • Call Management Functional Descriptor: 指明设备如何处理呼叫管理。

  • Abstract Control Management Functional Descriptor: 指明支持哪些命令(如 Set_Line_Coding)。

  • Union Functional Descriptor: 指定哪个是 Master 接口,哪个是 Slave 接口。

  • Header Functional Descriptor

Header Functional Descriptor
├── bLength            : 1 byte   → Total descriptor length (Fixed = 5 bytes)
├── bDescriptorType    : 1 byte   → 0x24 (CS_INTERFACE: Class-Specific Interface)
├── bDescriptorSubtype : 1 byte   → 0x00 (HEADER)
└── bcdCDC             : 2 bytes  → USB Class Definitions for Communication Devices Specification Release Number
                                    • 0x0110 = Release 1.10 (Common for ACM)
  • Call Management Functional Descriptor

Call Management Functional Descriptor
├── bLength            : 1 byte   → Total descriptor length (Fixed = 5 bytes)
├── bDescriptorType    : 1 byte   → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte   → 0x01 (CALL_MANAGEMENT)
├── bmCapabilities     : 1 byte   → The capabilities that this configuration supports:
│                                   • Bit 0 = 0: Device does not handle call management itself
│                                   • Bit 0 = 1: Device handles call management itself
│                                   • Bit 1 = 0: Call management commands does not sent over Data Class Interface
│                                   • Bit 1 = 1: Call management commands can be sent over Data Class Interface
│                                   • Bits 2-7: Reserved (Reset to zero)
└── bDataInterface     : 1 byte   → Interface number of Data Class interface optionally used for call management
                                    (Zero if no data interface is used)
  • ACM Functional Descriptor

Abstract Control Management (ACM) Functional Descriptor
├── bLength            : 1 byte   → Total descriptor length (Fixed = 4 bytes)
├── bDescriptorType    : 1 byte   → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte   → 0x02 (ABSTRACT_CONTROL_MANAGEMENT)
└── bmCapabilities     : 1 byte   → The capabilities that this configuration supports:
                                    • Bit 0: Comm_Feature (Supports Set_Comm_Feature, Clear_Comm_Feature, Get_Comm_Feature)
                                    • Bit 1: Line_Coding (Supports Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding, Serial_State)
                                    • Bit 2: Send_Break (Supports Send_Break)
                                    • Bit 3: Network_Connection (Supports Network_Connection)
                                    • Bits 4-7: Reserved (Reset to zero)
  • Union Functional Descriptor

Union Functional Descriptor
├── bLength            : 1 byte   → Total descriptor length (3 + Number of slave interfaces)
├── bDescriptorType    : 1 byte   → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte   → 0x06 (UNION)
├── bMasterInterface   : 1 byte   → The interface number of the Communication or Data Class interface
│                                   (Designated as the controlling interface for the union)
└── bSlaveInterface0   : 1 byte   → Interface number of the first subordinate interface in the union
│    ⋮
└── bSlaveInterface(N) : 1 byte   → Interface number of the last subordinate interface in the union

备注

关于 CDC ACM 传输协议规范,请参考 USB CDC ACM Specification

类特定请求

CDC ACM 设备的控制请求分为 标准请求(Standard Requests)类特定请求 (Class-Specific Requests)

本节主要介绍 CDC ACM 特有的 类特定请求,用于实现虚拟串口的串口参数配置、流控信号管理等核心功能。

请求名称

规范要求

描述

SEND_ENCAPSULATED_COMMAND

必须

主机向设备发送封装后的命令,数据格式需遵循设备支持的 控制协议(例如:AT 指令集)。

GET_ENCAPSULATED_RESPONSE

必须

主机从设备获取封装命令的响应数据,响应格式遵循设备支 持的协议。

SET_COMM_FEATURE

可选

设置特定通信特性的状态。具体目标特性由功能选择器指定。

GET_COMM_FEATURE

可选

查询特定通信特性的当前设置状态。

CLEAR_COMM_FEATURE

可选

清除特定通信特性的设置,将其重置为默认状态。

SET_LINE_CODING

可选 (+)

主机配置异步串行通信的线路编码属性(如波特率、停止位、 校验位、数据位)。

GET_LINE_CODING

可选 (+)

主机查询当前配置的异步串行通信线路编码属性。

SET_CONTROL_LINE_STATE

可选

主机控制 RS-232/V.24 标准的控制信号状态(如 DTR 和 RTS 信号电平)。用于流控或复位。

SEND_BREAK

可选

主机请求设备在发送端生成一段持续时间的“断开”(Break) 信号,模拟 RS-232 风格的线路中断。

备注

  • 上述请求均属于 通信类(Communications Class) 特定请求。

  • 对于 模拟调制解调器(Analog Modem) 应用,虽然规范将标记为 (+) 的请求列为可选,但 强烈建议 实现这些请求以保证兼容性。

类驱动

本节详细介绍了 CDC ACM 主机驱动的内部实现细节,包括驱动架构、类特定请求的支持情况以及传输资源的分配方案。

驱动框架

CDC ACM 主机协议栈采用分层架构设计,实现了 USB 传输层与上层数据流的解耦。从上至下,驱动架构依次划分为以下层次:

  • 应用层

    包含业务逻辑与数据处理回调。

    • 业务实现:根据实际场景(如无线网桥、数据采集)调用接口并处理收发的数据。

    • 数据缓冲:通过注册用户回调函数,应用层直接获取底层上传的数据包。

  • CDC ACM 类驱动

    严格遵循 USB CDC ACM 协议规范,实现了主机与 ACM 设备交互的核心业务逻辑。其主要职责包括:

    • 枚举与接口绑定:负责识别通信接口类 (0x02, CDC_COMM) 与数据接口类 (0x0A, CDC_DATA),自动解析接口描述符并申请对应的管道资源。

    • 传输参数协商:在连接建立初期,负责发送 SetLineCoding 等请求以初始化链路参数,确保主机与设备端的配置同步。

    • 通知事件处理:通过中断传输监听设备上报的状态通知(如 SerialState),并触发相应的事件回调。

    • 数据收发:将上层下发的数据流封装为 USB 批量传输请求 (URB),并将接收到的 USB 数据包通过回调函数将数据透传至应用层。

  • USB Core 驱动

    实时响应硬件中断,负责处理 USB 标准枚举、传输管理以及底层的物理数据传输调度等。

类驱动实现

CDC ACM 类驱动在系统架构中起着承上启下的作用,其实现逻辑主要围绕以下三个核心交互接口展开:

  • 主机类驱动回调 API:类驱动通过定义并注册一个标准的 usbh_class_driver_t 结构体与底层 USB Core 进行交互。

  • 面向应用的回调 API:类驱动通过 usbh_cdc_acm_cb_t 回调结构体向上层应用提供异步事件通知机制。

  • 面向应用的 API:应用层调用这些 API 后,驱动会切换内部状态机的状态,开启数据传输的调度。

驱动回调机制

../../_images/usb_host_cdc_acm_callback.png

备注

上图仅为说明不同层级的回调函数执行的流程,并未列出所有调用场景。

类驱动回调函数

类驱动需要定义一个标准的 usbh_class_driver_t 结构体,作为统一的入口注册到 USB Core 中,是 Core 层通知 Class 层“发生了某事”的主要手段。

  • id_table: 支持的设备 ID 列表,核心层使用此表与插入的设备进行匹配,以决定是否加载此驱动。

  • attach: 设备连接并匹配成功后

  • detach: 设备断开时调用。

  • setup: 枚举完成进入类请求阶段,用于发送类特定的标准控制请求,完成设备进入数据传输状态前的必要配置。

  • process: 类驱动驱动就绪后的状态机处理函数。

  • sof: SOF 中断时调用,用于处理对时序要求严格的逻辑,主要用于 同步传输

  • completed: 当通道上的传输完成时调用。

面向应用层的回调函数

CDC ACM 类驱动面向应用层的回调结构体 usbh_cdc_acm_cb_t,由用户层实现。一般可选择实现:

API

描述

init

在类驱动初始化时被调用,用于初始化应用相关的资源

deinit

在类驱动注销时被调用,用于注销应用相关的资源

attach

在类驱动执行attach回调时被调用,用于应用层处理设备连接事件

detach

在类驱动执行detach回调时被调用,用于应用层处理设备断开事件

setup

在类驱动执行setup回调时被调用,用于指示应用层类驱动已准备好进行数据传输

receive

在类驱动收到BULK IN数据时被调用,用于应用层处理设备上报的数据

transmit

在类驱动BULK OUT数据传输完成时被调用,用于应用层获取OUT传输状态

notify

在类驱动收到INTR IN数据时被调用,用于应用层处理设备上报的数据

加载与卸载类驱动

这两个函数负责内存资源的分配与释放,以及类驱动向 USB 核心的注册与注销。

usbh_cdc_acm_init() 用于加载 CDC ACM 主机类驱动的顶层函数,主要完成以下任务:

  • 保存用户提供的回调函数,并调用用户 init 回调。

  • 为 line_coding(当前设备参数)和 user_line_coding(用户期望参数)分配内存。

  • 调用 usbh_register_class() 注册 CDC ACM 类驱动到 USB 主机核心。

示例:

int usbh_cdc_acm_init(usbh_cdc_acm_cb_t *cb)
{
    /* 1. Save the user callback and call the user's ``init`` callback */
    if (cb != NULL) {
      cdc->cb = cb;
      if (cb->init != NULL) {
        ret = cb->init();
        if (ret != HAL_OK) {
          RTK_LOGS(TAG, RTK_LOG_ERROR, "User init err %d\n", ret);
          return ret;
        }
      }
    }
    /* 2. Allocate memory */
    cdc->line_coding = (usb_cdc_line_coding_t *)usb_os_malloc(sizeof(usb_cdc_line_coding_t));
    /* ... */

    /* 3. Register class driver*/
    usbh_register_class(&usbh_cdc_acm_driver);
    return ret;
}

连接与断连处理

当 USB 核心检测到匹配 CDC ACM 类的设备插入或拔出时,会调用以下回调函数。

usbh_cdc_acm_attach 是设备枚举的关键步骤,负责解析接口描述符并分配管道资源:

  • 查找通信接口 (Comm Interface):如果找到,解析其中断端点并打开 Interrupt IN 管道。

  • 查找数据接口 (Data Interface):如果找到,解析其批量端点并打开 Bulk IN 和 Bulk OUT 管道。

  • 初始化状态机为 IDLE 状态。

  • 调用用户 attach 回调,告知应用层连接状态。

示例:

static int usbh_cdc_acm_attach(usb_host_t *host)
{
  /* 1. Get the Communication interface and open the interrupt pipe */
  dev_id.bInterfaceClass = USB_CDC_COMM_INTERFACE_CLASS_CODE;
  itf_data = usbh_get_interface_descriptor(host, &dev_id);
  if (itf_data) {
    usbh_open_pipe(host, intr_in, ep_desc);
  }
  /* 2. Get the Data interface and open the Bulk IN & Bulk OUT pipe */
  dev_id.bInterfaceClass = USB_CDC_DATA_INTERFACE_CLASS_CODE;
  itf_data = usbh_get_interface_descriptor(host, &dev_id);
  /* Open bulk_in / bulk_out pipes */

  /* 3. Initialize the state machine */
  cdc->state = USBH_CDC_ACM_STATE_IDLE;

  /* 4. Notify the user layer */
  if ((cdc->cb != NULL) && (cdc->cb->attach != NULL)) {
      cdc->cb->attach();
  }
  return HAL_OK;
}

类驱动状态机

usbh_cdc_acm_process 回调函数是主机端 CDC ACM 类的核心状态机处理函数。 与设备端被动响应请求不同,主机端驱动需要主动维护设备状态。 它的核心职责是维护类驱动的生命周期状态(如 IDLE, TRANSFER, ERROR),处理线路编码(Line Coding)的设置与校验,以及分发数据传输任务。

状态机管理与调度

usbh_cdc_acm_process 通过当前类驱动状态管理控制传输(如波特率配置)和批量/中断数据传输的调度。

状态枚举

描述

关键动作

IDLE

空闲状态

等待用户指令或数据传输请求。

SET_LINE_CODING

发送波特率设置请求

发送 SET_LINE_CODING 请求 (EP0)。成功后自动转入 GET_LINE_CODING 以进行回读校验。

GET_LINE_CODING

获取/校验波特率

发送 GET_LINE_CODING 请求。获取后比对用户设定值,若一致则触发用户 line_coding_changed 回调。

SET_CONTROL_LINE_STATE

控制握手信号

配置 RTS/DTR 电平状态。

TRANSFER

数据传输中

根据管道号分发任务到具体的 TX/RX 处理函数。

ERROR

错误状态

尝试清除端点特征 (Clear Feature) 以恢复通信。

线路编码配置 (Set & Verify)

CDC ACM 主机驱动实现了一个“设置-回读-校验”的闭环流程,以确保设备正确接受了波特率等参数。

  • 设置 (Set): 发送 SET_LINE_CODING 请求。成功后,状态自动流转到 GET_LINE_CODING。

  • 回读 (Get): 发送 GET_LINE_CODING 请求,读取设备当前的配置,确保串口参数同步。

  • 校验 (Verify): 对比用户请求的配置和设备实际返回的配置。如果一致,说明配置成功。调用 line_coding_changed 回调通知应用层已经成功修改。

示例:

case USBH_CDC_ACM_STATE_SET_LINE_CODING:
    /* 1. Send a setting request */
    req_status = usbh_cdc_acm_process_set_line_coding(host, cdc->user_line_coding);
    if (req_status == HAL_OK) {
      /* 2. After successful setup, immediately goto to get the setting for verification */
      cdc->state = USBH_CDC_ACM_STATE_GET_LINE_CODING;
    }
    /* ... error handling ... */
    break;

case USBH_CDC_ACM_STATE_GET_LINE_CODING:
    req_status = usbh_cdc_acm_process_get_line_coding(host, cdc->line_coding);
    if (req_status == HAL_OK) {
      cdc->state = USBH_CDC_ACM_STATE_IDLE;
      /* 3. Verify whether the readback data is consistent with the user settings */
      if ((cdc->line_coding->b.dwDteRate == cdc->user_line_coding->b.dwDteRate) &&
          /* ... compare other fields ... */ ) {
          /* 4. Config Match, notify the application layer that the setting has been changed. */
          if ((cdc->cb != NULL) && (cdc->cb->line_coding_changed != NULL)) {
            cdc->cb->line_coding_changed(cdc->line_coding);
          }
      }
    }
    break;

传输处理分发

当处于传输状态时,根据触发事件的管道号(Pipe ID),将处理分发给具体的 TX(发送)、RX(接收)或 INTR(中断)处理函数。

示例:

case USBH_CDC_ACM_STATE_TRANSFER:
    /* Distribute transmission tasks according to pipe numbers */
    if (event.msg.pipe_num == cdc->bulk_out.pipe_num) {
        usbh_cdc_acm_process_tx(host);      // Handle BULK OUT transfer
    } else if (event.msg.pipe_num == cdc->bulk_in.pipe_num) {
        usbh_cdc_acm_process_rx(host);      // Handle BULK IN transfer
    } else if (event.msg.pipe_num == cdc->intr_in.pipe_num) {
        usbh_cdc_acm_process_intr_rx(host); // Handle Interrupt IN transfer(e.g. Serial State)
    }
    break;

错误恢复

在其他状态处理发生错误时尝试清除端点特性(Clear Feature)并恢复到 IDLE 状态。

示例:

switch (cdc->state) {
/* ... IDLE state ... */

case USBH_CDC_ACM_STATE_ERROR:
    /* Error recovery mechanism */
    req_status = usbh_ctrl_clear_feature(host, 0x00U);
    if (req_status == HAL_OK) {
        cdc->state = USBH_CDC_ACM_STATE_IDLE;
    }
break;
}

类特定请求处理

Setup 阶段处理

usbh_cdc_acm_setup 回调函数在枚举基本完成后被调用。 会发起第一个类特定请求:获取当前设备的线路编码(Get Line Coding),以同步主机和设备的状态。

示例:

static int usbh_cdc_acm_setup(usb_host_t *host)
{
    /* Initiate a Get Line Coding request */
    status = usbh_cdc_acm_process_get_line_coding(host, cdc->line_coding);
    return status;
}

面向应用的 API

提供给上层应用的接口,用于触发配置或数据传输。

数据传输处理

下面三个面向应用的 API 作为 USB 驱动与应用层的接口层,驱动将状态切换为 TRANSFER,并在 process 回调中对数据传输进行调度分发。

  • CDC ACM 数据接口

    • usbh_cdc_acm_transmit():应用层传入数据开启发送,处理零长度包 (ZLP) 逻辑,并通过用户 transmit 回调函数将传输结果告知应用层。

    • usbh_cdc_acm_receive() :开启接收,并通过用户 receive 回调函数将数据透传至应用层。

  • CDC ACM 通信接口

发送逻辑与零长包 (ZLP) 处理

在批量数据发送函数 usbh_cdc_acm_transmit(),驱动需要处理 USB 协议中的 Zero Length Packet (ZLP) 问题。

如果发送的数据长度正好是端点最大包长 (MPS) 的整数倍,主机必须额外发送一个 ZLP 以告知设备传输结束。这是 USB 批量传输的标准要求。

示例:

int usbh_cdc_acm_transmit(u8 *buf, u32 len)
{
    usbh_cdc_acm_host_t *cdc = &usbh_cdc_acm_host;
    usb_host_t *host = cdc->host;
    usbh_pipe_t *pipe = &cdc->bulk_out;

    if (pipe->xfer_state == USBH_EP_XFER_IDLE) {
        pipe->xfer_buf = buf;
        pipe->xfer_len = len;

        /* If the data length is greater than 0 and an integer multiple of MPS, a ZLP needs to be sent */
        if ((pipe->xfer_len > 0) && (pipe->xfer_len % pipe->ep_mps) == 0) {
            pipe->trx_zlp = 1;
        } else {
            pipe->trx_zlp = 0;
        }

        cdc->state = USBH_CDC_ACM_STATE_TRANSFER;
        pipe->xfer_state = USBH_EP_XFER_START;
        /* ... notify state change ... */
    }
}

类特定请求实现

本驱动栈遵循 USB CDC ACM 规范,封装了核心 类特定请求 (Class-Specific Requests) 的实现与发送流程。 虽然规范将部分请求标记为可选,但为了支持标准的虚拟串口应用,主机驱动实现了以下请求。 开发者可参考源码路径 {SDK}/component/usb/host/cdc_acm 进行扩展。

类特定请求

描述

SetLineCoding

配置线路编码。向设备发送波特率、停止位、校验位等参数。在纯透传模式下,此请求主要用于完成协议握手流程。

GetLineCoding

获取线路编码。读取设备当前的线路配置参数,用于校验配置是否生效或同步设备默认状态。

SetControlLineState

设置控制信号状态。用于控制 RTS (Request To Send) 和 DTR (Data Terminal Ready) 信号电平,常用于流控握手或通知设备主机侧已就绪。

通道配置

CDC ACM 主机驱动在设备枚举阶段 (usbh_cdc_acm_attach 回调函数) 中解析配置描述符,根据接口子类自动查找并申请相应的传输资源,实现完整的数据与控制通道。

数量

描述

2

默认控制传输 0。 用于发送标准 USB 请求及 CDC 类特定控制请求(如 SetLineCoding)。

1

中断输入传输 (Interrupt IN)。 归属于通信接口 (CCI),用于接收设备主动上报的通知事件(Notify),最大超时时间设为 1000 tick。

1

批量 IN 传输,归属于数据接口 (DCI),用于接收设备上传的原始数据流 (RX Data),支持大数据包传输与 ZLP 处理。

1

批量 OUT 传输,归属于数据接口 (DCI),用于向设备发送原始数据流 (TX Data),驱动层优化了 DMA 调度以确保高带宽传输。

API 说明

驱动 API

应用示例

应用设计

本节详细介绍 CDC ACM 主机应用的完整开发流程,涵盖加载驱动、热插拔管理、数据收发机制以及资源释放。

加载驱动

使用 CDC ACM 主机驱动前,需定义配置结构体并注册回调函数,随后依次调用接口加载 USB 主机核心驱动及 ACM 类驱动。

步骤说明:

  • 硬件配置:设置 USB 速度模式(High Speed/Full Speed)及相关中断优先级。

  • 回调注册:定义 usbh_cdc_acm_cb_t 结构体,挂载各个阶段(连接、断开、数据传输、通知)的处理函数。

  • 加载核心驱动:调用 usbh_init() 加载 USB 主机核心驱动。

  • 加载类驱动:调用 usbh_cdc_acm_init() 加载 CDC ACM 类驱动。

/* 1. Configure USB speed (High Speed or Full Speed), isr priority and main task priority. */
static usbh_config_t usbh_cfg = {
    .speed = USB_SPEED_HIGH,
    .ext_intr_enable = USBH_SOF_INTR,
    .isr_priority = INT_PRI_MIDDLE,
    //...
};

/* 2. Define USB user-level callbacks. */
static usbh_user_cb_t usbh_usr_cb = {
    .process = cdc_acm_cb_process    /* USB callback to handle class-independent events in the application */
};

/* 3. Define user callbacks for CDC ACM events. */
static usbh_cdc_acm_cb_t cdc_acm_usr_cb = {
    .init   = cdc_acm_cb_init,        /* Class init callback */
    .deinit = cdc_acm_cb_deinit,      /* Class deinit callback */
    .attach = cdc_acm_cb_attach,      /* Device attach callback */
    .detach = cdc_acm_cb_detach,      /* Device detach callback */
    .setup  = cdc_acm_cb_setup,       /* Class setup callback */
    .transmit = cdc_acm_cb_transmit,  /* Data transmit callback */
    .receive  = cdc_acm_cb_receive,   /* Data receive callback */
    .notify   = cdc_acm_cb_notify,    /* Interrupt notify callback */
};

int status = 0;

/*4. Initialize USB host core driver with configuration. */
status = usbh_init(&usbh_cfg, &usbh_usr_cb);
if (status != HAL_OK) {
   return;
}

/* 5. Initialize CDC ACM class driver.  */
status = usbh_cdc_acm_init(&cdc_acm_usr_cb);
if (status != HAL_OK) {
    /* If class driver init fails, clean up the core driver */
    usbh_deinit();
    return;
}

热插拔事件处理

通过注册 usbh_cdc_acm_cb_t 中的 attachdetach 回调函数来监听 CDC ACM 设备的连接与断开。

在示例代码中,利用信号量(Semaphore)机制来同步状态:

  • Attach: 当设备插入并枚举成功后,触发 attach 回调,释放 cdc_acm_attach_sema,通知主线程开始数据测试。

  • Detach: 当设备拔出时,触发 detach 回调,释放 cdc_acm_detach_sema,触发热插拔管理线程进行资源清理与重新加载。

/* 1. Configure USB status change callback */
static usbd_cdc_acm_cb_t cdc_acm_cb = {
    .status_changed = cdc_acm_cb_status_changed
};

/* 2. Configure Callback executed in ISR context */
static void cdc_acm_cb_status_changed(u8 old_status, u8 status)
{
    RTK_LOGS(TAG, RTK_LOG_INFO, "Status change: %d -> %d \n", old_status, status);

    cdc_acm_attach_status = status;
    rtos_sema_give(cdc_acm_attach_status_changed_sema);
}

/* 3. Create thread handling the state machine */
static void cdc_acm_hotplug_thread(void *param)
{
    int ret = 0;
    UNUSED(param);

    for (;;) {
        if (rtos_sema_take(cdc_acm_attach_status_changed_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
            if (cdc_acm_attach_status == USBD_ATTACH_STATUS_DETACHED) {
                RTK_LOGS(TAG, RTK_LOG_INFO, "DETACHED\n");

                /* 1.Clean up resources */
                usbd_cdc_acm_deinit();
                ret = usbd_deinit();
                if (ret != 0) {
                    break;
                }

                /* 2.Re-initialize for next connection */
                ret = usbd_init(&cdc_acm_cfg);
                if (ret != 0) {
                    break;
                }
                ret = usbd_cdc_acm_init(CONFIG_CDC_ACM_BULK_OUT_XFER_SIZE, CONFIG_CDC_ACM_BULK_IN_XFER_SIZE, &cdc_acm_cb);
                if (ret != 0) {
                    usbd_deinit();
                    break;
                }
            } else if (cdc_acm_attach_status == USBD_ATTACH_STATUS_ATTACHED) {
                RTK_LOGS(TAG, RTK_LOG_INFO, "ATTACHED\n");
            } else {
                RTK_LOGS(TAG, RTK_LOG_INFO, "INIT\n");
            }
        }
    }
    RTK_LOGS(TAG, RTK_LOG_INFO, "Hotplug thread fail\n");
    rtos_task_delete(NULL);
}

数据收发处理

当 CDC ACM 设备枚举成功后,主机可进行控制传输、数据传输及中断通知的接收。

static void cdc_acm_request_test(void)
{
    int ret;
    usbh_cdc_acm_line_coding_t line_coding;
    usbh_cdc_acm_line_coding_t new_line_coding;

    /* Wait for device attach */
    ......

    /* 1. Get current line coding from device */
    ret = usbh_cdc_acm_get_line_coding(&line_coding);
    if (ret != HAL_OK) {
        /* Get failed */
    }

    /* 2. Modify configuration parameters */
    /* Baudrate: 38400, Stop bits: 1, Parity: None, Data bits: 8*/
    line_coding.b.dwDteRate = 38400;
    line_coding.b.bCharFormat = CDC_ACM_LINE_CODING_CHAR_FORMAT_1_STOP_BITS;
    line_coding.b.bParityType = CDC_ACM_LINE_CODING_PARITY_NO;
    line_coding.b.bDataBits = 8;

    /* 3. Set new line coding to device */
    ret = usbh_cdc_acm_set_line_coding(&line_coding);
    if (ret != HAL_OK) {
        /* Set failed */
    }

    /* Wait for the setting to take effect */
    rtos_time_delay_ms(10);

    /* 4. Get line coding again to verify */
    ret = usbh_cdc_acm_get_line_coding(&new_line_coding);

    /* 5. Compare set values with read-back values */
    ......
}
  • 数据传输 (Bulk Transfer)

    驱动采用异步传输机制,配合信号量实现同步逻辑:

    • 发送: 调用 usbh_cdc_acm_transmit() 发送数据。发送完成后,驱动回调 transmit 函数,释放 cdc_acm_send_sema

    • 接收: 调用 usbh_cdc_acm_receive() 准备接收数据。接收到数据后,驱动回调 receive 函数,释放 cdc_acm_receive_sema

/* RX Callback: Handle Reception Completion */
static int cdc_acm_cb_receive(u8 *buf, u32 len, u8 status)
{
    if (status == HAL_OK) {
    u16 cdc_acm_bulk_in_mps = usbh_cdc_acm_get_bulk_ep_mps();

    /* 1. Copy data to internal buffer (Logic for buffer overflow check included) */
    if ((len > 0) && ((cdc_acm_total_rx_len + len) <= USBH_CDC_ACM_LOOPBACK_BUF_SIZE)) {
        /* memcpy(cdc_acm_loopback_rx_buf + cdc_acm_total_rx_len, buf, len); */
    }
    cdc_acm_total_rx_len += len;

    /* 2. Check for ZLP, short packet, or buffer full to complete the transfer */
    if ((len == 0) || (len % cdc_acm_bulk_in_mps)
        || ((len % cdc_acm_bulk_in_mps == 0) && (len < USBH_CDC_ACM_LOOPBACK_BUF_SIZE))
        || (cdc_acm_total_rx_len > USBH_CDC_ACM_LOOPBACK_BUF_SIZE)) {

        cdc_acm_total_rx_len = 0;

        /* 3. Signal the receive semaphore */
        rtos_sema_give(cdc_acm_receive_sema);
    }
    } else {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "RX fail: %d\n", status);
    }
    return HAL_OK;
}

/* TX Callback: Handle Transmission Completion */
static int cdc_acm_cb_transmit(u8 status)
{
    if (status == HAL_OK) {
        /* TX done, signal semaphore */
        rtos_sema_give(cdc_acm_send_sema);
    } else {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "TX fail: %d\n", status);
    }

    return HAL_OK;
}

/* Task thread: Handle Loopback Test Logic */
static void cdc_acm_loopback_test(void)
{
    int i;
    int ret;

    /* Initialize TX buffer */
    //...

    for (i = 0; i < USBH_CDC_ACM_LOOPBACK_CNT; i++) {

        /* Prepare RX buffer and check device status */
        //...

        /* 1. Transmit Data */
        ret = usbh_cdc_acm_transmit(cdc_acm_loopback_tx_buf, USBH_CDC_ACM_LOOPBACK_BUF_SIZE);
        if (ret < 0) {
            RTK_LOGS(TAG, RTK_LOG_ERROR, "TX fail: %d\n", ret);
            return;
        }

        /* 2. Wait for TX Complete signal */
        if (rtos_sema_take(cdc_acm_send_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
            /* 3. Prepare to Receive Data */
            usbh_cdc_acm_receive(cdc_acm_loopback_rx_buf, USBH_CDC_ACM_LOOPBACK_BUF_SIZE);
        }

        /* 4. Wait for RX Complete signal and Verify Data */
        if (rtos_sema_take(cdc_acm_receive_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
            /* Check rx data integrity */
            //...
        }
    }
    RTK_LOGS(TAG, RTK_LOG_INFO, "Bulk loopback test PASS\n");
}
  • 通知处理 (Interrupt Notify)

    通过 usbh_cdc_acm_notify_receive() 监听来自设备的中断传输(如 Serial State 变化)。当收到通知时,触发 notify 回调。

/* Callback: Handle Interrupt Notify */
static int cdc_acm_cb_notify(u8 *buf, u32 len, u8 status)
{
    /* Handle buf and len */
    //...

    if (status == HAL_OK) {
        /* Notify received, signal semaphore to notify the task */
        rtos_sema_give(cdc_acm_notify_sema);
    } else {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "Notify fail: %d\n", status);
    }
    return HAL_OK;
}

/* Task Thread: Execute Notify Test Logic */
static void cdc_acm_notify_test_thread(void *param)
{
    /* Wait for device ready */
    //...

    /* 1. Optional: Change control line state to trigger device notification */
    usbh_cdc_acm_set_control_line_state();

    rtos_time_delay_ms(2000);

    for (int i = 0; i < USBH_CDC_ACM_LOOPBACK_CNT; i++) {
        /* 2. Submit Interrupt IN Request to listen for notification */
        usbh_cdc_acm_notify_receive(cdc_acm_notify_rx_buf, USBH_CDC_ACM_NOTIFY_BUF_SIZE);

        /* 3. Wait for Notify Callback */
        if (rtos_sema_take(cdc_acm_notify_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
            /* Notification received. Check data (e.g. Serial State at offset 8) */
            RTK_LOGS(TAG, RTK_LOG_INFO, "Intr rx success(0x%02x 0x%02x)\n",
                cdc_acm_notify_rx_buf[9], cdc_acm_notify_rx_buf[8]);
        }
    }
    RTK_LOGS(TAG, RTK_LOG_INFO, "Intr notify test PASS\n");
    rtos_task_delete(NULL);
}

备注

完整的数据测试逻辑请参考 SDK 示例代码: {SDK}/component/example/usb/usbh_cdc_acm/example_usbh_cdc_acm.c

卸载驱动

当设备断开或需要关闭 USB 主机功能时,需按顺序卸载类驱动和主机核心驱动,并释放相关系统资源。

/* 1. Deinitialize CDC ACM class driver. */
usbh_cdc_acm_deinit();

/* 2. Deinitialize USB host core driver */
usbh_deinit();

运行方式

本节介绍了一个完整的 USB CDC ACM 主机示例,演示了 Ameba 作为主机如何识别 CDC 设备,并进行波特率设置、数据回环测试(Loopback)以及中断通知测试。

该示例代码路径: {SDK}/component/example/usb/usbh_cdc_acm,可为开发者设计 CDC ACM 主机提供完整的设计参考。

配置与编译

  • Menuconfig 配置

    amebaxxx_gcc_project 目录下,输入 ./menuconfig.py,按下面步骤选择 USBH CDC ACM , 保存退出。

    - Choose `CONFIG USB --->`:
      [*] Enable USB
          USB Mode (Host)  --->
      [*] CDC ACM
    
  • 编译与烧录

    执行编译命令,并烧录生成的 Image 文件至开发板:

    cd amebaxxx_gcc_project
    ./build.py -a usbh_cdc_acm
    

结果验证

  • 启动主机

    重启开发板,观察串口日志,应显示如下启动信息:

    [ACM-I] USBD CDC ACM demo start
    
  • 连接设备

    使用 USB 线缆将的 CDC ACM 设备(例如另一块运行本协议栈透传设备方案示例的 Ameba 开发板)连接至本开发板的 USB 端口。

  • 虚拟串口通信测试

    如果连接后运行正常,将依次进行控制传输测试和批量传输回环测试(Loopback Test),串口日志显示如下成功信息:

    ```
    [ACM-I] Ctrl test PASS
    [ACM-I] Bulk loopback test start, loopback times:100, size: 1024
    [ACM-I] Bulk loopback test PASS
    ```
    

网络通信主机方案

概述

USB 通信设备类 (CDC) 下的以太网控制模型 (ECM) 定义了一套通过 USB 接口传输以太网帧的标准协议。在主机 (Host) 模式下,系统通过该协议识别并驱动外部 USB 网络设备(如 USB 转以太网适配器、LTE 蜂窝模组),将其抽象为本地网络接口。

Ameba 平台集成了符合 USB-IF 标准的 CDC ECM 主机协议栈,支持与系统内置 LwIP 网络协议栈的无缝对接。该方案为设备提供了灵活的网络扩展能力,使其能够通过 USB 接口便捷地接入有线局域网或 4G 蜂窝网络。

Ameba 接 USB 转以太网(有线网络)适配器

../../_images/usb_host_cdc_ecm_overview_user_rj45.svg

Ameba 接 USB 4G dongle

../../_images/usb_host_cdc_ecm_overview_user_lte.svg

特性

  • 支持标准 CDC ECM 设备 (如 USB 网卡、LTE 模组)

  • 与 LwIP 协议栈深度集成,抽象为标准网络接口 (Netif)

  • 支持复合设备,可并行处理多接口功能 (如 AT 命令 + 网络)

  • 支持热插拔

  • 优化批量传输,提供高性能数据吞吐

应用场景

作为 USB 主机 (Host),Ameba 负责对连接的 CDC ECM 设备进行枚举、配置及驱动加载。此方案允许用户在不修改板级硬件设计的情况下,灵活拓展网络连接能力,满足多样化的应用需求,例如:

  • 有线以太网扩展 (USB-to-Ethernet):Ameba 通过 USB 接口驱动外置的标准 USB 转以太网适配器(支持 Realtek RTL815x 等主流芯片方案)。该方案为未配备原生以太网接口的硬件平台提供了即插即用的有线接入能力。

  • 4G 蜂窝网络接入 (LTE Dongle):Ameba 通过 USB 接口连接支持 ECM 模式的 4G 模组或 USB Dongle。协议栈将 USB 数据流转换为标准网络数据包,实现通过蜂窝网络接入互联网。此场景适用于户外 IoT 网关、车载终端等需要移动网络回传或作为 Wi-Fi 链路备份的应用。

  • 双网口路由与桥接:结合 Ameba 原生的 Wi-Fi 接口,利用 USB 接口额外扩展物理网络通道。Ameba 可作为网络枢纽,在不同网络接口间执行数据转发、负载均衡或故障切换,组成简易的无线路由器或双网口桥接设备。

协议简介

CDC (Communication Device Class) 是 USB 规范定义的通用通信设备类标准。ECM (Ethernet Control Model) 作为其子类(Subclass),专门定义了如何在 USB 接口上封装和传输以太网数据帧,使 USB 设备能够在操作系统中被识别为标准的网络接口控制器 (NIC)。

协议文档

USB-IF 官方发布了 CDC 类基础协议及 ECM 子类规范。开发过程中请参考以下核心文档:

规范类型

文档

CDC 1.2 (通信类基础协议)

https://www.usb.org/sites/default/files/CDC1.2_WMC1.1_012011.zip

ECM 1.2 (以太网控制模型)

包含在上述 CDC 1.2 压缩包中的 ECM120.pdf。

术语定义

本文档涉及的通用 CDC ECM 技术术语定义如下:

术语

描述

CCI (Communication Class Interface)

通信类接口。用于管理设备的连接状态、发送控制命令和接收网络状态通知(如网线插拔)。通常使用控制端点和中断端点。

DCI (Data Class Interface)

数据类接口。用于实际传输以太网数据包。通常使用批量 (Bulk) 输入和输出端点。

以太网帧 (Ethernet Frame)

CDC ECM 传输的数据载荷,通常遵循 IEEE 802.3 标准(包含目标 MAC、源 MAC、EtherType 及 Payload)。

通知 (Notification)

设备通过中断端点主动向主机报告的异步事件,例如 NETWORK_CONNECTION (连接状态改变) 或 CONNECTION_SPEED_CHANGE (速率改变)。

功能描述符 (Functional Descriptor)

CDC 特有的描述符,嵌入在标准配置描述符中,用于描述网络设备的具体能力(如 MAC 地址、最大段长度)。

协议框架

CDC ECM Host 协议栈采用分层架构设计,旨在实现 USB 传输层与 LwIP 网络协议栈的解耦。

../../_images/usb_host_cdc_ecm_spec_arch_cn.svg

组件职责

  • 网络应用层

    位于架构最上层,它是一个具体的网络应用。

  • LwIP/网络协议栈 (Network Stack)

    网络协议栈不感知底层的 USB 细节,仅操作一个抽象的标准网络接口 (Netif),给上层提供网络服务。负责处理 TCP/IP、UDP、DHCP 等网络层协议逻辑。

  • CDC ECM 类驱动 (Class Driver)

    核心中间层,实现了 ECM 规范定义的行为:

    • 枚举解析:解析 CDC 功能描述符以获取 MAC 地址。

    • 控制管理:配置数据包过滤器 (Packet Filter),处理网络状态中断通知。

    • 数据收发:将网络栈下发的 pbuf 封装为 USB 传输请求 (URB/Transfer),并将接收到的 USB 数据包还原为以太网帧上交 LwIP。

  • USB Core & HCD (Host Controller Driver)

    底层驱动,负责处理 USB 标准枚举、端点管理以及底层的物理数据传输调度(如 DMA 操作),向类驱动屏蔽不同硬件控制器的差异。

通信机制

标准的 CDC ECM 设备通常由两个 USB 接口 (Interface) 组成,两者通过 联合功能描述符 (Union Functional Descriptor) 关联:

通信接口 (CCI) - 控制与通知

  • 控制传输 (Control Transfer)

    • 映射端点: 默认控制端点 0 (Endpoint 0)。

    • 枚举与配置: 传输标准的 USB 描述符以及 CDC 特有的功能描述符。

    • 类特定请求: 处理 ECM 协议控制命令,最核心的是 SetEthernetPacketFilter,用于配置设备接收广播、多播或单播包。

  • 中断传输 (Interrupt Transfer)

    • 中断输入 (Interrupt IN): 必选。用于设备向主机发送通知 (Notification)。

    • 典型应用:

      NETWORK_CONNECTION: 报告网线插入 (Link Up) 或拔出 (Link Down) 状态。 CONNECTION_SPEED_CHANGE: 报告当前的上下行连接速率。

数据接口 (DCI) - 批量管道

数据接口通常包含两个替代设置 (Alternate Settings),这是 ECM 协议设计的关键点:

  • Alt Setting 0:空闲模式。不包含任何端点。当网络未连接或驱动未加载时,主机应将接口切换至此模式以节省总线带宽。

  • Alt Setting 1:工作模式。包含一对批量端点 (Bulk IN / Bulk OUT)。

    • Bulk IN:设备 -> 主机。上传接收到的网络数据包。

    • Bulk OUT:主机 -> 设备。发送要转发到网络的以太网帧。

描述符结构

CDC ECM 设备在标准配置描述符中,通过 类特定接口描述符 (Class-Specific Interface Descriptor) 来详细定义网络能力。这些描述符通常被统称为 功能描述符 (Functional Descriptors)。

CDC ECM 描述符拓扑 (Descriptor Topology)

Device Descriptor
└── Identifies basic device information (USB Version 2.00)

Configuration Descriptor
├── Contains total length of the entire configuration, power supply information, etc.
│
├── CDC Control (CDC Control) Interface Descriptor (Interface 0)
│       ├── Standard Interface Descriptor (AlternateSetting 0, Control Class)
│       ├── Header Functional Descriptor
│       ├── Union Functional Descriptor
│       ├── Ethernet Networking Functional Descriptor
│       └── Endpoint Descriptor(Interrupt IN)
│
└── CDC Data (CDC-Data) Interface Descriptor (Interface 1)
        ├── Alternate Setting 0: Control transfer active state (control transfer only)
        │
        ├── Alternate Setting 1: Data transfer active state (with data endpoint)
        ├── Endpoint Descriptor(Bulk In)
        └── Endpoint Descriptor(Bulk Out)

Device Qualifier Descriptor
└── Device information while running in another speed mode

Other Speed Configuration Descriptor
├── Configuration information while running in another speed mode.
│
├── CDC Control (CDC Control) Interface Descriptor (Interface 0)
│       ├── Standard Interface Descriptor (AlternateSetting 0, Control Class)
│       ├── Header Functional Descriptor
│       ├── Union Functional Descriptor
│       ├── Ethernet Networking Functional Descriptor
│       └── Endpoint Descriptor(Interrupt IN)
│
└── CDC Data (CDC-Data) Interface Descriptor (Interface 1)
        ├── Alternate Setting 0: Control transfer active state (control transfer only)
        │
        ├── Alternate Setting 1: Data transfer active state (with data endpoint)
        ├── Endpoint Descriptor(Bulk In)
        └── Endpoint Descriptor(Bulk Out)

功能描述符 (Functional Descriptor)

  • Header Functional Descriptor

Header Functional Descriptor
├── bLength (1 byte): Fixed 0x05
├── bDescriptorType (1 byte): 0x24 (CS_INTERFACE)
├── bDescriptorSubtype (1 byte): 0x00 (HEADER)
└── bcdCDC (2 bytes): CDC specification version
  • Union Functional Descriptor

Union Functional Descriptor
├── bLength (1 byte): 0x05 + n (n = number of subordinate interfaces)
├── bDescriptorType (1 byte): 0x24 (CS_INTERFACE)
├── bDescriptorSubtype (1 byte): 0x06 (UNION)
├── bControlInterface (1 byte): Master interface number
└── bSubordinateInterface[n] (n bytes): One or more slave interface numbers
  • Ethernet Networking Functional Descriptor

Ethernet Networking Functional Descriptor
├── bLength (1 byte): Total descriptor length
├── bDescriptorType (1 byte): 0x24 (CS_INTERFACE)
├── bDescriptorSubtype (1 byte): 0x0F
├── iMACAddress (1 byte): MAC address string index
├── bmEthernetStatistics (4 bytes): Supported statistics counters
├── wMaxSegmentSize (2 bytes): Maximum frame size (e.g., 1518)
├── wNumberMCFilters (2 bytes): Multicast filtering capability
└── bNumberPowerFilters (1 byte): Number of wake patterns

类特定请求

CDC ECM Host 驱动通过控制端点 0 发送以下请求控制设备行为。

请求名称 (Request)

要求

描述

SendEncapsulatedCommand

可选

发送封装命令

GetEncapsulatedResponse

可选

获取封装响应

SetEthernetMulticastFilters

可选

设置组播地址列表过滤。

SetEthernetPowerManagementPatternFilter

可选

配置电源管理模式(如网络唤醒 WoL)。

GetEthernetPowerManagementPatternFilter

可选

获取电源管理模式过滤器

SetEthernetPacketFilter

必选

设置数据包过滤器。主机使用此命令通知设备需要接收哪些类型的包(单播、广播、多播、混杂模式等)。

GetEthernetStatistic

可选

获取设备的传输统计信息(如丢包数、错误帧数)。

数据传输格式

CDC ECM 的数据传输非常直接,USB 载荷 (Payload) 即为原始的 以太网帧 (Ethernet Frame),不包含额外的头部封装(这一点不同于 RNDIS 或 NCM)。下图显示了一个完整的以太网满帧(长度 1514)被分割成 3 个 USB 包传输。

../../_images/usb_host_cdc_ecm_spec_split_ethernet_packet.svg

备注

ZLP (Zero Length Packet)

如果传输的以太网帧长度恰好是 USB 端点最大包长 (wMaxPacketSize, 如 HS 下为 512 字节) 的整数倍,主机或设备需要发送一个零长包 (ZLP) 来标识传输结束。

类驱动

本节详细介绍了 CDC ECM 主机驱动的内部实现细节,包括驱动框架、类特定请求的支持情况以及端点资源的分配方案。

具体实现

USB Host CDC ECM 主机驱动栈基于模块化设计,通过分层架构实现了网络协议栈与 USB 硬件控制器之间的高效交互。该架构确保了数据传输的稳定性与系统的可扩展性。

其核心架构与数据流向如下图所示:

../../_images/usb_host_cdc_ecm_class_flow.png

按功能职责划分为以下几个核心模块:

网络协议栈适配 (LwIP)

负责网络层数据包的封装与解封。

作为 USB 驱动与 TCP/IP 协议栈(LwIP)的接口层,它充当了数据链路层的角色:

  • 发送数据 (TX):作为数据生产者,接收来自 LwIP 的 PBUF 数据包,将其转换为 USB 传输请求。

  • 接收数据 (RX):作为数据消费者,接收 USB 底层上报的以太网帧,封装为 PBUF 并提交给 LwIP 进行上层协议处理。

ECM 驱动架构

CDC ECM 主机端驱动采用组件化设计,由 ECM 类协议驱动 与 传输监控模块 两大核心组件构成。两者协同工作,分别负责标准协议的逻辑实现与通信链路的可靠性保障。

  • ECM 类协议驱动 (ECM Class Driver)

    该模块严格遵循 USB CDC ECM 协议规范,实现了主机与 ECM 设备交互的核心业务逻辑。其主要职责包括:

    • 枚举与配置解析:负责识别 CDC 通信接口与数据接口,解析设备描述符,并提取 MAC 地址等关键参数。

    • 网络控制管理:通过控制端点管理设备行为,例如配置数据包过滤器 (Packet Filter) 和组播列表。

    • 数据收发抽象:提供标准化的数据收发 API,作为上层网络协议栈 (LwIP) 与底层 USB 传输之间的桥梁。

  • 传输监控任务 (Monitor Task)

    这是一个独立的守护任务 (Daemon),旨在增强驱动在复杂场景下的健壮性。

    • 状态轮询:周期性地检查所有活跃 USB 管道 (Pipe) 的健康状态。

    • 自愈机制:一旦检测到传输异常(如超时或阻塞),立即触发恢复处理流程,实现网络连接的自动修复,确保通信不中断。

核心处理任务 (Main Task)

负责 USB 协议栈核心逻辑与状态机管理。

Main Task 是 USB Host 驱动的 大脑,基于 事件驱动 (Event-Driven) 机制运行。其核心职责包括:

  • 状态机维护:管理 USB 枚举状态机与 CDC ECM 类驱动的工作状态。

  • 事件处理:响应来自 ISR 或上层应用的传输事件。

  • 传输调度:负责调度 URB (USB Request Block) 请求,协调控制传输、批量传输与中断传输的时序逻辑。

中断服务(ISR)

负责硬件中断响应与底层驱动抽象。

该模块位于驱动栈的最底层,直接对接 USB 主机控制器硬件:

  • 中断处理:实时响应硬件中断(如传输完成、端口状态变化、错误中断)。

  • 硬件抽象:向上传递统一的事件信号(Event)给 Main Task,向下屏蔽不同 USB 控制器(IP Core)的寄存器操作差异,实现硬件无关性。

类特定请求实现

本驱动栈严格遵循 USB CDC ECM 1.2 规范,已封装了核心 类特定请求 (Class-Specific Requests) 的组成与发送流程。

驱动层默认实现了 SetEthernetPacketFilter、SetEthernetMulticastFilters 以及 GetEthernetStatistic 的处理逻辑。开发者可参考源码实现扩展其他类型的请求。源码路径: {SDK}/component/usb/host/cdc_ecm

类特定请求类型

备注

SetEthernetPacketFilter

设置以太网数据包过滤器。 用于控制设备接收哪些类型的网络包(如:单播、广播、多播或混杂模式)。

SetEthernetMulticastFilters

设置组播地址过滤器。 当上层网络协议栈(如 IGMP)需要监听特定组播组时,通过此请求将组播 MAC 地址列表下发至设备

GetEthernetStatistic

获取以太网统计数据。 用于读取设备内部维护的统计计数器(如:发送/接收成功帧数、CRC 错误数、丢包数等),通常用于网络诊断。

端点配置

CDC ECM 主机驱动在设备枚举阶段,会解析配置描述符,并根据接口类型自动查找并申请相应的端点资源,以建立完整的通信管道。

管道类型

描述

控制 IN/OUT 端点

默认控制端点 0 (EP0)。 用于发送标准 USB 请求及 CDC 类特定控制请求(如 SetEthernetPacketFilter)。

中断 IN 端点

中断输入端点 (Interrupt IN)。 归属于通信接口 (CCI),用于接收设备主动上报的网络通知(如网线插拔状态 NetworkConnection)。

批量 IN 端点

归属于数据接口 (DCI),用于接收设备上传的网络数据包 (RX Data)。

批量 OUT 端点

归属于数据接口 (DCI),用于向设备发送网络数据包 (TX Data)。

API 说明

驱动 API

应用示例

应用设计

本节详细介绍 CDC ECM 主机驱动的完整开发流程,涵盖驱动加载、热插拔管理、网络数据收发机制以及资源释放。

加载驱动

使用 CDC ECM Host 驱动前,需定义配置结构体并注册回调函数,随后依次加载 USB 主机核心驱动及 ECM 类驱动。

步骤说明:

  • 硬件配置:设置 USB 速度模式(High Speed/Full Speed)及中断/任务优先级。

  • 回调注册:定义用户回调结构体,挂载各个阶段(连接、断开、数据传输)的处理函数。

  • 加载核心驱动:调用 usbh_init() 加载 USB 核心驱动。

  • 加载类驱动:调用 usbh_cdc_ecm_init() 加载 CDC ECM 类驱动。

/*
 * 1. Configure USB speed (High Speed or Full Speed) ,isr priority and main task priority.
 */
static usbh_config_t usbh_ecm_cfg = {
        .speed = USB_SPEED_HIGH,
        .isr_priority = USBH_ECM_ISR_PRIORITY,
        .main_task_priority = USBH_ECM_MAIN_THREAD_PRIORITY,
};
/*
 * Define USB user-level callbacks.
 */
static usbh_user_cb_t usbh_ecm_usr_cb = {
        .process = cdc_ecm_cb_process,            /* USB callback to handle class-independent events in the application */
        .validate = cdc_ecm_cb_device_check,      /* USB callback to validate a device when multiple devices are connected to a hub */
};

/*
 * 2. Define user callbacks for CDC ECM events.
 */
static usbh_cdc_ecm_state_cb_t cdc_ecm_usb_cb = {
        .init   = cdc_ecm_cb_init,                /* USB init callback */
        .deinit = cdc_ecm_cb_deinit,              /* USB deinit callback */
        .attach = cdc_ecm_cb_attach,              /* USB attach callback */
        .detach = cdc_ecm_cb_detach,              /* USB detach callback */
        .setup  = cdc_ecm_cb_setup,               /* USB setup callback */
        .bulk_send     = cdc_ecm_cb_bulk_send,    /* Data transmission complet callback */
        .bulk_received = cdc_ecm_cb_bulk_receive, /* Data transmission complet callback */
        .intr_received = cdc_ecm_cb_intr_receive, /* Data transmission complet callback */
};

int ret = 0;

/**
 * 3. Initialize USB host core driver with configuration.
 */
ret = usbh_init(&usbh_ecm_cfg, &usbh_ecm_usr_cb);
if (ret != HAL_OK) {
        return;
}

/*
 * 4. Initialize CDC ECM class driver.
 */
ret = usbh_cdc_ecm_init(&cdc_ecm_usb_cb);
if (ret != HAL_OK) {
        /* If class driver init fails, clean up the core driver */
        usbh_deinit();

        return;
}

热插拔事件处理

作为主机,系统必须能够健壮地处理 USB 网卡的动态插入与移除。SDK 默认支持热插拔机制,并联动 LwIP 协议栈管理网络接口状态(如 DHCP 重启、默认路由切换)。

处理逻辑:

  • 设备拔出 (Detach):触发回调释放信号量,应用线程捕获后执行驱动卸载,并释放堆内存。

  • 设备插入 (Attach):USB 核心检测到设备,重新执行枚举和驱动加载流程。

  • 链路状态维护:监控物理链路 (Link Up/Down) 状态,根据状态触发 DHCP 请求或释放 IP,并配置路由。

/* USB detach callback */
static usbh_cdc_ecm_state_cb_t cdc_ecm_usb_cb = {
        .detach = cdc_ecm_cb_detach,
};

/* Callback executed in main task */
static void cdc_ecm_cb_detach(void)
{
        cdc_ecm_is_ready = 0;
        usb_os_sema_give(cdc_ecm_detach_sema);
}

static int usbh_cdc_ecm_doinit(void)
{
        int status;

        status = usbh_init(&usbh_ecm_cfg, &usbh_ecm_usr_cb);
        if (status != HAL_OK) {
                RTK_LOGS(TAG, RTK_LOG_ERROR, "Host init fail %d\n", status);
                return USBH_CORE_INIT_FAIL;
        }

        status = usbh_cdc_ecm_init(&cdc_ecm_usb_cb, params);
        if (status != HAL_OK) {
                RTK_LOGS(TAG, RTK_LOG_ERROR, "Init driver fail %d\n", status);
                return USBH_CLASS_INIT_FAIL;
        }

        return HAL_OK;
}

/* Thread Context: Handle the state machine */
static void ecm_hotplug_thread(void *param)
{
        int ret = 0;

        UNUSED(param);

        for (;;) {
                usb_os_sema_take(cdc_ecm_detach_sema, USB_OS_SEMA_TIMEOUT);
                RTK_LOGS(TAG, RTK_LOG_INFO, "Hot plug\n");

                /* Stop transfer, release resource */
                usbh_cdc_ecm_deinit();
                usbh_deinit();
                usbh_cdc_ecm_tx_status_check();
                usbh_ecm_timer_unregister();

                RTK_LOGS(TAG, RTK_LOG_INFO, "Free heap size: 0x%08x\n", usb_os_get_free_heap_size());

                /* Re-init */
                ret = usbh_cdc_ecm_doinit();
                if (ret != HAL_OK) {
                        RTK_LOGS(TAG, RTK_LOG_ERROR, "Init fail %d\n", ret);
                        break;
                }
        }
}

/**
  * Get ecm device connect status
  */
int usbh_cdc_ecm_get_connect_status(void)//1 up
{
        u8 ret1 = cdc_ecm_is_ready;
        u8 ret2 = ecm_hw_connect;
        int ret = ret1 & ret2;

        return ret;;
}

/* Link Status Task */
static void ecm_link_change_thread(void *param)
{
        u8 *mac;
        u32 dhcp_status = 0;
        u8 link_is_up = 0;
        eth_state_t ethernet_unplug = ETH_STATUS_IDLE;

        UNUSED(param);
        RTK_LOGS(TAG, RTK_LOG_INFO, "Enter link status task!\n");

        cdc_ecm_do_init();

        while (1) {
                link_is_up = usbh_cdc_ecm_get_connect_status();

                if (1 == link_is_up && (ethernet_unplug < ETH_STATUS_INIT)) {   // unlink -> link
                        RTK_LOGS(TAG, RTK_LOG_INFO, "Do DHCP\n");

                        ...

                        dhcp_status = LwIP_IP_Address_Request(NETIF_ETH_INDEX);
                        if (DHCP_ADDRESS_ASSIGNED == dhcp_status) {
                                netifapi_netif_set_default(pnetif_eth); //Set default gw to ether netif
                                RTK_LOGS(TAG, RTK_LOG_INFO, "Switch to link\n");
                        } else {
                                RTK_LOGS(TAG, RTK_LOG_INFO, "DHCP Fail\n");
                        }
                } else if (0 == link_is_up && (ethernet_unplug >= ETH_STATUS_INIT)) {   // link -> unlink
                        ethernet_unplug = ETH_STATUS_DEINIT;
                        netif_set_default(pnetif_sta);  //switch to other netif
                        RTK_LOGS(TAG, RTK_LOG_INFO, "Swicth to unlink\n");
                } else {
                        rtos_time_delay_ms(1000);
                }
        }
}

以太网帧发送流程 (Ethernet Output)

当上层 LwIP 协议栈有数据包需要发送时,会调用 USB CDC ECM 类驱动提供的 usbh_cdc_ecm_send_data() 接口。

流程说明:

  • 提交请求:LwIP 调用发送函数,驱动层调用 usbh_cdc_ecm_bulk_send() 提交 Bulk OUT 传输请求。

  • 等待完成: 驱动通过 usb_os_sema_take() 阻塞等待传输完成信号量。

  • USB 传输:SB Core 处理底层 DMA 传输,将数据发送至设备。

  • 回调释放:传输完成后,触发:func:cdc_ecm_cb_bulk_send() 回调,释放信号量。

  • 返回上层:函数获取到信号量,返回 LwIP,表示发送结束。

static usbh_cdc_ecm_state_cb_t cdc_ecm_usb_cb = {
        .bulk_send     = cdc_ecm_cb_bulk_send,
};

/* USB transfer finish callback */
static int cdc_ecm_cb_bulk_send(usbh_urb_state_t state)
{
        usb_os_sema_give(cdc_ecm_tx_sema);
        if (state != USBH_URB_DONE) {
                RTK_LOGS(TAG, RTK_LOG_ERROR, "BULK TX fail %d\n", state);
        }

        return HAL_OK;
}

/* Tansfer APi, used for LwIP */
int usbh_cdc_ecm_send_data(u8 *buf, u32 len)
{
        int ret;
        u8 retry_cnt = 0;

        while (1) {
                ret = usbh_cdc_ecm_bulk_send(buf, len);
                if (ret == HAL_OK) {
                        //success
                        break;
                }
                if (++retry_cnt > 100) {
                        RTK_LOGS(TAG, RTK_LOG_ERROR, "TX drop(%d)\n", len);
                        ret = HAL_ERR_UNKNOWN;
                        break;
                } else {
                        usb_os_delay_ms(1);
                }
        }

        /* *wait cdc_ecm_cb_bulk_send to give the sema */
        if (ret == HAL_OK) {
                usb_os_sema_take(cdc_ecm_tx_sema, USB_OS_SEMA_TIMEOUT);
        }

        return ret;
}

以太网帧接收流程 (Ethernet Input)

当 USB 网卡接收到网络数据时,通过 Bulk IN 端点上传给主机。

流程说明:

  • 注册回调:加载驱动时将 ethernetif_mii_recv() 注册给类驱动的接收处理函数。

  • USB 接收:底层驱动完成 Bulk IN 传输后,触发 cdc_ecm_cb_bulk_receive()

  • 数据上交:回调函数调用 ethernetif_mii_recv()

  • 封装 PBUF:申请 LwIP 的 pbuf,将接收到的原始数据拷贝至 pbuf payload。

  • 递交协议栈:调用 netif->input() 数据包送入 TCP/IP 协议栈处理。

static usbh_cdc_ecm_state_cb_t cdc_ecm_usb_cb = {
        .bulk_received = cdc_ecm_cb_bulk_receive,
};

report_data = ethernetif_mii_recv;

static int cdc_ecm_cb_bulk_receive(u8 *buf, u32 length)
{
        if ((report_data != NULL) && (length > 0)) {
                report_data(buf, length);
        }

        return HAL_OK;
}

void ethernetif_mii_recv(u8 *buf, u32 frame_len)
{
        struct eth_drv_sg sg_list[MAX_ETH_DRV_SG];
        struct pbuf *p, *q;
        u32 total_len = frame_len;
        struct netif *netif = pnetif_eth;

        memcpy((u8*)RX_BUFFER, buf, frame_len);

        // Allocate buffer to store received packet
        p = pbuf_alloc(PBUF_RAW, total_len, PBUF_POOL);
        if (p == NULL) {
                RTK_LOGW(TAG, "\n\r[%s]Cannot allocate pbuf to receive packet(%d)\n", __func__, total_len);
                return;
        }

        // Create scatter list
        for (q = p; q != NULL && sg_len < MAX_ETH_DRV_SG; q = q->next) {
                sg_list[sg_len].buf = (unsigned int) q->payload;
                sg_list[sg_len++].len = q->len;
        }
        rltk_mii_recv(sg_list, sg_len);

        // Pass received packet to the interface
        if (ERR_OK != netif->input(p, netif)) {
                pbuf_free(p);
        }
}

驱动卸载

在系统关闭或切换模式时,应按加载的 反序 释放资源。

/* 1. Deinitialize CDC ECM class driver first */
usbh_cdc_ecm_deinit();

/* 2. Deinitialize USB host core driver */
usbh_deinit();

运行方式

本节以 USB 转以太网适配器互联 为例,演示如何将 Ameba 开发板配置为 USB CDC ECM 主机,并通过外接 USB CDC ECM Dongle 访问网络。

Ameba 识别 Dongle,通过 DHCP 获取 IP 地址,并执行 Ping 及 iPerf 测试。

该示例路径: {SDK}/component/example/usb/usbh_cdc_ecm,可为开发者设计网络路由等产品提供完整的设计参考。

配置与编译

  • Menuconfig 配置

    amebaxxx_gcc_project 目录下,输入 ./menuconfig.py,按下面步骤选择 CDC ECM, 保存退出。

[*] Enable USB
                USB Mode (Host)  --->
[*]     CDC ECM
                        Select USB Ethernet (USB Ethernet)  --->
  • 编译与烧录

    执行编译命令,并烧录生成的 Image 文件至开发板:

cd amebaxxx_gcc_project
./build.py -a usbh_cdc_ecm

结果验证

  • 启动设备

    重启开发板,串口日志应显示驱动加载成功:

[ECM-I] USBH ECM demo start
  • 连接网卡

    将 USB 网卡插入开发板,并连接网线。硬件拓扑如下:

../../_images/usb_host_cdc_ecm_example_hw_top.svg
  • 功能测试

    • 查询网络状态(AT+WLSTATE)

      检查是否成功获取到 IP 地址,更多细节参考 查询网络状态

    AT+WLSTATE
    
    [+WLSTATE]: _AT_WLAN_INFO_
    WLAN0 Status: Running
    ==============================
    ....
    
    Interface ethernet
    ==============================
    MAC => 00:e0:4c:58:64:18
    IP  => 192.168.31.87
    GW  => 192.168.31.1
    MSK  => 255.255.255.0
    
    • Ping 测试(AT+PING)

      测试与网关的连通性,更多细节参考 Ping 测试

    AT+PING=192.168.31.1
    
    [+PING]: _AT_WLAN_PING_TEST_
    
    OK
    
    [MEM] After do cmd, available heap 2978368
    
    #
    
    [ping_test] PING 192.168.31.1 32(60) bytes of data
    
    [ping_test] 32 bytes from 192.168.31.1: icmp_seq=1 time=0 ms
    
    [ping_test] 32 bytes from 192.168.31.1: icmp_seq=2 time=0 ms
    
    [ping_test] 32 bytes from 192.168.31.1: icmp_seq=3 time=0 ms
    
    [ping_test] 32 bytes from 192.168.31.1: icmp_seq=4 time=0 ms
    
    [ping_test] 4 packets transmitted, 4 received, 0% packet loss, average 0 ms
    [ping_test] min: 0 ms, max: 0 ms
    
    • 上行输入测试 (RX/Input Test)

      Ameba 作为 Server (接收),PC 作为 Client (发送),更多细节参考 网络带宽测试

    EVB:
    
    AT+IPERF=-s,-i,1
    [+IPERF]: _AT_WLAN_IPERF1_TCP_TEST_
    
    OK
    
    [MEM] After do cmd, available heap 2977792
    
    #
    Start TCP server! id = [0]
    tcp_server_func: Create socket fd = 0
    tcp_server_func: Bind socket successfully
    tcp_server_func: Listen port 5001
    tcp_server_func: Accept connection successfully
    tcp_s: id[0] Receive 9383 KBytes in 1000 ms, 76866 Kbits/sec
    tcp_s: id[0] Receive 11391 KBytes in 1000 ms, 93323 Kbits/sec
    tcp_s: id[0] Receive 11400 KBytes in 1000 ms, 93393 Kbits/sec
    tcp_s: id[0] Receive 11393 KBytes in 1000 ms, 93334 Kbits/sec
    tcp_s: id[0] Receive 11324 KBytes in 1000 ms, 92774 Kbits/sec
    tcp_s: [END] id[0] Totally receive 55040 KBytes in 5013 ms, frame_num = 38604, 89943 Kbits/sec
    TCP server stopped!
    
    PC:
    
    C:\Users\test\Desktop>iperf-2-1-9-win.exe -c 192.168.31.87 -i 1 -t 5
    ------------------------------------------------------------
    Client connecting to 192.168.31.87, TCP port 5001
    TCP window size: 64.0 KByte (default)
    ------------------------------------------------------------
    [  1] local 192.168.31.234 port 53419 connected with 192.168.31.87 port 5001
    [ ID] Interval       Transfer     Bandwidth
    [  1] 0.00-1.00 sec  9.25 MBytes  77.6 Mbits/sec
    [  1] 1.00-2.00 sec  11.1 MBytes  93.3 Mbits/sec
    [  1] 2.00-3.00 sec  11.1 MBytes  93.3 Mbits/sec
    [  1] 3.00-4.00 sec  11.1 MBytes  93.3 Mbits/sec
    [  1] 4.00-5.00 sec  11.0 MBytes  92.3 Mbits/sec
    [  1] 0.00-5.01 sec  53.8 MBytes  89.9 Mbits/sec
    
    • 下行输出测试 (TX/Output Test)

      Ameba 作为 Client (发送),PC 作为 Server (接收),更多细节参考 网络带宽测试

    EVB:
    
    AT+IPERF=-c,192.168.31.234,-i,1,-t,5
    [+IPERF]: _AT_WLAN_IPERF1_TCP_TEST_
    
    OK
    
    [MEM] After do cmd, available heap 2977792
    
    #
    Start TCP client! id = [0]
    tcp_client_func: Server IP=192.168.31.234, port=5001
    tcp_client_func: Create socket fd = 0
    tcp_client_func: Connect to server successfully
    tcp_c: id[0] Send 11317 KBytes in 1000 ms, 92715 Kbits/sec
    tcp_c: id[0] Send 11326 KBytes in 1000 ms, 92785 Kbits/sec
    tcp_c: id[0] Send 11343 KBytes in 1000 ms, 92926 Kbits/sec
    tcp_c: id[0] Send 11379 KBytes in 1000 ms, 93218 Kbits/sec
    tcp_c: id[0] Send 11332 KBytes in 1000 ms, 92832 Kbits/sec
    tcp_c: [END] id[0] Totally send 56709 KBytes in 5001 ms, 92893 Kbits/sec
    tcp_client_func: Close client socket
    TCP client stopped!
    
    PC:
    
    Desktop>iperf-2-1-9-win.exe -s -i 1
    ------------------------------------------------------------
    Server listening on TCP port 5001
    TCP window size: 64.0 KByte (default)
    ------------------------------------------------------------
    [  1] local 192.168.31.234 port 5001 connected with 192.168.31.87 port 56481
    [ ID] Interval       Transfer     Bandwidth
    [  1] 0.00-1.00 sec  11.1 MBytes  92.8 Mbits/sec
    [  1] 1.00-2.00 sec  11.1 MBytes  92.8 Mbits/sec
    [  1] 2.00-3.00 sec  11.1 MBytes  92.9 Mbits/sec
    [  1] 3.00-4.00 sec  11.1 MBytes  93.2 Mbits/sec
    [  1] 4.00-5.00 sec  11.1 MBytes  92.8 Mbits/sec
    [  1] 0.00-5.00 sec  55.4 MBytes  92.9 Mbits/sec
    

存储主机方案

概述

USB Mass Storage Class (MSC) 协议建立了主机与大容量存储设备之间的标准通信规范。

Ameba 基于 USB-IF 官方发布的 MSC 协议标准,实现了完备的 USB MSC 主机协议栈。支持通过 SCSI(Small Computer System Interface)命令集与 MSC 设备交互,实现稳定、高速的文件读写。

../../_images/usb_host_msc_overview.svg

特性

  • 支持标准的 MSC 设备(如 U 盘、SD 卡读卡器等)

  • 最大支持 32GB 存储容量

  • 支持 FAT32 文件系统的读写操作

  • 支持热插拔

  • 自动解析描述符,自适应速度模式

  • 批量传输长度等参数可配置

应用场景

作为 USB 存储主机,Ameba 可通过 USB 接口挂载外部大容量存储设备,并结合网络实现多种数据交互应用,例如:

  • 智能网络存储/私有云:结合网络协议栈打通本地存储与云端连接,支持文件的自动双向同步及基于 DDNS/VPN 的安全远程访问,实现轻量级家庭私有云。

  • 工业物联网网关:在弱网环境(如矿山、远洋)下网关将传感器采集数据或串口日志缓存至 USB 存储设备,待网络恢复后自动执行断点续传上传云端,确保工业数据的完整性与可靠性。

  • AI 媒体库与存储扩展:利用外部存储存储海量媒体文件或 AI 知识库,突破设备内部 Flash 容量限制,实现本地离线检索与低成本功能扩展。

  • 硬件安全密钥:设备在启动或权限验证阶段扫描 U 盘内的特定加密证书或 License 文件,校验通过后激活系统高级权限,充当物理隔离的低成本硬件授权锁。

协议简介

协议文档

USB-IF 官方发布了 MSC 类基础协议及关于 BOT 传输协议规范。开发过程中请参考以下核心文档:

软硬件层级结构

下图展示了命令和数据在主机和设备之间所经过的软硬件层级。

../../_images/usb_device_msc_arch_cn.svg

以读操作为例,当用户从 U 盘读取一个文件时:

  • 主机端:

    • 应用请求:用户在应用程序(如文件管理器)中发起读文件请求。

    • 文件系统转换:文件系统将文件名与偏移量转换为逻辑块地址(LBA)读取请求,并生成标准的 SCSI READ 命令。

    • 协议封装:主机 MSC 类驱动将 SCSI 命令封装为 MSC 协议规定的命令包格式。

    • 硬件发送:USB 主机控制器驱动通过 USB 物理端口将数据包发送至总线。

  • 设备端:

    • 硬件接收:USB 设备控制器从物理端口接收数据包。

    • 协议解析:MSC 设备类驱动校验数据包完整性,并解析出内部封装的 SCSI 命令。

    • 介质访问:根据解析出的命令参数(如 LBA 地址和长度),从底层存储介质(如 SD 卡)读取对应数据。

    • 返回数据和状态:将读取的数据返回给主机,并回应命令执行状态。

BOT 传输流程

当 MSC 设备连接至主机并完成枚举后,若识别为支持 BOT 模式的大容量存储设备,后续的数据通信将仅通过批量端点进行。批量传输不受严格的时间限制,能够最大程度保证数据的完整性。

根据 MSC BOT 传输协议规范,所有的传输均遵循 "命令 (Command) -> 数据 (Data) -> 状态 (Status)" 的三段式流程:

  • CBW (Command Block Wrapper):由主机发往设备,包内封装了具体的 SCSI 命令(如 READ, WRITE, INQUIRY 等)。

  • Data (数据阶段):传输实际的文件或控制数据(传输方向取决于 SCSI 命令类型,对于某些无数据交互的命令,此阶段可省略)。

  • CSW (Command Status Wrapper):由设备发往主机,用于报告上一条 CBW 命令的执行结果(成功、失败或阶段错误)。

../../_images/usb_device_msc_bot_flow_cn.svg

数据传输流程如下:

  • 主机发起请求:主机 MSC 类驱动将 SCSI 命令封装进 CBW 包,通过批量 OUT 端点发送给设备。

  • 设备解析与执行:设备接收 CBW 包并进行合法性检查,解析出 SCSI 命令。若 CBW 有效,设备将根据命令操作底层物理存储介质:

    • 写操作 (如 WRITE):通过批量 OUT 端点接收主机发送的数据流,并写入存储介质。

    • 读操作 (如 READ):从存储介质读取数据,通过批量 IN 端点回传给主机。

    • 无数据命令 (如 TEST UNIT READY):跳过数据阶段,直接进入状态阶段。

  • 设备反馈状态:数据传输完成后(或无需传输数据),设备通过批量 IN 端点发送 CSW 包,向主机报告命令执行结果。

  • 主机确认完成:主机解析接收到的 CSW 包,检查 bCSWStatus 字段以确认命令是否执行成功,从而结束本次操作。

类特定请求

MSC 设备的控制请求分为 标准请求(Standard Requests)类特定请求 (Class-Specific Requests)

本节主要介绍 MSC BOT 规范特有的 类特定请求,这些请求用于实现特有的存储功能,只有下面两个:

MSC 类特定请求

要求

描述

Bulk-Only Mass Storage Reset

必选

重置设备接口及其相关的端点

Get Max LUN

必选

查询该设备支持的最大逻辑单元数量

SCSI 命令

MSC BOT 规范下主要的 SCSI 命令如下:

SCSI 命令

要求

描述

INQUIRY

必选

主机发送的第一个命令,用于查询设备信息

REQUEST_SENSE

必选

当任何命令执行失败时,主机会发送此命令来获取详细的错误信息

TEST_UNIT_READY

必选

检查设备是否就绪

READ_CAPACITY(10)

必选

查询存储介质的容量

READ(10)

必选

核心的读数据命令

WRITE(10)

必选

核心的写数据命令

MODE_SENSE(6)

可选

查询设备的特定参数,如缓存策略、写保护状态等

ALLOW_MEDIUM_REMOVAL

可选

用来允许或禁止移除介质,实现“安全删除硬件”功能

START_STOP_UNIT

可选

用于加载/弹出介质

VERIFY(10)

可选

请求设备验证指定块的数据是否可读,但不传输数据

READ_FORMAT_CAPACITIES

可选

提供比 READ_CAPACITY 更详细的容量和格式信息

备注

详细 SCSI 标准可参考官方文档。

类驱动

USB 协议栈提供了基于 BOT(Bulk-Only Transport)传输协议,使用 SCSI 命令集的标准 MSC 主机类驱动。

通道配置

数量

描述

2

控制 IN/OUT 传输

1

批量 IN 传输

1

批量 OUT 传输

类特定请求实现

驱动已实现了 MSC 的两个类特定请求(Class-Specific Requests)。

MSC 类特定请求

说明

Bulk-Only Mass Storage Reset

当 BOT 传输出错时,主机请求重置传输

Get Max LUN

设备连接后,主机查询设备支持逻辑单元数量

SCSI 命令实现

驱动已实现的 MSC BOT 规范下的 SCSI 命令如下,开发者可参考现有实现来扩展其他命令。

  • INQUIRY

  • REQUEST_SENSE

  • TEST_UNIT_READY

  • READ_CAPACITY(10)

  • READ(10)

  • WRITE(10)

FatFS 磁盘驱动 API

类驱动提供了类型为 ll_diskio_drv 的 FatFS 磁盘驱动 USB_disk_Driver,实现了以下 API:

API

描述

disk_initialize

初始化USB磁盘

disk_deinitialize

注销USB磁盘

disk_status

获取USB磁盘状态:RES_OK:就绪,RES_ERROR:未就绪

disk_read

从USB磁盘中读取数据

disk_write

将数据写入USB磁盘,仅当FATFS使能_USE_WRITE时有效

disk_ioctl

支持以下FATFS IO控制指令:

  • CTRL_SYNC:强制将缓冲区的数据写入物理存储介质,对于USB磁盘,可认为是写穿透的,不需要SYNC

  • GET_SECTOR_COUNT:获取sector数量

  • GET_SECTOR_SIZE:获取sector大小

  • GET_BLOCK_SIZE:获取block大小

API 说明

驱动 API

应用示例

应用设计

加载驱动

调用下面的示例代码完成配置结构体定和回调函数定义,随后调用初始化接口,加载 USB 主机核心驱动及 MSC 类驱动。

static usbh_config_t usbh_cfg = {
  .speed = USB_SPEED_HIGH,
  .ext_intr_enable = USBH_SOF_INTR,
  .isr_priority = INT_PRI_MIDDLE,
  .main_task_priority = 3U,
  .tick_source = USBH_SOF_TICK,
  }

static usbh_msc_cb_t usbh_msc_usr_cb = {
  .attach = usbh_msc_cb_attach,                  /* USB device attach callback */
  .setup = usbh_msc_cb_setup,                    /* USB device setup done, indicate that device is ready for bulk transfer */
};

static usbh_user_cb_t usbh_usr_cb = {
  .process = usbh_msc_cb_process
};

int ret = 0;
ret = usbh_init(&usbh_cfg, &usbh_usr_cb);   /* Initialize USB host core driver with configuration and user callback. */
if (ret != HAL_OK) {
    return;
}

ret = usbh_msc_init(&usbh_msc_usr_cb);          /* Initializes the MSC host class with MSC class user callback. */
if (ret != HAL_OK) {
    usbd_msc_disk_deinit();
    return;
}

文件读写

加载驱动后才可以对 MSC 设备进行文件读取、写入。

static rtos_sema_t msc_attach_sema;
static __IO int msc_is_ready = 0;

static int usbh_msc_cb_attach(void)
{
  RTK_LOGS(TAG, RTK_LOG_INFO, "ATTACH\n");
  rtos_sema_give(msc_attach_sema);
  return HAL_OK;
}

static int usbh_msc_cb_setup(void)
{
  RTK_LOGS(TAG, RTK_LOG_INFO, "SETUP\n");
  msc_is_ready = 1;
  return HAL_OK;
}

static int usbh_msc_cb_process(usb_host_t *host, u8 msg)
{
  UNUSED(host);

  switch (msg) {
  case USBH_MSG_DISCONNECTED:
    msc_is_ready = 0;
    break;
  case USBH_MSG_CONNECTED:
    break;
  default:
    break;
  }

  return HAL_OK;
}

static u32 filenum = 0;
static u8 *msc_wt_buf;
static u8 *msc_rd_buf;
FATFS fs;
FIL f;
int drv_num = 0;
char logical_drv[4];
char path[64] = {'0'};
u32 br;
u32 bw;

rtos_sema_create(&msc_attach_sema, 0U, 1U);

msc_wt_buf = (u8 *)rtos_mem_zmalloc(USBH_MSC_TEST_BUF_SIZE);
if (msc_wt_buf == NULL) {
  RTK_LOGS(TAG, RTK_LOG_ERROR, "Fail to alloc test buf\n");
  goto exit_deinit;
}

msc_rd_buf = (u8 *)rtos_mem_zmalloc(USBH_MSC_TEST_BUF_SIZE);
if (msc_rd_buf == NULL) {
  RTK_LOGS(TAG, RTK_LOG_ERROR, "Fail to alloc test buf\n");
  goto exit_deinit;
}

/* Register USB disk driver to fatfs*/
drv_num = FATFS_RegisterDiskDriver(&USB_disk_Driver);
if (drv_num < 0) {
  RTK_LOGS(TAG, RTK_LOG_ERROR, "Fail to register\n");
  goto exit_deinit;
}

logical_drv[0] = drv_num + '0';
logical_drv[1] = ':';
logical_drv[2] = '/';
logical_drv[3] = 0;

while (1) {
  if (msc_is_ready) {
    rtos_time_delay_ms(10);  /* Wait for MSC device is ready for class-specific communication */
    break;
  }
}

/* Mount logical drive */
if (f_mount(&fs, logical_drv, 1) != FR_OK) {
  RTK_LOGS(TAG, RTK_LOG_ERROR, "Fail to mount logical drive\n");
  FATFS_UnRegisterDiskDriver(drv_num);
  goto exit_deinit;
}

while (1) {
  if (rtos_sema_take(msc_attach_sema, RTOS_SEMA_MAX_COUNT) != RTK_SUCCESS) {
    RTK_LOGS(TAG, RTK_LOG_ERROR, "Fail to take sema\n");
    continue;                /* Wait for MSC device attach*/
  }
}

/* Construct the device file path */
strcpy(path, logical_drv);
sprintf(&path[3], "TEST%ld.DAT", filenum);

/* Open test file */
f_open(&f, path, FA_OPEN_ALWAYS | FA_READ | FA_WRITE);
/* Write the data from a buffer to an opened file */
f_write(&f, (void *)msc_wt_buf, USBH_MSC_TEST_BUF_SIZE, (UINT *)&bw);
/* Move the file pointer to the file head */
f_lseek(&f, 0);
/* Read data from an opened file to a buffer */
f_read(&f, (void *)msc_rd_buf, USBH_MSC_TEST_BUF_SIZE, (UINT *)&br);
/* Close source file */
f_close(&f);

exit_deinit:

  rtos_sema_delete(msc_attach_sema);

  if (msc_wt_buf) {
    rtos_mem_free(msc_wt_buf);
  }
  if (msc_rd_buf) {
    rtos_mem_free(msc_rd_buf);
  }

  usbh_msc_deinit();
  usbh_deinit();
  return;

卸载驱动

在不再需要存储功能或系统关机时,按加载的反序释放资源。

/* Deinitialize MSC host class driver. */
usbh_msc_deinit();
/* Deinitialize USB host core driver. */
usbh_deinit();

运行方式

本节介绍了一个完整的 USB 大容量存储(MSC)应用示例,该示例演示了如何通过 MSC 协议栈,将 Ameba 开发板配置为 USB 存储主机。

当开发板连接至支持 FAT32 格式的 U 盘等标准 MSC 设备时,主机可以识别并基于 FatFS 进行简单的文件读写测试。

该示例路径: {SDK}/component/example/usb/usbh_msc,可为开发者设计自定义 USB 存储主机产品提供完整的设计参考。

配置与编译

  • Menuconfig 配置

    amebaxxx_gcc_project 目录下,输入 ./menuconfig.py ,按下面步骤选择 USBH MSC,保存退出。

    - Choose `CONFIG USB --->`:
    
      [*] Enable USB
          USB Mode (Host)  --->
      [*] MSC
    
  • 编译与烧录

    执行编译命令,并烧录生成的 Image 文件至开发板:

    cd amebaxxx_gcc_project
    ./build.py -a usbh_msc
    

结果验证

  • 启动设备

    重启开发板,观察串口日志,应显示如下启动信息:

    [MSC-I] USBH MSC demo start
    [MSC-I] Register USB disk
    [MSC-I] FatFS USB W/R performance test start...
    
  • 连接设备

    用 OTG 线将格式化为 FatFS 的 U 盘连接到开发板的 USB 端口。

  • 识别和测试

    Ameba 开发板将识别 MSC 设备并进行读写性能测试。

    [USBH-A] Device attached,speed 0
    [USBH-A] PID: 0x6545
    [USBH-A] VID: 0x0930
    [USBH-A] Address 1 assigned
    [USBH-A] MFG: TOSHIBA
    [USBH-A] PROD: TransMemory
    [USBH-A] SN: C03FD5F7715FE3417000DE76
    [USBH-A] Enum done, total 1 cfg
    [USBH-A] Switch to itf: 0
    [USBH-A] Class: 0x08
    [USBH-A] SubClass: 0x06
    [USBH-A] Protocol: 0x50
    [MSC-I] ATTACH
    [MSC-I] Max lun 1
    [MSC-I] Lun 0
    [MSC-I] SETUP
    [MSC-I] Open file: 0:/TEST0.DAT
    [MSC-I] W test: size 512, round 20...
    [MSC-I] W rate 204.0 KB/s for 20 round @ 49 ms
    [MSC-I] R test: size = 512 round = 20...
    [MSC-I] R rate 476.1 KB/s for 20 round @ 21 ms
    [MSC-I] W test: size 1024, round 20...
    [MSC-I] W rate 540.5 KB/s for 20 round @ 37 ms
    [MSC-I] R test: size = 1024 round = 20...
    [MSC-I] R rate 800.0 KB/s for 20 round @ 25 ms
    [MSC-I] W test: size 2048, round 20...
    [MSC-I] W rate 655.7 KB/s for 20 round @ 61 ms
    [MSC-I] R test: size = 2048 round = 20...
    [MSC-I] R rate 1212.1 KB/s for 20 round @ 33 ms
    [MSC-I] W test: size 4096, round 20...
    [MSC-I] W rate 1095.8 KB/s for 20 round @ 73 ms
    [MSC-I] R test: size = 4096 round = 20...
    [MSC-I] R rate 1600.0 KB/s for 20 round @ 50 ms
    [MSC-I] FatFS USB W/R performance test done
    [MSC-I] File close OK
    

音频主机方案

概述

USB 音频类 (USB Audio Class, UAC) 协议定义了通过 USB 接口传输音频数据的工业标准。在主机 (Host) 模式下,Ameba 平台能够通过该协议识别并驱动外部 USB 音频设备。

当前 Ameba 平台的 UAC 主机协议栈专注于 音频播放 (Playback) 场景。它集成了符合 USB-IF 标准的 UAC 1.0 协议,将外接的 USB UAC 设备抽象为系统本地音频输出接口。该方案支持即插即用,能够与系统内置音频处理框架无缝对接,为设备提供便捷、高质量的音频输出扩展能力。

Ameba USB 音频播放应用拓扑

../../_images/usb_host_uac_overview_user_cn.svg

特性

Ameba UAC Host 驱动旨在提供稳定、兼容的音频输出能力,主要特性如下:

  • 广泛的设备兼容性:支持符合 UAC 1.0 标准的 USB 设备(如 USB 音箱、USB 耳机、USB 转 3.5mm 适配器)。

  • 自动化枚举配置:自动解析设备描述符,识别音频流端点,并建立等时 (Isochronous) 传输通道。

  • 支持主流音频格式:参考 支持的音频格式 了解更多信息。

  • 系统级深度集成:向上层应用暴露统一的 API 接口,屏蔽底层 USB 传输细节。

  • 热插拔支持 (Hot-Plug):支持 USB 外设的即插即用与动态移除,无需重启系统。

应用场景

作为 USB 主机,Ameba 负责枚举 UAC 设备、解析音频描述符,并建立稳定的数据传输通道。该方案适用于对音频播放质量有要求,同时追求低开发复杂度的嵌入式应用,例如:

  • 智能音频播报终端:Ameba 通过外接 UAC 设备(如有源音箱或耳机),用于语音提示、广告播报或公共广播系统。

  • 数字标牌与信息亭:结合本地存储或网络流媒体,通过 USB 高保真设备播放背景音乐或多媒体解说,提升用户体验。

  • IoT 音频网关:作为轻量级音频响应节点,接收来自 Wi-Fi 或云端的音频指令/内容,并通过通用 USB 音频外设进行输出。

协议简介

UAC (USB Audio Class) 是由 USB-IF 定义的通用音频设备类标准,旨在规范数字音频数据流在 USB 接口上的封装与传输方式。

通过遵循 UAC 标准,USB 音频设备(如 USB 音响、耳机、麦克风)能够在主机系统中被自动识别为标准的音频输入/输出终端,无需安装专有驱动。当前 Ameba 平台的 UAC Host 实现主要聚焦于外接 USB 音频输出设备 的支持,将其抽象为系统的本地音频播放接口。

协议文档

USB-IF 官方发布了完整的 UAC 类基础协议规范。在开发过程中,请参考以下核心文档:

规范类型

文档链接

UAC 1.0 (Audio Device Class Definition for Audio Devices)

https://www.usb.org/sites/default/files/audio10.pdf

术语定义

本文档涉及的通用 UAC 1.0 技术术语定义如下:

术语

描述

AC Interface (Audio Control Interface)

音频控制接口。负责管理音频设备的拓扑结构(如 Input/Output Terminal、Feature Unit),并通过控制端点(Control Endpoint)实现音量调节、静音开关等控制指令的下发。

AS Interface (Audio Streaming Interface)

音频流接口。负责音频负载数据的实际传输,通常使用等时(Isochronous)端点。一个 AS 接口可以包含多个备用设置(Alternate Settings),分别对应不同的采样率、位宽或通道配置。

Terminal Type

终端类型。用于标识音频信号源或接收器的物理/逻辑属性(例如:USB Streaming Terminal 代表 USB 数据流,Speaker Terminal 代表扬声器)。

Feature Unit

功能单元。音频拓扑中的处理节点,提供具体的音频控制能力(如主音量调节、各声道增益、静音控制)。

Sample Rate / Bit Resolution

采样率与位宽。U 音频格式的核心参数。UAC 1.0 常见组合包括 48 kHz / 16-bit、44.1 kHz / 16-bit 等。

协议框架

UAC 系统架构通过定义不同的接口集合来支持音频数据的传输与控制。从逻辑功能上,UAC 接口主要分为两大类:音频控制接口 (Audio Control Interface)音频流接口 (Audio Streaming Interface)

../../_images/usb_device_uac10_audio_control_topology.png
  • 音频控制接口(AC Interface)

    • 主机通过音频控制接口管理音频设备的整体功能行为,例如音量调节、静音控制、输入源选择等。

    • 一个 AC 接口内部包含定义的拓扑结构(Topology),用于描述音频信号从输入终端(Input Terminal)到输出终端(Output Terminal)的流向及处理过程。

  • 音频流接口 (AS Interface)

    • 主机通过音频流接口负责传输实际的音频负载数据。

    • 一个 UAC 设备可包含多个 AS 接口,每个接口可配置为传输不同格式、不同采样率或不同位深的音频数据。

协议交互示例:

../../_images/usb_uac_spec.svg

描述符结构

UAC 设备除遵循标准的 USB 描述符(如设备描述符、配置描述符、端点描述符)外,还定义了类特定描述符 (Class-Specific Descriptors)。

这些描述符依据其所属接口,分为 类特定控制接口描述符(AC)类特定音频流接口描述符(AS)

Descriptor Topology

Device Descriptor
└── Identifies basic device information (USB Version 1.10)

Configuration Descriptor
├── Contains total length of the entire configuration, power supply information, etc.
│
├── Audio Control (AC) Interface Descriptor (Interface 0)
│       ├── Standard Interface Descriptor (AlternateSetting 0, Control Class)
│       └── Class-Specific Descriptor Collection
│               ├── Audio Control Interface Header (declares UAC version)
│               ├── Input Terminal (source of audio stream)
│               ├── Feature Unit (volume/mute controls, etc.)
│               ├── Output Terminal (destination of audio stream)
│               ├── Input Terminal (source of audio stream)
│               ├── Feature Unit (volume/mute controls, etc.)
│               └── Output Terminal (destination of audio stream)
│
├── Audio Streaming (AS) Interface Descriptor (Interface 1)
│       ├── Alternate Setting 0: Control transfer active state (control transfer only)
│       │
│       ├── Alternate Setting 1: Data transfer active state (with data endpoint)
│       │ ├── Standard Interface Descriptor (Interface 1, Streaming Class)
│       │ ├── Class-Specific AS Interface (associated USB streaming terminal)
│       │ ├── Format Descriptor (audio format:channel, bit width and frequency)
│       │ ├── Standard Endpoint Descriptor (ISO OUT endpoint)
│       │ └── Class-Specific Endpoint Descriptor (no special control)
│       │
│       ├── Alternate Setting 2
│       │ ...... Can configure multiple different setting as needed
│
└── Audio Streaming (AS) Interface Descriptor  (Interface 2)
        ├── Alternate Setting 0: Control transfer active state (control transfer only)
        │
        ├── Alternate Setting 1: Data transfer active state (with data endpoint)
        │ ├── Standard Interface Descriptor (Interface 2, Streaming Class)
        │ ├── Class-Specific AS Interface (associated USB streaming terminal)
        │ ├── Format Descriptor (audio format:channel, bit width and frequency)
        │ ├── Standard Endpoint Descriptor (ISO IN endpoint)
        │ └── Class-Specific Endpoint Descriptor (no special control)
        │
        ├── Alternate Setting 2
        │ ...... Can configure multiple different setting as needed

UAC Audio Control (AC) Interface Descriptor

  • Audio Control Interface Header

Audio Control Interface Header Descriptor
├── bLength            : 1 byte  → Total descriptor length (typically 9 + bInCollection × 1)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x01 (HEADER)
├── bcdADC             : 2 bytes → Audio Device Class Specification Release Number (0x0100)
├── wTotalLength       : 2 byte  → Total number of bytes for all AC descriptors (including this header and all Unit/Terminal descriptors)
├── baInterfaceNr(1)   : 1 byte  → Interface number of the first AudioStreaming or MIDIStreaming interface in the Collection.
│    ⋮
└── baInterfaceNr(N)   : 1 byte  → Interface number of the last AudioStreaming or MIDIStreaming interface in the Collection.
  • Input Terminal Descriptor

Clock Source Descriptor
├── bLength            : 1 byte  → Total descriptor length (fixed = 12)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x0A (Clock Source)
├── bTerminalID        : 1 byte  → Constant uniquely identifying the Terminal within the audio function.
├── wTerminalType      : 2 bytes → Constant characterizing the type of Terminal.
├── bAssocTerminal     : 1 byte  → D of the Output Terminal to which this Input Terminal is associated.
├── bNrChannels        : 1 byte  → Number of logical output channels in the Terminal’s output audio channel cluster.
├── wChannelConfig     : 2 bytes → Describes the spatial location of the logical channels.
├── iChannelNames      : 1 byte  → Index of a string descriptor, d
└── iTerminal          : 1 byte  → String descriptor index
  • Feature Unit Descriptor

Feature Unit Descriptor
├─ bLength                : 1 byte   → otal descriptor length in bytes = 7+(ch+1)*n
├─ bDescriptorType        : 1 byte   → = 0x24 (CS_INTERFACE)
├─ bDescriptorSubtype     : 1 byte   → = 0x06 (FEATURE_UNIT)
├─ bUnitID                : 1 byte   → Unique ID of this Feature Unit
├─ bSourceID              : 1 byte   → ID of the connected Source Unit or Terminal
├─ bControlSize           : 1 byte   → Size in bytes of an element of the bmaControls() array: n
├─ bmaControls[0]         : n bytes  → A bit set to 1 indicates that the mentioned Control is supported for master channel
├─ bmaControls[1]         : n bytes  → A bit set to 1 indicates that the mentioned Control is supported for logical channel1
│    ⋮
├─ bmaControls[N]         : n bytes  → A bit set to 1 indicates that the mentioned Control is supported for logical channel ch
└─ iFeature               : 1 byte   → String descriptor index
  • Output Terminal Descriptor

Output Terminal Descriptor
├─ bLength                : 1 byte   → Total descriptor length (fixed = 9 bytes)
├─ bDescriptorType        : 1 byte   → 0x24 (CS_INTERFACE)
├─ bDescriptorSubtype     : 1 byte   → 0x03 (OUTPUT_TERMINAL)
├─ bTerminalID            : 1 byte   → Unique ID of this terminal
├─ wTerminalType          : 2 bytes  → Terminal type
│                           ├─ 0x0301 = Speaker
│                           ├─ 0x0302 = Headphones
│                           ├─ 0x0603 = SPDIF
│                           └─ Other values refer to Appendix B
├─ bAssocTerminal         : 1 byte   → Associated Input Terminal ID
├─ bSourceID              : 1 byte   → ID of the connected Source Unit or Terminal
└─ iTerminal              : 1 byte   → String index for describing this terminal

Audio Streaming Interface Descriptor

  • Class-Specific AS Interface Descriptor

Class-Specific AS Interface Descriptor
├─ bLength                : 1 byte   → Fixed as 0x07 (7 bytes)
├─ bDescriptorType        : 1 byte   → 0x24(CS_INTERFACE)
├─ bDescriptorSubtype     : 1 byte   → 0x01(AS_GENERAL)
├─ bTerminalLink          : 1 byte   → Associated Terminal ID (Input or Output Terminal)
├─ bDelay                 : 1 bytes  → Delay introduced by this interface (in number of frames)
└─ wFormatTag             : 2 byte   → Audio data format (e.g., 0x0001 = PCM)
  • Audio Streaming Format Type Descriptor

Audio Streaming Format Type Descriptor
├─ bLength            : 1 byte   → Total length of descriptor in bytes (8 + (num_freq × 3))
├─ bDescriptorType    : 1 byte   → = 0x24(CS_INTERFACE)
├─ bDescriptorSubtype : 1 byte   → = 0x02(FORMAT_TYPE)
├─ bFormatType        : 1 byte   → = 0x01(FORMAT_TYPE_I)
├─ bNrChannels        : 1 byte   → Number of channels (e.g., 2)
├─ bSubslotSize       : 1 byte   → Container size for each audio sample (in bytes)
│                                   • Typical values: 1, 2, 3, 4
├─ bBitResolution     : 1 byte   → Number of valid bits in each sample(≤ bSubslotSize × 8)
├─                                  • Example: 16 represents 16-bit PCM
├─ bSamFreqType       : 1 byte   → sampling frequency count
└─ tSamFreq(n)        : 3×n byte → Sample rates in little-endian 3-byte format

备注

详细的字段定义请参考 USB-IF 官方 UAC 协议文档。

类特定请求

UAC 主机对设备的控制请求分为 标准请求(Standard Requests)类特定请求 (Class-Specific Requests)

本节主要介绍 UAC 特有的 类特定请求,这些请求用于实现音频设备的特有功能,主要包括音频控制请求(针对 AC 接口)和音频流请求(针对 AS 接口)

  • 音频控制请求 (AC 请求)

    音频控制请求是主机通过端点 0 发送的类专用控制传输,用于在 USB 音频设备内动态配置和管理音频功能。

音频控制类型

要求

描述

Mute Control Request

可选

操作音频功能中功能单元内的静音控制项

Volume Control Request

可选

操作音频功能中功能单元内的音量控制项

Mixer Unit Control Request

可选

操作音频功能中混音单元内的控制项

Selector Unit Control Request

可选

操作音频功能中选择器单元内的控制项

Processing Unit Control Request

可选

操作音频功能中处理单元内的控制项

Extension Unit Control Requests

可选

操作音频功能中扩展单元内的控制项

  • 音频流请求(AS 请求)

    音频流请求是主机通过端点 0 发起的类专用控制传输,用于配置和管理音频数据流相关参数的核心类请求,其作用聚焦于音频流的建立、参数配置、状态管理等。

音频控制类型

要求

描述

Interface Control Request

可选

操作音频功能中音频流接口内的控制项

Endpoint Control Request

可选

描述音频功能可为其音频流端点支持的请求

数据传输格式

UAC 音频数据流通常采用 线性 PCM (Linear PCM) 编码,并以 多声道交错 (Interleaved) 的方式进行封装。具体的数据排列顺序取决于通道数量和位深。

更多支持格式细节请参考 支持的音频格式 了解更多细节。

两声道 (2-Channel) 交错数据示例如下图:

../../_images/usb_device_uac_audio_2_channel_data_interleaved.svg

四声道 (4-Channel) 交错数据示例:

../../_images/usb_device_uac_audio_4_channel_data_interleaved.svg

N 声道 (N-Channel) 交错数据示例:

../../_images/usb_device_uac_audio_n_channel_data_interleaved.svg

备注

声道对齐规则

UAC 协议要求传输的声道数通常应为 2 的幂次(如 2, 4, 8, 16 等)。

如果实际物理声道数(如 10 声道)不符合此规则,则必须向上取整至最近的 2 的幂次(配置为 16 声道)进行传输,多余的通道位置填充无效数据。

类驱动

本节详细阐述了 USB Host UAC 1.0 驱动栈的内部架构、关键模块职责、类特定请求的支持情况以及底层通道资源的分配策略。

驱动框架

USB Host UAC 1.0 驱动栈采用分层模块化设计,通过清晰定义的接口实现音频子系统与 USB 硬件控制器之间的高效交互。该架构重点优化了 等时传输 (ISOC Transfer) 的实时性处理,确保高保真音频流的稳定输出。

系统自上而下分为以下核心功能模块:

音频适配层 (Audio Adapter Layer)

作为 USB 驱动与上层音频框架的中间件,该层主要负责:

  • 写音频数据 (Write):当数据的生产者 (Producer),从上层音频缓冲区读取 PCM 数据,并写到 UAC 类驱动的环形缓冲区。

  • 控制交互:将上层应用的音量调节、静音开关等操作,映射为 UAC 类驱动的控制 API 调用。

UAC 类驱动架构

这是 UAC 类驱动的核心组件,由协议处理逻辑与 环形缓冲管理 (Ringbuffer Management) 协同构成。

  • 枚举与配置解析:自动识别 Audio Control (AC) 和 Audio Streaming (AS) 接口,解析 Terminal 类型、Feature Unit 及采样率/位宽等参数。

  • 流管理:根据目标音频参数,动态选择 AS 接口的备用设置 (Alternate Setting),激活对应的等时 OUT 端点。

  • 缓冲调度:维护环形缓冲区,处理数据的封包与调度,确保持续、无抖动的数据供给。

  • 协议封装:封装标准 USB 请求与 UAC 类特定请求(如 SET_CUR),实现对设备的控制。

USB Core 驱动

实时响应硬件中断,负责处理 USB 标准枚举、传输管理以及底层的物理数据传输调度等。

核心交互接口

UAC Host 类驱动在系统架构中起着承上启下的作用,其实现逻辑主要围绕以下三个核心交互接口展开:

  • 主机类驱动回调 API:类驱动通过定义并注册一个标准的 usbh_class_driver_t 结构体与底层 USB Core 进行交互。

  • 面向应用的回调 API:类驱动通过 usbh_uac_cb_t 回调结构体向上层应用提供异步事件通知机制。

  • 面向应用的 API:应用层调用这些 API 后,驱动会切换内部状态机的状态,开启数据传输的调度。

驱动回调机制:

../../_images/usb_host_uac.svg

加载与卸载类驱动

这两个函数负责内存资源的分配与释放,以及类驱动向 USB 核心的注册与注销。

usbh_uac_init() 是加载 UAC 主机类驱动的顶层函数,主要完成以下任务:

  • 保存用户提供的回调函数,并调用用户 init 回调。

  • 保存用户配置的环形缓冲区的帧数 frame_cnt

  • 分配内存,包括用于控制传输的缓冲和用于 TX 音频传输的缓冲。

  • 调用 usbh_register_class() 注册 UAC 类驱动到 USB 主机核心。

示例:

int usbh_uac_init(usbh_uac_cb_t *cb, int frame_cnt)
{
    /* 1. Save the frame count param */
    uac->frame_cnt = frame_cnt;

    /* 2. Allocate memory */
    uac->audio_ctrl_buf = (u8 *)usb_os_malloc(UBSH_UAC_AUDIO_CTRL_BUF_MAX_LEN);
    uac->isoc_tx_buf = (u8 *)usb_os_malloc(USBH_UAC_ISOC_BUF_LENGTH);

    /* 3. Save the user callback and call the user's ``init`` callback */
    if (cb != NULL) {
      uac->cb = cb;
      if (cb->init != NULL) {
        ret = cb->init();
        if (ret != HAL_OK) {
          RTK_LOGS(TAG, RTK_LOG_ERROR, "User init err %d\n", ret);
          return ret;
        }
      }
    }

    /* 4. Register class driver*/
    usbh_register_class(&usbh_uac_driver);

    return HAL_OK;
}

连接与断连处理

当 USB 核心检测到匹配 UAC 类的设备插入或拔出时,会触发相应的回调函数。

usbh_uac_cb_attach 是设备枚举的关键步骤,负责解析接口描述符并分配管道资源:

  • 查找音频控制接口 (AC Interface):解析获取音频控制信息,包括音量范围、静音控制能力等。

  • 查找音频数据接口 (AS Interface):解析获取音频格式信息及端点描述符。

  • 打开管道:根据获取到的描述符信息,分配并打开 ISOC OUT 管道。

  • 初始化状态机:将状态置为 UAC_STATE_GET_MUTE 初始状态,准备获取音频信息。

  • 通知应用层:调用用户 attach 回调,告知应用层连接状态。

static int usbh_uac_cb_attach(usb_host_t *host)
{
  /* 1. Parse descriptors to get AC and AS information */
  status = usbh_uac_parse_interface_desc(host);
  if (status) {
    return status;
  }

  /* 2. Open the pipe for steaming transfer */
  if (uac->as_isoc_out) {
      as_itf = uac->as_isoc_out;
      as_itf->choose_alt_idx = 0;

      pipe = &(as_itf->pipe);
      ep_desc = &(as_itf->itf_info_array[as_itf->choose_alt_idx].ep_desc);

      usbh_open_pipe(host, pipe, ep_desc);
  }

  /* 3. Initialize the state machine */
  uac->ctrl_state = UAC_STATE_GET_MUTE;

  /* 4. Notify the user layer */
  if ((uac->cb != NULL) && (uac->cb->attach != NULL)) {
      uac->cb->attach();
  }
  return HAL_OK;
}

类驱动状态机

usbh_uac_cb_process 回调函数是主机端 UAC 类的核心状态机处理函数。

与设备端被动响应请求不同,主机端驱动需要主动维护设备状态。它的核心职责是维护类驱动的生命周期状态,并分发传输任务。

状态机管理与调度

usbh_uac_cb_process 根据当前类驱动状态管理控制传输(如采样率配置)和数据传输的调度。

状态枚举

描述

关键动作

IDLE

空闲状态

等待用户指令或数据传输请求。

TRANSFER

数据传输中

根据管道号分发任务到具体的 TX/RX 处理函数。

ERROR

错误状态

尝试清除端点特征 (Clear Feature) 以恢复通信。

传输处理分发示例

当处于 TRANSFER 状态时,根据触发事件的管道号(Pipe ID),将处理分发给具体的传输处理函数。

示例:

case UAC_STATE_TRANSFER:
     /* Distribute transmission tasks according to pipe numbers */
     if (event.msg.pipe_num == 0) {
         ret = usbh_uac_ctrl_setting(host, 0);      // Handle ctrl message transfer
     }
  break;

错误恢复

在其他状态处理发生错误时,驱动会尝试发送 Clear Feature 请求并恢复到 IDLE 状态。

示例:

/* ... Error state ... */
case UAC_STATE_ERROR:
    /* Error recovery mechanism */
    ret = usbh_ctrl_clear_feature(host, 0x00U);
    if (ret == HAL_OK) {
        uac->xfer_state = UAC_STATE_IDLE;
    }
break;

数据传输处理

UAC 数据传输主要分为两类:音频控制接口数据传输音频数据流传输

音频控制传输(Control Transfer)

负责管理音频设备的功能行为,驱动实现了如下状态机流程来处理配置请求:

case UAC_STATE_SET_OUT_ALT:
    ret = usbh_uac_process_set_out_alt(host);
    if (ret == HAL_OK) {
        uac->ctrl_state = UAC_STATE_SET_OUT_FREQ;
    } else if (ret != HAL_BUSY) {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "OUT alt err\n");
        uac->ctrl_state = UAC_STATE_SET_OUT_FREQ;
    }
    break;

音频数据流传输(ISOC Transfer)

为确保音频流的实时性与低延迟,UAC 类驱动通过 USB 硬件中断机制(帧首中断 SOF 或传输完成中断 Complete)来精准触发下一帧数据的传输调度。

  • Completed (传输完成):传输完成中断,表示当前数据已成功发送至总线。

当端点传输间隔 (bInterval) 为 1 时(即每帧都需要传输),为了最大化带宽利用率并降低延迟,驱动采取 紧凑调度策略:在上一帧数据的 Completed 回调中,立即填装并提交下一帧的数据传输请求。

  • SOF (Start Of Frame):帧起始中断。

当端点传输间隔 (bInterval) 大于 1 时(即每隔 N 帧传输一次),驱动采取 间隔调度策略,利用 SOF 中断计数器跟踪帧号,一旦达到预定的发送间隔,立即准备并提交下一帧数据传输请求。

代码示例 (Completed 回调逻辑):

static usbh_class_driver_t usbh_uac_driver = {
    .completed = usbh_uac_cb_completed,
};

static int usbh_uac_cb_completed(usb_host_t *host, u8 pipe_num)
{
    u32 cur_frame = usbh_get_current_frame_number(host);

    if (pdata_ctrl->next_xfer == 1) {
        /* Check if this is the audio streaming pipe */
        if ((uac->as_isoc_out) && (pipe_num == uac->as_isoc_out->pipe.pipe_num)) {
            pipe->xfer_state = USBH_EP_XFER_IDLE;

            /* If ring buffer has data, prepare next transfer */
            if (!usb_ringbuf_is_empty(&(pdata_ctrl->buf_manager))) {

                /* Trigger next xfer logic: check if interval is met */
                if (usbh_uac_frame_num_dec(usbh_uac_frame_num_inc(cur_frame, 1), pipe->frame_num) >= pipe->ep_interval) {
                    usbh_uac_isoc_out_process_xfer(host, cur_frame);
                } else {
                    /* Wait for next SOF if interval not met yet */
                    pipe->xfer_state = USBH_EP_XFER_WAIT_SOF;
                }
            } else {
                /* Buffer empty, go idle */
                pipe->xfer_state = USBH_EP_XFER_IDLE;
            }
        }
    }

    return HAL_OK;
}

面向应用的 API

提供给上层应用的接口,用于获取信息、配置设备和传输数据。

配置类

数据流类

  • usbh_uac_write():将 PCM 数据写入驱动内部的环形缓冲区。类驱动会自动根据音频格式将大数据包分割为符合 USB 帧大小的小包。

  • usbh_uac_start_play():启动播放流程,驱动开始从环形缓冲区提取数据并发送。

  • usbh_uac_stop_play():停止播放,终止 ISOC OUT 传输。

类特定请求实现

本驱动栈已内置 USB Audio Class 1.0 规范定义的核心请求封装。当前实现重点支持音频播放场景,默认启用了针对 Feature Unit 的控制请求。

开发者可在 {SDK}/component/usb/host/uac 路径下查看源码,并根据需求扩展更多请求类型。

类特定请求类型

备注

Mute Control

静音控制。发送 SET_CUR 请求至 Feature Unit 的 Mute Control Selector,实现静音/取消静音。

Volume Control

音量控制。发送 SET_CUR 请求至 Feature Unit 的 Volume Control Selector,设置增益值。

通道配置

在设备枚举阶段,驱动会解析配置描述符。当检测到音频流接口 (AS Interface) 处于活动状态时,驱动将自动申请相应的 USB 管道 (Pipe) 资源。

管道类型

用途描述

控制 IN/OUT 通道

用于发送标准设备请求(枚举、配置)以及 UAC 类特定请求(音量、采样率设置)。

等时 OUT 通道

归属于 Audio Streaming (AS) 接口的 Active Alternate Setting(通常为 Alt 1)。用于主机向 UAC 设备发送 PCM 音频数据流(TX Data)。

API 说明

驱动 API

应用示例

应用开发指南

本节详细介绍开发一个完整的 USB UAC 1.0 主机应用所需的流程,涵盖驱动初始化、热插拔事件处理、音频数据写入机制以及资源释放策略。

驱动初始化

在使用 UAC 1.0 Host 驱动前,必须按顺序完成硬件参数配置、回调函数注册以及核心协议栈的初始化。

步骤说明:

  • 硬件配置:设置 USB 速度模式(Full Speed)及中断/任务优先级。

  • 回调注册:定义用户回调结构体,挂载各个阶段(连接、断开、数据传输)的处理函数。

  • 核心启动:依次调用 usbh_init()usbh_uac_init() 启动协议栈。

/*
 * 1. Configure USB speed (Full Speed) ,isr priority and main task priority.
 */
static usbh_config_t usbh_cfg = {
        .speed = USB_SPEED_FULL,
        .isr_priority = INT_PRI_MIDDLE,
        .main_task_priority = USBH_UAC_MAIN_THREAD_PRIORITY,
        .ext_intr_enable = USBH_SOF_INTR,
};
/*
 * Define USB user-level callbacks.
 */
static usbh_user_cb_t usbh_usr_cb = {
        .process = usbh_uac_cb_process,            /* USB callback to handle class-independent events in the application */
};

/*
 * 2. Define user callbacks for UAC events.
 */
static usbh_uac_cb_t usbh_uac_cfg = {
        .init   = usbh_uac_cb_init,                /* USB init callback */
        .deinit = usbh_uac_cb_deinit,              /* USB deinit callback */
        .attach = usbh_uac_cb_attach,              /* USB attach callback */
        .detach = usbh_uac_cb_detach,              /* USB detach callback */
        .setup  = usbh_uac_cb_setup,               /* USB setup callback */
        .isoc_transmitted     = usbh_uac_cb_isoc_transmitted,    /* Data transmission complet callback */
};

int ret = 0;

/**
 * 3. Initialize USB host core driver with configuration.
 */
ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
if (ret != HAL_OK) {
        return;
}

/*
 * 4. Initialize UAC class driver.
 */
ret = usbh_uac_init(&usbh_uac_cfg, USBH_UAC_FRAME_CNT);
if (ret != HAL_OK) {
        /* If class driver init fails, clean up the core driver */
        usbh_deinit();

        return;
}

热插拔事件处理

系统必须具备健壮的热插拔处理机制,以应对 UAC 设备的动态移除与重新插入。SDK 提供了标准的状态机与回调机制来通知上层应用。

处理逻辑:

  • 设备拔出 (Detach):触发回调释放信号量,应用线程捕获后执行反初始化,并释放堆内存。

  • 设备插入 (Attach):USB 核心检测到设备,重新执行枚举和驱动加载流程。

/* USB detach callback */
static usbh_uac_cb_t usbh_uac_cfg = {
        .detach = usbh_uac_cb_detach,
};

/* Callback executed in main task */
static int usbh_uac_cb_detach(void)
{
        RTK_LOGS(TAG, RTK_LOG_INFO, "DETACH\n");
        rtos_sema_give(usbh_uac_detach_sema);
        usbh_uac_is_ready = 0;
        return HAL_OK;
}

/* Thread Context: Handle the state machine */
static void usbh_uac_hotplug_thread(void *param)
{
        int ret = 0;

        UNUSED(param);

        for (;;) {
                usb_os_sema_take(usbh_uac_detach_sema, USB_OS_SEMA_TIMEOUT);
                RTK_LOGS(TAG, RTK_LOG_INFO, "Hot plug\n");

                /* Stop transfer, release resource */
                usbh_uac_deinit();
                usbh_deinit();

                rtos_time_delay_ms(10);
                RTK_LOGS(TAG, RTK_LOG_INFO, "Free heap size: 0x%08x\n", usb_os_get_free_heap_size());

                /* Re-init */
                ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
                if (ret != HAL_OK) {
                        break;
                }

                ret = usbh_uac_init(&usbh_uac_cfg, USBH_UAC_FRAME_CNT);
                if (ret < 0) {
                        usbh_deinit();
                        break;
                }
        }
}

音频数据流机制

UAC 类驱动采用环形缓冲区 (Ring Buffer) 机制来缓冲上层应用产生的音频数据,并通过 SOF (Start of Frame) 和传输完成中断来驱动数据的持续发送。

  • 数据写入 (Audio Write)

上层应用调用 usbh_uac_write() 接口将 PCM 数据填入环形缓冲区。如果缓冲区已满,该函数将根据设定的 timeout_ms 进行阻塞等待。

static int usbh_uac_write_ring_buf(usbh_uac_buf_ctrl_t *pdata_ctrl, u8 *buffer, u32 size, u32 *written_len)
{
        u32 written_size = handle->written;

        /* Fill it into the end of the data that was not completed last time to form a whole package */
        if (written_size) {
                xfer_len = usbh_uac_next_packet_size(pdata_ctrl);
                can_copy_len = xfer_len - written_size;
                copy_len = size < can_copy_len ? size : can_copy_len;

                usb_ringbuf_write_partial(handle, buffer, copy_len);
                offset += copy_len;
                *written_len += copy_len;

                if (size >= can_copy_len) {
                        size -= copy_len;
                        usb_ringbuf_finish_write(handle);
                        pdata_ctrl->sample_accum = pdata_ctrl->last_sample_accum;
                } else {
                        return 0;
                }
        }

        /* Fill the entire package in a cycle */
        do {
                if (usb_ringbuf_is_full(handle)) {
                        return 1;
                }

                xfer_len = usbh_uac_next_packet_size(pdata_ctrl);

                if (size >= xfer_len) {
                        usb_ringbuf_add_tail(handle, buffer + offset, xfer_len);

                        *written_len += xfer_len;
                        size -= xfer_len;
                        offset += xfer_len;

                        pdata_ctrl->sample_accum = pdata_ctrl->last_sample_accum;
                } else {
                        break;
                }
        } while (1);

        /* Write the remaining data at the end */
        if (size > 0) {
                if (usb_ringbuf_is_full(handle)) {
                        return 1;
                }

                usb_ringbuf_write_partial(handle, buffer + offset, size);

                *written_len += size;
        }
        return 0;
}

/* Tansfer APi, used for Audio */
u32 usbh_uac_write(u8 *buffer, u32 size, u32 timeout_ms)
{
        /* check usb status */
        if (usbh_uac_usb_status_check() != HAL_OK) {
                return 0;
        }

        /* loop to write data to the ringbuffer */
        while (written_len < size && pdata_ctrl->next_xfer) {

                if (usb_ringbuf_is_full()) {
                        if (usbh_uac_wait_isoc_with_status_check(pdata_ctrl, timeout_ms) != HAL_OK) {
                                break;
                        }
                }

                try_len = size - written_len;
                just_written = 0;

                usbh_uac_write_ring_buf(pdata_ctrl, buffer + written_len, try_len, &just_written);
                RTK_LOGS(TAG, RTK_LOG_DEBUG, "Want %msg, wrote %msg\n", try_len, just_written);

                if (just_written > 0) {
                        written_len += just_written;
                        last_zero = 0;
                } else {
                        //wait sema and retry
                        last_zero = 1;
                }
        }

        return written_len;
}
  • 数据发送 (Audio Output)

底层驱动通过中断回调自动从环形缓冲区取出数据并发送至 USB 总线。

  • SOF 中断:周期性触发(每 1ms)。检查当前帧号是否达到发送间隔(Interval),若是,则从 RingBuffer 取数据并提交传输。

  • Commplete 中断:上一次传输完成后触发。检查 RingBuffer 是否有剩余数据,如有则安排下一帧的发送任务。

static usbh_class_driver_t usbh_uac_driver = {
        .sof = usbh_uac_cb_sof,
        .completed = usbh_uac_cb_completed,
};

static void usbh_uac_isoc_out_process_xfer(usb_host_t *host, u32 cur_frame)
{
        if (!usb_ringbuf_is_empty(&(pdata_ctrl->buf_list))) {
                // check valid data
                pbuf = usb_ringbuf_get_head(&(pdata_ctrl->buf_list));
                if (pbuf && pbuf->buf_len > 0) {
                        pipe->frame_num = usbh_uac_frame_num_inc(cur_frame, 1);
                        pipe->xfer_buf = pbuf->buf;
                        pipe->xfer_len = pbuf->buf_len;
                        usbh_transfer_data(host, pipe);
                        pipe->xfer_state = USBH_EP_XFER_BUSY;
                }
}

static int usbh_uac_cb_sof(usb_host_t *host)
{
        /* this class right not just support isoc out */
        if (pdata_ctrl->next_xfer == 1) {
                /*
                        if cur_frame - last frame_num  >= interval, means we should trigger a xfer asap
                        if xfer_state = USBH_EP_XFER_WAIT_SOF, it means that last xfer has been done, so in sof intr, we should check whether the next frame will be the xfer frame
                */
                if ((usbh_get_elapsed_frame_cnt(host, pipe->frame_num) >= pipe->ep_interval) ||
                        ((pipe->xfer_state == USBH_EP_XFER_WAIT_SOF) &&
                         (usbh_uac_frame_num_dec(usbh_uac_frame_num_inc(cur_frame, 1), pipe->frame_num) >= pipe->ep_interval))) {
                        usbh_uac_isoc_out_process_xfer(host, cur_frame);
                }
        }
        return HAL_OK;
}

static int usbh_uac_cb_completed(usb_host_t *host, u8 pipe_num)
{
        if (pdata_ctrl->next_xfer == 1) {
                if ((uac->as_isoc_out) && (pipe_num == uac->as_isoc_out->pipe.pipe_num)) {
                        usbh_uac_isoc_out_process_complete(host);
                        if (!usb_ringbuf_is_empty(&(pdata_ctrl->buf_list))) {
                                //trigger next xfer after binterval
                                if (usbh_uac_frame_num_dec(usbh_uac_frame_num_inc(cur_frame, 1), pipe->frame_num) >= pipe->ep_interval) {
                                        usbh_uac_isoc_out_process_xfer(host, cur_frame);
                                } else {
                                        pipe->xfer_state = USBH_EP_XFER_WAIT_SOF;
                                }
                        } else {
                                /* TX ISOC OUT token only when play*/
                                pipe->xfer_state = USBH_EP_XFER_IDLE;
                        }
                }
        }

        return HAL_OK;
}

驱动卸载

在系统关闭或需要完全重置 USB 栈时,必须严格按照 先类驱动,后核心驱动 的反向顺序释放资源,以避免内存泄漏或指针错误。

/* 1. Deinitialize UAC class driver first */
usbh_uac_deinit();

/* 2. Deinitialize USB host core driver */
usbh_deinit();

示例工程运行指南

本节以 Ameba 连接 USB 耳机播放音频 为例,演示如何将 Ameba 开发板配置为 USB UAC 1.0 主机,并通过外接标准 USB 耳机输出音频。

该示例默认行为:识别 UAC 设备 -> 配置为 48 kHz / 16-bit / 2-channels -> 循环播放一段预置的 PCM 音频。

该示例路径: {SDK}/component/example/usb/usbh_uac,可为开发者设计语音播报、音频网关等产品提供完整的设计参考。

配置与编译

  • Menuconfig 配置

    amebaxxx_gcc_project 目录下,输入 ./menuconfig.py,按下面步骤选择 UAC, 保存退出。

[*] Enable USB
                USB Mode (Host)  --->
[*]     UAC
  • 编译与烧录

    执行编译命令,并烧录生成的 Image 文件至开发板:

cd amebaxxx_gcc_project
./build.py -a usbh_uac

结果验证

  • 启动设备

    复位开发板,观察串口日志(Log UART)。当出现以下 Log 时,表示 USB Host 初始化成功:

[UAC-I] USBH UAC demo start
  • 连接设备

    将兼容 UAC 1.0 的 USB 耳机插入开发板。

  • 功能测试

    • 自动播放测试

      连接成功后,系统会自动开始音频流传输。

      预期结果:耳机中听到预置的音频片段(默认循环播放 60 次,每次 1 秒)。

      备注

      可在 example_usbh_uac.c 中修改循环次数配置。

    • 静音控制测试 (Mute)

      在串口控制台输入命令:

      • uach_mute 1:耳机静音。

      • uach_mute 0:取消静音,恢复声音。

    • 音量控制测试 (Volume)

      在串口控制台输入命令:

      • uach_vol 10:设置音量为 10%(声音变小)。

      • uach_vol 90:设置音量为 90%(声音变大)。

视频主机方案

概述

USB 视频类 (USB Video Class, UVC) 协议定义了通过 USB 接口传输视频数据的工业标准。在主机 (Host) 模式下,Ameba 平台能够通过该协议识别并驱动外部 USB 摄像头设备。

Ameba 平台的 UVC 主机协议栈专注于 视频采集 (Capture) 场景。它集成了符合 USB-IF 标准的 UVC 1.5 协议,将外接的 USB 摄像头抽象为系统本地视频输入接口。该方案支持即插即用,能够与系统内置视频处理框架无缝对接,为设备提供便捷、灵活的视觉感知扩展能力。

../../_images/usb_host_uvc_user.svg

特性

Ameba UVC Host 驱动旨在提供高效、兼容的视频输入能力,主要特性如下:

  • 广泛的设备兼容性:支持符合 UVC 标准的 USB 摄像头设备(如 USB 网络摄像头、工业相机、USB 显微镜)。

  • 自动化枚举配置:自动解析设备描述符,识别视频流接口与控制接口,并建立等时 (Isochronous) 传输通道。

  • 主流视频格式支持

    • 压缩格式:MJPEG, H.264

    • 非压缩格式:YUV

  • 系统级深度集成:向上层应用暴露统一的 API 接口,屏蔽底层 USB 传输细节。

  • 热插拔支持 (Hot-Plug):支持 USB 外设的即插即用与动态移除,无需重启系统。

应用场景

作为 USB 主机,Ameba 负责枚举 USB 摄像头、解析视频描述符,并建立稳定的图像数据流。该方案适用于对视觉采集有需求,同时追求低功耗与快速集成的嵌入式应用,例如:

  • 智能监控与安防:Ameba 通过 USB 摄像头实时采集画面,结合网络传输或本地存储,用于家庭监控、门铃猫眼或工业现场监视。

  • 视觉识别终端:作为边缘计算节点的前端采集设备,获取图像数据并传递给后续的 AI 算法进行人脸识别、二维码扫描或物体检测。

  • 视频通话设备:结合 Wi-Fi 或蜂窝网络模组,通过通用 USB 摄像头采集用户视频流,实现低成本的 VoIP 视频对讲功能。

协议简介

UVC (USB Video Class) 协议定义了在 USB 规范框架下,实现主机与视频捕获设备之间 控制管理视频流传输 的标准接口。主机驱动通过此协议建立视频数据通道,实现摄像头的实时预览、录制及参数调节。

协议文档

USB-IF 官方发布了 UVC 类基础协议及多种 Payload 格式规范。开发过程中请参考以下核心文档:

规范类型

文档

UVC 1.5 (视频类基础协议)

USB_Video_Class_1_5.zip

Payload Specs (负载格式)

包含在上述压缩包的 USB_Video_Payload_*.pdf 文件中。

术语定义

本文档涉及的通用 UVC (USB Video Class) 技术术语定义如下:

术语

描述

VC Interface (Video Control Interface)

视频控制接口。作为 UVC 设备的核心控制中心,负责管理视频设备的拓扑结构(如 Unit 和 Terminal 的连接关系)。主机通过该接口向设备发送控制请求,例如调整亮度、对比度或进行云台控制(Pan/Tilt/Zoom)。

VS Interface (Video Streaming Interface)

视频流接口。负责视频负载数据的实际传输,通常使用等时(Isochronous)或批量(Bulk)管道。每个 VS 接口包含具体的视频格式信息(如 YUV, MJPEG, H.264)以及相关的帧描述符。

Input Terminal (IT)

输入终端。视频数据流进入 UVC 功能拓扑的入口点。常见的输入终端包括摄像头传感器(Camera Sensor)或复合视频输入接口。它代表了数据的物理源头。

Output Terminal (OT)

输出终端。视频数据流离开 UVC 功能拓扑的出口点。最常见的输出终端是 USB Streaming Terminal,表示数据将通过 USB 总线发送给主机。

Processing Unit (PU)

处理单元。位于输入终端之后的处理节点,用于对视频图像本身进行调整。它提供了对图像质量的控制能力,例如亮度(Brightness)、对比度(Contrast)、色调(Hue)、饱和度(Saturation)和清晰度(Sharpness)。

Extension Unit (XU)

扩展单元。UVC 规范允许厂商自定义的功能模块。通过 XU,厂商可以定义标准 UVC 规范之外的特定控制指令,并在主机端通过配套驱动或应用进行访问。

Probe & Commit Control

协商与提交控制。这是视频流建立过程中的关键机制。主机先发送 "Probe" 请求查询设备支持的带宽和参数,协商一致后发送 "Commit" 请求锁定配置,随后才能开启视频流传输。

Payload Header

负载头。UVC 视频流数据包中的头部信息,包含了帧翻转(Frame ID)、时间戳(PTS/SCR)以及错误标志位等关键同步信息。

协议框架

UVC Host 协议栈采用分层架构设计,旨在实现 USB 传输层与上层视频应用或多媒体框架的解耦。

../../_images/usb_host_uvc_arch.svg

组件职责

  • 应用层 (Application)

    位于架构最顶层,负责具体的业务逻辑处理。包括视频预览、录制应用,或基于视频流的 AI 算法与网络推流服务。

  • 视频中间件层 (Video Middleware)

    起到承上启下的抽象作用。向上为应用层提供统一的数据获取接口,屏蔽底层差异;负责视频流的编解码处理、格式转换及缓冲队列管理。

  • UVC 类驱动 (Class Driver)

    核心中间层,实现了 UVC 规范定义的行为:

    • 拓扑解析:解析视频控制接口 (VC) 的内部拓扑(Unit/Terminal)以及视频流接口 (VS) 的格式描述符。

    • 流协商:实现 Probe 和 Commit 流程,协商分辨率、帧率及带宽等参数。

    • 帧重组与提交:解析 UVC 负载头 (Payload Header),处理帧起始/结束标识 (FID/EOF),将分散的 USB 包重组为完整视频帧,并以帧为单位提交给中间层。

  • USB Core & HCD (Host Controller Driver)

    底层驱动,负责处理 USB 标准枚举、同步管道管理以及底层的物理数据传输调度。

通信机制

标准的 UVC 设备通过 接口关联描述符 (IAD) 聚合以下接口:

视频控制接口 (VC Interface) - 拓扑控制

  • 传输机制:基于默认管道 0 (EP0) 的控制传输 (Control Transfer)。

  • 核心职能:发送类特定请求以控制 Unit/Terminal 属性,通过 Probe (探测) 和 Commit (提交) 流程完成参数协商。

视频流接口 (VS Interface) - 数据管道

  • 传输机制:采用同步传输 (Isochronous) 模式承载高带宽视频流。

  • 特性:保障带宽与低延迟(无重传),适用于实时视频预览。

描述符结构

UVC 设备除遵循标准的 USB 描述符(如设备描述符、配置描述符、管道描述符)外,还定义了类特定描述符 (Class-Specific Descriptors)。

这些描述符依据其所属接口,分为 类特定视频控制接口描述符(VC) 和 类特定视频流接口描述符(VS)。

Descriptor Topology

Device Descriptor
└── Identifies basic device information (USB Version 2.00, Composite Device)

Configuration Descriptor
├── Contains total length, power supply (500mA), etc.
│
├── Interface Association Descriptor (IAD)
│       └── Groups Interface 0 (VC) and Interface 1 (VS) as a Video Function
│
├── Video Control (VC) Interface Descriptor (Interface 0)
│       ├── Standard Interface Descriptor (AlternateSetting 0, Video Control Class)
│       ├── Class-Specific VC Header (declares UVC version, clock frequency)
│       ├── Class-Specific Descriptor Collection (Topology)
│       │       ├── Input Terminal (Camera)
│       │       ├── Processing Unit
│       │       ├── Extension Unit (Vendor Specific Controls)
│       │       └── Output Terminal (USB Streaming)
│       └── Standard Endpoint Descriptor (Interrupt IN for Status)
│
└── Video Streaming (VS) Interface Descriptor (Interface 1)
                ├── Alternate Setting 0: Control transfer active state (negotiation only, no data endpoint)
                │ ├── Standard Interface Descriptor (Interface 1, Streaming Class)
                │ ├── Class-Specific VS Header
                │ ├── Format Descriptor
                │ │     └── Frame Descriptor (various resolutions and frame rates)
                │ ├── Format Descriptor
                │ │     └── Frame Descriptor (various resolutions and frame rates)
                │ ├── Still Image Frame Descriptor
                │ └── Color Matching Descriptor
                │
                ├── Alternate Setting 1: Data transfer active state (with data endpoint)
                │ ├── Standard Interface Descriptor (Interface 1, Streaming Class)
                │ └── Standard Endpoint Descriptor (ISO IN endpoint, e.g., Low Bandwidth)
                │
                ├── Alternate Setting 2
                │ ...... Can configure multiple different settings as needed (e.g., Medium to High Bandwidth)
                │
                └── ...... (Other Alternate Settings for different packet sizes)

Device Qualifier Descriptor
└── Device information while running in another speed mode (e.g., High Speed vs Full Speed capability)

Other Speed Configuration Descriptor
├── Configuration information while running in another speed mode
│
├── Interface Association Descriptor (IAD)
│       └── Groups Interface 0 (VC) and Interface 1 (VS) as a Video Function
│
├── Video Control (VC) Interface Descriptor (Interface 0)
│       ├── Standard Interface Descriptor (AlternateSetting 0, Video Control Class)
│       └── Class-Specific Descriptor Collection (Same Topology as main configuration)
│               ├── Input Terminal
│               ├── Processing Unit
│               ├── Extension Unit
│               └── Output Terminal
│
└── Video Streaming (VS) Interface Descriptor (Interface 1)
                ├── Alternate Setting 0: Control transfer active state (negotiation only)
                │ ├── Standard Interface Descriptor (Interface 1, Streaming Class)
                │ ├── Class-Specific VS Header
                │ ├── Format Descriptor
                │ │     └── Frame Descriptor
                │ └── Color Matching Descriptor
                │
                ├── Alternate Setting 1: Data transfer active state
                │ ├── Standard Interface Descriptor (Streaming Class)
                │ └── Standard Endpoint Descriptor (ISO IN endpoint)
                │
                └── ...... (Other alternate settings typically available in other speed modes)

UVC Vedio Control (VC) Interface

  • Vedio Control Interface Descriptor

Interface Header Descriptor
├── bLength            : 1 byte  → Total descriptor length (13 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x01 (VC_HEADER)
├── bcdUVC             : 2 bytes → Video Class Specification Release Number (0x0100 = 1.00)
├── wTotalLength       : 2 bytes → Total number of bytes for all VC descriptors
├── dwClockFreq        : 4 bytes → Clock frequency in Hz (e.g., 0x02DC6C00 = 48 MHz)
├── bInCollection      : 1 byte  → Number of VideoStreaming interfaces
└── baInterfaceNr(1)   : 1 byte  → Interface number of the first VideoStreaming interface (0x01)
  • Input Terminal Descriptor (Camera)

Input Terminal Descriptor
├── bLength            : 1 byte  → Total descriptor length (18 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x02 (VC_INPUT_TERMINAL)
├── bTerminalID        : 1 byte  → Unique ID of this Terminal (0x01)
├── wTerminalType      : 2 bytes → 0x0201 (ITT_CAMERA)
├── bAssocTerminal     : 1 byte  → ID of associated Output Terminal (0x00 = None)
├── iTerminal          : 1 byte  → String descriptor index
├── wObjectiveFocalMin : 2 bytes → Min focal length (0 = not supported)
├── wObjectiveFocalMax : 2 bytes → Max focal length
├── wOcularFocalLength : 2 bytes → Ocular focal length
├── bControlSize       : 1 byte  → Size of bmControls (3 bytes)
└── bmControls         : 3 bytes → Bitmap of supported controls
                                                        • D1: Auto-Exposure Mode
                                                        • D3: Exposure Time (Absolute)
                                                        • D5: Focus (Absolute)
                                                        • D9: Zoom (Absolute)
                                                        • D11: Pan (Absolute)
                                                        • D15: Tilt (Absolute)
  • Processing Unit Descriptor

Processing Unit Descriptor
├── bLength            : 1 byte  → Total descriptor length (11 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x05 (VC_PROCESSING_UNIT)
├── bUnitID            : 1 byte  → Unique ID of this Unit (0x02)
├── bSourceID          : 1 byte  → ID of the source connected to this unit (0x01 = Camera IT)
├── wMaxMultiplier     : 2 bytes → Max digital zoom multiplier
├── bControlSize       : 1 byte  → Size of bmControls (2 bytes)
└── bmControls         : 2 bytes → Bitmap of supported image controls
                                                        • D0: Brightness
                                                        • D1: Contrast
                                                        • D2: Hue
                                                        • D3: Saturation
                                                        • D4: Sharpness
                                                        • D6: White Balance Temperature
  • Extension Unit Descriptor

Extension Unit Descriptor
├── bLength            : 1 byte  → Total descriptor length (29 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x06 (VC_EXTENSION_UNIT)
├── bUnitID            : 1 byte  → Unique ID of this Unit (0x03)
├── guidExtensionCode  : 16 bytes→ Vendor-specific GUID (e.g., {0FB885C3-...})
├── bNumControls       : 1 byte  → Number of controls in this XU (0x05)
├── bNrInPins          : 1 byte  → Number of input pins (0x01)
├── baSourceID[1]      : 1 byte  → ID of the source connected (0x02 = PU)
├── bControlSize       : 1 byte  → Size of bmControls (4 bytes)
└── bmControls         : 4 bytes → Bitmap of supported vendor controls
  • Output Terminal Descriptor

Output Terminal Descriptor
├── bLength            : 1 byte  → Total descriptor length (9 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x03 (VC_OUTPUT_TERMINAL)
├── bTerminalID        : 1 byte  → Unique ID of this terminal (0x05)
├── wTerminalType      : 2 bytes → 0x0101 (TT_STREAMING)
├── bAssocTerminal     : 1 byte  → Associated Input Terminal ID
├── bSourceID          : 1 byte  → ID of the connected Source (0x04 = XU)
└── iTerminal          : 1 byte  → String descriptor index

UVC Vedio Stream (VS) Interface

  • Video Stream Interface Descriptor

Class-Specific VS Input Header Descriptor
├── bLength            : 1 byte  → Total descriptor length (15 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x01 (VS_INPUT_HEADER)
├── bNumFormats        : 1 byte  → Number of video formats supported (0x02)
├── wTotalLength       : 2 bytes → Total length of all VS specific descriptors
├── bEndpointAddress   : 1 byte  → Address of the ISO IN endpoint (0x81)
├── bmInfo             : 1 byte  → Capabilities (0x00)
├── bTerminalLink      : 1 byte  → ID of the Output Terminal in VC interface (0x05)
├── bStillCaptureMethod: 1 byte  → Method of still image capture (0x02)
├── bTriggerUsage      : 1 byte  → Trigger usage (0x00)
├── bControlSize       : 1 byte  → Size of control field (1 byte)
└── bmaControls(n)     : n bytes → Controls for each format
  • Video Stream Format Type Descriptor

Video Streaming Format Type Descriptor(MJPEG)
├── bLength            : 1 byte  → Total descriptor length (11 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x06 (VS_FORMAT_MJPEG)
├── bFormatIndex       : 1 byte  → Index of this format (0x01)
├── bNumFrameDescriptors: 1 byte → Number of frame descriptors (0x09)
├── bmFlags            : 1 byte  → Characteristics (0x01 = Fixed Sample Size)
├── bDefaultFrameIndex : 1 byte  → Default frame index (0x01)
├── bAspectRatioX      : 1 byte  → X dimension of aspect ratio
├── bAspectRatioY      : 1 byte  → Y dimension of aspect ratio
├── bmInterlaceFlags   : 1 byte  → Interlace information (0x00 = Progressive)
└── bCopyProtect       : 1 byte  → Duplication restrictions (0x00)
  • Video Stream Frame Type Descriptor

Video Streaming Frame Type Descriptor (MJPEG)
├── bLength            : 1 byte  → Total descriptor length (e.g., 34 bytes)
├── bDescriptorType    : 1 byte  → 0x24 (CS_INTERFACE)
├── bDescriptorSubtype : 1 byte  → 0x07 (VS_FRAME_MJPEG)
├── bFrameIndex        : 1 byte  → Index of this frame (0x01)
├── bmCapabilities     : 1 byte  → Still image support, etc.
├── wWidth             : 2 bytes → Frame Width (e.g., 0x0A20 = 2592)
├── wHeight            : 2 bytes → Frame Height (e.g., 0x0798 = 1944)
├── dwMinBitRate       : 4 bytes → Min Bit Rate (bps)
├── dwMaxBitRate       : 4 bytes → Max Bit Rate (bps)
├── dwMaxVideoFrameBuf : 4 bytes → Max Frame Buffer Size (bytes)
├── dwDefaultFrameInterval: 4 bytes→ Default frame interval in 100ns units
├── bFrameIntervalType : 1 byte  → 0 = Continuous, Non-0 = Discrete
└── dwFrameInterval(n) : 4×n bytes→ Discrete frame intervals supported (e.g., 33ms for 30fps)

备注

详细的字段定义请参考 USB-IF 官方 UVC (USB Video Class) 1.5 协议文档。

备注

注意:确认当前速度下的设备能力 (FS vs HS)

请务必注意,Camera 在全速 (Full Speed) 和高速 (High Speed) 模式下往往会呈现不同的描述符结构:

  • 格式支持差异:由于带宽限制,某些非压缩格式(如 YUY2)可能仅在 High Speed 模式下可见。在 Full Speed 下,设备可能仅支持 MJPEG 或极低分辨率的 YUY2。

  • 描述符变化:上述的 VS_FRAME_UNCOMPRESSED 描述符在 Full Speed 连接时可能根本不存在,或者其分辨率和帧率等参数会发生巨大变化。

类特定请求

UVC Host 驱动通过控制管道 0 发送以下请求控制设备行为。这些请求通常针对特定的 Unit ID 或 Interface。

请求名称 (Request)

要求

描述

SET_CUR

必选

设置当前属性值。用于 Probe/Commit 协商,或控制亮度、对比度等 PU 属性。

GET_CUR

必选

获取当前属性值。读取协商后的参数或当前设备状态。

GET_MIN / GET_MAX

可选

获取属性的可调节范围。主机驱动利用此范围限制应用层的输入值。

GET_RES

可选

获取属性的分辨率(步进值)。

GET_DEF

可选

获取属性的默认值。

GET_LEN

可选

获取数据长度。

流协商流程 (Stream Negotiation)

在正式启动视频流传输 (Stream On) 之前,主机与设备必须 严格遵循 UVC 状态机进行参数与带宽的协商,否则设备可能拒绝启动视频流。根据官方 spec,该过程包含 Probe (三步握手)Commit (提交) 两个主要阶段。

流协商时序图

../../_images/usb_host_uvc_probecommit.svg

核心步骤详解

  1. PROBE1: 发起协商 (SET_CUR)

    主机向视频流接口 (VS Interface) 发送 SET_CUR (PROBE) 请求,携带期望的视频参数结构体。

    • 目的:主机提出“我想要这样的分辨率、帧率和格式”。

  2. PROBE2: 获取反馈 (GET_CUR)

    主机发送 GET_CUR (PROBE) 请求,回读设备实际能够支持的参数。

    • 设备修正:如果主机请求的参数不支持,设备会返回修正后的最接近参数。

    • 关键数据:此时返回的结构体中,dwMaxPayloadTransferSize 字段由设备计算填充,这是后续选择带宽的依据。

  3. PROBE3: 最终确认 (SET_CUR)

    主机将步骤 2 中回读到的(可能被修正过的)参数,再次通过 SET_CUR (PROBE) 发送给设备。

    • 目的:双方状态同步,主机确认接受设备的修正建议,结束协商阶段。

  4. COMMIT: 提交生效 (SET_CUR)

    参数达成一致后,主机发送 SET_CUR (COMMIT) 请求。

    • 目的:通知设备“参数已确定”。

备注

关键参数

在 Probe/Commit 流程中,主机主要依据以下三个核心字段来确定视频流配置:

  • bFormatIndex:格式索引 (Format Index)

  • bFrameIndex:分辨率索引 (Frame Index)

  • dwFrameInterval:帧率间隔 (Frame Interval)

其他参数

结构体中的其余参数(如 wCompWindowSize, wCompQuality 等)是否生效或可调,完全取决于 具体设备的实际支持情况。对于大多数标准设备,通常仅需关注上述三个核心参数,其他参数维持设备返回的默认值即可。

关于 Probe/Commit 控制结构体 (Probe and Commit Control) 的完整定义及所有字段详解,请参阅官方 spec。

流带宽匹配与开启 (Stream Activation)

当参数协商完成后,主机需要根据设备反馈的带宽需求,选择合适的硬件接口设置来正式开启传输管道。

流带宽分配图

../../_images/usb_host_uvc_select_bw.svg
  • 匹配带宽 (Match Bandwidth)

驱动程序会根据协商阶段获取的 dwMaxPayloadTransferSize 值,遍历视频流接口的所有备用设置 (Alternate Settings)。

  • 切换接口 (Set Interface)

一旦找到合适的备用设置,主机发送标准 USB 请求 SET_INTERFACE。这一步操作标志着 USB 同步传输 (Isochronous Transfer) 带宽的正式保留,视频流随即开始传输。

备注

带宽匹配原则

在同步传输模式下,带宽的选择至关重要,驱动需遵循 够用且最小 的原则:

  • 寻找目标:在所有的 Alternate Settings 中,寻找 wMaxPacketSize (端点最大包长) 大于等于协商值 dwMaxPayloadTransferSize 的设置。

  • 最优选择:在满足上述条件的所有设置中,选择 wMaxPacketSize 最小的那一个。

若带宽不足, 会导致数据包被截断,引起画面花屏或丢帧。若带宽过大, 会占用宝贵的 USB 总线周期性带宽,可能导致总线上其他设备(如 USB 音频)因带宽不足而无法工作。

数据传输格式

UVC 的视频数据并非纯粹的原始数据流,而是封装在带有 负载头 (Payload Header) 的数据包中。主机驱动必须首先解析并剥离此头部,才能获取有效的视频 载荷 (Payload)

Payload Header 详细结构

../../_images/usb_host_uvc_payloadheader.png
  • 基本头部字段 (Basic Header Fields)

    所有 Payload Header 起始处都包含两个核心字节:

    • HLE (Header Length): 1 字节。指定整个头部的长度(以字节为单位),包括 HLE 本身、BFH 以及可选的 PTS/SCR 字段。

    • BFH[0] (Bit Field Header): 1 字节。位域标志位,指示了后续数据的属性及可选字段的存在。

  • BFH[0] 位域定义

    Bit

    Name

    描述

    D0

    FID

    Frame Identifier (帧标识符)。 用于区分不同的视频帧或段(具体行为取决于视频格式,见下文)。

    D1

    EOF

    End of Frame (帧结束)。 指示当前包是否为帧或段的最后一个数据包。

    D2

    PTS

    Presentation Time Stamp (显示时间戳存在位)。 置 1 时,表示头部包含 4 字节的 PTS 字段。

    D3

    SCR

    Source Clock Reference (源时钟参考存在位)。 置 1 时,表示头部包含 6 字节的 SCR 字段。

    D4

    RES

    Reserved (保留位)。通常置 0。

    D5

    STI

    Still Image (静止图像)。 置 1 表示该样本属于静态图片(Stream-based 格式通常置 0)。

    D6

    ERR

    Error Bit (错误位)。 置 1 表示设备在流传输过程中发生了错误。

    D7

    EOH

    End of Header (头部结束位)。 置 1 表示 BFH 字段结束(即没有扩展头部)。

  • 可选扩展字段

    • PTS (4 字节): 当 BFH[0].D2 (PTS) 置位时存在。表示视频帧的显示时间。

    • SCR (6 字节): 当 BFH[0].D3 (SCR) 置位时存在。用于音视频同步的源时钟参考。

视频数据的传输格式

在 UVC 规范中,视频数据的传输格式主要分为两大类: 基于帧 (Frame-based)基于流 (Stream-based)。这两种模式决定了主机如何解析视频数据以及如何处理 Payload Header。

  • Frame-based

    这是最常见的模式,适用于 MJPEG、 Uncompressed (YUV/NV12) 等格式。

    • 特点:视频数据被严格划分为一幅幅独立的图像(帧)。

    • 传输逻辑:主机驱动关注“帧的边界”,通过检测 Payload Header 中的 FID 翻转或 EOF 标志来组装完整的一帧画面。

  • Stream-based

    主要用于 H.264/H.265 等压缩流格式。

    • 特点:数据被视为连续的字节流,没有严格的物理“帧”边界概念(或者边界由解码器内部处理)。

    • 传输逻辑:主机驱动主要负责搬运数据流,通常不依赖 Payload Header 来判定帧的开始或结束,而是将数据直接交给上层应用或解码器去解析内容。

备注

工程实践:Stream-based 设备的实际行为

虽然 UVC 规范 指出 Stream-based 格式可以不使用 FID 和 EOF 来划分边界,但在实际工程应用中:

  • 设备端 (Camera)

    绝大多数厂商即使在输出 H.264/H.265 等 Stream-based 格式时,仍然会尽可能遵循 Frame-based 的规则,通过翻转 FID 或设置 EOF 来标记数据块(如 NAL Unit 或帧切片)的边界。

  • 主机端 (Host)

    大多数通用 UVC 驱动(如 Linux uvcvideo、Windows 系统驱动)为了简化逻辑,默认倾向于统一按照 FID 跳变或 EOF 标志来切分数据包。

小技巧

工程实践:主机驱动的数据处理机制

在实际工程实现中,UVC 主机驱动(Host Driver)对 Frame-based 与 Stream-based 格式采取统一的传输处理策略,并不针对流式传输做特殊区分。其核心处理逻辑如下:

  • 传输层的格式无关性

    驱动层仅负责数据的搬运与重组,不感知具体的视频编码格式或流类型。无论设备描述符声明为何种数据类型,驱动均将其视为通用的数据载荷。

  • 统一的组包机制

    驱动严格依赖 Payload Header 中的 FID (Frame Identifier) 状态翻转与 EOF (End of Frame) 标志位进行数据定界。驱动会将属于同一逻辑序列的数据包组装为一个完整的 Payload Buffer,并将其作为驱动层面的 一帧 提交至用户空间。

  • 解析职责分离

    数据内容的解析由 应用层 (Application Layer) 负责。

    例如,对于 H.264 等 Stream-based 格式,驱动提交的 Buffer 可能包含多个 NAL Unit 或部分帧数据,应用层需自行解析流数据结构以获取实际的视频帧。

类驱动

本节详细介绍了 USB UVC 主机驱动的内部实现细节,包括驱动架构、视频流管理、类特定请求的支持情况以及管道资源的分配方案。

具体实现

USB Host UVC 主机驱动栈基于模块化设计,通过分层架构实现了上层应用与 USB 硬件控制器之间的高效交互。该架构确保了高带宽视频数据的稳定捕获与处理,并提供了灵活的帧缓冲管理机制。

其核心架构与数据流向如下图所示:

../../_images/usb_host_uvc_flow.svg

按功能职责划分为以下几个核心模块:

应用层接口适配 (Application Adapter)

负责视频帧数据 (Video Frame) 的缓冲管理与提取。

作为 USB 驱动与用户应用层 (User Application) 的接口层,它管理着视频数据的最终输出:

UVC 类驱动架构

UVC 主机端驱动由 UVC 类协议驱动 (UVC Class Driver) 负责标准协议的握手、描述符拓扑解析以及视频流的维护。

该模块严格遵循 USB Video Class 1.1/1.5 协议规范,实现了主机与 UVC 设备交互的核心业务逻辑。其主要职责包括:

  • 枚举与接口绑定:负责解析视频控制接口 (VC Interface) 和视频流接口 (VS Interface),建立 UVC 内部拓扑结构(如 Unit 和 Terminal)。

  • 格式协商 (Probe & Commit):在视频流启动前,执行 Probe/Commit 标准流程,与设备协商分辨率、帧率及最佳的带宽设置。

  • 动态带宽分配:根据协商结果,自动选择最匹配的 Alt Setting,申请相应的同步 (Isochronous) 管道资源。

核心处理任务 (Process Task)

负责 USB 协议栈核心逻辑与状态机管理。

  • 状态机维护:管理 UVC 特定的状态流转,包括 UVC_STATE_CTRL (控制传输处理)、 UVC_STATE_TRANSFER (数据传输处理) 以及流启动过程中的状态迁移。

  • 事件处理:响应底层传输完成事件,协调控制传输与数据流传输的时序。

  • 动态热插拔:处理设备的连接 (Attach) 与移除 (Detach) 事件,自动释放流缓冲区与管道资源。

拼帧服务

负责原始 USB 数据包的解析与重组(Stream Decoding):

  • URB 处理:接收底层上报的 URB (USB Request Block) 数据包,解析 UVC 负载头 (Payload Header)。

  • 帧组装:处理 FID (Frame ID) 翻转逻辑,剥离协议头并将有效视频载荷填充至帧缓冲区 (Frame Buffer)。

  • 错误检测:检测传输过程中的丢包或错误标识 (Error Bit),保证视频帧的完整性。若开启硬件加速,此模块将直接对接硬件解码器。

类特定请求实现

本驱动栈遵循 USB Video Class 规范,封装了核心 类特定请求 (Class-Specific Requests) 的实现与发送流程。

驱动层主要实现了对视频探针控制 (Probe Control) 和提交控制 (Commit Control) 的支持。源码路径: {SDK}/component/usb/host/uvc

类特定请求类型

备注

SET_CUR

设置当前属性。 主要用于 Probe 和 Commit 阶段,向设备发送期望的视频流参数(如 dwFrameInterval, bFormatIndex)。

GET_CUR

获取当前属性。 读取设备当前生效的配置参数,用于校验设置或获取当前状态。

管道配置

UVC 主机驱动在设备枚举阶段 (usbh_uvc_attach) 解析配置描述符,自动识别视频控制与视频流接口,并根据 Alt Setting 申请相应的管道资源。

管道类型

描述

控制 IN/OUT 管道

默认控制管道 0 (EP0)。 用于发送标准请求及 UVC 特定请求(如 Video Probe and Commit Control)。

同步 IN 管道

归属于视频流接口 (VS Interface)。 用于接收高带宽的视频负载数据。驱动支持根据 USB 速度(Full/High Speed)自动选择合适的包大小 (MPS) 与传输模式。

API 说明

驱动 API

应用示例

UVC Host 驱动开发指南

本节详细介绍 UVC (USB Video Class) 主机驱动的完整开发流程,涵盖驱动初始化、热插拔管理、视频流控制与数据处理,以及不同应用场景下的示例说明。

驱动初始化

使用 UVC Host 驱动前,需定义配置结构体并注册回调函数,随后依次调用核心接口启动 USB 主机控制器及 UVC 类驱动。

步骤说明:

  • 硬件配置:设置 USB 速度模式及相关中断优先级。

  • 回调注册:定义 usbh_uvc_cb_t 结构体,挂载各个阶段(初始化、连接、断开、参数设置完成)的处理函数。

  • 核心初始化:调用 usbh_init() 初始化 USB 核心栈。

  • 类驱动加载:调用 usbh_uvc_init() 初始化 UVC 类驱动。

/*
 * 1. Configure USB speed, ISR priority, and main task priority.
 */
static usbh_config_t usbh_cfg = {
        .speed = USB_SPEED_HIGH,
        .ext_intr_enable = USBH_SOF_INTR,
        .isr_priority = INT_PRI_MIDDLE,
        .main_task_priority = CONFIG_USBH_UVC_MAIN_THREAD_PRIORITY,
        .tick_source = USBH_SOF_TICK,
};

/*
 * 2. Define USB user-level callbacks.
 */
static usbh_user_cb_t usbh_usr_cb = {
        .process = uvc_cb_process,
};

/*
 * 3. Configure UVC user-level HW priority.
 */
static usbh_uvc_ctx_t uvc_cfg = {
#if USBH_UVC_USE_HW
        .hw_isr_pri = CONFIG_USBH_UVC_HW_IRQ_PRIORITY,
#endif
};

/*
 * 4. Define user callbacks for UVC events.
 */
static usbh_uvc_cb_t uvc_cb = {
        .init = uvc_cb_init,
        .deinit = uvc_cb_deinit,
        .attach = uvc_cb_attach,
        .detach = uvc_cb_detach,
        .setup = uvc_cb_setup,
        .setparam = uvc_cb_setparam,
};

int ret = 0;

/*
 * 5. Initialize USB host core driver with configuration.
 */
ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
if (ret != HAL_OK) {
        goto free_sema_exit;
}

/*
 * 6. Initialize UVC class driver.
 */
ret = usbh_uvc_init(&uvc_cfg, &uvc_cb);
if (ret != HAL_OK) {
        /* If class driver init fails, clean up the core driver */
        usbh_deinit();
        goto usb_deinit_exit;
}

热插拔事件处理

通过注册 usbh_uvc_cb_t 中的 attach 和 detach 回调函数来监听 UVC 摄像头的连接与断开。

在示例代码中,利用信号量(Semaphore)机制来同步状态:

  • Attach: 当摄像头插入并枚举成功后,触发 attach 回调,释放 uvc_attach_sema,通知主线程创建视频采集任务。

  • Detach: 当摄像头拔出时,触发 detach 回调,释放 uvc_detach_sema,触发热插拔管理线程进行资源清理与重新初始化。

/* USB detach callback */
static usbh_uvc_cb_t uvc_cb = {
        .detach = uvc_cb_detach,
};

/* Callback executed when device is removed */
static int uvc_cb_detach(void)
{
        RTK_LOGS(TAG, RTK_LOG_INFO, "DETACH\n");
        rtos_sema_give(uvc_detach_sema);
        usbh_uvc_is_ready = 0;
        return HAL_OK;
}

/* Thread Context: Handle the state machine for Hotplug */
static void uvc_hotplug_thread(void *param)
{
        int ret = 0;

        UNUSED(param);

        for (;;) {
                /* Wait for detach signal */
                if (rtos_sema_take(uvc_detach_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
                        rtos_time_delay_ms(100);

                        /* Stop transfer, release resource */
                        usbh_uvc_deinit();
                        usbh_deinit();
                        rtos_time_delay_ms(10);
                        RTK_LOGS(TAG, RTK_LOG_INFO, "Free heap: 0x%x\n", rtos_mem_get_free_heap_size());

                        /* Re-init Host Core */
                        ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
                        if (ret != HAL_OK) {
                                RTK_LOGS(TAG, RTK_LOG_ERROR, "Init USBH fail\n");
                                break;
                        }

                        /* Re-init UVC Class */
                        ret = usbh_uvc_init(&uvc_cfg, &uvc_cb);
                        if (ret < 0) {
                                RTK_LOGS(TAG, RTK_LOG_ERROR, "Init UVC fail\n");
                                usbh_deinit();
                                break;
                        }
                }
        }

        RTK_LOGS(TAG, RTK_LOG_ERROR, "Hotplug thread fail\n");
        rtos_task_delete(NULL);
}

/* Main entry task to initialize USB and wait for connection */
static void example_usbh_uvc_task(void *param)
{
        rtos_task_t uvc_task;
        rtos_task_t hotplug_task;
        int ret = 0;

        /* ... Initialization of semaphores and mutexes ... */

        /* Init USB Host Core */
        ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
        if (ret != HAL_OK) {
                goto free_sema_exit;
        }

        /* Init UVC Class Driver */
        ret = usbh_uvc_init(&uvc_cfg, &uvc_cb);
        if (ret != HAL_OK) {
                usbh_deinit();
                goto usb_deinit_exit;
        }

        /* Create Hotplug detection thread */
        ret = rtos_task_create(&hotplug_task, "uvc_hotplug_thread", uvc_hotplug_thread, NULL, 1024U, CONFIG_USBH_UVC_HOTPLUG_THREAD_PRIORITY);
        if (ret != RTK_SUCCESS) {
                goto usbh_uvc_deinit_exit;
        }

        /* Wait for device attach callback to release semaphore */
        if (rtos_sema_take(uvc_attach_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
                /* Create the main UVC test/streaming thread */
                .......
        }

        goto example_exit;

        /* ... Error handling labels ... */
        .......
}

视频流控制与数据处理

当 UVC 设备枚举成功后,主机需要配置视频参数并启动视频流,随后周期性获取图像帧数据。

  1. 参数配置 (Set Parameters)

    在启动视频流之前,通过 usbh_uvc_set_param() 设置期望的格式(MJPEG/H264/YUV)、分辨率和帧率。设置请求发出后,需等待 uvc_setparam_sema 信号量,确信该组参数已被设备接受。

  2. 开启视频流 (Stream On)

    调用 usbh_uvc_stream_on() 以此开启指定接口的视频传输,设备开始在 USB 总线上发送 ISOC 数据包。

  3. 帧数据获取与处理

    驱动内部根据 CONFIG_USBH_UVC_FRAME_BUF_SIZE 定义的大小维护一个基于 malloc 分配的 帧缓冲池 (Frame Pool)。应用层需在循环中遵循 "Get Frame -> Process -> Put Frame" 的流程:

    • 获取 (Get Frame):

      调用 usbh_uvc_get_frame() 从驱动获取一帧已填充的数据。

      备注

      Drop Oldest 机制:

      此函数内部实现了 丢弃最旧帧 策略。为了保证应用层总是获取到 最新 (Newest) 的图像,如果消费者的处理速度慢于生产速度,驱动会自动丢弃队列中尚未读取的老旧帧,确保帧缓冲池可以获取当前的最新帧。

    • 处理 (Process):

      在应用层对数据进行消费(如拷贝显示、保存文件、网络上传或仅做统计分析)。

      备注

      数据处理说明:

      UVC 驱动作为生产者以 为单位(即聚合了一张图像的所有 Payload)将数据交付给消费者。驱动层不分析 Payload 内部的具体数据内容,因此应用层需根据具体的视频格式自行实现 Payload 的解析与处理逻辑。

    • 归还 (Put Frame):

      无论应用层如何处理该帧(即使只是统计数据大小或者决定丢弃该帧),处理完毕后 必须 调用 usbh_uvc_put_frame()

      备注

      此函数将缓冲区归还给驱动内部的帧池。否则帧池将会耗尽,导致后续无法再获取新数据。

  4. 停止视频流 (Stream Off)

    当不再需要视频数据或准备断开连接时,调用 usbh_uvc_stream_off()。这将通知设备停止 ISOC 传输,释放总线带宽,并复位驱动内部的相关状态。

/* Define user callbacks for UVC events */
static usbh_uvc_cb_t uvc_cb = {
        .setup = uvc_cb_setup,
        .setparam = uvc_cb_setparam,
};

/* Define USB user-level callbacks */
static usbh_user_cb_t usbh_usr_cb = {
        .process = uvc_cb_process,
};

/* Define USB user-level setup callback */
static int uvc_cb_setup(void)
{
        RTK_LOGS(TAG, RTK_LOG_INFO, "SETUP\n");
        usbh_uvc_is_ready = 1;
        rtos_sema_give(uvc_start_sema);
        return HAL_OK;
}

/* Define USB user-level setparam callback */
static int uvc_cb_setparam(void)
{
        RTK_LOGS(TAG, RTK_LOG_INFO, "SETPARAM\n");
        rtos_sema_give(uvc_setparam_sema);
        return HAL_OK;
}

/* Define USB user-level process callback */
static int uvc_cb_process(usb_host_t *host, u8 msg)
{
        UNUSED(host);

        switch (msg) {
        case USBH_MSG_DISCONNECTED:
                usbh_uvc_is_ready = 0;
                break;

        case USBH_MSG_CONNECTED:
                break;

        default:
                break;
        }

        return HAL_OK;
}

/* Main UVC Test Thread */
static void uvc_test(void *param)
{
        usbh_uvc_frame_t *buf;
        int ret = 0;

        /* Wait for the device to be ready (Enumeration Complete) */
        if (rtos_sema_take(uvc_start_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {

                /* 1. Configure UVC Parameters */
                /* Set the desired format, resolution, and frame rate */
                uvc_s_ctx.fmt_type = CONFIG_USBH_UVC_FORMAT_TYPE; // e.g., MJPEG
                uvc_s_ctx.width = CONFIG_USBH_UVC_WIDTH;          // e.g., 1080
                uvc_s_ctx.height = CONFIG_USBH_UVC_HEIGHT;        // e.g., 720
                uvc_s_ctx.frame_rate = CONFIG_USBH_UVC_FRAME_RATE;// e.g., 30
                uvc_s_ctx.frame_buf_size = CONFIG_USBH_UVC_FRAME_BUF_SIZE;

                /* Trigger the UVC parameter setting process */
                ret = usbh_uvc_set_param(&uvc_s_ctx, CONFIG_USBH_UVC_IF_NUM_0);
                if (ret != RTK_SUCCESS) {
                        RTK_LOGS(TAG, RTK_LOG_ERROR, "Set param req failed: %d\n", ret);
                        goto exit;
                }

                /* Wait for the semaphore indicating parameter setting is actually completed */
                if (rtos_sema_take(uvc_setparam_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
                        RTK_LOGS(TAG, RTK_LOG_INFO, "Set params OK\n");
                } else {
                        RTK_LOGS(TAG, RTK_LOG_ERROR, "Set params timeout\n");
                        goto exit;
                }

                /* ... Initialize consumer tasks (e.g., VFS thread or HTTPC thread) ... */

                /* 2. Start Video Stream */
                RTK_LOGS(TAG, RTK_LOG_INFO, "Stream on\n");
                ret = usbh_uvc_stream_on(&uvc_s_ctx, CONFIG_USBH_UVC_IF_NUM_0);
                if (ret) goto exit;

                /* 3. Main Capture Loop */
                while (/* Condition: e.g., Loop Count < MAX or Device Connected */) {

                        if (!usbh_uvc_is_ready) break; // Device disconnected

                        // 3.1 Get Frame from USB Stack
                        /* Retrieve a filled frame buffer from the UVC driver */
                        buf = usbh_uvc_get_frame(CONFIG_USBH_UVC_IF_NUM_0);

                        if (buf == NULL) {
                                /* Frame not ready yet, wait and retry */
                                rtos_time_delay_ms(10);
                                continue;
                        }

                        // 3.2 Process the Frame Data
                        /* CRITICAL: Buffer overflow detected! */
                        /* This means the camera sent a frame larger than our allocated buffer. */
                        /* ACTION: Please increase 'CONFIG_USBH_UVC_FRAME_BUF_SIZE' in example_usbh_uvc.c */
                        /* to match the camera's actual output size for the current resolution/format. */
                        if (buf->byteused > CONFIG_USBH_UVC_FRAME_BUF_SIZE) {
                                RTK_LOGS(TAG, RTK_LOG_ERROR, "Frame %d overflow %d > %d\n", img_cnt, buf->byteused, CONFIG_USBH_UVC_FRAME_BUF_SIZE);

                                /* Even on error, we must return the frame buffer */
                                usbh_uvc_put_frame(buf, CONFIG_USBH_UVC_IF_NUM_0);
                                return;
                        } else if (buf->byteused > 0) {
                                /* CONSUMER LOGIC: */
                                /* The actual data processing happens here. */
                                /* - Simple Mode:  Just count bytes. */
                                /* - VFS Mode: Write `buf->buf` to SD Card. */
                                /* - HTTPC Mode: Send `buf->buf` to Network. */
                                usbh_uvc_img_prepare(buf);
                                ......
                        }

                        // 3.3 Put Frame back to USB Stack
                        /* CRITICAL: Must return the buffer to driver for the next capture */
                        usbh_uvc_put_frame(buf, CONFIG_USBH_UVC_IF_NUM_0);
                }

                /* 4. Stop Video Stream */
                if (usbh_uvc_is_ready) {
                        usbh_uvc_stream_off(CONFIG_USBH_UVC_IF_NUM_0);
                        RTK_LOGS(TAG, RTK_LOG_INFO, "Stream off\n");
                }
        }
exit:
        rtos_task_delete(NULL);
}

驱动卸载

当设备断开或需要关闭 USB 主机功能时,需按顺序卸载类驱动和主机核心驱动,并释放相关系统资源。

/* 1. Deinitialize UVC class driver. */
usbh_uvc_deinit();

/* 2. Deinitialize USB host core driver */
usbh_deinit();

Example 示例应用

本示例演示了 Ameba 作为 USB UVC 主机如何捕获摄像头视频帧。为了满足不同的应用场景,示例提供了三种工作模式:

  • USBH_UVC_APP_SIMPLE基础测试模式,仅捕获视频帧但不进行处理(直接丢弃),用于验证驱动通路和统计吞吐量。

  • USBH_UVC_APP_VFSSD 卡存储模式,通过 VFS 接口将捕获的视频帧写入 SD 卡。

  • USBH_UVC_APP_HTTPC网络上传模式,将捕获的视频帧发送至 HTTP 服务器。

用户可以通过修改代码中的 CONFIG_USBH_UVC_APP 宏定义来选择当前生效的模式。

  1. 软件配置


打开 example_usbh_uvc.c 文件,根据测试需求修改 CONFIG_USBH_UVC_APP 宏定义等:

/* Supported application example: USBH_UVC_APP_SIMPLE, USBH_UVC_APP_VFS, USBH_UVC_APP_HTTPC */
#define CONFIG_USBH_UVC_APP                        USBH_UVC_APP_SIMPLE

/* Supported formats: USBH_UVC_FORMAT_MJPEG, USBH_UVC_FORMAT_YUV, USBH_UVC_FORMAT_H264
* Note: Users must verify which formats their specific camera supports and
* adjust the definition below accordingly. */
#define CONFIG_USBH_UVC_FORMAT_TYPE                USBH_UVC_FORMAT_MJPEG

/* Target resolution and compression ratio.
* If the specific camera device does not support
* these values, the host stack will automatically select the closest match.
* Always check the logs to confirm the actual parameters applied. */
#define CONFIG_USBH_UVC_WIDTH                      1280
#define CONFIG_USBH_UVC_HEIGHT                     720
#define CONFIG_USBH_UVC_FRAME_RATE                 30

/* Frame buffer size in bytes
* Size depends on format, resolution, and scene complexity.
* Please increase this value if an oversize error occurs. */
#define CONFIG_USBH_UVC_FRAME_BUF_SIZE             (150 * 1024)

根据所选模式,还需要关注以下配置项:

若使用 HTTPC 模式, 需配置目标服务器的 IP 地址和端口,并确保 Wi-Fi 连接信息正确。

#define USBH_UVC_HTTPC_SERVER            "192.168.1.100"
#define USBH_UVC_HTTPC_PORT              5090
  1. 编译与烧录


在 SDK 根目录 {SDK}/ 下执行以下命令以配置环境,选择目标 SoC,编译工程并生成 Image 文件:

# Initialize environment (required for every new terminal)
source env.sh

# Select Target SoC (replace xxx with your specific SoCs)
ameba.py soc xxx

ameba.py build -a usbh_uvc -p

通过 Ameba Image Tool 下载至开发板。

  1. 结果验证


通用测试步骤

  1. 将 USB 摄像头连接至开发板 USB 接口。

  2. 复位开发板,观察串口日志。

  3. 确保无 USB 相关报错信息(如枚举失败)。

根据所选模式,查看下方的具体验证步骤与预期日志:

测试说明

此模式下,系统识别摄像头后将周期性捕获图像(默认 200 帧),仅计算吞吐量,不保存数据。

预期日志

```
[UVC-I] Set paras ok: MJPEG 1280*720@60fps
[UVC-I] Stream on
[UVC-I] Captured frame 0, len=20832
[UVC-I] Captured frame 1, len=20912
...
[UVC-I] Captured frame 199, len=108264
[UVC-I] TP 4126 KB/s @ 4953 ms, fps 40/s
[UVC-I] TP 4.0 MB/s-40 (0_20930888/200)
[UVC-I] Stream off
[UVC-I] Test done
```

自定义主机方案

概述

USB Vendor Class (Vendor Specific) 利用 USB 规范的开放性,允许厂商自定义的私有协议或请求,支持开发者灵活定义配置与传输类型。 Ameba 基于底层 USB 协议栈提供了主机端的 Vendor 驱动设计参考示例,与自定义的 USB 设备建立高自由度的专属数据通道。 作为 Vendor 主机,开发者可以基于此开发私有命令集与特定格式数据与 Vendor 设备进行交互,实现超越标准设备类限制的定制化控制与功能扩展。

../../_images/usb_host_vendor_overview.svg

特性

  • 支持热插拔

  • 自动解析描述符,自适应速度模式

  • 支持批量/中断/等时三种传输类型

  • 支持数据完整性校验测试和数据回传测试

  • 与 Vendor 设备应用示例适配,具体参考 自定义设备方案

应用场景

作为具备高度可定制性的 USB 主机,Ameba 可通过 USB 接口建立主机与设备间专属的控制与通信通道,突破标准类的功能限制,实现以下典型应用,例如:

  • 自定义协议通信:定义私有命令集与数据帧格式,用于设备与主机间的专用通信(如参数配置、故障诊断、状态查询)。

  • 安全固件升级:利用自定义 Vendor 协议传输固件,实现私有的安全升级机制。

  • 特殊外设控制:控制非标准 USB 设备(如特定传感器、加密狗或自定义 IO 扩展板)。

协议简介

请参考自定义设备方案的对应章节:协议简介

类驱动

驱动架构

协议栈提供了一个 Vendor Class(厂商自定义类)主机驱动的设计参考示例。 不同于 HID 或 MSC 等标准类驱动,它不绑定特定的行业协议,而是提供了一个灵活的基础框架,赋予开发者极大的自由度来定制私有的 USB 通信协议。

驱动架构采用分层设计,自上而下包含三个核心层次:

  • 用户应用层:实现具体的业务逻辑。用户仅需通过注册回调结构体即可与驱动交互,利用封装好的接口发送或接收数据,无需关心底层 USB 协议细节。

  • Vendor 类驱动层:(本驱动示例核心实现)建立在 USB 主机核心栈之上,作为中间层连接底层硬件与上层应用。负责管理通道资源、维护状态机以及处理各类数据传输等。是开发者进行二次开发的主要基础。

  • 核心驱动层: 负责底层 USB 硬件中断处理、总线枚举、传输请求 (URB) 调度及标准协议栈管理。

类驱动实现

Vendor 类驱动在系统架构中起着承上启下的作用,其核心交互逻辑主要围绕以下三个接口展开:

  • 主机类驱动回调 API:类驱动通过注册标准的 usbh_class_driver_t 结构体,实现与底层 USB Core 进行交互。

  • 面向应用的回调 API:通过应用层在初始化时注册的 usbh_vendor_cb_t ,向上层应用提供异步事件通知(如连接、断开、数据接收)。

  • 面向应用的 API:提供给应用层调用的功能接口。调用后驱动会切换内部状态机的状态,开启数据传输的调度。

驱动回调机制

../../_images/usb_host_vendor_callback.png

备注

上图仅为说明不同层级的回调函数执行的流程,并未列出所有调用场景。

类驱动回调函数

类驱动需要定义一个标准的 usbh_class_driver_t 结构体,作为统一的入口注册到 USB Core 中,是 Core 层通知 Class 层“发生了某事”的主要手段。

  • id_table: 支持的设备 ID 列表,核心层使用此表与插入的设备进行匹配,以决定是否加载此驱动。

  • attach: 设备连接并匹配成功后

  • detach: 设备断开时调用。

  • setup: 枚举完成进入类请求阶段,用于发送类特定的标准控制请求,完成设备进入数据传输状态前的必要配置。

  • process: 类驱动驱动就绪后的状态机处理函数。

  • sof: SOF 中断时调用,用于处理对时序要求严格的逻辑,主要用于 同步传输

  • completed: 当通道上的传输完成时调用。

面向应用层的回调函数

Vendor 类驱动面向应用层的回调结构体 usbh_vendor_cb_t,由用户层实现。一般可选择实现:

API

描述

init

在类驱动初始化时被调用,用于初始化应用相关的资源

deinit

在类驱动注销时被调用,用于注销应用相关的资源

attach

在类驱动执行attach回调时被调用,用于应用层处理设备连接事件

detach

在类驱动执行detach回调时被调用,用于应用层处理设备断开事件

setup

在类驱动执行setup回调时被调用,用于指示应用层类驱动已准备好进行数据传输

receive

在类驱动收到IN数据时被调用,用于应用层处理设备上报的数据

transmit

在类驱动OUT数据传输完成时被调用,用于应用层获取OUT传输状态

加载与卸载类驱动

usbh_vendor_init() 是用于加载 Vendor 类驱动的顶层函数。

  • 执行用户回调初始化:如果注册了用户 init 回调函数,则优先调用它,这给了上层应用一个机会执行应用层特有的初始化逻辑。

  • 注册类驱动:调用 usbh_register_class(),将定义好的 usbh_class_driver_t 类驱动实例注册到向主机核心驱动,使主机准备好响应枚举。

示例:

typedef struct {
    usbh_vendor_xfer_t bulk_in_xfer; //BULK IN pipe xfer.
    // Other pipe xfer if exit.
    usbh_vendor_cb_t *cb;           //User callback.
    usb_host_t *host;               //USB host core.
    usbh_vendor_state_t state;      //Class Driver state.
} usbh_vendor_host_t;

/* Define Vendor host */
static usbh_vendor_host_t usbh_vendor_host;
/* Define Vendor Class Driver */
static usbh_class_driver_t usbh_vendor_driver = {
    .id_table = vendor_devs,
    .attach = usbh_vendor_attach,
    .detach = usbh_vendor_detach,
    .setup = usbh_vendor_setup,
    .process = usbh_vendor_process,
    .sof = usbh_vendor_sof,
};

int usbh_vendor_init(usbh_vendor_cb_t *cb)
{
    usbh_vendor_host_t *vendor = &usbh_vendor_host;
    /* 1. Execute User Init Callback */
    if (cb != NULL) {
        vendor->cb = cb;
        if (cb->init != NULL) {
            ret = cb->init();
            if (ret != HAL_OK) {
                RTK_LOGS(TAG, RTK_LOG_ERROR, "User init err %d\n", ret);
                return ret;
            }
        }
    }
    /* 2. Register Class Driver */
    usbh_register_class(&usbh_vendor_driver);
    return HAL_OK;
}

连接与断连处理

设备的连接与断开由 USB Core 自动检测,并调用类驱动的对应回调进行资源管理。

设备连接

当 Core 层枚举到匹配 usbh_dev_id_t 的 Vendor 设备时, usbh_vendor_attach 回调被调用。该阶段的主要职责是:

  • 获得并解析对应的接口描述符。

  • 根据端点描述符,通过 usbh_open_pipe() 打开对应通道。

  • 初始化每个通道对应的传输管理结构体 usbh_vendor_xfer_t

  • 调用用户 attach 回调,告知应用层。

示例:

static int usbh_vendor_attach(usb_host_t *host)
{
    usbh_vendor_host_t *vendor = &usbh_vendor_host;
    usbh_itf_data_t *itf_data;
    usbh_dev_id_t dev_id = {0,};
    /* 1. Sets up device identification parameters to match a vendor class interface */
    dev_id.bInterfaceClass = VENDOR_CLASS_CODE;
    dev_id.bInterfaceSubClass = VENDOR_SUBCLASS_CODE;
    dev_id.bInterfaceProtocol = VENDOR_PROTOCOL;
    dev_id.mMatchFlags = USBH_DEV_ID_MATCH_ITF_INFO;  //Core only needs to match interface information (ignore VID/PID)
    /* 2. Search for interfaces that match the dev_id rule in the devices associated with the host */
    itf_data = usbh_get_interface_descriptor(host, &dev_id);

    if (itf_data == NULL) {
       // Match failure: Although the device is connected, there is no driver supporting the Interface
        RTK_LOGS(TAG, RTK_LOG_ERROR, "Get itf desc fail\n");
        return HAL_ERR_PARA;
    } else {
        /* 3. If found, initializes the vendor host with the host pointer and sets the state to transfer */
        vendor->host = host;
        vendor->state = VENDOR_STATE_XFER;

        /* 4. Get data in/out endpoints */
        usbh_vendor_get_endpoints(host, itf_data->itf_desc_array);

        /* 5. Calls the `attach` callback function if it exists */
        if ((vendor->cb != NULL) && (vendor->cb->attach != NULL)) {
            vendor->cb->attach();
        }
        return HAL_OK;
    }
}

设备和驱动匹配机制

在 USB 主机驱动中, attach 回调函数与 usbh_dev_id_t 结构体共同协作,完成了从“通用设备连接”到“特定接口绑定”的关键过程。

匹配过程如下:

  • 定义匹配规则:过配置 usbh_dev_id_t 结构体,指定目标设备特征(如类代码、协议等)。

    • Match Flags: 使用 mMatchFlags 精确控制匹配策略(如仅匹配 VID/PID,或匹配特定 Class/SubClass)。

    • 灵活性: 精确控制是匹配整个设备还是设备中的某个特定接口(这对复合设备尤为重要)。

    • 开发者根据实际连接的 Vendor 设备进行适配。

  • 搜索匹配:在 attach 回调中,调用核心 API usbh_get_interface_descriptor()

    • USB Core 会遍历当前连接设备的所有接口描述符,与传入的 usbh_dev_id_t 规则进行比对,若匹配成功,则返回对应的接口描述符指针。

类驱动状态机

usbh_vendor_process 回调函数是主机端 Vendor 类的核心状态机处理函数。与设备端被动响应请求不同,主机端驱动需要主动维护设备状态。 USB 主机类驱动采用状态机来管理数据传输处理,驱动由 API 调用和底层中断反馈更新状态。

状态机一般包含三种核心状态:

  • VENDOR_STATE_IDLE(空闲状态):系统初始化或连接断开后的待机状态,等待新的传输请求或事件触发。

  • VENDOR_STATE_XFER(数据传输状态):核心数据处理状态,根据事件消息中的 通道号 (pipe_num) 分发处理相应的传输事件。

  • VENDOR_STATE_ERROR(错误处理状态):异常处理状态,尝试通过标准请求 (Clear Feature) 实现错误恢复。

示例:

switch (vendor->state) {
case VENDOR_STATE_IDLE:
    break
case VENDOR_STATE_XFER:
    /* Distribute and process according to the pipe number */
    if (event.msg.pipe_num == vendor->bulk_in_xfer.pipe.pipe_num) {
        usbh_vendor_bulk_process_rx(host);
    } else if (event.msg.pipe_num == vendor->isoc_out_xfer.pipe.pipe_num) {
        usbh_vendor_isoc_process_tx(host);
    }
    /* Deal with other pipe */
    break;
case VENDOR_STATE_ERROR:
    /* Try to recover */
    if (usbh_ctrl_clear_feature(host, 0x00U) == HAL_OK) {
        vendor->state = VENDOR_STATE_IDLE;// Restoration successful, reset to IDLE
    }
    break;
}

数据传输处理

本小节详细介绍了 Vendor 类驱动中的数据传输处理流程。主机端的传输主要分为 TRX API -> Process -> Callback 三个阶段:

  • 传输请求发起:由应用层调用 API 主动发起 OUT(发送)或 IN(接收)传输请求。

  • ‌状态机管理:驱动程序通过维护传输状态机,实现与设备端的数据交互。

  • 回调通知:传输完成或出错时,通过 usbh_vendor_cb_t 回调通知应用层。该回调机制实现了应用层与底层驱动的解耦,应用层无需轮询等待。

  • 启动发送: 应用层调用 TX API ,如 usbh_vendor_bulk_transmit()

  • API 内部实现:

    • 设置通道的传输缓存和传输长度。

    • 设置通道为传输状态 USBH_EP_XFER_START

    • 调用 usbh_notify_class_state_change() 通知核心状态发生变化,触发状态机调度处理。

  • 核心处理: usbh_vendor_process 回调中检测到事件,根据通道号分发给对应的处理函数。

  • 底层调度: 调用 usbh_transfer_process() 开启底层传输和处理传输结果。

  • 完成通知: 调用用户 transmit 回调函数通知上层发送结果。

备注

USB DMA 要求数据缓冲区地址和 Cache line 对齐,所以应用层传下的收发缓存区地址必须地址对齐。

特殊处理逻辑

  • BULK ZLP 处理

    在批量传输中,如果数据长度正好是 MPS(最大包长)的整数倍,驱动会自动标记 trx_zlp 以便在数据后自动接收/发送一个 ZLP 结束传输。

    示例:

    if ((pipe->xfer_len > 0) && ((pipe->xfer_len % pipe->ep_mps) == 0)
        && (pipe->ep_type == USB_CH_EP_TYPE_BULK)) { //ZLP
        pipe->trx_zlp = 1;
    }
    
  • 同步传输和帧同步

    同步传输对时间敏感,利用 SOF 中断处理同步传输的时序控制。

    • SOF 回调: usbh_vendor_sof 回调函数在每帧起始触发,用于追踪当前帧号。

    • 发送调度: 确保数据包严格按照帧间隔在正确的(微)帧内传输。

      • 发送处理函数中判断后续仍有传输时,会将通道状态置为 USBH_EP_XFER_WAIT_SOF

      • 在 SOF 回调中检查帧间隔,满足条件时调用 usbh_notify_class_state_change() 触发核心调度进行实际的数据发送。

    示例:

    static int usbh_vendor_sof(usb_host_t *host)
    {
        usbh_vendor_host_t *vendor = &usbh_vendor_host;
    
        /* 1.Obtain the current frame number for ISOC scheduling */
        int cur_frame = usbh_get_current_frame_number(host);
    
        /* 2. Waiting for the correct SOF interval to start next transfer
            - if cur_frame - last frame_num >= interval, means we should trigger a xfer ASAP.
            - if xfer_state = USBH_EP_XFER_WAIT_SOF, it means that last xfer has been done, so in sof intr, we should check whether the next frame will be the xfer frame
        */
        if ((usbh_get_elapsed_frame_cnt(host, pipe->frame_num) >= pipe->ep_interval) ||
            ((pipe->xfer_state == USBH_EP_XFER_WAIT_SOF) && (cur_frame - out_xfer->cur_frame) % pipe->ep_interval == 0)){
            //3. Set next transfer parameters: e.g. buffer and length
    
            //4. Start next transfer
            usbh_notify_class_state_change(host, pipe->pipe_num);
        }
    
        return HAL_OK;
    }
    

通道配置

Vendor 主机驱动在设备枚举阶段 (usbh_vendor_attach 回调函数) 中解析配置描述符,根据接口描述符自动查找并申请相应的传输资源,实现完整的数据与控制通道。

该示例中的通道配置如下:

数量

描述

2

默认控制传输 0

1

批量 IN 传输

1

批量 OUT 传输

1

中断 IN 传输

1

中断 OUT 传输

1

同步 IN 传输

1

同步 OUT 传输

API 说明

驱动 API

应用示例

应用设计

本节详细介绍 Vendor 应用的完整开发流程,涵盖驱动加载、热插拔处理、如何建立传输以及如何处理数据等核心环节。 但其传输的数据内容(如 Loopback 测试数据)是无具体业务含义的。开发者可以基于此框架:

  • 定义私有命令集:替换示例中的测试数据,实现具体的私有协议解析逻辑。

  • 调整资源配置:根据实际业务吞吐量,调整 FIFO 大小和缓冲区大小。

加载驱动

使用 Vendor 驱动前,需定义配置结构体并注册回调函数,随后调用初始化接口加载 USB 设备核心驱动及 Vendor 类驱动。

步骤说明:

  • 硬件配置:配置 USB 速度模式(High Speed/Full Speed)及中断优先级等。

  • 回调注册:定义用户回调结构体 usbh_vendor_cb_t,和 usbh_user_cb_t 挂载各个阶段的处理函数。

  • 加载核心驱动:调用 usbh_init() 加载 USB 核心驱动。

  • 加载类驱动:调用 usbh_vendor_init() 加载 Vendor 类驱动。

示例:

static usbh_config_t usbh_cfg = {
  .speed = USB_SPEED_HIGH,
  .ext_intr_enable = USBH_SOF_INTR,
  .isr_priority = INT_PRI_MIDDLE,
  .main_task_priority = 3U,
  .tick_source = USBH_SOF_TICK,
};

static usbh_vendor_cb_t vendor_usr_cb = {
  .attach = vendor_cb_attach,
  .detach = vendor_cb_detach,
  .setup = vendor_cb_setup,
  .transmit = vendor_cb_transmit,
  .receive  = vendor_cb_receive,
};

static usbh_user_cb_t usbh_usr_cb = {
  .process = vendor_cb_process
};

int ret = 0;

/* Initialize USB host core driver with configuration. */
ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
if (ret != HAL_OK) {
  return;
}

/* Initialize class driver with application callback handler. */
ret = usbh_vendor_init(&vendor_usr_cb);
if (ret != HAL_OK) {
  /* If class driver init fails, clean up the core driver */
  usbh_deinit();
  return;
}

热插拔事件处理

作为主机,系统必须能够健壮地处理 USB 设备的动态插入与移除,SDK 默认支持热插拔机制。

处理逻辑:

  • 设备插入 (Attach):USB 核心检测到设备,重新执行枚举和驱动加载流程。

  • 设备拔出 (Detach):触发回调释放信号量,应用线程捕获后执行反初始化,并释放堆内存。

示例:

static rtos_sema_t vendor_detach_sema;

/* USB detach callback */
static usbh_vendor_cb_t vendor_usr_cb = {
  .detach = vendor_cb_detach,
};

/* Callback executed in ISR context */
static int vendor_cb_detach(void)
{
  RTK_LOGS(TAG, RTK_LOG_INFO, "DETACH\n");
  rtos_sema_give(vendor_detach_sema);
  return HAL_OK;
}

/* Thread handling the state machine */
static void vendor_hotplug_thread(void *param)
{
  int ret = 0;

  UNUSED(param);

  for (;;) {
    if (rtos_sema_take(vendor_detach_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
      rtos_time_delay_ms(100);//make sure disconnect handle finish before deinit.

      /* 1. Clean up resources */
      usbh_vendor_deinit();
      /* 2. De-initialize USB core */
      usbh_deinit();

      rtos_time_delay_ms(10);

      RTK_LOGS(TAG, RTK_LOG_INFO, "Free heap: 0x%08x\n", rtos_mem_get_free_heap_size());
      /* 3. Re-initialize for next connection */
      ret = usbh_init(&usbh_cfg, &usbh_usr_cb);
      if (ret != HAL_OK) {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "Init USBH fail: %d\n", ret);
        break;
      }

      ret = usbh_vendor_init(&vendor_usr_cb);
      if (ret != HAL_OK) {
        RTK_LOGS(TAG, RTK_LOG_ERROR, "Init vendor fail: %d\n", ret);
        usbh_deinit();
        break;
      }
    }
  }

  rtos_task_delete(NULL);
}

数据收发处理

当 Vendor 主机枚举成功后,可以调用 API 开启数据收发。 以下流程适用于 BULK、INTR 和 ISOC 类型的传输,此处以 BULK 传输为例。

static __IO int vendor_is_ready = 0;
static rtos_sema_t vendor_bulk_send_sema;
static rtos_sema_t vendor_bulk_receive_sema;
static u8 vendor_bulk_loopback_tx_buf[USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE] __attribute__((aligned(CACHE_LINE_SIZE)));
static u8 vendor_bulk_loopback_rx_buf[USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE] __attribute__((aligned(CACHE_LINE_SIZE)));

static usbh_vendor_cb_t vendor_usr_cb = {
   .setup = vendor_cb_setup,
   .transmit = vendor_cb_transmit,
   .receive  = vendor_cb_receive,
};

static int vendor_cb_setup(void)
{
   vendor_is_ready = 1;
   return HAL_OK;
}

/**
* @brief USB Vendor transmit callback function
* @param ep_type transmission type (BULK, INTERRUPT, ISOC)
* @return HAL status
*/
static int vendor_cb_transmit(u8 ep_type)
{
   //Release the different semaphore according to different transmission, e.g. transmission type
   switch (ep_type) {
   case USB_CH_EP_TYPE_BULK:
      rtos_sema_give(vendor_bulk_send_sema);
      break;
      //Other transmission
   default:
      break;
   }

   return HAL_OK;
}

/**
* @brief USB Vendor receive callback function
* @param ep_type transmission type (BULK, INTERRUPT, ISOC)
* @param buf Received data buffer
* @param len Length of received data
* @param status Transfer status
* @return HAL status
*/
static int vendor_cb_receive(u8 ep_type, u8 *buf, u32 len, int status)
{
   // Get endpoint maximum packet size for each type
   u16 vendor_bulk_in_mps = usbh_vendor_get_bulk_ep_mps();

   switch (ep_type) {
   case USB_CH_EP_TYPE_BULK:
         // 1. Check if transfer transfer is complete successfully
         // 2. Reset total length and signal completion
         rtos_sema_give(vendor_bulk_receive_sema);
      } else {
         RTK_LOGS(TAG, RTK_LOG_ERROR, "%d RX fail: %d\n", ep_type, status);
      }

      break;
   }
   return HAL_OK;
}

/*---------------- BULK TRX Test --------------------*/

/*1. Wait for device to be ready for data transfer*/
while (1) {
   if (vendor_is_ready) { /* Check if device is ready */
      rtos_time_delay_ms(10);
      break;
   }
}

/* 2. BULK TX test */
/* Initiate transfer with TX configuration: transfer length and times */
usbh_vendor_bulk_transmit(vendor_bulk_loopback_tx_buf, USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE, USBH_VENDOR_BULK_LOOPBACK_CNT);

/* 3. Wait for transfer completion semaphore */
if (rtos_sema_take(vendor_bulk_send_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
   // Success indicates transfer completion
}

/* 4. BULK RX test */
/* Initiate transfer with RX configuration: transfer length and times */
usbh_vendor_bulk_receive(vendor_bulk_loopback_rx_buf, USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE, USBH_VENDOR_BULK_LOOPBACK_CNT);

/* 5. Wait for transfer completion semaphore */
if (rtos_sema_take(vendor_bulk_receive_sema, RTOS_SEMA_MAX_COUNT) == RTK_SUCCESS) {
   // Success indicates transfer completion
}

三种不同传输类型在接收成功后 receive 回调中的处理方式不同:

处理逻辑:‌

批量传输接收后累积数据直到满足特定条件后才释放信号量。 这主要是因为协议规定当批量传输长度为 MPS 整数倍时,需要追加一个 ZLP 零长度包标识传输结束。 用户层需要区别这种情况,所以需要等待完整的数据包序列或特定的结束条件(如 ZLP 零长度包、短包或缓冲区满)才能确认一次完整的传输完成。 这种机制确保了数据的完整性和正确性,避免了在数据未完全接收时就进行后续处理。

示例:

static int vendor_cb_receive(u8 ep_type, u8 *buf, u32 len, int status)
{
   /* 1. Get endpoint maximum packet size of BULK IN endpoint */
   u16 vendor_bulk_in_mps = usbh_vendor_get_bulk_ep_mps();

   switch (ep_type) {
   case USB_CH_EP_TYPE_BULK:
      /* 2. Check if transfer was successful */
      if (status == HAL_OK) {
         /* 3 Update total received length */
         vendor_bulk_total_rx_len += len;

         /* 4. Determine if transfer is complete based on: Zero-length packet (ZLP)/Short packet (not multiple of MPS)/ Buffer full condition */
         if ((len == 0) || (len % vendor_bulk_in_mps)
            || ((len % vendor_bulk_in_mps == 0) && (len < USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE))
            || (vendor_bulk_total_rx_len > USBH_VENDOR_BULK_LOOPBACK_BUF_SIZE)) {
             /* 5. Reset total length and signal completion */
            vendor_bulk_total_rx_len = 0;
            rtos_sema_give(vendor_bulk_receive_sema);
         }
      } else {
         RTK_LOGS(TAG, RTK_LOG_ERROR, "%d RX fail: %d\n", ep_type, status);
      }
      break;
   }
   return HAL_OK;
}

备注

  • 分配的用于收发的缓存必须要 Cache-line 地址对齐。

  • 完整的数据收发逻辑请参考 SDK 示例代码: {SDK}/component/example/usb/usbh_vendor/example_usbh_vendor.c

卸载驱动

在不再需要 USB 功能或系统关机时,按加载的反序释放资源。

示例:

/* Deinitialize Vendor class driver. */
usbd_vendor_deinit();

/* Deinitialize USB host core driver. */
usbd_deinit();

运行方式

该示例代码路径: {SDK}/component/example/usb/usbh_vendor,可为开发者设计自定义 Vendor 产品提供完整的参考方案。

本节介绍了一个完整的 Vendor 主机示例,该示例演示了如何通过 Vendor 协议栈实现与设备之间的自定义数据双向通信。

配置与编译

  • Menuconfig 配置

    amebaxxx_gcc_project 目录下,输入 ./menuconfig.py,按下面步骤选择 USBH VENDOR , 保存退出。

    - Choose `CONFIG USB --->`:
      [*] Enable USB
          USB Mode (host)  --->
      [*] Vendor
    
  • 编译与烧录

    执行编译命令,并烧录生成的 Image 文件至开发板。

    cd amebaxxx_gcc_project
    ./build.py -a usbh_vendor
    

结果验证

  • 启动设备

    重启开发板,观察串口日志,应显示如下启动信息:

    [VND-I] USBH vendor demo start
    
  • 连接主机

    使用 USB 线缆将开发板连接特定厂商的 USB 设备(例如,另一块运行本 USB 协议栈自定义设备方案的开发板,详见 自定义设备方案

  • 数据通信测试

    Ameba 开发板将识别 Vendor 设备并自动进行收发测试。

    ```
    [VND-I] ISOC test start, times:100, size: 1024
    [VEN-I] ISOC OUT test finish 100/100:
    [VEN-I]   0   1   2   3   4   5   6   7   8   9
    
    ```
    [VEN-I]  90  91  92  93  94  95  96  97  98  99
    [VEN-I] ISOC IN test finish 100/100:
    [VEN-I]   0   1   2   3   4   5   6   7   8   9
    
    ```
    [VEN-I]  90  91  92  93  94  95  96  97  98  99
    [VND-I] ISOC test PASS
    [VND-I] BULK loopback test start, times:100, size: 512
    [VND-I] INTR loopback test start, times:100, size: 1024
    [VND-I] BULK loopback test PASS 100/100
    [VND-I] INTR loopback test PASS 100/100
    [VND-I] USBH vendor demo stop