USB枚举、协商、传输:以USB相机为例看识别与格式协商

枚举、协商、传输:以相机为例看识别与格式协商

概述:主机能够枚举一颗从未见过的芯片,并非因为主机预先识别了该芯片,而是因为设备按 USB 规范用描述符描述自身。描述符说明设备的身份与接口数量,Probe/Commit 协商确定输出格式。

1. 基础:协议分层、双方角色与通信方式

要理解这套过程,先分清两层规范,它们叠加在一起,职责不同:

规范 负责内容
总线层 USB(Universal Serial Bus,通用串行总线) 电气连接、寻址、数据包的收发、四种传输类型
设备类层 UVC(USB Video Class,USB 视频类) 摄像头的描述符格式、控制请求、数据格式

UVC 建立在 USB 之上。USB 只负责把字节送到某个端点,不关心这些字节承载的是视频还是其他内容;UVC 才规定摄像头的描述符格式与可用的控制请求。

双方的角色分工是理解后续流程的前提:

  • 主机(PC 上的操作系统与驱动):发起所有请求、为设备分配地址、决定加载哪个驱动、决定取数据的时机。
  • 设备(MCU 加摄像头固件):只负责应答。设备不能主动向主机发送数据,视频数据同样由主机发起读取:主机查询端点是否有数据,设备才返回数据。

设备侧要能完成应答,以下三项都是必需的:

前提 作用
USB 设备控制器(含 PHY 物理层) 电气层收发,D+ 上拉,端点 FIFO
设备协议栈 解析标准请求,管理地址、端点与状态机
一份自描述的描述符表 设备身份的唯一信息源

USB 定义了四种传输类型,本文涉及前三种,第四种是视频流的可替代方案:

传输类型 用在哪 特点
控制传输(Control),固定使用 EP0 枚举、协商、读状态 每条请求都保证送达并回执
批量传输(Bulk) 视频数据 不丢包,但带宽与时延不保证
中断传输(Interrupt) UVC 的事件通知 周期轮询,延迟有上限
等时传输(Isochronous) 视频数据的另一种选择 保带宽与时延,出错不重传

端点(endpoint)是设备内部的数据出入口,每个端点有地址和方向。主机的每一次通信都发往设备上的某个端点,而不是发往设备的某个功能。枚举与协商都经由 0 号端点(EP0),视频数据使用独立的端点。

设备侧只要按同样格式应答描述符,更换芯片后主机同样能够识别。以下从设备插入开始,依次描述这八个步骤。

2. 枚举流程:关键信息集中在第 5 步

设备插入后,主机会按固定顺序执行八个步骤,整体分为两段:前段识别设备并分配地址,后段读回全部能力并登记进系统。
#mermaid-svg-0MnJYvSzojFUZlSP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0MnJYvSzojFUZlSP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0MnJYvSzojFUZlSP .error-icon{fill:#552222;}#mermaid-svg-0MnJYvSzojFUZlSP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0MnJYvSzojFUZlSP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0MnJYvSzojFUZlSP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0MnJYvSzojFUZlSP .marker.cross{stroke:#333333;}#mermaid-svg-0MnJYvSzojFUZlSP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0MnJYvSzojFUZlSP p{margin:0;}#mermaid-svg-0MnJYvSzojFUZlSP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0MnJYvSzojFUZlSP .cluster-label text{fill:#333;}#mermaid-svg-0MnJYvSzojFUZlSP .cluster-label span{color:#333;}#mermaid-svg-0MnJYvSzojFUZlSP .cluster-label span p{background-color:transparent;}#mermaid-svg-0MnJYvSzojFUZlSP .label text,#mermaid-svg-0MnJYvSzojFUZlSP span{fill:#333;color:#333;}#mermaid-svg-0MnJYvSzojFUZlSP .node rect,#mermaid-svg-0MnJYvSzojFUZlSP .node circle,#mermaid-svg-0MnJYvSzojFUZlSP .node ellipse,#mermaid-svg-0MnJYvSzojFUZlSP .node polygon,#mermaid-svg-0MnJYvSzojFUZlSP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0MnJYvSzojFUZlSP .rough-node .label text,#mermaid-svg-0MnJYvSzojFUZlSP .node .label text,#mermaid-svg-0MnJYvSzojFUZlSP .image-shape .label,#mermaid-svg-0MnJYvSzojFUZlSP .icon-shape .label{text-anchor:middle;}#mermaid-svg-0MnJYvSzojFUZlSP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0MnJYvSzojFUZlSP .rough-node .label,#mermaid-svg-0MnJYvSzojFUZlSP .node .label,#mermaid-svg-0MnJYvSzojFUZlSP .image-shape .label,#mermaid-svg-0MnJYvSzojFUZlSP .icon-shape .label{text-align:center;}#mermaid-svg-0MnJYvSzojFUZlSP .node.clickable{cursor:pointer;}#mermaid-svg-0MnJYvSzojFUZlSP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0MnJYvSzojFUZlSP .arrowheadPath{fill:#333333;}#mermaid-svg-0MnJYvSzojFUZlSP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0MnJYvSzojFUZlSP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0MnJYvSzojFUZlSP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0MnJYvSzojFUZlSP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0MnJYvSzojFUZlSP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0MnJYvSzojFUZlSP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0MnJYvSzojFUZlSP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0MnJYvSzojFUZlSP .cluster text{fill:#333;}#mermaid-svg-0MnJYvSzojFUZlSP .cluster span{color:#333;}#mermaid-svg-0MnJYvSzojFUZlSP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0MnJYvSzojFUZlSP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0MnJYvSzojFUZlSP rect.text{fill:none;stroke-width:0;}#mermaid-svg-0MnJYvSzojFUZlSP .icon-shape,#mermaid-svg-0MnJYvSzojFUZlSP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0MnJYvSzojFUZlSP .icon-shape p,#mermaid-svg-0MnJYvSzojFUZlSP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0MnJYvSzojFUZlSP .icon-shape .label rect,#mermaid-svg-0MnJYvSzojFUZlSP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0MnJYvSzojFUZlSP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0MnJYvSzojFUZlSP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0MnJYvSzojFUZlSP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-0MnJYvSzojFUZlSP .host>*{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-0MnJYvSzojFUZlSP .host span{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-0MnJYvSzojFUZlSP .host tspan{fill:#084298!important;}#mermaid-svg-0MnJYvSzojFUZlSP .req>*{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-0MnJYvSzojFUZlSP .req span{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-0MnJYvSzojFUZlSP .req tspan{fill:#155724!important;}#mermaid-svg-0MnJYvSzojFUZlSP .key>*{fill:#fff3cd!important;stroke:#ffc107!important;color:#856404!important;}#mermaid-svg-0MnJYvSzojFUZlSP .key span{fill:#fff3cd!important;stroke:#ffc107!important;color:#856404!important;}#mermaid-svg-0MnJYvSzojFUZlSP .key tspan{fill:#856404!important;} 后半段:读回全部能力,登记为设备
前半段:先识别设备,再分配地址
主机复位

  • 速度协商
    GET_DESCRIPTOR

DEVICE 8 字节
GET_DESCRIPTOR

DEVICE 18 字节
SET_ADDRESS

分配地址
GET_DESCRIPTOR

CONFIGURATION
GET_DESCRIPTOR

STRING
SET_CONFIGURATION

端点使能
按接口类代码

加载驱动并注册设备

前两步为什么要分两次读取设备描述符?因为主机尚不知道该设备的端点能够接收多大的数据包,首次只读取 8 字节以取得 bMaxPacketSize0,再读取完整的 18 字节。

这八个步骤中,主机与设备各自的动作:

主机侧 设备侧
1 检测到接入,复位设备,协商是全速还是高速 接受复位
2 读取 8 字节设备描述符 返回设备描述符的前 8 字节
3 读取完整 18 字节 返回完整设备描述符,含 VID / PID / 配置数量
4 分配一个新地址 记录地址,此后仅以该地址应答
5 读取配置描述符集合 返回全部能力,即第 3 节所列结构
6 读取字符串描述符 返回厂商 / 产品 / 序列号等文本
7 选定配置 按配置内容使能各端点
8 按接口类代码加载驱动,将设备注册进系统 此后等待主机协商输出格式

第 5 步是全部信息的来源。 一次 GET_DESCRIPTOR(CONFIGURATION) 取回整个配置描述符集合(configuration descriptor set),按固定顺序排列:

复制代码
Configuration
├── IAD(Interface Association Descriptor,接口关联描述符)
├── VC 接口 + 一串 CS_INTERFACE 描述符 + 中断端点
└── VS 接口 + 若干 format 描述符 + 若干 frame 描述符 + 数据端点

主机侧看到的设备能力,全部来自这一次读取的字节。这批字节的结构是固定的,下文以 UVC 摄像头为例说明。

3. UVC 的描述符:命名的三个层次与 IAD 关联

UVC 设备的标准结构是一对接口 :一个 VideoControl(VC,视频控制接口,负责控制)加一个 VideoStreaming(VS,视频流接口,负责数据)。接口类代码(bInterfaceClass)固定为 0x0E,子类 0x01 为 VC、0x02 为 VS。主机读到 0x0E 即判定这是摄像头,加载系统自带的 UVC 驱动。

一对 VC 与 VS 构成一个功能(function) ,由 IAD 将两个接口关联并声明为一个整体,否则系统会将它们视为两个互不相关的设备。当一台设备要提供多路视频时,就在一个 configuration 下放置多组 IAD + VC + VS

由此形成命名的三个层次。左侧是设备声明 的结构,右侧是主机据此建立的结构:
#mermaid-svg-QtoKgUfDLFC8uk3n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QtoKgUfDLFC8uk3n .error-icon{fill:#552222;}#mermaid-svg-QtoKgUfDLFC8uk3n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QtoKgUfDLFC8uk3n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QtoKgUfDLFC8uk3n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QtoKgUfDLFC8uk3n .marker.cross{stroke:#333333;}#mermaid-svg-QtoKgUfDLFC8uk3n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QtoKgUfDLFC8uk3n p{margin:0;}#mermaid-svg-QtoKgUfDLFC8uk3n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster-label text{fill:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster-label span{color:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster-label span p{background-color:transparent;}#mermaid-svg-QtoKgUfDLFC8uk3n .label text,#mermaid-svg-QtoKgUfDLFC8uk3n span{fill:#333;color:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n .node rect,#mermaid-svg-QtoKgUfDLFC8uk3n .node circle,#mermaid-svg-QtoKgUfDLFC8uk3n .node ellipse,#mermaid-svg-QtoKgUfDLFC8uk3n .node polygon,#mermaid-svg-QtoKgUfDLFC8uk3n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QtoKgUfDLFC8uk3n .rough-node .label text,#mermaid-svg-QtoKgUfDLFC8uk3n .node .label text,#mermaid-svg-QtoKgUfDLFC8uk3n .image-shape .label,#mermaid-svg-QtoKgUfDLFC8uk3n .icon-shape .label{text-anchor:middle;}#mermaid-svg-QtoKgUfDLFC8uk3n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QtoKgUfDLFC8uk3n .rough-node .label,#mermaid-svg-QtoKgUfDLFC8uk3n .node .label,#mermaid-svg-QtoKgUfDLFC8uk3n .image-shape .label,#mermaid-svg-QtoKgUfDLFC8uk3n .icon-shape .label{text-align:center;}#mermaid-svg-QtoKgUfDLFC8uk3n .node.clickable{cursor:pointer;}#mermaid-svg-QtoKgUfDLFC8uk3n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QtoKgUfDLFC8uk3n .arrowheadPath{fill:#333333;}#mermaid-svg-QtoKgUfDLFC8uk3n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QtoKgUfDLFC8uk3n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QtoKgUfDLFC8uk3n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QtoKgUfDLFC8uk3n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QtoKgUfDLFC8uk3n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QtoKgUfDLFC8uk3n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster text{fill:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n .cluster span{color:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QtoKgUfDLFC8uk3n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QtoKgUfDLFC8uk3n rect.text{fill:none;stroke-width:0;}#mermaid-svg-QtoKgUfDLFC8uk3n .icon-shape,#mermaid-svg-QtoKgUfDLFC8uk3n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QtoKgUfDLFC8uk3n .icon-shape p,#mermaid-svg-QtoKgUfDLFC8uk3n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QtoKgUfDLFC8uk3n .icon-shape .label rect,#mermaid-svg-QtoKgUfDLFC8uk3n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QtoKgUfDLFC8uk3n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QtoKgUfDLFC8uk3n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QtoKgUfDLFC8uk3n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-QtoKgUfDLFC8uk3n .dev>*{fill:#e2d9f3!important;stroke:#6f42c1!important;color:#3b2a6b!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .dev span{fill:#e2d9f3!important;stroke:#6f42c1!important;color:#3b2a6b!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .dev tspan{fill:#3b2a6b!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .fn>*{fill:#fff3cd!important;stroke:#ffc107!important;color:#856404!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .fn span{fill:#fff3cd!important;stroke:#ffc107!important;color:#856404!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .fn tspan{fill:#856404!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .itf>*{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .itf span{fill:#d4edda!important;stroke:#28a745!important;color:#155724!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .itf tspan{fill:#155724!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .host>*{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .host span{fill:#cce5ff!important;stroke:#0d6efd!important;color:#084298!important;}#mermaid-svg-QtoKgUfDLFC8uk3n .host tspan{fill:#084298!important;} 主机侧视图
设备声明的结构(描述符中的三层)
显示设备名
各注册为一个摄像头
设备级

iProduct = 2

Img+Data Camera
功能 1

IAD iFunction = 6

Img Camera
功能 2

IAD iFunction = 7

Data Camera
接口 0 + 1

VC 与 VS

iInterface = 6
接口 2 + 3

VC 与 VS

iInterface = 7
设备树

Img+Data Camera
摄像头列表

Img Camera

Data Camera

固件中的字符串表就是这三层的原始形式,节点中的下标数字对应这张表的行号:

c 复制代码
static const char *ustring[] =
{
    "Language",           /* 0  语言 ID            */
    "<Vendor> Co.,Ltd",   /* 1  iManufacturer      */
    "Img+Data Camera",    /* 2  iProduct     ← 设备级 */
    serialId,             /* 3  iSerialNumber      */
    "Configuration",      /* 4  iConfiguration     */
    "Interface",          /* 5                     */
    "Img Camera",         /* 6  功能 1 的 iFunction / iInterface */
    "Data Camera"         /* 7  功能 2 的 iFunction / iInterface */
};

设备描述符中 iProduct = 2,第一组 IAD/VC/VS 的 iFunctioniInterface 为 6,第二组为 7。这几个字段本身都是字符串描述符的索引号。

将这份字节按层级展开,就是主机在第 5 步读取到的结构:

text 复制代码
Configuration 1
├─ IAD #1                                    iFunction = 6 → "Img Camera"
│  ├─ Interface 0(VC)   class 0x0E / subclass 0x01
│  │  ├─ CS_INTERFACE : Header
│  │  ├─ CS_INTERFACE : Input Terminal   (相机终端)
│  │  ├─ CS_INTERFACE : Processing Unit
│  │  ├─ CS_INTERFACE : Output Terminal
│  │  └─ Endpoint 0x87   中断传输 IN
│  └─ Interface 1(VS)   class 0x0E / subclass 0x02   iInterface = 6
│     ├─ VS Input Header
│     ├─ Format 1..6     未压缩(NV12 / YUY2 / Y16 / Y8 / RGB888 / RGB565)
│     │  └─ 每种格式包含若干 Frame 描述符
│     ├─ Format 7        MJPEG
│     │  └─ 若干 Frame 描述符
│     └─ Endpoint 0x81   批量传输 IN
└─ IAD #2                                    iFunction = 7 → "Data Camera"
   ├─ Interface 2(VC)   class 0x0E / subclass 0x01
   │  ├─ CS_INTERFACE : Header / Terminal / Unit ...
   │  └─ Endpoint 0x88   中断传输 IN
   └─ Interface 3(VS)   class 0x0E / subclass 0x02   iInterface = 7
      ├─ VS Input Header
      ├─ Format 1        未压缩
      │  └─ 6 组 Frame:每组 = 数据帧 + 同尺寸的信息帧
      └─ Endpoint 0x82   批量传输 IN

CS_INTERFACE 是类特定接口描述符(class-specific interface descriptor),VC 内部的终端与处理单元都由它定义。

接口编号在固件中写作 USB_DYNAMIC,由协议栈在枚举时分配,因此上文的 0/1/2/3 是典型结果而非硬编码值。

主机据此建立的设备树,同一台设备会同时出现在两个位置:

text 复制代码
设备管理器
├─ 通用串行总线控制器
│  └─ USB 复合设备
│     └─ <Vendor> Co.,Ltd  Img+Data Camera     ← iProduct(设备级,只有一个)
└─ 摄像头(Imaging Devices)
   ├─ Img Camera                                ← 功能 1 的 iFunction
   └─ Data Camera                               ← 功能 2 的 iFunction

两个位置描述的是同一份数据,区别只在于主机把它放在设备树的哪一层。

两个功能承载的数据形态不同,这是分成两路的原因:

功能 1(Img Camera) 功能 2(Data Camera)
内容 图像帧 结构化数据帧
格式数 6 种未压缩 + 1 种压缩(MJPEG) 1 种未压缩
帧组织 每种格式若干组独立的图像帧参数 成对:数据帧 + 同尺寸的信息帧
尺寸 图像分辨率 小尺寸矩阵(如 16×12 至 512×384)

第二路每帧配一份信息帧,对应「传感器矩阵 + 元信息(帧头 / 量程 / 时间戳)」这类数据的常见组织方式。

4. 枚举完成识别,能力由 Probe/Commit 协商确定

描述符已向主机提供格式清单,但主机尚未选定使用其中哪一项,帧率也未确定。这一步要用 UVC 的类特定控制请求(class-specific request)再协商一轮,全程同样经由 EP0。这类请求由 UVC 定义,只是借用了 USB 的控制传输:
设备侧硬件输出通路 设备侧 UVC Class 处理 USB EP0 主机 UVC 驱动 设备侧硬件输出通路 设备侧 UVC Class 处理 USB EP0 主机 UVC 驱动 #mermaid-svg-64hj7ClcDwCS6PsP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-64hj7ClcDwCS6PsP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-64hj7ClcDwCS6PsP .error-icon{fill:#552222;}#mermaid-svg-64hj7ClcDwCS6PsP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-64hj7ClcDwCS6PsP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-64hj7ClcDwCS6PsP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-64hj7ClcDwCS6PsP .marker.cross{stroke:#333333;}#mermaid-svg-64hj7ClcDwCS6PsP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-64hj7ClcDwCS6PsP p{margin:0;}#mermaid-svg-64hj7ClcDwCS6PsP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-64hj7ClcDwCS6PsP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-64hj7ClcDwCS6PsP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-64hj7ClcDwCS6PsP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-64hj7ClcDwCS6PsP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-64hj7ClcDwCS6PsP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-64hj7ClcDwCS6PsP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-64hj7ClcDwCS6PsP .sequenceNumber{fill:white;}#mermaid-svg-64hj7ClcDwCS6PsP #sequencenumber{fill:#333;}#mermaid-svg-64hj7ClcDwCS6PsP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-64hj7ClcDwCS6PsP .messageText{fill:#333;stroke:none;}#mermaid-svg-64hj7ClcDwCS6PsP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-64hj7ClcDwCS6PsP .labelText,#mermaid-svg-64hj7ClcDwCS6PsP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-64hj7ClcDwCS6PsP .loopText,#mermaid-svg-64hj7ClcDwCS6PsP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-64hj7ClcDwCS6PsP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-64hj7ClcDwCS6PsP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-64hj7ClcDwCS6PsP .noteText,#mermaid-svg-64hj7ClcDwCS6PsP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-64hj7ClcDwCS6PsP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-64hj7ClcDwCS6PsP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-64hj7ClcDwCS6PsP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-64hj7ClcDwCS6PsP .actorPopupMenu{position:absolute;}#mermaid-svg-64hj7ClcDwCS6PsP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-64hj7ClcDwCS6PsP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-64hj7ClcDwCS6PsP .actor-man circle,#mermaid-svg-64hj7ClcDwCS6PsP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-64hj7ClcDwCS6PsP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} GET_CUR(VS_PROBE) 读取候选格式与帧参数1返回该格式支持的分辨率 / 帧率区间2SET_CUR(VS_PROBE) 探测3校验 payload 能否承载4SET_CUR(VS_COMMIT) 确定5commit 回调6以 bFormatIndex / bFrameIndex / dwFrameInterval 初始化输出通路7数据端点开始传输数据8

VS_PROBE 用于探测,VS_COMMIT 用于确定。关键在于最后三步:Commit 之前硬件通路尚未启动 。回调取得 bFormatIndexbFrameIndexdwFrameInterval 三个下标,组成一份帧参数,才去初始化封装硬件与输出接口。因此相机已插入但未打开预览时,设备不会向数据端点发送像素。

协商完成后即与 EP0 无关:像素经由独立的 Bulk IN 或 Isochronous IN 端点传输,由主机按期读取。此后主机侧负责接收,设备侧按协商结果输出。

5. 三个最容易混淆的地方

以下三处名称相近,实际调试时最容易误判:

  1. 端点号 ≠ 内部通道号0x810x82 是 USB 端点地址,设备固件内部可能另有「通道 1/2/3」编号,两套编号之间没有数值对应关系,仅表示固件在配置中把某路内部通道映射到了某个端点。

  2. 设备名不是摄像头名称 。设备树读取 iProduct,摄像头列表读取各 function 的 iFunction,两者本就是不同的字段,因此同一台设备会出现在两个位置。

  3. IAD 不可省略。缺少 IAD 时,VC 与 VS 会被视为两个独立接口,Windows 下常表现为「识别到 USB Video 设备但无法打开」,因为 UVC 驱动需要依靠配对关系才能确定控制接口与数据接口属于同一路视频。

一句话:USB 的即插即用建立在设备按规范描述自身的基础上。主机发起请求、设备应答,这一交互贯穿三个阶段:枚举时交换描述符确定身份,协商时用 Probe/Commit 确定格式,开始传输后设备向端点输出数据。

相关推荐
xx~t4 天前
嵌入式——单片机2
嵌入式硬件·学习·51单片机·串口·蜂鸣器
sunoo-2294 天前
【51单片机实战】温控下位机完整实现:DS18B20采集 + 自定义二进制串口协议 + PWM声光告
笔记·学习·51单片机·串口
橘色的喵4 天前
XCOM:一个 4 MB 的串口调试工具
串口·lua·串口工具·xcom
susplus5 天前
【51单片机通信协议】串口
51单片机·串口·嵌入式·通信协议
小黑(感觉你阻抗≥1MΩ)10 天前
TYPE-C电路
usb·电路·原理图·type-c
一个平凡而乐于分享的小比特10 天前
解剖USB控制器移植:从VeriSilicon到MindMotion的“外科手术式”改造
usb·verisilicon·mindmotion
ZenasLDR12 天前
LDR6600实现AR眼镜边充电边投屏功能
接口·芯片·usb
派勤电子15 天前
多串口工控主板选型:分清原生板载与扩展COM,少走选型弯路
串口·工控主板·工控硬件·多串口工控主板·mini‑itx·3.5寸板·matx主板
csdn_aspnet21 天前
C# 多个串口多个线程发送数据和接收数据
c#·串口·thread·com·serialport