在 Arduino Due 上获取 USB 网络摄像头视频流 - 第一部分:入门






4.80/5 (20投票s)
使用 Atmel Studio 入门 Arduino Due
引言
本项目旨在展示如何从USB摄像头获取视频流。此外,您还可以学习如何使用ARM Cortex-M3 SAM3X设备的各个部分,了解USB 2.0和USB视频类(UVC)。我将解释每段代码的作用以及各种任务的正确执行顺序。
阅读这些文章时,请注意这是我第一个嵌入式C项目,第一个Arduino Due项目,第一个USB和UVC项目,而且我的母语不是英语。
在花费了大约半年时间完成项目后,我现在可以告诉你,Arduino Due不足以处理视频,我一开始就怀疑这一点,但我告诉自己应该尝试一下,然后根据情况再做调整。我能够从网络摄像头获取视频流并在TFT显示器上以两种分辨率(160x120和176x144)实时显示灰度视频,而没有丢帧。也许你们中的一些人能够进一步改进,实现所需的320x240(我的TFT显示器的最大分辨率),这可能需要使用DMA控制器和正确连接TFT显示器。但无论如何,我认为您无法获得彩色视频,因为即使是未压缩的视频流也需要大量的重新计算。
让我们从列出项目组成部分开始。
项目硬件
- Arduino Due;
- 320x240 TFT显示器扩展板,请注意,这款显示器非常暗淡,表面像镜子一样——对我来说是个非常糟糕的显示器,或者是我不知道如何正确初始化它。如果有人能提供更多关于这类TFT显示器的信息,那就太好了。
- Micro A 插头 – A 插座 USB 数据线;
- 一部手机(非iPhone)USB数据/充电A插头 – micro B 插头线,每个人都应该有一根这样的线;
- 一个网络摄像头。我购买了这款,因为它是在那个在线商店里最便宜的。
项目软件
我只使用了免费工具或免费版本。
- Atmel Studio 6.x,用于编写我的代码。这是Atmel提供的免费软件包。看起来像微软的Visual Studio。下载前需要填写一份表格;
- Docklight,一个RS-232监控程序,用于读取Arduino Due的调试消息。我使用的是免费版本,它不能做一些对本项目不重要的功能。也可以使用其他类似的程序代替Docklight。
- Arduino软件。我们不会使用Arduino软件,它只是安装了与主板通信所需的USB驱动程序,并包含一个名为bossac.exe的工具。这个工具非常重要,因为主板就是通过它来编程的。我不会详细介绍如何在Atmel Studio中安装它,因为人们已经很好地描述了,例如在这篇文章中。
Documents
我在文本中将引用以下文件
- ARM Cortex M3权威指南第二版。这本书让你全面了解Cortex-M3以及与其他版本的区别。不必阅读这本书的所有章节。可以合法购买,也可以从Pirate Bay等种子追踪器上免费下载。
- SAM3X完整规格。SAM3X8E是Arduino Due主板上的处理器。[1]
- USB 2.0规范。压缩包内有一个文件usb_20.pdf – 非常重要的文件。[2]
- USB视频类1.0规范。我在其网站上找不到它,因为有更新的版本(1.1和1.5),但事实证明我购买的摄像头符合1.0版本,所以我从某个Linux论坛下载了该文档。[3]
- USB视频载荷未压缩1.0规范。这份文件描述了我相机使用的传输流。你的相机可能有不同的传输流,每种流都有相应的文档。稍后我将展示如何找出你相机的传输流。[4]
入门
安装
请安装Atmel Studio 6.x、Docklight、Arduino软件。将Arduino Due连接到电脑USB端口,等待操作系统安装USB驱动程序(在Arduino软件文件夹中提供),在驱动程序属性中查看它为Arduino Due使用的COM端口号并记下。然后仔细阅读上面提到的关于如何在Atmel Studio中安装Arduino Due外部工具的文章。最后,它应该看起来像我图片中的样子。请注意,我的COM端口号是4,你的可能不同。
要使用它,只需在没有错误的情况下编译程序,然后将鼠标悬停在“工具”菜单上,选择“Arduino Due programmer”。Studio将使用bossac.exe工具将你的程序上传到Arduino Due。
请注意,稍后我们将使用Docklight,由于COM端口不能共享,因此必须在Docklight中关闭它才能进行编程。此外,每次在Docklight中打开COM端口时,Arduino Due都会重启,这是一个非常有用的功能。
一切安装完毕后,让我们从嵌入式版本的“Hello World”开始——闪烁LED。这将帮助我们入门,教我们如何初始化微控制器的主要组件,并给我们一个指示(闪烁),表明我们的代码实际上正在工作并且没有卡死。这是一个有用的步骤,因为我没有调试设备,而且Atmel Studio不为ARM处理器提供模拟器。
创建新项目
打开Atmel Studio,选择菜单“文件”->“新建”->“项目”,选择“GCC C可执行项目”,随意命名。我将在文本中把带有main函数的那个文件称为主文件。
在下一个窗口中选择目标设备。Arduino Due的微控制器是ATSAM3X8E,在列表中找到它,选择并点击“确定”。
在这个点击过程中,Atmel Studio为我们做了一些工作。让我们看看。
在/cmsis/src文件夹中有2个文件:startup_sam3xa.c和system_sam3xa.c。
第一个文件中的代码在我们自己的代码执行之前运行:它定义了向量表,将全局变量初始化为零,C库等,并且有一个分支到main函数(我们的程序从这里开始)。我认为这段启动代码属于高级主题,我自己了解不多,除了查找中断(异常)的正确名称,我不会使用它。如果你想了解它的作用,请阅读一些关于“裸机编程”的文章,例如这篇。
在接下来的章节中,我将展示初始化Arduino Due需要做什么,你将看到所有必要的寄存器和代码序列。我还会简化Atmel Studio生成的代码并添加正确的注释。
基本初始化
第二个自动创建的文件进行了一些基本初始化。我们主文件中的main函数会立即调用位于system_sam3xa.c中的SystemInit()。
#include "sam.h"
int main(void)
{
SystemInit();
while(1);
}
“转到实现”该函数。首先,它初始化闪存读取操作,由于闪存的运行速度比核心慢得多,我们需要指定等待状态[1, p.1434]。对于84MHz频率,根据表45-62,等待状态必须是4。这正是SystemInit()中的值。等待状态也意味着,如果代码中有条件和跳转,运行速度会降低,因为每次这样的操作后,核心都需要清空流水线,等待4个周期(在我们的例子中),才能从内存中访问下一条指令。一旦访问了内存,它就可以一次获取2到4条指令,因此不需要再次等待。
EFC0->EEFC_FMR = EEFC_FMR_FWS(4);
EFC1->EEFC_FMR = EEFC_FMR_FWS(4);
启动后,微控制器以4MHz的嵌入式快速RC振荡器运行[1, p.522]。我们需要将其切换到使用更稳定的3-20MHz晶体振荡器,该振荡器根据Arduino Due原理图连接了一个12MHz的晶体。这正是下一段代码的作用。
if ( !(PMC->CKGR_MOR & CKGR_MOR_MOSCSEL) )
{
PMC->CKGR_MOR = CKGR_MOR_KEY_PASSWD | SYS_BOARD_OSCOUNT | CKGR_MOR_MOSCRCEN | CKGR_MOR_MOSCXTEN;
while ( !(PMC- >PMC_SR & PMC_SR_MOSCXTS) )
{
}
}
首先,它检查3-20MHz振荡器是否未被选中,我们知道它没有被选中。然后,如果未选中,它会启用它。最后,它会等待晶体振荡器启动并稳定下来。我将省略我们不需要的部分,代码现在看起来像这样:
/* Initialize main oscillator */
PMC->CKGR_MOR |= CKGR_MOR_KEY_PASSWD | SYS_BOARD_OSCOUNT | CKGR_MOR_MOSCXTEN;
while ( !(PMC->PMC_SR & PMC_SR_MOSCXTS) );
请注意,写入主振荡器寄存器CKGR_MOR需要密码,其值为0x37。
一旦3-20MHz晶体振荡器初始化完成,我们就将其从嵌入式RC振荡器切换过来,并等待切换完成。
PMC->CKGR_MOR = CKGR_MOR_KEY_PASSWD | SYS_BOARD_OSCOUNT | CKGR_MOR_MOSCRCEN | CKGR_MOR_MOSCXTEN | CKGR_MOR_MOSCSEL; while ( !(PMC->PMC_SR & PMC_SR_MOSCSELS) );
同样,我通过使用OR而不是显示我们已经使用的其他参数来简化它,并强调这一步的具体作用。
/* Switch to 3-20MHz Xtal oscillator */
PMC->CKGR_MOR |= CKGR_MOR_KEY_PASSWD | CKGR_MOR_MOSCSEL;
while ( !(PMC->PMC_SR & PMC_SR_MOSCSELS) );
下一段代码切换到主时钟,我们不需要完全切换,因为启动后系统就从主时钟运行[1, p.522]。主时钟来自嵌入式快速RC振荡器(启动时默认为此)或我们上面初始化并选择的3-20MHz晶体振荡器。因此,我们不需要切换到主时钟,因为它从一开始就被使用,并且下面的代码可以被省略。
PMC->PMC_MCKR = (PMC->PMC_MCKR & ~(uint32_t)PMC_MCKR_CSS_Msk) | PMC_MCKR_CSS_MAIN_CLK;
while (!(PMC->PMC_SR & PMC_SR_MCKRDY));
我们需要的是将时钟频率从12MHz改变到84MHz。为此,有分频器和PLL(锁相环)[1, p.524]。下一段代码初始化它们。
PMC->CKGR_PLLAR = SYS_BOARD_PLLAR;
while ( !(PMC->PMC_SR & PMC_SR_LOCKA) );
请注意,SYS_BOARD_PLLAR 定义在此文件的顶部,并提供[1]第524页第27.6.1节中描述的值,输出为168MHz。需要将其除以2才能获得84MHz,这正是下一段代码的作用。
/* Switch to main clock */
PMC->PMC_MCKR = (SYS_BOARD_MCKR & ~PMC_MCKR_CSS_Msk) | PMC_MCKR_CSS_MAIN_CLK;
while ( !(PMC->PMC_SR & PMC_SR_MCKRDY) )
{
}
正如你所见,注释是错误的,这段代码对于这个小操作来说有点复杂。我将把它重写如下:
/* Setting up prescaler */
PMC->PMC_MCKR = PMC_MCKR_PRES_CLK_2 | PMC_MCKR_CSS_MAIN_CLK;
while ( !(PMC->PMC_SR & PMC_SR_MCKRDY) ) ;
它指定了预分频器(除以2),并保留了主时钟选择。此时,一切都已准备好切换到PLLA(84MHz)时钟。
/* Switch to PLLA */
PMC->PMC_MCKR = SYS_BOARD_MCKR;
while ( !(PMC->PMC_SR & PMC_SR_MCKRDY) )
{
}
请注意,我删除了SYS_BOARD_MCKR宏,并使用了它包含的内容。system_sam3xa.c现在看起来像这样:
#include "sam3xa.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Clock settings (84MHz) */
#define SYS_BOARD_OSCOUNT (CKGR_MOR_MOSCXTST(0x8))
#define SYS_BOARD_PLLAR (CKGR_PLLAR_ONE | CKGR_PLLAR_MULA(0xdUL)
| CKGR_PLLAR_PLLACOUNT(0x3fUL) | CKGR_PLLAR_DIVA(0x1UL))
void SystemInit( void )
{
/* Set FWS according to SYS_BOARD_MCKR configuration */
EFC0->EEFC_FMR = EEFC_FMR_FWS(4);
EFC1->EEFC_FMR = EEFC_FMR_FWS(4);
/* Initialize main oscillator */
PMC->CKGR_MOR |= CKGR_MOR_KEY_PASSWD | SYS_BOARD_OSCOUNT | CKGR_MOR_MOSCXTEN;
while ( !(PMC->PMC_SR & PMC_SR_MOSCXTS) );
/* Switch to 3-20MHz Xtal oscillator */
PMC->CKGR_MOR |= CKGR_MOR_KEY_PASSWD | CKGR_MOR_MOSCSEL;
while ( !(PMC->PMC_SR & PMC_SR_MOSCSELS) );
/* Initialize PLLA */
PMC->CKGR_PLLAR = SYS_BOARD_PLLAR;
while ( !(PMC->PMC_SR & PMC_SR_LOCKA) );
/* Setting up prescaler */
PMC->PMC_MCKR = PMC_MCKR_PRES_CLK_2 | PMC_MCKR_CSS_MAIN_CLK;
while ( !(PMC->PMC_SR & PMC_SR_MCKRDY) ) ;
/* Switch to PLLA */
PMC->PMC_MCKR = PMC_MCKR_PRES_CLK_2 | PMC_MCKR_CSS_PLLA_CLK;
while ( !(PMC->PMC_SR & PMC_SR_MCKRDY) ) ;
}
#ifdef __cplusplus
}
#endif
I/O初始化
为了闪烁LED,必须将某些引脚配置为输出,并且它们的电压电平必须周期性地改变。
有几个寄存器控制引脚的行为,它们被分组到PIO控制器中:PIOA、PIOB等。[1, p.618]。为了让它们正常工作,我们必须启用它们的时钟[1, p.528 - 28.7]。我将启用所有四个PIO时钟,尽管现在我们只需要PIOC和PIOA时钟。TFT显示器连接将需要使用其他PIO。
/* Activates clock to PIO controllers */
PMC->PMC_PCER0 =(1u << ID_PIOA) | (1u << ID_PIOB) | (1u << ID_PIOC) | (1u << ID_PIOD);
根据Arduino Due原理图,LED RX连接到PIOC线30,LED TX连接到PIOA线21。请看下面的代码进行引脚初始化,它非常直观,并显示了所有必要的寄存器。
/* RX LED pin initialization (output, default high)*/
uint32_t Pin = 1u << (PIO_PC30_IDX & 0x1F); //Moving 1 to position 30
PIOC->PIO_IDR = Pin; //No interrupt
PIOC->PIO_PUDR = Pin; //No pull-up
PIOC->PIO_MDDR = Pin; //No open collector
PIOC->PIO_CODR = Pin; //Low level
PIOC->PIO_OER = Pin; //Output
PIOC->PIO_PER = Pin; //Enables
/* TX Pin pin initialization (output, default high) */
Pin = 1u << (PIO_PA21_IDX & 0x1F);
PIOA->PIO_IDR = Pin;
PIOA->PIO_PUDR = Pin;
PIOA->PIO_MDDR = Pin;
PIOA->PIO_CODR = Pin;
PIOA->PIO_OER = Pin;
PIOA->PIO_PER = Pin;
我将这段代码添加到SystemInit()函数的底部。所有初始化代码都将放在这里。
定时器初始化
仅配置引脚不足以闪烁LED,需要某种周期性事件来开关LED。定时器可以触发这样的事件。最简单的定时器是SysTick[1, p.192]。通常,操作系统使用它在任务之间切换。
/* Initialization of SysTick */
/* Reload value is every 1ms. -1 is for 1 tick to reload value */
SysTick->LOAD = ((84000000/1000) & SysTick_LOAD_RELOAD_Msk) - 1;
/* Clearing current value */
SysTick->VAL = 0;
/* Source is not divided by 8, enables SysTick, enables interrupt */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk;
同样,我将这段代码添加到SystemInit()函数底部,在引脚初始化之后。
正如我们所见,SysTick中断已启用,并将每1毫秒触发一次。这意味着它必须被捕获并服务。
捕获SysTick中断
现在引脚已配置,并且有一个每1毫秒触发一次的事件,是时候让LED闪烁了。1kHz的频率我们看不到闪烁,但1Hz是可以的。因此,我们需要一个变量,它会在每次中断时增加1。一旦达到1000——我们切换引脚状态并将变量归零。转到我们的主文件,添加变量和中断处理程序。为了知道中断处理程序的名称,我参考了startup_sam3xa.c文件。
#include "sam.h"
uint32_t SysTickCounter;
void SysTick_Handler(void)
{
uint32_t Pin_LEDRX;
uint32_t Pin_LEDTX;
SysTickCounter ++;
if(1000 == SysTickCounter)
{
SysTickCounter = 0;
Pin_LEDRX = 1 << (PIO_PC30_IDX & 0x1Fu);
Pin_LEDTX = 1 << (PIO_PA21_IDX & 0x1Fu);
if(PIOC->PIO_PDSR & Pin_LEDRX)
{
PIOC->PIO_CODR = Pin_LEDRX; //Lights on
PIOA->PIO_CODR = Pin_LEDTX;
}
else
{
PIOC->PIO_SODR = Pin_LEDRX; //Lights off
PIOA->PIO_SODR = Pin_LEDTX;
}
}
}
int main(void)
{
SystemInit();
while(1);
}
编译它,然后上传到Arduino Due。
它应该看起来像这样:http://youtu.be/pRXwDmMAFhE
结论
在这篇文章中,我展示了一些在Arduino框架之外使用Arduino Due进行实验的基础知识。在下一篇文章中,我将展示如何初始化UART以及如何将一些消息打印回你的电脑。我将创建各种打印函数,用于字符串、十六进制、二进制和十进制格式的数字。我还将初始化TFT显示器,并展示如何在上面显示内容。
源代码在此处。
第二部分在此处。
更新 2015-06-06:重新上传了UVC 1.0和UVC 1.0未压缩格式规范(zip格式,因为不允许pdf),更新了USB 2.0和SAM3X8E规范的链接,因为它们已移动到其他位置。对SAM3X8E规范的引用页码反映了其新版本。
更新 2015-07-10:重新上传了不包含Debug文件夹的源代码。