自己动手做一个USB摄像头

手里有一块AT32的开发板和一个OV2640摄像头模块,因为做智能车模型需要一个摄像头,就想能不能废物利用一下,用这俩做一个即插即用的USB摄像头,能够直接用在树莓派的Linux系统上,而不需要在Linux上再另外开发摄像头驱动了。但真正做起来之后才发现,淘宝上几十块一个的摄像头开发起来竟然这么麻烦,涉及到了很多东西,完完全全是一个大坑,断断续续做了两个来月才总算实现了。期间一度想直接下单一个,又清晰又稳定,但想想还是不甘心,谁让当初就这么下手了呢。。

 o(╥﹏╥)o

在本文中,我将从下面四个方面来介绍USB摄像头的开发:

  1. 开发板;

  2. 摄像头模块;

  3. USB协议;

  4. UVC协议;

  5. 摄像头模块和USB传输的配合;

  6. 最终效果展示。

侧重于过程和方法,太详细的知识点和操作步骤就不展开说了,因为只要大体方向确定了,相关的标准和资料在网上都可以找到,一篇文章也没有办法把所有涉及到的东西都详细地说透,但按照下面的方法,再结合我的源码(公众号后台回复“USB摄像头”),花点时间和心思是可以做出来的。

01

开发板

我这次用的是有一次参加比赛赠(白)送(piao)的开发板,单片机型号是雅特力的AT32F403A,对标的STM32F103,几乎能直接运行其Hex文件的那种,不过它是M4内核,最高频率能到240M,还是很强大的。前几天还看到了雅特力公众号的一篇推文,说已经成功打入国内前三大汽车厂,估计可能是对运算性能要求较高的非安全相关ECU,但其实我更好奇的是,这前三大汽车厂是指哪三家?

这款单片机还有一个我觉得很实用的地方,也是它一个比较有特色的功能,就是它支持将零等待FLASH配置为内置SRAM。所谓零等待FLASH,就是存放在其中的指令代码在取用的时候没有延迟,而非零等待FLASH取指令的时候,如果系统时钟太快,就需要插入等待时钟。零等待FLASH取指令的速度是非零等待FLASH的2.5倍。代码会优先存放于零等待FLASH,当空间不够的时候再放到非零等待FLASH。

默认情况下:

零等待FLASH:256KB,非零等待FLASH:256KB,SRAM:96KB。

可配置为:

零等待FLASH:128KB,非零等待FLASH:384KB,SRAM:224KB。

这就使得这一款单片机可以适用于更多的场景,在运行复杂算法或图像处理等场合可以配置为SRAM,当对RAM大小没有太高需求的时候可以用来存放指令代码,提高运行速度。

开发板因为是比赛送的,所以比较简陋,只带一个User Key和三个LED灯,但该有的接口都有,有一个MicroUSB接口,这次开发USB摄像头正好可以使用。

自己动手做一个USB摄像头

就是这个,连着摄像头模块,我们可以看到左边还带着一个小板,是AT-Link,类似ST-Link,接个USB线就可以下载和调试,而且集成了串口,初始化串口后就可以直接使用print输出信息,我的例程中也使用了串口。

关于单片机和开发板就介绍这么多,感兴趣的同学可以去雅特力官网找相关资料,挺全面的。几乎常见的应用场景都有Demo。

02

摄像头模块

摄像头型号是OV2640,广角,最高像素200W。通讯上涉及到两种通讯协议。

在初始化摄像头模块或中途改变摄像头配置的时候,需要写一些指令寄存器,相当于给摄像头模块设置状态,这个时候用的是SCCB协议。SCCB和IIC很像,但又不完全相同,也是一根时钟线一根数据线,半双工。我的例程中已经做好了驱动,直接用就行,初始化寄存器的值通常也不需要改变。如果遇到问题想要修改指令寄存器的值,可以在数据手册中查找到每个寄存器详细的定义。

当初始化完毕后,摄像头就开始通过DCMI接口往外传输图像数据,DCMI接口简单说就是用8位并口传输像素值,用帧同步、行同步和像素同步信号来进行传输时的流控制,从而完成一帧图像数据的发送。

下面这幅图展示了帧同步信号(VSYNC)、行同步信号(HREF)和像素同步信号(PCLK)之间的关系,三个信号高电平有效还是低电平有效都是可以通过寄存器来进行配置的。以图中的信号为例,当一帧图像开始传输的时候,帧同步信号由高置为低,帧结束的时候重新置为高,行同步信号同理,只不过有效的电平极性相反;像素同步信号由无效翻转为有效的时候,就可以从8位并行数据口读取数据了。

自己动手做一个USB摄像头

接收摄像头数据最重要的就是把上面的这个时序理解通透,之后就问题不大了。在我的例程中,三个信号都是高电平有效,采用中断的方式来接收数据,下面来说一下逻辑:

  1. 三个信号对应三个外部中断。

  2. 中断优先级:帧同步信号>行同步信号>像素同步信号。

  3. 中断触发方式:帧同步和行同步信号:上升沿和下降沿都触发,在中断处理函数中,通过读取信号电平状态,来判断是帧/行开始还是帧/行结束;像素同步信号:上升沿触发(即电平翻转为有效的时候触发)。

  4. 初始化状态:帧同步信号中断打开,行同步和像素同步中断关闭。

  5. 当一帧图像开始传输的时候,帧同步信号翻转从而触发帧同步中断,在中断函数中初始化数据存储Buffer和数据接收长度等内容,并开启行同步中断。

  6. 当一行数据开始传输的时候,行同步中断被触发,在其中开启像素同步信号中断,并初始化行相关的数据,如行长度等。

  7. 当一个像素值被传输过来的时候,像素同步中断被触发,在其中读取并口数据,并存入Buffer,同时增加数据长度、行长度等需要的内容。

  8. 当一行数据传输完毕的时候,行同步信号翻转,同样会触发行同步中断,这时通过判断行同步信号的电平状态来判断是行结束,关闭像素同步信号中断,以防误触发。

  9. 一帧图像传输完毕后,同样触发帧同步中断,逻辑同上一条,通过判断电平状态来判断一帧图像传输完毕,关闭行同步中断和像素同步中断。

以上是接收一帧图像的基本逻辑,当然在实际应用过程中做了一些特殊情况的判断和处理,并交替使用了两个存储Buffer,详见下文第五部分和例程源码。

初始化的时候,可以设置图像传输的速度,最直接影响的其实就是像素同步信号的频率,进而影响帧率。在代码中, ov2640_speed_ctrl 函数中给出了几组值,大家可以试一下,不同的图像分辨率可能需要不同的传输速度,我就不在这详细说了,因为我也没搞太懂。。大家需要的时候可以拿着示波器,对照着数据手册中寄存器的定义,好好研究下传输速度,如果传输的图像不正确,多数是因为传输速度设置的和图像大小不匹配。

03

USB协议

USB协议的学习和开发是一项庞大且系统的工作,它不像UART、IIC等通信协议,只掌握简单的数据传输时序就可以了,它可以被划分为好几层,先来推荐一篇博文:

https://blog.csdn.net/g200407331/article/details/51682597

(公众号不支持外链,需复制到浏览器打开)

总的来说,USB协议中,主从机是被严格区分的,所有数据的传送都由主机发起,主机会定时发起帧(Frame)的传输,高速USB总线的帧周期为125us,全速以及低速 USB 总线的帧周期为 1ms。在一帧中,可能包含了一个或多个事务(Transaction),即一次接收信息或发送信息的过程。一个事务不能跨帧传输。事务由一个或多个包(Packet)组成,包是信息传输的最小单元,包括了同步段、标识符字段、数据字段、校验字段、结束字段等。

事务有以下三类:

  1. 输入(IN)事务:即主机向从机请求数据。

  2. 输出(OUT)事务:即主机向从机发送数据。

  3. 设置(SETUP)事务:即主机对从机的状态进行初始化和设置。

在事务之上,USB定义了四种传输类型:

  1. 控制传输:由SETUP事务组成,可以读取数据和发送数据,用来读取USB设备描述符(见下文)和配置USB设备。

  2. 批量传输:用于传输大量数据,有握手和校验机制,要求数据不能出错,但不能保证实时性,通常用于存储和打印设备。

  3. 中断传输:主机会按描述符中的查询间隔向从机请求数据,查询是否有数据要发送,中断传输有较高的优先级,仅次于同步传输,通常传输数据量较小,适用于鼠标、键盘等场合。

  4. 同步传输:有最高的优先级,以尽量确保实时性,但没有握手和校验机制,不能保证数据的准确性,因此适用于对实时性要求较高的音视频传输。

所以,USB摄像头传输图像数据采用同步传输。

我们知道,使用鼠标键盘等通用USB设备的时候,我们只需要将设备插入电脑,电脑就能自动识别,并实现特定的功能,不需要额外安装任何驱动,这是怎么实现的呢?其实,就是靠我们上文提到的设备描述符

当电脑识别到一个USB设备插入的时候,会自动启动控制传输,来读取该设备的描述符,描述符中包含着这个设备的名称、类别、属性等,描述符的定义符合USB协议中的标准定义,所以电脑也能够通过分析描述符来知道这是个什么设备。以USB摄像头为例,描述符中就包含着设备种类(图像传输设备)、图像分辨率、格式等很多内容,有的设备支持一些属性的设置,例如亮度对比度等,也会在描述符中告诉电脑,电脑如果想设置的话,也通过控制传输来进行设置。

每一类设备又有自己的一套标准,来定义这类设备的描述符应该怎样填写、数据应该怎样传输。这套标准是基于USB标准之上的。在我们电脑的Windows或Linux系统中,已经集成了常见的USB设备的驱动,驱动程序和USB设备遵循着同样的标准,也就是商量好了沟通的语言,就能够相互配合,完成数据的传输。

USB摄像头对应的标准是UVC(USB Video Class),下面我们就来看一下UVC协议。

04

UVC协议

在我的例程源码中有一个UVCStandard文件夹,里面就是UVC的标准,最新的是V1.5。综述是 UVC 1.5 Class specification.pdf 这个文档,里面定义了USB摄像头这类设备的描述符应该怎样定义,每一个参数代表什么意思。标准中还给了一个描述符的例子,是 USB_Video_Example 1.5.pdf 这个文档,可以把这俩文档结合起来看。但官方给的这个例子很复杂,作为入门可能比较难理解,我的例程就比较简单,是一个最最基础的USB摄像头设备,大家可以对比来看。对于一个USB设备来说,描述符是相当重要的,把描述符搞明白了,就成功了一大半了。

描述符中会定义图像传输的格式,标准中对每一种格式都给了一个文档,来详细定义这个格式的描述符应该怎样填写,和图像数据应该怎样传输。

实际应用过程中,发现Linux的UVC驱动兼容性还是不如Windows的好,对于不是很规范的UVC设备,Windows通常能够识别并正常工作,Linux对设备的规范性要求相对来说更严格,我做的最初版本就是能在Windows上运行,而Linux识别不到,无奈之下开始查Linux的UVC驱动源码,根据驱动输出的Debug信息(sudo dmesg)找出问题点在哪里,再去摄像头中相应的地方修改程序,直到符合Linux的要求为止。摄像头在Linux上能正常工作的话,Windows上通常都是没问题的。

05

摄像头模块和USB传输的配合

在程序跑起来之后,摄像头模块源源不断地发来数据,USB又按时来读取并发送数据,他俩之间的配合又是一个令人头疼的问题。最理想的情况是图像的接收速度和发送速度相同,这样就可以分配一个Buffer,循环使用,将摄像头模块传输来的数据依次写进去,随后让USB模块去读取并发送。但事实上他俩的传输速度很难做到完全同步,如果USB发送的快了,就会导致新的数据还没来得及写入Buffer,USB读取的还是旧数据或是空数据;如果摄像头传来的数据速度太快,就会导致图像数据还没来得及被USB发送出去,就又被新的数据覆盖了,会导致一帧数据的前后不匹配。

为了避免上面的问题,我用了两个图像数据存储Buffer,循环使用,基本逻辑是:

首先确保USB发送数据的速度大于接收摄像头模块数据的速度。在此基础上,摄像头模块发来的数据先存入Buffer1,当一帧数据接收完毕后,将Buffer1标志位置为有效,Buffer2标志位置为无效,然后把存储Buffer切换为Buffer2,这样下一帧图像数据发送过来的时候就会存入Buffer2,再下一帧的时候,再切回Buffer1。这样就可以使得,被置为有效的Buffer的数据在一段时间内是不被改变的,当USB开始发送一帧数据的时候,先判断两个Buffer的标志位,选择有效的Buffer进行读取并发送,当一帧数据发送完成后,如果下一帧数据还没有接收完,那么仍然是发送当前的Buffer中的数据,实际上是用静态图像当做动态图像,欺骗了一下主机。当接收完一帧图像,切换到另一个Buffer的时候,可能出现接收数据和发送数据同时操作一个Buffer的情况,但此时必然是USB发送数据在先,而且由于发送数据的速度大于接收数据的速度,就会使得旧数据不会在发送之前被覆盖,这也是要保证USB传输速度一定要大于接收摄像头模块数据的速度的原因。

这样光说可能不是很容易理解,还是需要结合源码来详细分析。数据接收部分在at32f4xx_it.c 中,有三个外部中断函数,分别对应上文所说的三个摄像头接收中断。数据发送函数在 uvcstream.c 中,UVC_SendPack_Irq 函数在USB同步传输时被调用,每1ms调用一次,每次传输176字节,其中前两个字节是Header(详见UVC协议的 USB_Video_Payload_MJPEG_1.5.pdf),也就是说USB每1ms发送174字节的数据。这样一来,USB发送数据的速度就确定了,接收摄像头模块数据的速度可以用示波器监测摄像头模块的帧同步信号来进行观察,二者之间做好配合就可以了。

关于图像格式,因为我们采用了两个图像存储Buffer,按像素640*480来计算,如果使用未压缩的RGB格式,每个像素会占用两个字节,这样一帧图像的大小就是640*480*2≈600KB!这对我们这次的单片机来说内存是远远不够用的,幸好OV2640模块自带了DSP处理器,可以直接输出压缩后的JPEG格式,UVC协议也支持这种格式,实测一帧640*480的JPEG图像大小不到40K,上文提到过这款单片机的内存最大可以配置到224K,这样就足够分配两个图像存储Buffer了,接收到的数据我们也不需要做任何处理,直接发送出去即可。所以,这个USB摄像头的重难点不在数据处理,而在于使用了多种通信协议,以及他们之间的配合。

以上就是USB摄像头开发所有的要点了,当然,我这个摄像头只是最基础版的,只支持一种图像大小和格式,也不支持任何属性的设置,只能作为入门学习使用。

06

效果展示

Windows下,将电脑和开发板上的MicroUSB接口用USB线连接,就能同时满足供电和数据层传输的需求。插入后在设备管理器中就可以找到这个摄像头:

自己动手做一个USB摄像头

打开Windows自带的照相机程序,如果你的电脑有自带的摄像头,会在右上角出现一个切换摄像头的图标,点击它就可以在自带摄像头和我们开发的USB摄像头之间切换。需要注意的是,USB摄像头的同步传输只有在有程序调用这个摄像头的时候才会被开启,如果没有任何程序使用摄像头,那么电脑和USB摄像头之间是没有数据交换的。我的台式机因为没有其他摄像头所以默认就是USB摄像头:

自己动手做一个USB摄像头

我们再来看Linux上的效果,插入摄像头后,输入指令:sudo lsusb -v,可以查看所有USB设备的描述符信息,在其中能找到我们的USB摄像头:

自己动手做一个USB摄像头

输入指令:ls /dev/video*  能够查看当前系统中的video设备,如果USB摄像头被正确识别,通常会有一个video0,有时候是video1,但video10/11/12/13这些一般都不是我们的设备,这些好像是Linux的v4l2驱动自动生成的虚拟设备。

自己动手做一个USB摄像头

我这次使用OpenCV来读取并显示摄像头图像,代码如下:

自己动手做一个USB摄像头

效果:

自己动手做一个USB摄像头

以上就是所有内容了,欢迎大家交流指正~

上一篇:修改STM32主频


下一篇:手把手教你开发一款属于自己的Arduino开发板