16、USB Vendor Class与自定义协议

说实话,USB 标准里那几大类——HID、Mass Storage、CDC、Audio——覆盖了绝大多数消费电子场景。但你要是做工业设备、仪器仪表、或者跟 FPGA 通信,这些标准类就不够用了。为什么?因为标准类定义死了数据的格式和含义,你没法塞自己的私有协议进去。

这时候就该 Vendor Specific Class 登场了。说白了,就是 USB 规范给你留了个后门:你声明自己是厂商自定义设备,然后想怎么传数据就怎么传。我最早接触这个,是在一个用 Android 平板控制 FPGA 数据采集的项目里。标准 HID 传不了大数据,CDC 又太啰嗦,最后选了 Vendor Class + 批量端点,效果出奇的好。

16.1 Vendor Specific Class 原理

USB 设备描述符里有个 bDeviceClass 字段。标准设备填 0x00(在接口描述符里定义类),或者填 0x02(CDC)、0x03(HID)这些。Vendor Class 填什么?填 0xFF

0xFF 就是厂商自定义类的标志。告诉主机:别拿标准驱动来套我,我自己写驱动。Android 这边,你需要在 usb_device_filter.xml 里匹配 VID 和 PID,然后通过 UsbDeviceConnection 直接操作。

核心要点:Vendor Class 设备不需要遵循任何标准协议格式。控制传输、批量传输、中断传输的数据内容完全由你定义。代价是——主机端没有现成驱动,你得自己写。

我个人习惯把 Vendor Class 设备描述成「一张白纸」。你想画什么画都行,但画笔得自己带。Android 的 UsbManager 就是你的画笔。

16.2 自定义请求:Setup Packet 发送与解析

控制传输是 USB 的「命令通道」。标准请求(比如获取描述符、设置地址)由 USB 核心处理。但 Vendor Class 允许你发 自定义请求——也就是自己定义 bmRequestTypebRequestwValuewIndexwLength 这五个字段的含义。

16.2.1 Setup Packet 结构

一个控制传输的 Setup 包固定 8 字节:

偏移字段说明
0bmRequestType方向(主机→设备/设备→主机)+ 类型(标准/类/厂商)+ 接收者(设备/接口/端点)
1bRequest请求号。标准请求用 0~11,厂商自定义随便用
2-3wValue16 位参数,随你定义
4-5wIndex16 位参数,通常用于指定接口或端点
6-7wLength数据阶段长度(如果有的话)

举个例子。我想让 FPGA 设备复位某个寄存器,可以这样定义:

  • bmRequestType = 0x40(主机→设备,厂商类型,设备接收)
  • bRequest = 0x01(我自定义的「复位寄存器」命令)
  • wValue = 寄存器地址
  • wIndex = 0(保留)
  • wLength = 0(无数据阶段)

16.2.2 Android 端发送自定义请求

Android 的 UsbDeviceConnection 提供了 controlTransfer() 方法。签名如下:

int controlTransfer(int requestType, int request, int value, 
                    int index, byte[] buffer, int length, int timeout);

对应到上面的例子:

UsbDeviceConnection conn = ...;
int regAddr = 0x1234;
int result = conn.controlTransfer(
    0x40,       // bmRequestType: 主机→设备, 厂商, 设备
    0x01,       // bRequest: 复位寄存器
    regAddr,    // wValue: 寄存器地址
    0,          // wIndex: 保留
    null,       // 无数据
    0,          // 数据长度0
    100         // 超时100ms
);
if (result < 0) {
    // 传输失败,检查USB线或设备状态
}

避坑指南:我曾经在 controlTransfer 的 timeout 参数上吃过亏。设太短(比如 10ms),设备还没准备好就超时了。设太长(比如 5000ms),UI 线程卡死。建议 100~200ms,配合异步线程使用。

16.2.3 设备端解析(以 FPGA 为例)

FPGA 端的 USB 控制器(比如 Cypress FX3、FTDI FT600)收到 Setup 包后,会解析 bmRequestType 和 bRequest。如果是厂商请求,就进入你的自定义状态机。伪代码思路:

// FPGA Verilog 伪代码
always @(posedge clk) begin
    if (setup_packet_valid) begin
        if (bmRequestType == 8'h40 && bRequest == 8'h01) begin
            // 复位寄存器
            reg_file[wValue] <= 32'h0;
            ack_handshake <= 1;
        end
    end
end

嗯,这里要注意:FPGA 端必须正确回复握手包(ACK),否则主机侧会一直重试直到超时。

16.3 批量端点自定义协议设计

控制传输适合小数据量的命令交互。真要传几百 KB 甚至几 MB 的数据,得靠 批量端点。批量传输的特点是:可靠(有 CRC 和重传),但延迟不确定(USB 总线空闲时才传)。

设计自定义协议时,我建议遵循一个简单的 帧格式

偏移长度字段说明
02 字节帧头标识固定值 0xAA55,用于同步
22 字节数据长度有效载荷长度(不含头)
41 字节命令字0x01=读, 0x02=写, 0x03=配置等
51 字节校验方式0x00=无校验, 0x01=CRC8
6N 字节有效载荷实际数据
6+N1 或 2 字节校验值根据校验方式决定

为什么加帧头标识?因为批量传输是流式的,没有消息边界。万一丢了一个包,后续数据就全错位了。0xAA55 可以帮助接收方重新同步。

重要:批量端点的最大包大小取决于 USB 速度。全速是 64 字节,高速是 512 字节,超高速是 1024 字节。设计帧时,确保一帧不超过最大包大小,否则需要拆包。

16.3.1 Android 端批量读写

Android 的批量读写接口很简单:

UsbEndpoint bulkOut = ...; // 批量输出端点
UsbEndpoint bulkIn  = ...; // 批量输入端点

// 发送数据
byte[] txData = buildFrame(0x01, payload); // 组装帧
int sent = conn.bulkTransfer(bulkOut, txData, txData.length, 100);

// 接收数据
byte[] rxBuf = new byte[512];
int received = conn.bulkTransfer(bulkIn, rxBuf, rxBuf.length, 100);

这里有个坑:bulkTransfer() 返回的是实际传输的字节数。如果返回 -1,说明传输失败。我曾经遇到一个情况:设备端批量端点没准备好,主机一直返回 -1,查了半天发现是 FPGA 固件里端点使能位没置起来。

16.4 实战:与 FPGA/自定义硬件通信

好了,理论说完了。咱们来一个完整的实战案例。假设你有一个 FPGA 开发板,通过 USB 连接到 Android 平板。FPGA 内部实现了 Vendor Class 设备,包含一个控制端点(0x00)和一对批量端点(0x01 输出,0x82 输入)。

16.4.1 整体流程

下面这张图展示了通信的完整流程:

Android ↔ FPGA 自定义协议通信流程 Android 平板 1. 枚举设备 VID/PID 匹配 2. 声明接口 claimInterface() 3. 控制传输 发送自定义请求 4. 批量传输 发送/接收数据帧 5. 解析响应 校验帧头/CRC USB 总线(批量/控制传输) FPGA 设备 1. 端点 0 处理 解析 Setup 包 2. 控制请求响应 执行寄存器操作 3. 批量端点就绪 端点使能 4. 接收/发送帧 解析帧头 5. 数据处理 采集/控制逻辑 控制请求 批量数据

16.4.2 关键代码片段

Android 端核心代码(Kotlin 风格):

class UsbFpgaCommunicator(private val device: UsbDevice, private val conn: UsbDeviceConnection) {
    
    private val bulkOut: UsbEndpoint
    private val bulkIn: UsbEndpoint
    
    init {
        val iface = device.getInterface(0)
        bulkOut = iface.getEndpoint(0)  // 批量输出
        bulkIn = iface.getEndpoint(1)   // 批量输入
        conn.claimInterface(iface, true)
    }
    
    // 发送自定义控制请求:复位 FPGA 内部计数器
    fun resetCounter(): Boolean {
        val result = conn.controlTransfer(0x40, 0x02, 0x0000, 0, null, 0, 100)
        return result >= 0
    }
    
    // 发送批量数据帧
    fun sendDataFrame(cmd: Byte, payload: ByteArray): Boolean {
        val frame = buildFrame(cmd, payload)
        val sent = conn.bulkTransfer(bulkOut, frame, frame.size, 200)
        return sent == frame.size
    }
    
    // 接收批量数据帧
    fun receiveDataFrame(): ByteArray? {
        val buf = ByteArray(512)
        val received = conn.bulkTransfer(bulkIn, buf, buf.size, 200)
        if (received > 0) {
            return buf.copyOf(received)
        }
        return null
    }
    
    private fun buildFrame(cmd: Byte, payload: ByteArray): ByteArray {
        val len = payload.size
        val frame = ByteArray(6 + len + 1) // 头6字节 + 数据 + CRC8
        frame[0] = 0xAA.toByte()
        frame[1] = 0x55.toByte()
        frame[2] = (len and 0xFF).toByte()
        frame[3] = (len shr 8).toByte()
        frame[4] = cmd
        frame[5] = 0x01 // CRC8 校验
        System.arraycopy(payload, 0, frame, 6, len)
        frame[6 + len] = calculateCRC8(payload)
        return frame
    }
}

个人经验:FPGA 端的批量端点处理,一定要用双缓冲(double buffering)。否则主机发得快了,FPGA 还没处理完上一包,就会丢数据。我踩过这个坑,后来在 FX3 固件里开了 AUTO DMA 通道,问题解决。

16.4.3 调试技巧

调试自定义协议,最痛苦的是「不知道数据传没传对」。我的三板斧:

  1. 逻辑分析仪抓 USB 总线——比如 Saleae 配合 USB 解码插件,直接看 Setup 包和批量数据内容。
  2. Android 端打印原始字节——把收发的 byte[] 转成 Hex 字符串 log 出来,对照协议文档逐字节检查。
  3. FPGA 端回环测试——让 FPGA 把收到的数据原样返回,Android 端对比发送和接收是否一致。

我记得有一次,FPGA 返回的数据总是少两个字节。查了半天,发现是帧头解析逻辑里把长度字段的大小端搞反了。这种问题,用逻辑分析仪一看就明白。

16.5 小结

Vendor Class 和自定义协议,是 Android USB 开发里最灵活、也最需要细心的地方。你想想看,没有标准约束,一切靠自己定义——帧格式、命令字、校验方式、超时策略。好处是你可以针对硬件特性做极致优化,坏处是调试起来确实费头发。

但话说回来,一旦你把这套框架搭好,后续跟 FPGA、DSP、MCU 通信就都是套路了。控制传输发命令,批量传输传数据,帧头同步加校验——这套组合拳,我用了好几年,稳得很。

核心总结:

  • Vendor Class = bDeviceClass 0xFF,自己定义一切
  • 控制传输用于命令,批量传输用于数据
  • 自定义帧格式必须包含同步头、长度、命令、校验
  • 调试三板斧:逻辑分析仪、Hex 日志、回环测试

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