UVC Payload Header 功能分析
zhilu.zhang
zhilu.zhang
发布于 2021-03-11 / 346 阅读 / 0 评论 / 0 点赞

UVC Payload Header 功能分析

摘要:本文基于 UVC 1.1 版本协议说明了 UVC Payload Header 在通过 USB ISOC 和 BULK 方式发送视频数据过程中的作用,以及在 Payload Header 中添加 PTS SRC 的方法。(本文很多内容来自 UVC 1.1 协议文档)

1.UVC Payload 结构

UVC 中的 Payload(译:载荷)即为通过 USB 发送的图像帧数据。

UVC 接口类型分为两种子类型,其中的 VC(VideoControl) 接口可以通过 PU、CT、XU 等向 UVC 设备发送配置命令;VS (VideoStreaming) 接口则可以通过 Payload 相关的 USB 描述符配置设备支持的图像类型、分辨率、帧率等参数,并提供图像帧的传输功能。

UVC 协议为 Payload 定义了如下图所示的结构,即每个 Payload 分为 Payload header 和 Payload data 两部分。

下图所示为在 USB High Speed 下通过 ISOC IN 端点传输 Payload 结构的一种情况。可以看出在一次传输过程中只存在一个 PayloadHeader,这一点在 BULK 传输过程中也是成立的。

从上图可以获得的信息是,在 ISOC 传输方式下满足:

  • 同一个 SOF 传输周期内,只存在一个 Payload Header

  • 一个 SOF 传输周期内传输一个完整的 Payload

2.Payload header 协议规定的格式

UVC 协议规定 Payload header 必须具备以下格式:

Offset

Field

Size

Value

Description

0

bHeaderLength

1

Number

包括该域,Payload header 占用的字节数

1

bmHeaderInfo

1

Bitmap

提供采样的数据信息,也提供 Payload header 可选的域。

D0:Frame ID-对于基于帧的格式,每当新的视频帧开始时,该位在0和1间切换。 对于基于流的格式,在每个新的特定于编解码器的段的开头,该位在0和1之间切换。 此行为对于基于帧的有效载荷格式(例如DV)是必需的,对于基于流的有效载荷格式(例如MPEG-2 TS)是可选的。 对于基于流的格式,必须通过“视频探测和提交”控件的bmFramingInfo字段指示对此位的支持。

D1: End of Frame – 如果以下有效负载数据标记了当前视频或静止图像帧的结束(对于基于帧的格式),或者指示编解码器特定的段的结束(对于基于流的格式),则该位置1。 对于所有有效负载格式,此行为都是可选的。 对于基于流的格式,必须通过“视频探测和提交控件”的bmFramingInfo字段指示对此位的支持

D2: Presentation Time – 如果dwPresentationTime字段作为标头的一部分发送,则此位置1。

D3: Source Clock Reference – 如果dwSourceClock字段作为标头的一部分发送,则此位置1。

D4: 有效负载特定位。 请参阅各个有效负载规格以进行使用。

D5: Still Image – 如果随后的数据是静止图像帧的一部分,则设置此位,并且仅用于静止图像捕获的方法2和3。 对于时间编码格式,该位指示随后的数据是帧内编码帧的一部分。

D6: Error – 如果此有效载荷的视频或静止图像传输出错,则设置此位。 流错误代码控件将反映错误原因。

D7: End of header – 如果这是数据包中的最后一个报头组,则设置此位,其中报头组引用此字段以及由该字段中的位标识的任何可选字段(为将来的扩展定义)。

下面的格式则是可选的,是否选中由上面的 bmHeaderInfo 域决定:

可选结构的顺序由 bmHeaderInfo 中定义的顺序决定,使用小端字节序。由于头信息未来可能会拓展,因此各个域的 Offset 也是未确定的。UVC 设备会在 VS 的 "Paylaod Format Descriptors" 中决定是否支持这些域。

Offset

Field

Size

Value

Description

Variable

dwPresentationTime

4

Number

Presentation Time Stamp (PTS).

Variable

scrSourceClock

6

Number

由两部分组成的 Source Clock Reference (SCR) 值。

下面对上表中的 PTS 和 SCR 做展开描述:

  1. PTS:开始原始帧捕获时,以本机设备时钟为单位的源时钟时间。 对于包括单个视频帧的多个有效载荷传输,可以重复此字段,但要限制该值在整个视频帧中保持相同。PTS与视频探针控制相应的dwClockFrequency字段中指定的单位相同。

  2. SCR:

  • D31..D0:本机设备时钟单元中的源时间时钟

  • D42..D32:1KHz SOF令牌计数器

  • D47..D43:保留,设置为零。

最低有效的32位(D31..D0)包含从源处的系统时间时钟(STC)采样的时钟值。时钟分辨率应根据本规范表4-47中定义的设备的探测和提交响应的dwClockFrequency字段。该值应符合相关的流有效载荷规范。采样STC的时间必须与USB总线时钟相关联。

为此,SCR的下一个最高11位(D42..D32)包含一个1 KHz SOF计数器,表示在对STC进行采样时的帧号。在任意SOF边界对STC进行采样。SOF计数器的大小和频率与与USB SOF令牌关联的帧号相同。但是,不需要匹配当前的帧号。这允许使用可以触发SOF令牌(但无法准确获取帧号)的芯片组来实现,以保留其自己的帧计数器。

保留最高有效的5位(D47..D43),并且必须将其设置为零。

包含SCR值的有效负载报头之间的最大间隔为100ms或视频帧间隔,以较大者为准。 允许间隔更短。

3.USB Gadget UVC 功能对 Payload header 的配置

3.1.生成 Payload 的流程

UVC 图像帧缓冲区数据可以通过 ISOC 或者 BULK 传输类型,使用 USB Request 发送。

两种传输类型在生成 Payload 的流程上没有区别,我们以 ISOC 传输类型为例介绍. ISOC 使用uvc_video_encode_isoc函数将 UVC 帧缓冲区数据填充到 USB Request 的缓冲区,由于一个 USB Request 一次可以发送出去的数据也就是一个完整的 Payload。

static void uvc_video_encode_isoc(struct usb_request *req, struct uvc_video *video,
                struct uvc_buffer *buf)
{
...
        /* Add the header. */
        ret = uvc_video_encode_header(video, buf, mem, len);
        mem += ret;
        len -= ret;
        /* Process video data. */
        ret = uvc_video_encode_data(video, buf, mem, len);
...
}

从上面的代码可以看出 Payload header 使用uvc_video_encode_header()函数填充;Payload data 使用uvc_video_encode_data()填充。由于每个 USB Request 可以发出数据的长度是确定的,后者就是使用帧缓冲区数据填充剩余的字节数据而已。

我们主要看一下uvc_video_encode_header()函数,可以看出它的实现方法很简单,目前就是填充了缓冲区的前两个字节(因为没有选中 Payload header 的可选域)。

static int uvc_video_encode_header(struct uvc_video *video, struct uvc_buffer *buf, u8 *data, int len)
{
        data[0] = 2;
        data[1] = UVC_STREAM_EOH | video->fid;

        if (buf->bytesused - video->queue.buf_used <= len - 2)
                data[1] |= UVC_STREAM_EOF;

        return 2;
}

3.2. USB Host 查看抓取到的 Payload

由于通过 ISOC 发送 Payload 不涉及 USB 底层协议,我们可以通过 Wireshark 直接抓取到 ISOC 传输过程的数据。

3.2.1.双字节 Payload Header UVC设备抓包分析

Linux USB Gadget UVC 功能在创建 Payload Header 时默认使用双字节长度,即不包含 PTS 和 SCR域。下图是使用 Wireshark 对传输过程的抓包截图。

  • 该 UVC 配置的数据传输长度就为 3072 字节;

  • 根据前面的分析一次传输过程的数据即为一个完整的 Payload,则 Data 的前两个字节就是 Payload header.且此时值为 0x0280,符合uvc_video_encode_header()函数配置 Payload header的一种情况。

当然根据uvc_video_encode_header()函数的实现,也存在 Payload header 值为0x0281的情况。如下图所示:

根据前面 USB Gadget UVC 的代码分析,实际上data[1]bit0位表示的是帧ID。一个图像帧往往需要多个 Request 才能发送出去。如果当前帧ID为TRUE,则当该帧发送完成后,下一个帧ID就应当设置为FALSE。USB Host UVC 驱动可以根据帧ID获取帧的分界线。

3.2.2. 多字节 Payload Header UVC设备抓包分析

使用 wireshark 抓取支持多字节 Payload Header 的 UVC 数据包,从各个 URB ISO Data 字段的前12个字节就可以看出 Payload Header 的填充信息:

  • 第一个字节为0xc,表示 Payload Header 的总长度为12字节

  • 第二个字节为0x8d

    • 配置了EOF标志位

    • 配置了PTS标志位

    • 配置了SCR标志位

    • 当前帧ID配置为TRUE

根据前面的介绍,我们得知这三个 URB 包含的 Payload Header PTS 和 SRC 域值分别为:

无法复制加载中的内容

从上表中可以得知的信息是:

  • 这三个URB指示的同一个帧采集时刻标记为0x08594495

  • 三个URB存在不同的STC

  • 三个URB在同一个SOF内发送

3.3. 增加 PTS 域和 SCR 域

PTS 域 和 SCR 域用于视频类型的播放控制。如果 UVC 设备配置了这两个域,则 USB Host 会对这两个域进行解析。以 Linux 的 V4L2 框架为例,在用户空间获取帧缓冲区时就可以同时得到每个帧计算出的时间戳。

在前面我们已经提到了 USB Gadget UVC 功能中生成 Payload Header 的接口函数。我们提供了仅用于演示的 demo 级修改方法。它的主要流程如下:

  • 获取 PTS 的值并保存到 Payload Header 的特定位置。第一个图像帧从系统中获取,此后基本按照每帧增加33毫秒。数值需要以48MHz为分辨率。这里处理的不太好,如使用33毫秒左右时间间隔,实际上保持每秒30P需要使用33.33毫秒作为间隔;48MHz获取自UVC功能的配置项目,这里写死了;

  • 设置 STC 的值到 Payload Header 的特定位置。每个 Payload Header 都有不同的值;

  • 设置 SOF 值到 Payload Header 的特定位置;

  • 设置 Payload Header 的基本参数,如占用的字节长度和必须配置的比特位。

#define UVC_PAYLOAD_HEADER_CRS_SET_SOF(sof)     ((sof)&(0xFFFFFFFFU>>5))
#define UVC_PAYLOAD_HEADER_SET_UNIT(ns)         ((ns)*48/1000)
#include <asm/unaligned.h>
/* 函数用于生成 PTS,每帧增加 33ms,实际上不够精确 */
static u64 uvc_video_encode_header_getpts(void)
{
    static u64 timestamp = 0;
    u64 expect_timestamp = 0;
    struct timespec ts;
    ktime_get_ts(&ts);
    expect_timestamp = timespec_to_ns(&ts);
    if((expect_timestamp - timestamp > 40*1000000) || (expect_timestamp - timestamp < 20*1000000))
    {
        timestamp = expect_timestamp;
    }else
    {
        timestamp += 33*1000000;
    }
    return timestamp;
}

static int
uvc_video_encode_header(struct uvc_video *video, struct uvc_buffer *buf,
        u8 *data, int len)
{
    struct timespec ts;
    u64 timestamp = 0;
    u32 pts = 0;
    u32 stc = 0;
    u16 sof = 0;
    static u64 sof_count = 0;

    /* 设置 PTS 域到 header 中 */
    timestamp = uvc_video_encode_header_getpts();
    pts = UVC_PAYLOAD_HEADER_SET_UNIT(timestamp);
    put_unaligned_le32(pts, &data[2]);
    /* 设置 STC 值到 header 中 */
    ktime_get_ts(&ts);
    timestamp = timespec_to_ns(&ts); 
    stc = UVC_PAYLOAD_HEADER_SET_UNIT(timestamp);
    put_unaligned_le32(stc, &data[6]);
    /* 设置 SOF 值 */
    sof_count++;
    sof = UVC_PAYLOAD_HEADER_CRS_SET_SOF(sof_count);
    put_unaligned_le16(sof, &data[10]);
    /* Payload Header 结构 */
    data[0] = 12;
    data[1] = UVC_STREAM_EOH | video->fid | UVC_STREAM_SCR | UVC_STREAM_PTS;
    if (buf->bytesused - video->queue.buf_used <= len - 2)
    {
        data[1] |= UVC_STREAM_EOF;
        sof_count = 0;
    }
    return 12;
}

3.4.UVC 协议介绍相关内容介绍

UVC 协议文档介绍了很多与流同步、帧率控制等相关的内容,其中有些与 PTS 域和 SCR 域存在一些关联。我们根据 UVC 协议文档介绍一下这些部分。

3.4.1. 不支持 PTS 域和 SCR 域的情况

并不是所有的视频格式都适用 PTS 域和 SCR 域。比较容易理解的说法是基于流的视频格式,生成 Payload Header 时不应该使能这两个域:

原因大概是 H264 等流媒体编码中本身就已经包含了 PTS 的控制,也就不需要 UVC 协议提供这些支持。在网上有一些对 H264 Payload Header 的抓包分析,部分 UVC 设备的 PTS 和 SCR 域虽然存在,但是数值都保持为0。这实际上并不符合 UVC 协议,是否会影响 UVC 功能则由 USB Host 驱动决定,可能会影响设备的兼容性。

因此只有在基于帧的视频类型如 MJPG4 和 YUY2,PTS 和 SCR 域才可能会起作用。

3.4.2. 流同步与速率匹配

为了正确地同步来自一个媒体源的多路音频流和视频流,该媒体源必须向接收者提供当前流的延迟、时钟周期参考信息,以及一种为接收者提供一种方式用于决定正确的流采样展示时间

3.4.2.1. 延迟

该媒体流需要报告它内部的延迟。该延迟反映的每个流是由缓冲、压缩、解压缩或者处理操作引入的滞后。如果没有每个流的延迟信息,媒体接收者就不能正确地修正展示时间。

3.4.2.2. 时钟参考

时钟参考信息被媒体接收者用于实现时钟速率匹配。时钟匹配指的是媒体接收者渲染时钟和媒体源采样时钟的同步。如果没有时钟速率匹配,则流可能出现缓冲区 overrun 和 underrun 错误。由于执行音频样本速率转换相对容易,这与音频流没有问题。然而,执行视频采样率转换非常困难,因此需要一种速率控制方式。

3.4.2.3.展示时间(Presentation Time)

对固定速率的流来说,展示时间可以由数据流推导出来。对可变速率的流来说,每个采样必须陪同一个展示时间戳。媒体接收者负责将时间戳转换为本地的计量单位,并考虑源延迟。即使视频流可能以固定的速率到达媒体接收者,然如果他们受到可变速率压缩和编码的影响,他们便不被视为固定速率流,并且需要采样的时间戳。

4.Linux USB Host UVC 驱动如何处理 Payload header

4.1.通过FID区分不同的图像帧

下面的函数在 URB 包回调函数中被调用,主要用于处理 Payload Header 中的 FID 标志位、调用其它函数对 PTS、SCR 标志位进行解析。它的主要工作流程如下:

  • 获取 FID 标志位判断当前处理的URB和上一个URB是否属于同一个图像帧

  • 调用 PTS、SCR 解析函数

  • 如果 uvc_buf->state 不为 UVC_BUF_STATE_ACTIVE,则设置 timestamp

  • 保存当前 FID

static int uvc_video_decode_start(struct uvc_streaming *stream,
                struct uvc_buffer *buf, const __u8 *data, int len)
{
...
        fid = data[1] & UVC_STREAM_FID;
        if (stream->last_fid != fid) {
                stream->sequence++;
                if (stream->sequence)
                        uvc_video_stats_update(stream);
        }
        uvc_video_clock_decode(stream, buf, data, len);
        uvc_video_stats_decode(stream, data, len);
...
        if (buf->state != UVC_BUF_STATE_ACTIVE) {
                struct timespec ts;
...
                uvc_video_get_ts(&ts);
                buf->buf.field = V4L2_FIELD_NONE;
                buf->buf.sequence = stream->sequence;
                buf->buf.vb2_buf.timestamp = timespec_to_ns(&ts);
                /* TODO: Handle PTS and SCR. */
                buf->state = UVC_BUF_STATE_ACTIVE;
        }
...
        stream->last_fid = fid;
        return data[0];
}

4.2.处理EOF标志位

当一个帧发送完成后,UVC 设备还会发送一个在 Payload Header 中设置了 EOF 标志位的包。

uvc_video_decode_end()函数在 Payload 数据处理完成后被调用。

static void uvc_video_decode_end(struct uvc_streaming *stream,
                struct uvc_buffer *buf, const __u8 *data, int len)
{
        if (data[1] & UVC_STREAM_EOF && buf->bytesused != 0) {
                uvc_trace(UVC_TRACE_FRAME, "Frame complete (EOF found).\n");
...
        }
}

4.3.使用 PTS、SCR 更新帧缓冲区的 timestamp

uvc_video_clock_update()函数的计算过程比较复杂,我们只需要了解它可以根据 Payload Header 的 PTS、SCR 域计算出适合 USB Host 的图像帧 timestamp 即可。

void uvc_video_clock_update(struct uvc_streaming *stream,
                            struct vb2_v4l2_buffer *vbuf,
                            struct uvc_buffer *buf)
{
...
        /* Update the V4L2 buffer. */
        vbuf->vb2_buf.timestamp = timespec_to_ns(&ts);
...
}

该函数的计算原理为:(翻译自函数注释)

uvc_video_clock_update - 更新 buffer 时间戳

该函数通过 USB SOF 的时钟域将 buffer 的 PTS 时间戳转换成 Host 时钟域,并将结果保存到 V4L2 buffer 时间戳成员中。

Device 和 Host 时钟之间的关联是不清楚的。然而,Device 和 Host 共享通用的 USB SOF 时钟,它可以用于恢复 Device 和 Host 时钟间的关联。