在 Arduino Due 上获取 USB 网络摄像头视频流 - 第八部分:流式传输





5.00/5 (9投票s)
理解流式数据
引言
第 7 部分 在这里。
这是最后一篇文章。在这里,我将尝试解释我自己用来理解流式传输的一种方法。我将记录关键的流数据,以便了解它在一段时间内的变化情况,从而可以决定如何处理流数据。
流初始化及一些思考
到目前为止,管道 #0 已初始化并仅用于与设备的端点零进行控制传输。如前几篇文章所述,流式端点的编号是 2,并且是等时(周期性)端点。等时传输只有一个事务,它由一个 IN 或 OUT 数据包后跟一个 DATA0 数据包组成,没有像控制传输那样的确认或阶段,因此更简单。
如果数据包丢失,它就丢失了,不会提供恢复或信号。这对于视频或音频流来说足够了——你不能中断正在进行的实时内容来“恢复”丢失的数据。
为了与设备的端点编号二进行通信,我将使用 SAM3X 的管道 #1。初始化与我为控制管道 #0 所做的类似,有几个不同之处
uint32_t HCD_InitiateIsochronousINPipeOne(uint8_t Address, uint8_t EndpointNumber)
{
//Enables a pipe
UOTGHS->UOTGHS_HSTPIP |= UOTGHS_HSTPIP_PEN1;
//Double-bank pipe
UOTGHS->UOTGHS_HSTPIPCFG[1] |= (UOTGHS_HSTPIPCFG_PBK_3_BANK & UOTGHS_HSTPIPCFG_PBK_Msk);
//Pipe size is formatted: 7 is 1024 bytes
UOTGHS->UOTGHS_HSTPIPCFG[1] |= ((7 << UOTGHS_HSTPIPCFG_PSIZE_Pos) & UOTGHS_HSTPIPCFG_PSIZE_Msk);
//Pipe token
UOTGHS->UOTGHS_HSTPIPCFG[1] |= (UOTGHS_HSTPIPCFG_PTOKEN_IN & UOTGHS_HSTPIPCFG_PTOKEN_Msk);
//Pipe type is ISOCHRONOUS
UOTGHS->UOTGHS_HSTPIPCFG[1] |= (UOTGHS_HSTPIPCFG_PTYPE_ISO & UOTGHS_HSTPIPCFG_PTYPE_Msk);
//Pipe endpoint number
UOTGHS->UOTGHS_HSTPIPCFG[1] |= (((EndpointNumber) << UOTGHS_HSTPIPCFG_PEPNUM_Pos)
& UOTGHS_HSTPIPCFG_PEPNUM_Msk);
//Allocates memory
UOTGHS->UOTGHS_HSTPIPCFG[1] |= UOTGHS_HSTPIPCFG_ALLOC;
//Check if pipe was allocated OK
if(0 == (UOTGHS->UOTGHS_HSTPIPISR[1] & UOTGHS_HSTPIPISR_CFGOK))
{
//Disables pipe and prints debug message about failure
UOTGHS->UOTGHS_HSTPIP &= ~UOTGHS_HSTPIP_PEN1;
PrintStr("Pipe 1 was not allocated.\r\n");
return 0;
}
//Address of this pipe
UOTGHS->UOTGHS_HSTADDR1 = (UOTGHS->UOTGHS_HSTADDR1 & ~UOTGHS_HSTADDR1_HSTADDRP1_Msk)
| UOTGHS_HSTADDR1_HSTADDRP1(Address);
UOTGHS->UOTGHS_HSTPIPIER[1] = UOTGHS_HSTPIPIER_UNDERFIES; //Underflow interrupt enable
UOTGHS->UOTGHS_HSTIER = UOTGHS_HSTIER_PEP_1; //Enables pipe interrupt
//Debug message about successful pipe initiation
PrintStr("Pipe 1 has been allocated successfully.\r\n");
return 1;
}
第 5 篇文章描述了管道初始化过程。
请注意,对于管道 #1,区别在于:管道是等时的(不是控制的),令牌总是 IN,因为数据将从设备传输到主机;使用了三个数据缓冲区(实际上使用 2 个缓冲区结果相同),而控制传输只使用一个;启用了欠载中断,当主机无法足够快地读取数据缓冲区时(比数据传入更快)会触发此中断;管道大小为 1024 字节——我将其设置为最大值,因为我从之前的文章中知道该摄像头具有该大小的端点备用设置。
为什么要使用三个或两个缓冲区?因为在等时传输期间数据会快速传入。一旦第一个缓冲区被填满数据,程序就可以在读取它的同时,第二个缓冲区正在被填满。然后发生缓冲区切换——程序读取缓冲区 2,同时填满缓冲区 1,依此类推。
思考
SAM3X 无论是否设置了 AUTO_SWITCH 都会切换缓冲区。即使从设备返回的数据大小为 0,它也会切换到下一个缓冲区,尽管当前缓冲区是空的,而且我找不到停止它的方法。
其结果是,在切换到下一个缓冲区之前,主机必须处理当前缓冲区的数据——125 微秒(USB 总线微帧之间的间隔)。
如果端点大小为 128 字节,则有足够的时间直接输出到 TFT 显示器,但如果端点大小为 512 或 1024 字节——由于 TFT 模块的引脚连接到 Arduino Due 板的连接分散(如果模块连接方式不同——所有数据引脚连接到一个端口,而不是像现在这样连接到三个不同的端口——它可能还有足够的时间),输出操作会花费太长时间(参见文章 3)。所以,我可以使用 128 字节的事务并将数据直接输出到 TFT 显示器,或者将其缓冲起来,然后一次性输出整个视频帧。
稍后我将展示 128 字节的数据传输不足以及时传输整个帧,因此唯一的方法是将数据存储到缓冲区中,因为此操作发生在 RAM 中,因此非常快。
下一个问题是缓冲区大小。SAM3X 具有 64 + 32 kB 的 RAM。由于我将输出灰度视频,我只需要存储每个像素的两个字节中的一个,这为前三种帧尺寸提供了 160x120=19200、176x144=25344 或 320x240=76800 字节(没有理由尝试输出 640x480 等更高的帧尺寸,因为 TFT 屏幕只有 320x240)。19200 和 25344 字节的缓冲区工作正常,但当我分配 76800 字节时,它构建得很好,但在上传到 Arduino Due 后——它变成了一个砖块,什么都不工作,通过 RS232 端口没有消息,什么都没有。我还尝试注释掉所有 Print
函数,因为这些打印的字符串可以加载到 RAM 中并占用急需的空间——但这仍然没有帮助,所以我怀疑 C 库的隐藏代码或其他我不知道的原因。
经过多次尝试和失败,我决定仅停留在 160x120 和 176x144 的屏幕尺寸上。这足以学习 USB 流式传输,并且如果拥有更强大的 ARM 处理器,任务就可以成功扩展到处理更大的屏幕尺寸。
回到流初始化
在前一篇文章中,我以函数 USBD_SetInterfaceAlternateSettingEnd
结束,并说流式处理从这里开始,让我更新它
void USBD_SetInterfaceAlternateSettingEnd(uint16_t ByteReceived)
{
PrintStr("Alternate setting has been set.\r\n");
if(HCD_InitiateIsochronousINPipeOne(DeviceAddress, 2))
{
hcd_IsochronousControlStructure.TransferEnd = VP_ProcessPartialPayload_Logging;
//Enables sending IN packets endlessly
UOTGHS->UOTGHS_HSTPIPINRQ[1] = UOTGHS_HSTPIPINRQ_INMODE;
//Enables data received interrupt
UOTGHS->UOTGHS_HSTPIPIER[1] = UOTGHS_HSTPIPIER_RXINES;
//Unfreezes pipe 1
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
}
粗体是我的添加内容。
首先,为设备的端点 #2 初始化了管道 #1。如果管道初始化成功,它会将函数指针 TransferEnd
分配给一个函数,该函数将在每次数据从 USB 设备传入时被调用(VP_ProcessPartialPayload_Logging
将在稍后描述——“partial”意味着它只处理整个视频帧数据的一小部分)。寄存器 Host Pipe 1 IN Request Register (UOTGHS_HSTPIPINRQ[1]
) 中的 INMODE 位控制管道 IN 数据包的发送。我将其设置为 1,这使得 SAM3X 每隔 USB 总线微帧就向管道 #1 指定的设备端点发送 IN 数据包,直到管道 #1 被冻结(停止)。现在它被冻结了,因为我之前在代码中从未解冻它。我还启用了管道 #1 上的“数据已接收”中断,这将触发数据处理。最后,管道 #1 被解冻,使得主机向设备发送 IN 数据包。
此时,设备在收到主机的 IN 数据包后会发送 DATA0
数据包,其中包含部分视频流数据。为了处理它,让我们回到 HCD 中断处理程序并进行更改
//...
//Getting pipe interrupt number
uint8_t PipeInterruptNumber = HCD_GetPipeInterruptNumber();
if(0 == PipeInterruptNumber)
{
HCD_HandleControlPipeInterrupt(); //Pipe 0 interrupt
return;
}
if(1 == PipeInterruptNumber)
{
HCD_HandlePipeInterrupt(PipeInterruptNumber); //Other pipes interrupt
return;
}
//Manage Vbus error
if (0 != (UOTGHS->UOTGHS_SR & UOTGHS_SR_VBERRI))
{
UOTGHS->UOTGHS_SCR = UOTGHS_SCR_VBERRIC; //Ack VBus error interrupt
PrintStr("VBus error.\r\n");
return;
}
//...
照例,粗体代码是我的添加内容。如果中断属于管道 1,则调用函数 HCD_HandlePipeInterrupt
void HCD_HandlePipeInterrupt(uint8_t PipeNumber)
{
//Check if IN packet Data came in interrupt happened
if(UOTGHS->UOTGHS_HSTPIPISR[PipeNumber] & UOTGHS_HSTPIPISR_RXINI)
{
UOTGHS->UOTGHS_HSTPIPICR[PipeNumber] = UOTGHS_HSTPIPICR_RXINIC; //Ack IN DATA interrupt
HCD_ProcessINData(PipeNumber);
return;
}
//Underflow condition - means all banks are full, cannot receive data anymore
if(UOTGHS->UOTGHS_HSTPIPISR[PipeNumber] & UOTGHS_HSTPIPISR_UNDERFI)
{
UOTGHS->UOTGHS_HSTPIPICR[PipeNumber] = UOTGHS_HSTPIPICR_UNDERFIC; //Ack underflow
UOTGHS->UOTGHS_HSTPIPINRQ[PipeNumber] = 0; //Stop IN packet generation
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_RXINEC; //Stop IN interrupts
PrintStr("Error: underflow.\r\n");
return;
}
PrintStr("Uncaught Pipe ");
PrintDEC(PipeNumber);
PrintStr(" interrupt.\r\n");
}
此函数处理并确认两个中断——“数据已接收
”中断和“欠载
”中断,它还会通知关于管道 1 上的任何其他中断,理论上这不应该发生——以防万一。
请记住,在管道 1 初始化期间启用了“欠载
”中断,我之所以这样做是因为在使用 USB 代码进行实验时经常会遇到它。这是一个很好的指示,表明你编写的代码不够快,无法处理传入的数据流,原因可能是处理器速度慢、数据流太快或代码错误。无论原因如何,我都会确认它,以避免重复收到此中断,停止 IN 数据包生成,并向 RS-232 端口打印失败消息。
如果代码到达此函数末尾,意味着发生了其他中断,代码不识别它,因此不处理它。会打印通知消息。
而最重要的一部分是处理“数据已接收
”中断的地方。照例,中断被确认,并调用 HCD_ProcessINData
函数
void HCD_ProcessINData(uint8_t PipeNumber)
{
//Returns byte count received into FIFO buffer
uint16_t ByteReceived = UOTGHS->UOTGHS_HSTPIPISR[PipeNumber] >> UOTGHS_HSTPIPISR_PBYCT_Pos;
//Getting pointer to FIFO buffer
register volatile uint32_t* ptrFIFOBuffer =
(volatile uint32_t*)&(((volatile uint32_t(*)[0x8000/4])UOTGHS_RAM_ADDR)[PipeNumber]);
//Calling Video processing and passing into it pointer to FIFO buffer and its size
(*hcd_IsochronousControlStructure.TransferEnd)(ByteReceived, (uint32_t*)ptrFIFOBuffer);
}
HCD 级别的で数据处理包括获取接收到的字节数、获取指向 USB 模块 FIFO 缓冲区的指针,以及调用 TransferEnd
指向的函数指针。请注意,目前 TransferEnd
指向 VP_ProcessPartialPayload_Buffering
函数,现在该描述它了。
记录流
在流式传输之前,我们需要了解我们从摄像头获得了什么。函数 VP_ProcessPartialPayload_Buffering
正是为了这个目的——它收集一定数量事务的关键数据,停止流,然后以可读格式输出收集到的数据。让我定义什么是“关键数据”
- 事务的顺序号。如果我记录 3000 个事务,我必须收到编号从 0 到 2999 的记录。
- 从设备收到的 DATA0 数据包中的总字节数。
- 报头大小。根据 **[4]** 的第 2.4 段,收到的数据分为报头和有效负载。有效负载必须被提取并缓冲,因为它包含视频数据,而报头则必须被读取,以便了解当前帧的结束位置,查看是否发生了任何错误以及其他补充信息。报头大小始终是收到数据的第一字节,应为 12 字节。
- 报头的第二个字节(也是收到的数据的第二个字节)是名为
BitField
的字段。EOF 位在帧的最后一个事务中设置——它将用作开始输出缓冲视频帧数据的指示。这基本上就是我将使用的所有信息。作为替代,可以使用FID
位,因为它每次新帧开始时都会切换,例如,如果一帧由 50 个事务组成——FID
将是 50 次 0,然后是 50 次 1,然后是 50 次 0,依此类推。请注意,还有另外 10 个字节的某些定时信息和时钟参考信息我没有使用。 SOF
(帧开始)编号。SAM3X 为 USB 总线 SOF 数据包分配编号,这只是一个内部编号,对微控制器内的其他任何东西都没有意义。它们位于寄存器UOTGHS_HSTFNUM
(Host Frame Number Register)中:FNUM
字段是一个 11 位字段,包含帧编号(每 1ms),MFNUM
字段是一个 3 位字段,包含微帧编号(每 125 微秒,即每帧中的八次)。- 当前缓冲区。用于查看 SAM3X 如何切换它们。
示例
这是自开始以来的第 2835 次事务,接收了 1024 字节,其中 12 字节是报头,
BitField
中的 EOF 位为零,这意味着该事务位于视频帧的某个位置,这是第 621 次 SOF 和第二次 mSOF,bank0
包含数据。
用于存储此类信息的结构
typedef struct
{
uint16_t TotalSize;
uint8_t HeaderSize;
uint8_t HeaderBitfield;
uint32_t Frame;
uint32_t CurrentBank;
} VP_HeaderInfoType;
打印它的函数
void VP_PrintHeaderInfo(uint32_t Count);
照例,我不在这些文章中显示打印函数,因为它们很容易理解且篇幅很长。请参阅文章的代码部分。
对于所有视频记录和处理函数及定义,我创建了文件 VideoProcessing.h 和 VideoProcessing.c。要记录的事务数量定义在 VideoProcessing.c 的顶部。
#define LOG_SIZE (3000)
之前提到的 VP_ProcessPartialPayload_Logging
函数也在这里
void VP_ProcessPartialPayload_Logging(uint16_t ByteReceived, uint32_t* ptrFIFOBuffer)
{
uint32_t Temp32;
ptrHeaderInfo[Index].Frame = UOTGHS->UOTGHS_HSTFNUM;
ptrHeaderInfo[Index].CurrentBank = UOTGHS->UOTGHS_HSTPIPISR[1];
if(0 != ByteReceived)
{
Temp32 = *ptrFIFOBuffer++;
ptrHeaderInfo[Index].TotalSize = ByteReceived;
ptrHeaderInfo[Index].HeaderSize = Temp32;
ptrHeaderInfo[Index].HeaderBitfield = Temp32 >> 8;
}
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_FIFOCONC;
Index ++;
if(LOG_SIZE <= Index)
{
UOTGHS->UOTGHS_HSTPIPINRQ[1] = 0;
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_RXINEC;
VP_PrintHeaderInfo(LOG_SIZE);
}
}
对于每个事务,它会保存开始帧(SOF)以及 mSOF 编号和包含数据的缓冲区,然后如果从设备接收到大于零字节的数据,它会保存报头信息。一旦存储了所有我想要的信息——通过向管道 #1 的 UOTGHS_HSTPIPIDR
寄存器写入 FIFOCONC
位来释放当前缓冲区。
索引会随着每次事务而增加,当它接近 LOG_SIZE
时,停止生成 IN 数据包,禁用接收到的数据中断,并显示记录的数据。
我将对端点 #2 执行 3000 次事务,应用不同的备用设置:1、2 和 3,这意味着 128、512 和 1024 字节(参见文章 6)。提醒一下,备用设置在这里设置
void USBD_SetInterfaceAlternateSettingBegin(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;
//Alternate setting: ..., #3 - 1024 bytes, #2 - 512 bytes, #1 - 128 bytes
ControlRequest.wValue = EP_SIZE;
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);
}
由于我之前已经展示了这个函数,我添加了注释并定义了 EP_SIZE
常量,以便在另一个地方方便地将其更改为 1、2 或 3。
我将为这三种备用设置运行日志记录函数,然后讨论输出
128 字节备用设置日志记录
此处显示了一个完整和两个部分可见的视频帧。您可以清楚地看到视频帧结束的边界——BitField
中的 EOF 位被设置——然后开始帧间暂停。每个 128 字节部分(事务)每隔 USB 总线微帧(0.125ms)到达。视频帧之间的暂停间隔变化(46 和 76 ms)。
注意:要计算时间间隔,可以减去两个 SOF 值,结果时间以毫秒为单位。例如,一个完整的帧在第 2003 行结束,其 SOF 为 517,微 SOF 为 2。开始在第 1843 行,SOF 为 497,微 SOF 为 2。用 517 减去 497 得 20,用 2 减去 2 得 0,因此该帧视频数据传输了正好 20ms。
让我们看看视频帧传输了多少数据。#2001 - #1843 = 158 个事务。每个事务包含 128 - 12 = 116 字节的视频数据。总共发送了 116*158 = 18328 字节的视频数据到主机。160x120 视频帧的完整帧大小为 160x120*2=38400 字节。因此,这证明使用备用设置 #1 进行 128 字节传输将不起作用,摄像头根本不发送完整帧数据,只发送大约一半。我还会展示这样的视频看起来如何。
512 字节备用设置日志记录
此处显示了一个完整和两个部分可见的视频帧。您可以清楚地看到视频帧结束的边界——BitField
中的 EOF 位被设置。不像上一个备用设置那样有第二个 12 字节事务。请注意,一个暂停大约是 2ms,下一个是 31ms,这意味着当下一个视频帧数据到来之前,在 2ms 的暂停时间内,不可能将整个帧从缓冲区输出到 TFT 显示器。因此,必须发生某种帧跳过。在第二次暂停期间不需要帧跳过,因为它允许足够的时间输出视频帧。
另外请注意,帧内出现了空事务,这与上一个备用设置不同。如果 SAM3X 在收到空数据包时没有切换缓冲区,这会给我们更多的时间。
1024 字节备用设置日志记录
这个与前一种情况非常相似。在相邻视频帧传输之间也有长短暂停,因此帧跳过也在此处应用。这里的区别是视频帧传输中空事务的数量。略有增加。
总结
- 对于相同的视频帧大小,当事务大小增加时,空事务的数量会增加——无法利用这一事实,因为该数量从零到七不等,不稳定,并且不像上图中的第 2359 和 2360 行那样始终存在。
- 对于相同的事务大小,视频帧传输之间的暂停可能不同——短暂停需要帧跳过。
- EOF 位将用于触发从缓冲区到 TFT 显示器的帧输出。如果 EOF 位连续在一行中设置——可以忽略它。
到目前为止,任务看起来很简单。
在屏幕上设置帧区域
在尝试显示视频之前,必须在屏幕上设置视频区域。区域尺寸必须与视频帧尺寸相同。我将把区域放在 TFT 显示器的中间,这部分代码位于主文件中
#define LEFT_PIXEL 60
#define TOP_PIXEL 80
#define RIGHT_PIXEL 179
#define BOTTOM_PIXEL 239
#define TOTAL_PIXELS 19200 //120x160=19200
//...
//Painting screen blue
for(uint32_t i=0; i<76800; i++)
{
LCD_SET_DB(arColors[3]);
LCD_CLR_WR();
LCD_SET_WR();
}
//Setting frame area in the middle
LCD_SetArea(LEFT_PIXEL, TOP_PIXEL, RIGHT_PIXEL, BOTTOM_PIXEL);
//Cursor at left upper corner
LCD_SetCursor(RIGHT_PIXEL, TOP_PIXEL);
LCD_CLR_CS();
//Painting frame white
for(uint32_t i=0; i<TOTAL_PIXELS; i++)
{
LCD_SET_DB(arColors[0]);
LCD_CLR_WR();
LCD_SET_WR();
}
HCD_SetEnumerationStartFunction(USBD_GetDeviceDescriptorBegin);
HCD_Init();
while(1);
//...
之前我们已经将整个屏幕涂成了蓝色。现在我设置了区域,将光标放在区域的开始位置,并以同样的方式将其涂成白色——只需输出 160x120=19200 个白色像素,TFT 显示器会在当前行的末尾自动换行。
请注意,左、上、右和底部可能不是它们原本的样子,因为显示器被旋转了。
将帧涂成白色不是必需的,因为它将被视频覆盖。我这样做只是为了展示它相对于显示器整体尺寸的大小。
结果如下所示
缓冲帧函数
设备将记录或缓冲传入的数据。记录功能刚刚讨论过,一旦我们看到了实际来自摄像头的什么,就可以开始理解缓冲了。通常,整个帧存储在 Arduino Due 的 RAM 内存中。如果碰巧在开始接收新帧时存储的帧未满——则将其丢弃(帧跳过)。我给这个函数起了个类似记录的函数名,但用“buffering”代替了“logging”。
void VP_ProcessPartialPayload_Buffering(uint16_t ByteReceived, uint32_t* ptrFIFOBuffer)
{
register uint32_t ByteReceived_DW, i, Temp32;
register uint16_t Temp16;
register uint8_t Bitfield;
//Empty packet - return and release FIFO buffer
if(0 == ByteReceived)
{
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_FIFOCONC;
return;
}
//Reading 12 inf bytes
Temp32 = *ptrFIFOBuffer++;
Bitfield = (Temp32 >> 8); //For last frame portion determination
Temp32 = *ptrFIFOBuffer++;
Temp32 = *ptrFIFOBuffer++;
//Calculating data size in double words (4 bytes)
ByteReceived_DW = ((ByteReceived >> 2) - 3);
//Saving 2 bytes at once
for(i=0; i<ByteReceived_DW; i++)
{
Temp32 = *ptrFIFOBuffer++;
Temp16 = (Temp32 & 0xFFu); //Byte 0
Temp16 |= ((Temp32 >> 8) & 0xFF00u); //Byte 2
ptrVideoFrame[Index++] = Temp16; //Saving bytes 0 & 2 as 0 & 1
}
//Releasing FIFO buffer
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_FIFOCONC;
//Checking if frame has been loaded
if(Bitfield & 0x02)
{
ShowFrame();
}
}
在记录过程中,我展示了有时会接收到空数据包。通过简单地从缓冲函数返回并切换缓冲区来处理它们(否则会发生“欠载”中断)。
//Empty packet - return and release FIFO buffer
if(0 == ByteReceived)
{
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_FIFOCONC; //Release current bank
return;
}
所有其他事务至少有 12 字节长,这是因为根据 **[4]**,标准的未压缩有效负载报头是 12 字节。因此,在读取视频帧数据之前,应先读取完整的报头(12 字节)。此外,第二个字节(BitField
)包含 EOF 位,我将其用作开始输出存储视频帧的指示。
//Reading first 12 header bytes
Temp32 = *ptrFIFOBuffer++;
Bitfield = (Temp32 >> 8); //For the last frame transaction determination
Temp32 = *ptrFIFOBuffer++;
Temp32 = *ptrFIFOBuffer++;
请注意,读取 12 字节只需要 3 次读取操作——这是 32 位系统的优势。这也是为什么需要以下代码——数据大小以字节为单位给出,但 SAM3X 以双字(double words)读取。
//Calculating data size in double words (4 bytes)
ByteReceived_DW = ((ByteReceived >> 2) - 3);
现在知道需要多少次“双字”(4 字节)迭代,就可以实际存储视频数据,一次保存两个像素。
//Saving 2 bytes at once
for(i=0; i<ByteReceived_DW; i++)
{
Temp32 = *ptrFIFOBuffer++;
Temp16 = (Temp32 & 0xFFu); //Byte 0
Temp16 |= ((Temp32 >> 8) & 0xFF00u); //Byte 2
ptrVideoFrame[Index++] = Temp16; //Saving bytes 0 & 2 as 0 & 1
}
在 YUY2 视频格式中,每个像素用 2 个字节编码,但颜色信息分布在 4 个字节中。换句话说,要解码一个像素的颜色,还需要读取下一个像素。要解码下一个像素的颜色信息,需要读取前一个像素——它们成对工作。
正如我所说,我不是彩色输出,而是灰度输出。灰度信息位于每个像素的第一个字节。所以这正是上面代码所做的——提取每个像素的第一个字节,因为它一次处理 4 个字节——字节 0 和字节 2 是两个已处理像素的第一个字节。然后将其打包成一个字(2 字节)并存储到视频帧缓冲区中。
数据存储后,当前数据缓冲区被释放。
最后,检查是否是视频帧的最后一个事务。
//Checking if frame has been loaded
if(Bitfield & 0x02)
{
ShowFrame();
}
测试 EOF 位(帧结束)——BitField
中的第二个位。如果是——视频帧必须显示在屏幕上。
此函数在与“记录”函数相同的位置分配。要使用其中一个,请注释掉另一个,反之亦然。
void USBD_SetInterfaceAlternateSettingEnd(uint16_t ByteReceived)
{
PrintStr("Alternate setting has been set.\r\n");
if(HCD_InitiateIsochronousINPipeOne(DeviceAddress, 2))
{
//hcd_IsochronousControlStructure.TransferEnd = VP_ProcessPartialPayload_Logging;
hcd_IsochronousControlStructure.TransferEnd = VP_ProcessPartialPayload_Buffering;
//Enables sending IN packets endlessly
UOTGHS->UOTGHS_HSTPIPINRQ[1] = UOTGHS_HSTPIPINRQ_INMODE;
//Enables data received interrupt
UOTGHS->UOTGHS_HSTPIPIER[1] = UOTGHS_HSTPIPIER_RXINES;
//Unfreezes pipe 1
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
}
显示帧函数
要在屏幕上显示视频帧,必须逐字读取视频缓冲区,将每个字节转换为像素并发送到 TFT 显示器。
void ShowFrame()
{
register uint8_t PixelPart;
register uint16_t Pixel;
register uint32_t i, Temp16;
//When frame is not fully stored (frame skipping)
if(Index < VIDEO_FRAME_SIZE)
return;
Index = 0;
UOTGHS->UOTGHS_HSTPIPINRQ[1] = 0;
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_RXINEC;
//Cursor at left upper corner
LCD_SetCursor(RIGHT_PIXEL, TOP_PIXEL);
LCD_CLR_CS();
i = 0;
do
{
Temp16 = ptrVideoFrame[i];
PixelPart = Temp16;
Pixel = RGB(PixelPart, PixelPart, PixelPart);
LCD_SET_DB(Pixel);
LCD_CLR_WR();
LCD_SET_WR();
PixelPart = (Temp16 >> 8);
Pixel = RGB(PixelPart, PixelPart, PixelPart);
LCD_SET_DB(Pixel);
LCD_CLR_WR();
LCD_SET_WR();
i++;
}
while(i<VIDEO_FRAME_SIZE);
UOTGHS->UOTGHS_HSTPIPINRQ[1] = UOTGHS_HSTPIPINRQ_INMODE;
UOTGHS->UOTGHS_HSTPIPIER[1] = UOTGHS_HSTPIPIER_RXINES;
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_PFREEZEC;
}
它首先要做的是帧跳过。为了解释这一点,请注意,当它输出视频数据时,此函数会停止向摄像头发送 IN 数据包。
UOTGHS->UOTGHS_HSTPIPINRQ[1] = 0;
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_RXINEC;
//...
UOTGHS->UOTGHS_HSTPIPINRQ[1] = UOTGHS_HSTPIPINRQ_INMODE;
UOTGHS->UOTGHS_HSTPIPIER[1] = UOTGHS_HSTPIPIER_RXINES;
UOTGHS->UOTGHS_HSTPIPIDR[1] = UOTGHS_HSTPIPIDR_PFREEZEC;
因此,当两个视频帧之间的暂停很短时(请参阅“记录流”部分),它可能无法接收到接下来几个视频帧的事务,从而导致存储的数据不完整(VIDEO_FRAME_SIZE
中的值)。在这种情况下,函数只需返回,有效地跳过未完全存储的视频帧。
//When frame is not fully stored (frame skipping)
if(Index < VIDEO_FRAME_SIZE)
return;
在视频数据可以输出之前,必须将 TFT 显示器的光标定位到左上角。
//Cursor at left upper corner
LCD_SetCursor(RIGHT_PIXEL, TOP_PIXEL);
LCD_CLR_CS();
一旦光标定位,它一次读取两个字节,将它们转换为只有灰度信息的 16 位像素,并将它们发送到 TFT 显示器。
i = 0;
do
{
Temp16 = ptrVideoFrame[i];
PixelPart = Temp16;
Pixel = RGB(PixelPart, PixelPart, PixelPart);
LCD_SET_DB(Pixel);
LCD_CLR_WR();
LCD_SET_WR();
PixelPart = (Temp16 >> 8);
Pixel = RGB(PixelPart, PixelPart, PixelPart);
LCD_SET_DB(Pixel);
LCD_CLR_WR();
LCD_SET_WR();
i++;
}
while(i<VIDEO_FRAME_SIZE);
很简单!
将所有设置组合在一起
上面的代码示例使用了一些常量,如 LOG_SIZE
、VIDEO_FRAME_SIZE
、LEFT_PIXEL
等。我将它们全部放入 WebCameraCapturing.h 中,并按帧大小分组。
#define Screen_160x120
// #define Screen_176x144
#ifdef Screen_160x120
#define LEFT_PIXEL 60
#define TOP_PIXEL 80
#define RIGHT_PIXEL 179
#define BOTTOM_PIXEL 239
#define TOTAL_PIXELS 19200 //120x160=19200
#define FRAME_INDEX 5 //120x160
#define EP_SIZE 3 //512 bytes
#endif // Screen_160x120
#ifdef Screen_176x144
#define LEFT_PIXEL 48
#define TOP_PIXEL 72
#define RIGHT_PIXEL 191
#define BOTTOM_PIXEL 247
#define TOTAL_PIXELS 25344 //176x144=25344
#define FRAME_INDEX 4 //176x144
#define EP_SIZE 3 //1024 bytes
#endif // Screen_176x144
#define LOG_SIZE (3000)
#define VIDEO_FRAME_SIZE (TOTAL_PIXELS/2)
因此,通过注释掉上面两行中的一个
#define Screen_160x120
// #define Screen_176x144
您将获得针对指定视频帧大小的所有参数设置——屏幕上的正确帧居中、视频缓冲区大小等。视频参数也在此处控制:端点大小和帧索引。
使用 160x120 帧大小进行流式传输
第一个是最小尺寸。我将尝试将端点大小设置为 128 字节(也是最小的),并证明我在“记录流”部分中的计算——它应该显示帧的一半左右。
设置
#define Screen_160x120
// #define Screen_176x144
#ifdef Screen_160x120
#define LEFT_PIXEL 60
#define TOP_PIXEL 80
#define RIGHT_PIXEL 179
#define BOTTOM_PIXEL 239
#define TOTAL_PIXELS 19200 //120x160=19200
#define FRAME_INDEX 5 //120x160
#define EP_SIZE 1 //128 bytes
#endif // Screen_160x120
输出
出于某种原因,当选择 128 字节备用设置时,摄像头没有发送足够的数据。
如果我尝试使用 512 或 1024 字节备用设置
#define Screen_160x120
// #define Screen_176x144
#ifdef Screen_160x120
#define LEFT_PIXEL 60
#define TOP_PIXEL 80
#define RIGHT_PIXEL 179
#define BOTTOM_PIXEL 239
#define TOTAL_PIXELS 19200 //120x160=19200
#define FRAME_INDEX 5 //120x160
#define EP_SIZE 2 //512 bytes
#endif // Screen_160x120
输出
(请注意,截图质量比实际效果略差,我没有总是用我的诺基亚手机拍得很好。)
视频本身已上传到 Youtube:https://www.youtube.com/watch?v=T4V321WtYuM
使用 176x144 帧大小进行流式传输
注释掉第一行,取消注释第二行
// #define Screen_160x120
#define Screen_176x144
//...
#ifdef Screen_176x144
#define LEFT_PIXEL 48
#define TOP_PIXEL 72
#define RIGHT_PIXEL 191
#define BOTTOM_PIXEL 247
#define TOTAL_PIXELS 25344 //176x144=25344
#define FRAME_INDEX 4 //176x144
#define EP_SIZE 3 //1024 bytes (#2 for 512 bytes will also work perfectly)
#endif // Screen_176x144
输出
相应的视频:https://www.youtube.com/watch?v=TjBFTG5kr5w
结论
输出视频流需要了解许多知识:硬件(在本例中是 Arduino Due)、USB 标准、UVC 标准和视频格式标准(在本例中是未压缩的)。将所有这些东西结合起来并不容易,但即使对于像我这样没有 ARM Cortex-m3 处理器、USB 和 UVC 经验的 C# 人来说,也并非不可能。
此外,现在可以使用更好的质量、更容易的方式和更大的帧尺寸来完成这项任务,如果提供了合适的 ARM 硬件(例如 BeagleBone 或 Raspberry),因为很明显 Arduino Due 没有足够的处理能力和内存来正确处理视频流。尽管如此,由于可以使用 Atmel Studio 轻松编程,无需任何额外的编程器和操作,因此我乐于用 Arduino 进行视频学习。
源代码可以在 这里 找到。