在 Arduino Due 上获取 USB 网络摄像头视频流 - 第七部分:完成枚举过程






4.86/5 (10投票s)
视频流之前的最后一篇文章
引言
第6部分在这里。
到目前为止,我已掌握完成USB设备枚举所需的所有信息。对于所有USB设备,枚举过程包括设置设备地址、配置和接口备用设置。视频类设备包括一个额外的步骤,称为协商,在此过程中会进行几次控制传输:设置和获取视频探测控制,以及在设置备用设置之前提交控制。
本文还介绍了带和不带数据阶段的OUT控制传输。
设置地址
在此之前,所有传输都是IN传输,意味着从设备到主机。另一方面,设置地址传输是OUT传输:主机将地址发送到设备,设备使用它。此传输很有趣,因为它没有数据阶段,SETUP事务(SETUP阶段中唯一的事务)本身就包含地址号。
可以看出,没有数据阶段,因为SETUP阶段的DATA0包包含了Set Address传输所需的所有信息。设备在主机IN包之后必须在HANDSHAKE阶段以空DATA包响应。
启动SETUP事务的SETUP包并具有进入DATA0包的数据的函数如下所示
void USBD_SetAddressBegin()
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
ControlRequest.bRequest = USB_REQ_SET_ADDRESS;
ControlRequest.wValue = DeviceAddress;
ControlRequest.wIndex = 0;
ControlRequest.wLength = 0;
HCD_SendCtrlSetupPacket(0, USB_REQ_DIR_OUT, ControlRequest, NULL, 0, USBD_SetAddressEnd);
}
DeviceAddress
定义为0x05(您可以指定1到127之间的任何数字)。wIndex
和wLength
必须为零,因为没有数据阶段,HCD_SendCtrlSetupPacket
函数调用中的参数ptrBuffer
为NULL,TransferBufferSize
为零。
此类请求在[2, p.256]
中有描述。
此函数在显示完整配置后立即调用(请参阅上一篇文章)
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\nGETTIN FULL CONFIGURATION.\r\n");
USBD_GetConfigurationDescriptorBegin(TotalSize);
}
else
{
PrintStr("Full configuration has been obtained.\r\n");
USBD_ParseFullConfigurationDescriptor(Buffer, ByteReceived);
PrintStr("Setting device address.\r\n");
USBD_SetAddressBegin();
}
}
由于之前没有进行过OUT传输,中断处理程序将无法正确处理它,因此必须进行更改。
//...
//Start DATA stage
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_SETUP)
{
if(hcd_ControlStructure.Direction == USB_REQ_DIR_IN) //Now need to send IN packet
{
//Change to IN phase
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_DATA_IN;
HCD_SendCtrlInPacket();
return;
}
if(hcd_ControlStructure.Direction == USB_REQ_DIR_OUT)
{
if(hcd_ControlStructure.ByteCount == 0) //Empty data-stage
{
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_IN;
HCD_SendCtrlInPacket();
return;
}
}
}
//...
发送SETUP包后,“SETUP已发送中断”将我们带到HCD_HandleControlPipeInterrupt
函数中的这段代码。粗体是我为处理无数据阶段的OUT传输而添加的内容。如果传输是OUT,它会检查要发送的字节数量,如果为零(在本例中为真),它会指定下一个阶段是HANDSHAKE,并且必须发生空IN事务。HCD_SendCtrlInPacket
函数发送IN包,这反过来会启动HANDSHAKE阶段,此函数只发送IN令牌,并在第5篇文章中进行了描述。
在发送HANDSAHE阶段的IN令牌后,设备会响应一个空的DATA包,中断处理程序必须识别该条件。
//...
//Received IN Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXINI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC; //Ack interrupt
//Data stage: IN token has been send and DATA0/1 came in
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_IN)
{
HCD_GetCtrlInDataPacket(); //Getting data
return;
}
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_IN)
{
(*hcd_ControlStructure.TransferEnd)(0); //Notifies about completion
return;
}
return;
}
//...
如果处理程序从设备接收到IN包,并且阶段是ZLP_IN(零长度包IN),则表示HANDSHAKE阶段已完成,可以通知调用者传输完成。TransferEnd
函数指针在USBD_SetAddressBegin
中指定为USBD_SetAddressEnd
函数。
void USBD_SetAddressBegin()
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
ControlRequest.bRequest = USB_REQ_SET_ADDRESS;
ControlRequest.wValue = DeviceAddress;
ControlRequest.wIndex = 0;
ControlRequest.wLength = 0;
HCD_SendCtrlSetupPacket(0, USB_REQ_DIR_OUT, ControlRequest, NULL, 0, USBD_SetAddressEnd);
}
USBD_SetAddressEnd
打印关于地址设置完成的消息,并启动下一个配置步骤,即设置配置(参见Delay function
部分)。
延迟函数
设备地址设置后,必须给设备2毫秒的恢复间隔[2, p.246]
。在此间隔结束时,设备必须开始接受指向新分配地址的后续请求。
我将创建一个处理这种情况的函数
void HCD_StartDelayed(void (*TransferStart)(void), uint16_t Pause)
{
hcd_ControlStructure.TransferStart = TransferStart;
hcd_ControlStructure.Pause = Pause;
}
因此,下一个枚举操作(指定配置)将在地址设置结束后的2毫秒内开始。
void USBD_SetAddressEnd(uint16_t ByteReceived)
{
PrintStr("Device address has been set.\r\n");
PrintStr("Setting device configuration.\r\n");
HCD_StartDelayed(USBD_SetConfigurationBegin, 2);
}
注意:由于我使用Print
函数,因此此2毫秒的暂停不是必需的,因为通过RS232发送会产生比2毫秒更大的延迟。
设置配置
USB设备具有一个或多个配置,其中一个可以同时处于活动状态。我的设备只有一个配置(请参阅前面关于描述符的文章)。
要指定配置,其编号应作为参数传递。我从配置描述符中获取此编号(请参阅上一篇文章)。
所以参数是1。设置配置的传输与设置地址的传输类似——它们都是OUT传输,没有数据阶段,因此不需要更改USB中断处理程序。启动传输的函数
void USBD_SetConfigurationBegin()
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT | USB_REQ_TYPE_STANDARD | USB_REQ_RECIP_DEVICE;
ControlRequest.bRequest = USB_REQ_SET_CONFIGURATION;
ControlRequest.wValue = DeviceConfiguration;
ControlRequest.wIndex = 0;
ControlRequest.wLength = 0;
if(HCD_InitiatePipeZero(DeviceAddress))
{
HCD_SendCtrlSetupPacket(DeviceAddress, USB_REQ_DIR_OUT, ControlRequest,
NULL, 0, USBD_SetConfigurationEnd);
}
}
在我的情况下,DeviceConfiguration
定义为1。请注意bRequest
常量以及它与Set Address函数中使用的常量有何不同。地址已更改,必须重新初始化管道0。函数USBD_SetConfigurationEnd
将在Set Configuration传输完成后调用。
void USBD_SetConfigurationEnd(uint16_t ByteReceived)
{
PrintStr("Device configuration has been set.\r\n");
PrintStr("Setting video probe control.\r\n");
USBD_SetVideoProbeControlBegin();
}
此函数打印一些消息并开始下一个枚举步骤。
流协商
在启动流之前,必须协商其参数,例如格式、帧、有效负载大小等。主机提出所需的值,设备确认这些值或降低其无法支持的值。如果指定的值不正确,设备将返回正确的值。
UVC规范有专门的图片来描述该过程
因此,首先主机提出值 - PROBE_CONTROL(SET_CUR),然后它从设备读取这些值 - PROBE_CONTROL(GET_CUR),最后它设置(提交)这些值 - COMMIT_CONTROL(SET_CUR)。
SET_CUR和GET_CUR只是常量,其使用方式与bRequest
字段中的USB_REQ_SET_ADDRESS或USB_REQ_SET_CONFIGURATION相同。我在USB_Specification.h文件中定义了它们。
//Class-specific UVC requests
#define USB_REQ_SET_CUR 0x01
#define USB_REQ_GET_CUR 0x81
协商使用特殊结构[3, p.97]
在C语言中
typedef struct
{
uint16_t bmHint;
uint8_t bFormatIndex;
uint8_t bFrameIndex;
uint32_t dwFrameInterval;
uint16_t wKeyFrameRate;
uint16_t wPFrameRate;
uint16_t wCompQuality;
uint16_t wCompQualitySize;
uint16_t wDelay;
uint16_t Padding;
uint32_t dwMaxVideoFrameSize;
uint32_t dwMaxPayloadTransferSize;
//uint32_t dwClockFrequency;
//uint8_t bmFramingInfo;
//uint8_t bPreferedVersion;
//uint8_t bMinVersion;
//uint8_t bMaxVersion;
} uvc_video_probe_and_commit_controls_t;
注释掉的字段在UVC规范1.0版本中不存在,它们从1.1版本开始出现。从wKeyFramRate
到wCompQualitySize
的字段与我的摄像头不相关的压缩格式有关,因此这些字段将为零。
带数据阶段的OUT传输
协商过程中的第一个动作是将建议的值发送到设备。此传输包括将填充的uvc_video_probe_and_commit_controls_t
结构发送到设备的数据阶段。图形上,这种传输如下所示
由于之前没有发生过,HCD中断处理程序应进行更改,以正确处理带数据阶段的OUT事务。首先,在“SETUP发送中断”部分
//...
if(hcd_ControlStructure.Direction == USB_REQ_DIR_OUT)
{
if(hcd_ControlStructure.ByteCount == 0) //Empty data-stage
{
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_IN;
HCD_SendCtrlInPacket();
return;
}
else //Sending OUT-packet
{
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_DATA_OUT;
HCD_SendCtrlOutData();
}
}
//...
当字节计数不为零时,意味着OUT传输有数据要发送,并且需要数据阶段。指定传输阶段,并调用函数HCD_SendCtrlOutData
,该函数将提供数据并启动数据阶段。
void HCD_SendCtrlOutData(void)
{
PrintStr("Sending DATA0/1 to device.\r\n");
uint8_t ControlPipeSize = (UOTGHS->UOTGHS_HSTPIPCFG[0] & UOTGHS_HSTPIPCFG_PSIZE_Msk)
>> UOTGHS_HSTPIPCFG_PSIZE_Pos;
uint8_t volatile *ptrSendBuffer =
(uint8_t *)&(((volatile uint8_t(*)[0x8000])UOTGHS_RAM_ADDR)[0]);
if (hcd_ControlStructure.Index == hcd_ControlStructure.ByteCount)
{
//Everything has been sent, start HANDSHAKE stage by changing direction
hcd_ControlStructure.ControlRequestStage = USB_CTRL_REQ_STAGE_ZLP_IN;
HCD_SendCtrlInPacket();
return;
}
//Moving data to pipe 0 FIFO buffer
while ((hcd_ControlStructure.Index < hcd_ControlStructure.ByteCount) && ControlPipeSize)
{
*ptrSendBuffer++ = hcd_ControlStructure.ptrBuffer[hcd_ControlStructure.Index];
hcd_ControlStructure.Index++;
ControlPipeSize--;
}
//Start OUT transaction by sending OUT packet
HCD_SentCtrlOutPacket();
}
首先,它获取管道最大传输大小,以避免指定超出设备处理能力的数据。然后它获取指向应填充数据的管道FIFO缓冲区的指针。然后它检查是否所有数据都已发送,如果已发送,则启动IN包以启动HANDSHAKE阶段。如果有数据要发送,它只需逐字节复制到FIFO缓冲区,并检查是否已达到数据末尾或最大管道大小。一旦复制了一部分数据,OUT事务就会通过发送OUT包启动(SAM3X自动处理OUT事务的数据包发送和从设备获取ACK)。
此函数之后,可能还有两个后续操作:
1. 如果有更多数据要发送(阶段仍为DATA_OUT),则启动下一个OUT事务。
2. 捕获传输结束(此处指定HANDSHAKE阶段ZLP_IN),并通知调用方。
这两个动作都在控制端点中断处理方法HCD_HandleControlPipeInterrupt
中识别。一旦OUT事务完成,“OUT数据中断”触发,这会引导到函数中同名部分。
//...
//Received OUT Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_TXOUTI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_TXOUTIC; //Ack interrupt
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_OUT)
{
HCD_SendCtrlOutData(); //Sending more data
return;
}
//HANDSHAKE stage, ACK for empty OUT is received
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_OUT)
{
//Notifies about transaction completion
(*hcd_ControlStructure.TransferEnd)(hcd_ControlStructure.Index);
}
return;
}
//...
如果阶段保持为DATA_OUT,则会反复调用HCD_SendCtrlOutData
,直到没有数据要发送。
第二个动作在以下部分处理
//...
//Received IN Data interrupt
if(UOTGHS->UOTGHS_HSTPIPISR[0] & UOTGHS_HSTPIPISR_RXINI)
{
UOTGHS->UOTGHS_HSTPIPICR[0] = UOTGHS_HSTPIPICR_RXINIC; //Ack interrupt
//Data stage: IN token has been send and DATA0/1 came in
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_DATA_IN)
{
HCD_GetCtrlInData(); //Getting data
return;
}
if(hcd_ControlStructure.ControlRequestStage == USB_CTRL_REQ_STAGE_ZLP_IN)
{
(*hcd_ControlStructure.TransferEnd)(0); //Notifies about completion
return;
}
return;
}
//...
将调用TransferEnd
函数指针中指定的函数指针,以通知整个传输完成。
向设备建议参数 - PROBE_CONTROL(SET_CUR)
此时,所有低级(HCD)函数都已完成,我们可以继续进行枚举过程。这是上一篇文章中的一张图,其中包含必要的数字:
请注意,格式号只有1,最低帧号为5——160x120像素。最低速度替代设置号为1——每次事务128字节。
启动传输的函数
void USBD_SetVideoProbeControlBegin(void)
{
uvc_video_probe_and_commit_controls_t Parameters;
Parameters.bmHint = 0x0001;
Parameters.bFormatIndex = 1; //format #1
Parameters.bFrameIndex = 5; //160x120 frame size
Parameters.dwFrameInterval = 333333; //in 100ns units (from frame descriptor)
Parameters.wKeyFrameRate = 0; //unused
Parameters.wPFrameRate = 0; //unused
Parameters.wCompQuality = 0; //unused
Parameters.wCompQualitySize = 0; //unused
Parameters.wDelay = 0; //set by device
Parameters.dwMaxVideoFrameSize = 0; //set by device
Parameters.dwMaxPayloadTransferSize = 0; //set by device
USBD_CopyVideoProbeControl(Buffer, &Parameters);
PrintVideoProbeAndCommitControls(Buffer);
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT | USB_REQ_TYPE_CLASS | USB_REQ_RECIP_INTERFACE;
ControlRequest.bRequest = USB_REQ_SET_CUR;
ControlRequest.wValue = 0x0100; //Probe control selector (01)
ControlRequest.wIndex = 1; //Video Streaming interface number is 1
ControlRequest.wLength = 26; //Length of the returned block (parameter block)
HCD_SendCtrlSetupPacket(DeviceAddress, USB_REQ_DIR_OUT, ControlRequest,
Buffer, ControlRequest.wLength, USBD_SetVideoProbeControlEnd);
}
注意PrintVideoProbeAndCommitControls
函数,我在这里发送参数之前调用它,并且在从设备获取参数之后立即调用它。这是为了方便比较。wValue
高字节中的01表示探测控制,02表示提交控制。
我使用函数USBD_CopyVideoProbeControl
将Parameters
复制到缓冲区,我将解释为什么。我花了一些时间才明白我不能简单地这样转换Parameters
(uint8_t*)&Parameters
将其传递给HCD_SendCtrlSetupPacket
函数。原因是填充,成员在结构中对齐,有时会添加填充以填充到4字节。我将用图片说明这一点
因此,如您所见,该结构实际上占用了28(而不是26)字节,并且内部有填充(蓝色标记),因为在32位ARM处理器内存中,4字节字段dwMaxVideoFrameSize
不能分成两半。这就是为什么我不能简单地转换和传递此结构的变量,因为它插入了那2个无用的字节。因此,我创建了特殊函数来执行此复制操作
void USBD_CopyVideoProbeControl(uint8_t *ptrBuffer, uint8_t *ptrParameters)
{
for(uint8_t i = 0; i < 18; i++)
ptrBuffer[i] = ptrParameters[i];
for(uint8_t i = 18; i < 26; i++)
ptrBuffer[i] = ptrParameters[i+2];
}
此传输完成后将调用函数USBD_SetVideoProbeControlEnd
void USBD_SetVideoProbeControlEnd(uint16_t ByteReceived)
{
PrintStr("Proposed parameter values have been sent.");
PrintStr("\r\nGETTING VIDEO PROBE CONTROL.\r\n");
USBD_GetVideoProbeControlBegin();
}
它打印一些调试消息并启动下一个枚举步骤。
设备对建议参数的回答 - PROBE_CONTROL(GET_CUR)
以下函数启动从设备获取答案的传输
void USBD_GetVideoProbeControlBegin(void)
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_IN | USB_REQ_TYPE_CLASS | USB_REQ_RECIP_INTERFACE;
ControlRequest.bRequest = USB_REQ_GET_CUR;
ControlRequest.wValue = 0x0100; //Probe control selector (01)
ControlRequest.wIndex = 1; //Video Streaming interface number is 1
ControlRequest.wLength = 26; //Length of the returned block (parameter block)
HCD_SendCtrlSetupPacket(DeviceAddress, USB_REQ_DIR_IN, ControlRequest, Buffer,
ControlRequest.wLength, USBD_GetVideoProbeControlEnd);
}
bRequest
的值现在是GET_GUR,方向是IN。
USBD_GetVideoProbeControlEnd
获取答案,打印它并启动下一个枚举步骤
void USBD_GetVideoProbeControlEnd(uint16_t ByteReceived)
{
PrintStr("\r\nDEVICE ANSWER:\r\n");
PrintVideoProbeAndCommitControls(Buffer);
PrintStr("COMMITTING VIDEO PROBE CONTROL.\r\n");
USBD_SetVideoCommitControlBegin();
}
此和以前枚举步骤的程序输出
设备只更改了最后两个字段。帧大小可以很容易地检查,因为我已经知道YUY格式每像素有2字节,帧大小为160x120:160*120*2 = 38400字节。最大传输大小比我将要使用的要大得多。
设置流参数
一旦主机和设备协商并同意,就可以设置参数。
void USBD_SetVideoCommitControlBegin(void)
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT | USB_REQ_TYPE_CLASS | USB_REQ_RECIP_INTERFACE;
ControlRequest.bRequest = USB_REQ_SET_CUR;
ControlRequest.wValue = 0x0200; //Commit control selector (02)
ControlRequest.wIndex = 1; //Video Streaming interface number is 1
ControlRequest.wLength = 26; //Length of the returned block (parameter block)
HCD_SendCtrlSetupPacket(DeviceAddress, USB_REQ_DIR_OUT, ControlRequest,
Buffer, ControlRequest.wLength, USBD_SetVideoCommitControlEnd);
}
bRequest
再次是SET_CUR,但wValue
的高字节是0x02,表示COMMIT(而不是像以前的PROBE),Buffer
仍然保留上一步的响应。
此传输完成后将调用USBD_SetVideoCommitControlEnd
void USBD_SetVideoCommitControlEnd(uint16_t ByteReceived)
{
PrintStr("Video Commit Control has been set.\r\n");
PrintStr("SELECTING ALTERNATE SETTING.\r\n");
USBD_SetInterfaceAlternativeSettingBegin();
}
此函数打印调试消息并启动下一个枚举步骤。
启动流
在上一篇文章中我们看到,流接口描述符下没有端点,但在流接口的备用设置描述符下有。所以,最后要做的是选择具有适当端点数据大小的备用设置,正如我之前所说,我将从数据大小为126字节的最低#1开始。
void USBD_SetInterfaceAlternativeSettingBegin(void)
{
usb_setup_req_t ControlRequest;
ControlRequest.bmRequestType = USB_REQ_DIR_OUT |USB_REQ_TYPE_STANDARD |USB_REQ_RECIP_INTERFACE;
ControlRequest.bRequest = USB_REQ_SET_INTERFACE;
ControlRequest.wValue = 1; //Alternative setting #1: 128 bytes endpoint
ControlRequest.wIndex = 1; //Video Streaming interface number is #1
ControlRequest.wLength = 0; //No data
HCD_SendCtrlSetupPacket(DeviceAddress, USB_REQ_DIR_OUT, ControlRequest,
NULL, 0, USBD_SetInterfaceAlternateSettingEnd);
}
bRequest
是SET_INTERFACE,因为此请求是针对接口的。wValue
包含备用设置号,wIndex
是接口号——0是视频控制,1是视频流。此传输中没有数据阶段(wLength
为零)。
此传输完成后将调用USBD_SetInterfaceAlternateSettingEnd
void USBD_SetInterfaceAlternateSettingEnd(uint16_t ByteReceived)
{
PrintStr("Alternate setting has been set.\r\n");
PrintStr("PROCESSING THE STREAM.\r\n");
}
此处将启动流处理(在下一篇文章中)。
结论
我展示了如何枚举(初始化)USB网络摄像头。此处讨论的步骤适用于任何USB摄像头,并让您很好地理解要使用哪些命令(请求)、顺序以及如何应用从设备描述符中获取的参数。
在此阶段,流已准备就绪,摄像头正在生成视频,如果我开始向流端点发送IN数据包(在我的情况下为#2),我将获得带有视频数据的128字节DATAx数据包。视频格式(在我的情况下是未压缩的)规范有助于理解如何处理传入数据,因为并非所有数据都是视频数据,还有元数据有助于将视频帧的各个部分组合在一起。所有这些都将在下一篇文章中解释,我将尝试制作/上传一个视频来演示最终结果。我还将尝试以至少两种帧大小进行流式传输,因为Arduino Due的处理能力允许这样做。
源代码在此处。
第8部分在此处。