65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2015年8月9日

CPOL

17分钟阅读

viewsIcon

44128

downloadIcon

760

理解流式数据

引言

第 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 正是为了这个目的——它收集一定数量事务的关键数据,停止流,然后以可读格式输出收集到的数据。让我定义什么是“关键数据”

  1. 事务的顺序号。如果我记录 3000 个事务,我必须收到编号从 0 到 2999 的记录。
  2. 从设备收到的 DATA0 数据包中的总字节数。
  3. 报头大小。根据 **[4]** 的第 2.4 段,收到的数据分为报头和有效负载。有效负载必须被提取并缓冲,因为它包含视频数据,而报头则必须被读取,以便了解当前帧的结束位置,查看是否发生了任何错误以及其他补充信息。报头大小始终是收到数据的第一字节,应为 12 字节。

  4. 报头的第二个字节(也是收到的数据的第二个字节)是名为 BitField 的字段。EOF 位在帧的最后一个事务中设置——它将用作开始输出缓冲视频帧数据的指示。这基本上就是我将使用的所有信息。作为替代,可以使用 FID 位,因为它每次新帧开始时都会切换,例如,如果一帧由 50 个事务组成——FID 将是 50 次 0,然后是 50 次 1,然后是 50 次 0,依此类推。请注意,还有另外 10 个字节的某些定时信息和时钟参考信息我没有使用。
  5. SOF(帧开始)编号。SAM3X 为 USB 总线 SOF 数据包分配编号,这只是一个内部编号,对微控制器内的其他任何东西都没有意义。它们位于寄存器 UOTGHS_HSTFNUM(Host Frame Number Register)中:FNUM 字段是一个 11 位字段,包含帧编号(每 1ms),MFNUM 字段是一个 3 位字段,包含微帧编号(每 125 微秒,即每帧中的八次)。
  6. 当前缓冲区。用于查看 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.hVideoProcessing.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 字节备用设置日志记录

这个与前一种情况非常相似。在相邻视频帧传输之间也有长短暂停,因此帧跳过也在此处应用。这里的区别是视频帧传输中空事务的数量。略有增加。

总结

  1. 对于相同的视频帧大小,当事务大小增加时,空事务的数量会增加——无法利用这一事实,因为该数量从零到七不等,不稳定,并且不像上图中的第 2359 和 2360 行那样始终存在。
  2. 对于相同的事务大小,视频帧传输之间的暂停可能不同——短暂停需要帧跳过。
  3. 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_SIZEVIDEO_FRAME_SIZELEFT_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 进行视频学习。

源代码可以在 这里 找到。

© . All rights reserved.