摘要:本文基于 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 做展开描述:
PTS:开始原始帧捕获时,以本机设备时钟为单位的源时钟时间。 对于包括单个视频帧的多个有效载荷传输,可以重复此字段,但要限制该值在整个视频帧中保持相同。PTS与视频探针控制相应的dwClockFrequency字段中指定的单位相同。
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 时钟间的关联。