在 Arduino Due 上获取 USB 网络摄像头视频流 - 第六部分:完整的 USB 设备配置






4.27/5 (7投票s)
构建设备图
引言
第 5 部分在此。
在上一篇文章中,我展示了如何从 USB 设备获取一些控制数据。在本文中,我将展示如何从中获取**所有**必要信息。这些信息足以决定视频流。本文将展示支持的帧大小、视频格式、带宽等以及设备图。
配置
上述信息驻留在 USB 设备配置中。在我的设备中,只有 1 个配置(如上一篇文章中所述),如果您的设备有多个配置,请将本文应用于您设备的任何配置。
配置由许多描述符组成,其中一个(第一个)是配置描述符。无法单独获取位于配置内部的单个描述符,但是可以读取指定大小的数据,我将在以后使用它。
获取配置大小
第一个任务是获取整个配置的总大小,否则我们可能会耗尽缓冲区大小。总大小在配置描述符 **[2, p.265]** 中指定,因此我可以读取直到(包括)总大小所在位置的字节
**wTotalLength** 字段包含所需信息,这意味着只读取四个字节就足够了。
因此,我将创建一个控制请求,请求指定的配置并只读取 4 个字节
void USBD_GetConfigurationDescriptorBegin(uint16_t TotalLength)
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_IN | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
ControlRequest.bRequest = USB_REQ_GET_DESCRIPTOR;
ControlRequest.wValue = (USB_DT_CONFIGURATION << 8) | 0; //getting configuration #0
ControlRequest.wIndex = 0;
ControlRequest.wLength = TotalLength;
HCD_SendCtrlSetupPacket(0, USB_REQ_DIR_IN, ControlRequest, Buffer,
ControlRequest.wLength, USBD_GetConfigurationDescriptorEnd);
}
请注意,我将 **TotalLenth** 作为参数传递,我将调用此函数两次:第一次获取总配置大小(仅读取前 4 个字节),第二次读取所有内容(传递第一次调用时收到的总大小)。
我读取配置 0,因为我的设备只有一个配置。如果您的设备有多个配置,您可以在此处读取其中任何一个。例如,如果标准设备描述符(参见上一篇文章)返回 **bNumConfigurations** 字段值为 3,则表示设备有配置 0、1 和 2。其中一个数字必须在上述函数中使用。
一旦控制传输完成,就会调用函数 **USBD_GetConfigurationDescriptorEnd**
void USBD_GetConfigurationDescriptorEnd(uint16_t ByteReceived)
{
uint16_t TotalSize;
if(4 == ByteReceived)
{
TotalSize = (Buffer[3] << 8);
TotalSize |= Buffer[2];
PrintStr("Configuration Total size is ");
PrintDEC(TotalSize);
PrintStr(".\r\n");
}
else
{
//...
}
}
它检查两种可能的情况
- 收到 4 个字节意味着它检索配置的总大小(注意它是如何从 BYTE 缓冲区中提取 WORD 值的);
- 否则 - 意味着它检索完整配置
此函数不完整,我稍后会回到它。现在我想运行上述代码以获取总配置大小并将其与我在上一篇文章中定义的 1024 字节缓冲区大小进行比较。要运行它,我将在收到并显示标准设备描述符后立即包含对 **USBD_GetConfigurationDescriptorBegin** 的调用
void USBD_GetDeviceDescriptorEnd(uint16_t ByteReceived)
{
PrintStr("Standard device descriptor size is ");
PrintDEC(ByteReceived);
PrintStr(".\r\n");
PrintDeviceDescriptor(Buffer);
PrintStr("\r\nGETTING FIRST 4 BYTES OF THE CONFIGURATION.\r\n");
USBD_GetConfigurationDescriptorBegin(4); //Getting total size of configuration
}
在我构建程序并上传到 Arduino Due 后,响应是
可以看出,总大小为 587 字节,它将很好地 fit 到 1024 字节缓冲区中。
获取完整配置
以前我进行控制传输时,数据阶段只有一个事务。这次将有 10 个事务(587 / 64 = 9.171),9 个完整事务和 1 个短事务。这样的控制传输如下所示
为了接收任意数量的事务,应在当前事务完成后发送 IN 包以启动下一个事务。这发生在函数 **HCD_GetCtrlInDataPacket** 中(参见上一篇文章)。我之前提到过我将完成它,现在是时候这样做了
void HCD_GetCtrlInDataPacket(void) { PrintStr("Getting DATA0/1 from device.\r\n"); uint32_t IsReceivedPacketShort = UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_SHORTPACKETI; uint8_t ByteReceived = UOTGHS->UOTGHS_HSTPIPISR[0] >> UOTGHS_HSTPIPISR_PBYCT_Pos; volatile uint8_t *ptrReceiveBuffer = (uint8_t *)&(((volatile uint8_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]); //Copying bytes over from FIFO buffer while(ByteReceived) { hcd_ControlStructure.ptrBuffer[hcd_ControlStructure.Index] = *ptrReceiveBuffer++; hcd_ControlStructure.Index ++; ByteReceived--; } //If following is true, must move to HANDSHAKE stage if((hcd_ControlStructure.Index == hcd_ControlStructure.ByteCount) || IsReceivedPacketShort) { hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_OUT; HCD_SentCtrlOutEmptyPacket(); return; } //Next In transaction should be initiated here. HCD_SendCtrlInPacket(); }
标记为粗体是完成多事务控制传输所需的唯一更改,它只是在存储上一个 DATA0/1 包数据后立即启动向设备发送新的 IN 包。它只有在不满足完成 DATA 阶段的条件时才调用 **HCD_SendCtrlInPacket**。
一旦底层(HCD)准备就绪,让我们更改顶层(USBD)
void USBD_GetConfigurationDescriptorEnd(uint16_t ByteReceived)
{
uint16_t TotalSize;
if(4 == ByteReceived)
{
TotalSize = (Buffer[3] << 8);
TotalSize |= Buffer[2];
PrintStr("Configuration Total size is ");
PrintDEC(TotalSize);
PrintStr(".\r\n");
PrintStr("\r\nGETTING FULL CONFIGURATION.\r\n");
USBD_GetConfigurationDescriptorBegin(TotalSize);
}
else
{
PrintStr("Full configuration has been obtained.\r\n");
}
}
第一次调用 **USBD_GetConfigurationDescriptorBegin**,参数值为 4,以获取总配置大小(只需要前 4 个字节 - 值本身在最后 2 个字节中),第二次调用 **USBD_GetConfigurationDescriptorBegin**,参数值为 587(在我的情况下),以获取完整配置。一旦我们获取到它,就会显示消息“_Full configuration has been obtained._”。
构建、上传,这是输出
程序输出中清晰可见所有 10 个事务。
以人类可读格式显示配置
迭代描述符
您可能会注意到描述符有几个共同点,它们遵循相同的结构:第一个字段是 **bLength**,第二个字段是 **bDescriptorType**。使用 bLength 我们可以将数据切片,使用 bDescriptorType 我们可以确定单个部分是什么。每个部分都是一个特定的描述符。描述符类型可以在 USB 文档中找到标准描述符,在 UVC 文档中找到视频类特定类型描述符。类特定描述符将有第三个字段,进一步澄清其类型(我稍后会讲到)。
迭代函数如下所示
void USBD_ParseFullConfigurationDescriptor(uint8_t *ptrBuffer, uint16_t BufferSize)
{
uint16_t CurrentPosition = 0;
PrintStr("\r\nPRINTING FULL CONFIGURATION.\r\n");
while(BufferSize - CurrentPosition)
{
PrintStr("------------------------------------\r\n");
PrintStr("Size:");
PrintDEC(ptrBuffer[CurrentPosition]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[CurrentPosition + 1], 1);
PrintStr("\r\n");
CurrentPosition += ptrBuffer[CurrentPosition];
}
}
输出是
我的相机返回 46 个描述符。第一个类型是 0x02,它是配置描述符,我在文章开头展示了它的结构。为了打印它,我将创建一个函数
void PrintConfigurationDescriptor(uint8_t* ptrBuffer)
{
uint16_t Temp16;
PrintStr("Configuration descriptor:\r\n");
PrintStr("bLength:\t\t\t");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("bDescriptorType:\t\t");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("wTotalLength:\t\t\t");
Temp16 = ptrBuffer[3] << 8;
Temp16 |= ptrBuffer[2];
PrintDEC(Temp16);
PrintStr("\r\n");
PrintStr("bNumInterfaces:\t\t");
PrintDEC(ptrBuffer[4]);
PrintStr("\r\n");
PrintStr("bConfigurationValue:\t");
PrintDEC(ptrBuffer[5]);
PrintStr("\r\n");
PrintStr("iConfiguration:\t\t");
PrintDEC(ptrBuffer[6]);
PrintStr("\r\n");
PrintStr("bmAttributes:\t\t\t");
PrintBIN8(ptrBuffer[7]);
PrintStr("\r\n");
PrintStr("bMaxPower:\t\t\t");
PrintDEC(ptrBuffer[8]);
PrintStr("\r\n");
}
配置解析函数更改为
void USBD_ParseFullConfigurationDescriptor(uint8_t *ptrBuffer, uint16_t BufferSize) { uint16_t CurrentPosition = 0; PrintStr("\r\nPRINTING FULL CONFIGURATION.\r\n"); while(BufferSize - CurrentPosition) { PrintStr("-------------------------------------\r\n"); switch(ptrBuffer[CurrentPosition + 1]) { case 0x02: PrintConfigurationDescriptor(ptrBuffer + CurrentPosition); break; default: PrintStr("Size:"); PrintDEC(ptrBuffer[CurrentPosition]); PrintStr("\r\n"); PrintStr("Type:"); PrintHEX(&ptrBuffer[CurrentPosition + 1], 1); PrintStr("\r\n"); break; } CurrentPosition += ptrBuffer[CurrentPosition]; } }
我添加了 switch 语句并将打印描述符函数调用放入 case 部分。这种模式将用于所有描述符。
**注意:** 我不会在本文中显示其他 PrintXXXDescriptor 函数代码,因为它与 PrintConfigurationDescriptor 函数中的代码类似——只是 PrintStr-PrintDEC-PrintHEX-PrintBIN 函数的组合。您始终可以在源代码中查看它们。
配置描述符
程序输出是
我们已经讨论了前三个字段。
**bNumInterfaces** 显示此配置有 4 个接口,每个接口都有自己的用途(我稍后会讲到)。这里另一个重要说明是端点属于一个接口,每个接口可以有零个或多个端点。
**bConfigurationValue** 是我将用来激活此配置的值。根据 USB 规范,设备中有一个或多个配置,但一次只能有一个激活。因此,此值作为激活配置的命令的参数传递。
**iConfiguration** 未使用
**bmAttributes** 在此情况下显示设备只能通过 USB 端口供电,而不是自供电(来自外部电源或电池)。
**bMaxPower** 以 2mA 为单位。因此,此设备的最大功耗为 128mA,这很好,因为我可以毫无问题地使用 Arduino Due 的电源。
标准接口关联描述符
下一个类型是 0x0B,它是标准接口关联描述符。正如我们所看到的,上一个描述符返回的信息是设备有 4 个接口。如果一个设备例如有视频和音频功能,如何知道哪些接口属于哪个功能?这个描述符回答了这个问题。它显示了属于特定功能(类)的第一个和最后一个接口号,然后是所有其描述符,直到遇到指示下一个功能(类)描述符部分开始的下一个标准接口关联描述符。
这是 USB 规范中的结构
解析函数 switch 语句的更改
//...
case 0x02:
PrintConfigurationDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x0B:
PrintInterfaceAssociationDescriptor(ptrBuffer + CurrentPosition);
break;
//...
程序输出是
接口关联描述符出现了两次:一次紧随配置描述符之后,一次接近末尾。这意味着我的设备有两个功能,其代码在 **bFunctionClass** 字段中指定。
第一个代码是 0x0E,即视频类(宾果!这就是我一直在寻找的),第二个是 0x01,即音频类。
第一个类(视频功能)有两个接口(bInterfaceCount = 2),第一个接口号是 0,第二个是 1。**bFunctionSubClass** 是 0x03,这意味着这是一个视频接口集合,**bFunctionProtocol** 必须是 0。
第二个类(音频功能)也有两个接口,但第一个接口号是 2,第二个是 3。
由于音频功能讨论不属于本系列文章的一部分,我将不展示其描述符。为此,一旦 **bFunctionClass** 出现 0x0E 以外的值,我将从解析函数返回。
//...
case 0x02:
PrintConfigurationDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x0B:
if(0x0E != ptrBuffer[CurrentPosition + 4])
return;
PrintInterfaceAssociationDescriptor(ptrBuffer + CurrentPosition);
break;
//...
这将描述符输出缩短为仅与视频相关的描述符。原来是 46 个,现在是 34 个,因此有 12 个描述符属于音频功能。
标准接口描述符
下一个类型是 0x04,它是标准接口描述符 **[2, p.268]**。端点位于接口内部,每个接口都有自己的作用。由于上一个描述符返回信息说视频功能中有接口号 0 和接口号 1,因此我们应该看到两个。这是 USB 规范中的结构
解析函数 switch 语句的更改
//...
case 0x02:
PrintConfigurationDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x04:
PrintInterfaceDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x0B:
if(0x0E != ptrBuffer[CurrentPosition + 4])
return;
PrintInterfaceAssociationDescriptor(ptrBuffer + CurrentPosition);
break;
//...
程序输出是
输出清晰地显示了视频功能的两个接口(角色):接口号 0 和接口号 1(字段 **bInterfaceNumber**)。
请注意字段 **bAlternateSetting**,此处为 0。0 表示它是接口,如果不是 0,则表示此接口的不同设置。一旦它输出端点描述符,将更好地理解这一点,我将再次明确讨论此主题。
接口号 0 有一个端点,接口号 1 没有端点(字段 **bNumEndpoints**),我稍后会解释为什么前者没有端点。
两个接口的 **bInterfaceClass** 都设置为 0x0E,这意味着它们属于视频类。
第一个接口扮演视频控制角色(字段 **bInterfaceSubClass** 设置为 0x01),第二个扮演视频流角色(0x02)。又一个宾果!!!
**bInterfaceProtocol** 必须设置为 0。
接下来需要注意的重要细节是,这两个接口描述符后面跟着的描述符类型都是 0x24,这意味着**视频类特定描述符类型**。到目前为止,我们只处理了标准描述符,从未处理过类特定描述符。它们也遵循相同的模式——第一个字段是长度,第二个是类型。但是类型只会告诉我们与视频类的关联,而不会告诉其他任何东西,因此类特定描述符有第三个通用字段——子类型。
更复杂的是,子类型仅在视频控制或视频流子类中是唯一的。例如,代码 0x01 在视频控制子类中表示一件事,而在视频流子类中则表示完全不同的事情,因此其解析应具有上下文敏感性。所有类型为 0x24 且位于视频控制接口描述符之后的描述符必须以一种方式解析,所有类型为 0x24 且位于视频流接口之后的描述符必须以另一种方式解析。为此,我将引入一个额外的变量 **Is_VS_Descriptors**,其值在代码到达视频流接口描述符之前为“false”。然后,如果代码找到类型 0x24,它将重定向到适当的解析函数
void USBD_ParseFullConfigurationDescriptor(uint8_t *ptrBuffer, uint16_t BufferSize)
{
uint16_t CurrentPosition = 0;
uint32_t Is_VS_Descriptors = 0;
PrintStr("\r\nPRINTING FULL CONFIGURATION.\r\n");
while(BufferSize - CurrentPosition)
{
PrintStr("-------------------------------------\r\n");
switch(ptrBuffer[CurrentPosition + 1])
{
case 0x02:
PrintConfigurationDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x04:
PrintInterfaceDescriptor(ptrBuffer + CurrentPosition, &Is_VS_Descriptors);
break;
//...
PrintInterfaceDescriptor 更改为
void PrintInterfaceDescriptor(uint8_t* ptrBuffer, uint32_t* Is_VS_Descriptors)
{
//Set if SubClass is Video Streaming (0x02)
if(0x02 == ptrBuffer[6])
*Is_VS_Descriptors = 1;
//...
接口替代设置
标准接口描述符不仅描述自身,还描述接口替代设置。到目前为止运行的代码不仅返回我之前展示的两个接口描述符,还返回许多替代设置描述符。唯一的区别是 **bAlternateSetting** 不为零。通常,端点会跟随接口替代设置描述符,描述特定替代设置与其他设置具体有何不同。
这里显示了视频流接口的最后三个替代设置(它们在程序输出的最后)
视频控制描述符解析
为了解析视频控制描述符,我将添加一个独立的函数,其中包含自己的 switch 语句,并从主解析函数的 switch 语句中调用它。
//...
case 0x24: //Class-Specific interface
if(!Is_VS_Descriptors) //Video Control (VC) descriptors
USBD_ParseVCDescriptors(ptrBuffer + CurrentPosition)
break;
//...
和
void USBD_ParseVCDescriptors(uint8_t* ptrBuffer)
{
switch(ptrBuffer[2])
{
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
}
}
程序输出是
VC 子类部分有子类型:0x01、0x02、0x03 和 0x06。
这些描述符的名称是:VC 头、VC 输入终端、VC 输出终端、VC 扩展单元。
VC 头描述符
除其他信息外,此描述符显示哪些视频流接口属于此视频控制接口。正如本文已知的那样,我的相机只有一个视频流接口,因此这里应该提到一个 VS 接口号。根据 UVC 规范 **[3, p.49]**,描述符结构为
VC 解析函数的更改
//...
switch(ptrBuffer[2])
{
case 0x01:
PrintVCHeaderDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
}
//...
程序输出是
首先要看到的是,该设备的视频功能符合 1.0 版本(字段 **bcdUVC**)。很难找到它,最后我在一些 Linux 论坛上找到了该规范版本,人们在那里讨论驱动程序编写主题。我将其与 USB 网站上容易下载的 1.1 版本进行了比较,发现差异确实很少,我会在需要时说明。无论如何,我在第一篇文章中上传了 UVC 规范 1.0 作为参考。
只有一个视频流接口属于此视频控制接口(字段 **bInCollection**),并且提到的视频流接口编号为 1(字段 **baInterfaceNr**)。这与之前在 **标准接口描述符** 部分中发现的完全一致。
VC 输入终端描述符
下一个子类型是 0x02,即 VC 输入终端描述符 **[3, p52]**。在这里,它表示相机传感器单元,此描述符提供有关各种视频传感器控制(如焦点、曝光等)支持的信息。此外,由于它是相机物理单元的描述符,它具有唯一的 ID,所有单元描述符都具有它,并且还具有它们连接到的单元的 ID。我将使用此信息来构建设备图。
根据 UVC 规范的描述符结构
VC 解析函数的更改
//...
case 0x01:
PrintVCHeaderDescriptor(ptrBuffer);
break;
case 0x02:
PrintVCInputTerminalDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
唯一 ID 为 1(字段 **bTerminalID**),此单元的类型为 0x0201(字段 **wTerminalType**),即“摄像头传感器” **[3, 附录 B]**。它未连接到其他单元(**bAssocTerminal** 为 0),这是因为传感器是设备中的第一个单元,其他单元将连接到它。我的摄像头不支持光学变焦,因此所有三个 xxxFocalxxx 字段都为零。控制占用三个字节(字段 **bControlSize**):自动曝光模式和曝光时间 - 位 1 和 3(非常便宜的摄像头!)。
VC 处理单元描述符
下一个子类型是 0x05,即 VC 处理单元描述符 **[3, p.55]**。此描述符公开了更多与图像校正和数字乘法器相关的控件。
根据 UVC 规范的描述符结构
**注意**:如果您的相机符合 1.1 版本,则在 **iProcessing** 字段之后会有一个额外的位图(1 字节)字段 **bmVideoStandards**。
VC 解析函数的更改
//...
case 0x02:
PrintVCInputTerminalDescriptor(ptrBuffer);
break;
case 0x05:
PrintVCProcessingUnitDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
唯一 ID 为 2(字段 **bUnitId**),它连接到摄像头传感器(字段 **bSourceID** 为 1),没有数字乘法器(字段 **wMaxMultiplier**),控制占用 2 个字节(字段 **bControlSize**),并具有对比度、背光补偿、增益等。
VC 输出终端描述符
下一个子类型是 0x03,即 VC 输出终端描述符 **[3, p.51]**。顾名思义,它应该告诉我们有关设备输出的信息,在我们的案例中是有关 USB 输出的信息。
根据 UVC 规范的描述符结构
VC 解析函数的更改
//...
case 0x02:
PrintVCInputTerminalDescriptor(ptrBuffer);
break;
case 0x03:
PrintVCOutputTerminalDescriptor(ptrBuffer);
break;
case 0x05:
PrintVCProcessingUnitDescriptor(ptrBuffer);
break;
//...
程序输出是
唯一 ID 为 3(字段 **bTerminalID**),终端类型为 0x0101(字段 **wTerminalType**),即“处理通过视频流接口中的端点传输的信号的终端” **[3, p.117]**。输出终端连接到处理单元(字段 **bSourceID**)。
VC 扩展单元描述符
下一个子类型(也是 VC 描述符的最后一个)是 0x06,即 VC 扩展单元描述符 **[3, p.56]**。扩展单元描述符向计算机上的通用 USB 驱动程序提供有关使用哪个供应商特定驱动程序来处理设备的信息。要理解此描述符中的信息,必须对特定相机有深入的了解(例如,成为开发团队的一员)。无论如何,我都会打印它以展示供应商特定信息的提供方式,这可能对某些人来说很有趣。
根据 UVC 规范的描述符结构
VC 解析函数的更改
//...
case 0x05:
PrintVCProcessingUnitDescriptor(ptrBuffer);
break;
case 0x06:
PrintVCExtensionUnitDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
唯一 ID 为 4(字段 **bUnitID**),GUID 在字段 **guidExtensionCode** 中,用于识别供应商的驱动程序,它有 8 个由供应商驱动程序控制的控件(字段 **bNumControls**),连接到 ID 为 1 的单元,该单元是图像传感器(输入终端)- 字段 **bNrInPins** 和 **baSourceID**。此设备使用的控件(由供应商驱动程序已知)标记为 1,但这些控件是什么只有供应商驱动程序知道,因为此类信息未在描述符中披露。
请注意,GUID 在此以其存储方式显示——仅仅是一串字节。要在操作系统中看到它,您需要找到在线 GUID 转换器,并将字符串 5DC717A91941DA11AE0E000D56AC7B4C(已删除 0x 和空格)粘贴到其中,然后获得 {A917C75D-4119-11DA-AE0E-000D56AC7B4C}。
标准端点描述符
下一个描述符是类型为 0x05 的标准描述符,即标准端点描述符 **[2, p.269]**。它为我们提供了创建通信管道所需的信息,例如端点地址、类型、方向、最大传输大小和间隔。在上面的屏幕截图中,视频控制 (VC) 部分中有一个端点,视频流 (VS) 部分的每个备用设置下也有一个端点(参见**接口备用设置**部分)。因此,VC 中的端点用于更改焦点、曝光、倍增、对比度等控件,而 VS 部分中的一个端点用于流式传输。您会发现它们(VS 部分中的端点)除了最大传输大小和微帧中的事务数量(这就是带宽的控制方式)外,具有相同的参数。默认的 VS 接口没有端点,这意味着设备初始化后没有流式传输,只有在选择其中一个备用设置后(为此有一个特殊的标准命令),才会以所选备用设置端点的参数开始流式传输。
根据 USB 规范的描述符结构
主解析函数的变化
//...
case 0x04:
PrintInterfaceDescriptor(ptrBuffer + CurrentPosition, &Is_VS_Descriptors);
break;
case 0x05:
PrintEndpointDescriptor(ptrBuffer + CurrentPosition);
break;
case 0x0B:
if(0x0E != ptrBuffer[CurrentPosition + 4])
return;
PrintInterfaceAssociationDescriptor(ptrBuffer + CurrentPosition);
break;
//...
程序输出是
我的摄像头在视频控制部分有一个端点,在视频流部分有七个端点(尽管屏幕截图中只显示了三个,以免过长)。
视频控制端点是中断端点(位 1..0 = 11,字段 **bmAttributes**),方向是 IN(位 7 = 1,字段 **bEndpoitAddress**),端点号是 1(位 0...3,字段 **bEndpoitAddress**),最大传输大小是 10(位 10...0 = 1010,字段 **wMaxPacketSize**)。此端点必须每 2^(5-1)=16 个微帧轮询一次(字段 **bInterval**)。
根据视频类规范 **[3, p.39]**,此类端点用于报告某些操作(如更改焦点、亮度、对比度等)的完成情况,如果该操作需要超过 10 毫秒才能进行。我不会在本文中使用它。
更有趣的是那些属于视频流部分的端点。它们每个都位于接口备用设置描述符下,这意味着激活备用设置将应用紧随其后的端点的值。
让我们看看流端点有什么相似和不同之处。相似之处在于:它们都是等时(输出是周期性的——流的另一个词),它们都是 IN(从设备到主机),它们都有编号(地址)——2。不同之处在于:最大数据包大小为 128、512 和 1024 字节(在屏幕截图中),但也有一些 768 字节的未显示。另一个是在微帧中附加事务的数量,在屏幕截图中只有零值(**wMaxPacketSize** 字段的位 12..11),这意味着每微帧 1 个事务,但我的摄像头也有两个设置,每微帧 2 个事务,大小分别为 768 和 1024 字节,还有两个设置,每微帧 3 个事务,数据包大小分别为 768 和 1024 字节。
现在你可以很容易地看到带宽如何根据主机的能力而改变。由于本文是关于 Arduino Due 的,所以我会首先尝试使用最低的设置,然后逐步提高,直到它无法再处理流。我不会使用每微帧超过 1 个事务的设置,因为这对于 Arduino Due 来说太快了。这给我留下了三个可供使用的替代设置:1、2 和 3(所有设置都只有每帧一个事务)。
类特定中断端点描述符
下一个描述符,类型为 0x25,是类特定中断端点描述符 **[3, p.58]**。此描述符位于 VC 标准端点描述符下方,仅描述一件事——中断报告的最大存储大小。之所以需要它,是因为标准端点描述符只给出最大传输大小,但如果发送的结构大于最大传输大小,则必须执行多个事务,并将整个结构保存在具有所需大小的主机缓冲区中。此大小由描述符给出。
根据 UVC 规范的描述符结构
主解析函数的变化
//...
case 0x24: //Class-Specific interface
if(!Is_VS_Descriptors) //Video Control (VC) descriptors
USBD_ParseVCDescriptors(ptrBuffer + CurrentPosition);
break;
case 0x25:
PrintClassSpecificInterruptEndpointDescriptor(ptrBuffer + CurrentPosition);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[CurrentPosition]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[CurrentPosition + 1], 1);
PrintStr("\r\n");
break;
//...
程序输出是
可以看出,主机需要提供 10 字节的缓冲区来存储此中断端点的数据。
视频流描述符解析
最后一个未解析的部分是视频流或 VS 描述符。我将以与视频控制描述符相同的方式处理它——在它自己的解析函数中使用 case 语句
void USBD_ParseVSDescriptors(uint8_t* ptrBuffer)
{
switch(ptrBuffer[2])
{
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
}
}
它从主解析函数中调用
//... case 0x24: //Class-Specific interface if(!Is_VS_Descriptors) //Video Control (VC) descriptors USBD_ParseVCDescriptors(ptrBuffer + CurrentPosition); else //Video Streaming (VS) descriptors USBD_ParseVSDescriptors(ptrBuffer + CurrentPosition); break; //...
程序输出是
有子类型为 0x01、0x03、0x04、0x05 和 0x0D 的 VS 描述符,它们分别是 VS 输入头、VS 静态图像帧、VS 未压缩格式、VS 未压缩帧和 VS 颜色格式描述符。理解这些描述符中的信息,我们可以获得视频格式和帧分辨率等信息。
VS 输入头描述符
VS 输入头描述符提供有关视频功能和格式的总体信息 **[3, p.60]**。根据 UVC 标准,其结构为
VS 解析函数的更改
//...
case 0x01:
PrintVSInputHeaderDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
我的相机只有一个格式(字段 **bNumFormats**)。
VS 部分的大小为 223 字节(字段 **wTotalLength**)。
流端点号为 2(字段 **bEndpointAddress** 的位 0..1),它是 IN 端点(字段 **bEndpointAddress** 的位 7 为 1)。
我的相机不支持动态格式更改(字段 **bmInfo** 的位 0),这是合乎逻辑的,因为只有一个可用的格式。
流端点连接到 ID 为 3 的输出终端(字段 **bTerminalLink**)——参见前面在 VC 部分中显示的 VC 输出终端描述符。
相机有 3 种拍照方式。我的相机使用方法 2(字段 **bStillCaptureMethod**)。由于这不属于本文的范围,您可以在此处阅读更多信息 **[3, p.15]**。同样适用于接下来的两个字段:**bTreggerSupport** 和 **bTriggerUsage**。
要启动流,应向设备发送特殊结构。此结构填充了一些信息,**bmControls(x)** 定义了支持哪些字段。位 0 为 1,表示应将 **wKeyFrameRate** 字段发送到设备(当讨论发送该结构时会更清楚)。
VS 未压缩格式描述符
下一个子类型是 0x04,即 VS 未压缩格式描述符。其他格式可以是:MJPEG、MPEG-1 SS、MPEG-1 PS、H.264 等。我的相机格式碰巧是未压缩的,这很好,因为如果我有其中一种压缩格式,由于 Arduino Due 缺乏处理此类操作的能力,我将无法解压缩流。甚至没有足够的电源来显示彩色未压缩流,只能显示灰度。我想,如果输出是 RGB,我的 320x240 显示器需要它,那么就可以流式传输彩色,但相机提供 YCbCr。要将 YCbCr 转换为 RGB,需要对每个颜色分量执行一些小的(但对 Arduino Due 实时操作至关重要)计算,因此我将只流式传输 Y 分量,因为它代表亮度并提供灰度视频。
无论如何,在此阶段只知道格式是未压缩的。如何未压缩仍需从该描述符中理解。
根据未压缩负载规范 **[4, p.4]**,描述符结构为
VS 解析函数的更改
//...
case 0x01:
PrintVSInputHeaderDescriptor(ptrBuffer);
break;
case 0x04:
PrintVSUncompressedVideoFormatDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
此格式描述符的索引是 1(字段 **bFormatIndex**)。在我的相机中,只有一个格式描述符,因为它只能以一种格式进行流传输。
它可以以五种不同的分辨率进行流式传输(字段 **bNumFrameDescriptors** 为 5),我可以看到 5 个子类型为 0x05 的描述符跟随此描述符——它们将定义帧分辨率。
如果我取 GUID 5955593200001000800000AA00389B71(字段 **guidFormat**)并将其插入在线 guid 转换器,我将得到:{32595559-0000-0010-8000-00AA00389B71}。在 Google 上搜索它会给我格式 - **YUY2**。只有在这个时候才知道如何处理流数据**!!!**
此格式使用 16 位表示一个像素(字段 **bBitsPerPixel**)。您可以搜索 YUY2 格式并查看是否属实。
由于 YUY2 不是隔行扫描格式,所有相关字段都为零。此外,此流的复制不受限制(字段 **bCopyProtect**)——无论这意味着什么。
VS 未压缩帧描述符
下一个子类型是 0x05,它是 VS 未压缩帧描述符。每种格式都有几种支持的帧大小,这些帧大小通过紧随 VS 格式描述符之后的 VS 帧描述符列出。
根据未压缩有效载荷规范 **[4, p.6]**,描述符结构为
VS 解析函数的更改
//...
case 0x04:
PrintVSUncompressedVideoFormatDescriptor(ptrBuffer);
break;
case 0x05:
PrintVSUncompressedVideoFrameDescriptor(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
帧大小(仅显示 5 个中的 3 个)可以很容易地看到:160x120、176x144、320x240、(此处未显示)352x288 和 640x480。我的 TFT 屏蔽屏幕分辨率为 320x240,因此如果能以帧 #3 获取流就太好了。此处还显示了存储整个帧的缓冲区,并且可以轻松计算。例如,对于 160x120 分辨率:160*120 = 19200 像素,每个像素在 YUY2 中用 2 字节编码:19200 * 2 = 38400,您可以在字段 **dwMaxVideoFrameBufferSize** 中看到。**bFrameIndex** 中的值将用于指定流分辨率。此处指定的间隔均以 100ns 为单位,例如 333333 意味着 333333*100 = 33333300ns = 0.033s,即每秒 30 帧。
VS 静态图像帧描述符
下一个子类型是 0x03,它是 VS 静态图像帧描述符 **[3, p.64]**。它定义了相机如何拍照,我不会这样做,我只是展示它以完成阅读所有描述符,以便让本文的读者对其他内容有一个总体了解。
根据 UVC 标准,其结构为
VS 解析函数的更改
//...
case 0x01:
PrintVSInputHeaderDescriptor(ptrBuffer);
break;
case 0x03:
PrintVSStillImageFrameDescriptor(ptrBuffer);
break;
case 0x04:
PrintVSUncompressedVideoFormatDescriptor(ptrBuffer);
break;
//...
程序输出是
可以看出,有 5 种照片尺寸与上一个描述符中显示的帧尺寸相对应。我的相机提供未压缩流,因此 **bNumCompressionPatterns** 为零。
VS 颜色匹配描述符
下一个也是最后一个未显示的描述符子类型是 0x0D,它是 VS 颜色匹配描述符 **[3, p.65]**。它描述了颜色配置文件,我也不会使用它。
根据 UVC 标准,其结构为
VS 解析函数的更改
//...
case 0x05:
PrintVSUncompressedVideoFrameDescriptor(ptrBuffer);
break;
case 0x0D:
PrintVSColorMatchingDescriptors(ptrBuffer);
break;
default:
PrintStr("Size:");
PrintDEC(ptrBuffer[0]);
PrintStr("\r\n");
PrintStr("Type:");
PrintHEX(&ptrBuffer[1], 1);
PrintStr("\r\n");
PrintStr("SubType:");
PrintHEX(&ptrBuffer[2], 1);
PrintStr("\r\n");
break;
//...
程序输出是
所有值都为零,这意味着“未指定”,因此它根本没有提供太多信息,并且根据 UVC 规范,此描述符是可选的。
总结所获得的信息
在获取所有必要信息后,是时候将其组合成某种可视化表示,以便可以轻松地用于完成 USB 设备枚举和视频流启动
结论
此时,一切都已准备就绪,可以完成 USB 摄像头枚举过程并启动视频流。
源代码在此。
第 7 部分在此。