在ZYNQ上调试OV5640并用HDMI显示出来这个实验简直就是一部血泪史啊!!有木有!!一个小小的摄像头,调了4天。因此我准备把这次痛苦的经历和全部经验、细节写出来,以供记录学习ZYNQ的心酸历程。
在百度上输入ZYNQ VDMA OV5640等关键字,搜索的大部分是来自米联客的教程。在此特别地向米联客、ALINX、正点原子等等诸如此类的公司和机构表示感谢和深深的敬意,原因是他们向广大刚刚走进ZYNQ世界的新手提供了免费的教程。前辈们的博客对VDMA,VTC,Video Out,Video in几个模块的配置以及关于VDMA的寄存器讲的也很清楚,因此本文不详细描述这些模块的配置,另外很多篇博客也涉及到了Genlock以及Dynamic Genlock的讲解,本文只对两个概念引用了其他教程的原文。
这些教程和方法本质上是对官方提供的UG,PG等参考资料的转述和总结。
准备工作:
如果您想利用ZYNQ系列的搭建一个简单的摄像头采集系统。首先要有摄像头的例程和资料,不管是STM32F4还是Cyclone4 FPGA,不管是ZYNQ例程还是OV5640自动对焦模组应用指南(DVP接口)等资料都是非常重要的。因为这些例程和资料能够大量的减少您在初始化上所做的工作。
除了上述资料,您还需要了解: IIC或SCCB的通信协议,最好自己有写过IIC时序的经验(51单片机模拟过IIC也好,STM32模拟过IIC也罢,FPGA当然更好);摄像头最基本的时序问题,诸如VSYNC、HREF(与HSYNC共用一个引脚)、PCLK、RESET、PWDN、XCLK以及最重要的data引脚之间的时序;需要知道什么是RGB888什么是RGB565,需要知道RGB565是如何转换RGB888的;需要知道帧缓存的概念以及为什么要用到帧缓存;需要了解Dynamic Genlock的原理;一定要知道AXI4-Lite、AXI4-Full(AXI4)、AXI4-Stream之间的最大区别(具体的时序可以暂时不了解)。
本文分为以下7部分对这个简单的实验进行描述:
上电顺序:
SCCB时序:
SCCB和IIC传输单字节数据的方式是一摸一样的,仅仅是在ACK部分,不需要检测应答信号是高电平还是低电平(但是时钟信号还是存在的,也就是说传输一个字节需要9个时钟周期)。SCCB不支持多字节传输。
对于寄存器地址是双字节的OV5640来说,SCCB的读写时序如下:
写时序:
读时序:
这里由于篇幅原因就不帖代码了,相信能学到这个地方的老铁们,用EMIO配置SCCB问题不大,当然可以在评论区留下邮箱,看到之后会把整个工程发给你。如果着急可以点这下载(顺便我也赚个积分^_^)。如果您想用ILA读取SDA和SCL两个的时序,建议在Vivado综合之后把这两个信号对应的GPIO添加到Debug即可。
向SlaveAddr为0xaa的设备中的0xaa寄存器中写入0xaa(ILA抓取):
当然在初始化时候,用下边的方法也能验证SCCB是否正确。如图所示:
SCCB理解了之后,需要对OV5640进行配置。对OV5640的配置,无非看那200多个寄存器是如何配置的。如果您有一份分辨率和帧率都合适你的例程,那最好不过了。直接利用例程所给的寄存器及其配置参数,在代码格式上进行稍微的修改即可。这里我采用的是ALINX OV5640配置好的1280*720@60Hz例程。因此PLL以及PCLK部分可以不用管了,因为配置PCLK的频率是挺麻烦的一件事(CSDN以及博客园上也有一两篇博客贴出了OV5640如何配置PCLK的流程图,总而言之挺麻烦的,这里就不提及了)。
如果最后您的摄像头能捕捉到画面了,但发现颜色是反的(比如蓝色和红色返了),这时您可以配置0X4300,选择颜色输出的先后顺序。另外,在行时序中建议用HREF(注意HREF和HSYNC的区别)。
1.VDMA的基本框架:
上边说了那么多VDMA(video direct memory access),那么VDMA的框图究竟是怎么样的?其实从这个英文词组也不难得出,视频数据直接与内存交互呗…下边上专业术语:VDMA 用于将 AXI Stream 格式的数据流转换为 Memory Map 格式或将 Memory Map 格式的数据转换为 AXI Stream 数据流, 也就是说 VDMA 内核旨在提供从 AXI4 域到 AXI4-Stream 域的视频读/写传输功能,反之亦然,从而实现系统内存(主要指 DDR3) 和基于 AXI4-Stream 的目标视频 IP 之间的高速数据移动。下边上个框图:
一看这个系统框图就感觉很坑爹,有木有!我清晰的记得各大教学平台说AXI4-Stream无法进行内存映射,由于需要读写缓存数据,我接受了VDMA中有AXI4-Full这个东西。现在好了,AXI4-Full把他兄弟AXI4-Lite也叫来了。不过没关系,从图中看到AXI4-Lite是为了配置VDMA的寄存器的,这些寄存器中的数据能够控制VDMA的工作模式,同时也能表明VDMA的工作状态。
AXI4-Lite 可以对IP的寄存器进行编程(配置),从而实现软件动态配置 VDMA 的功能。 通过 AXI4-Lite 接口对寄存器进行编程后,控制/状态逻辑块会为 Data Mover 生成适当的命令,以在 AXI4 主接口上启动写入和读取命令。可配置的异步 line buffer 用于在将像素数据写入 AXI4-Memory Map 接口或 AXI4-Stream 接口之前临时保存像素数据。
VDMA 数据接口可以分为读、写两个通道,且写入和读取独立运行。 用户可以通过写通道将 AXI-Stream类型的数据流写入系统存储器(主要指 DDR3) 。在读通道中, VDMA 使用 AXI4 主接口从系统存储器读取数据并在 AXI4-Stream 主接口上输出。 可以看到, VDMA 本质上是一个数据搬运的 IP, 可以看作是为视频图像处理做特殊优化的带有帧缓冲功能的高性能 DMA, 为数据进出系统存储器提供了一种便捷的方案。VDMA 内核不仅具有帧缓冲功能,而且集成了视频专用功能, 如 Gen-Lock 和帧同步,用于完全同步的帧 DMA 操作和 2D DMA 传输。除了同步之外,还可以使用帧存储编号和 scatter gather(离散集中)或寄存器直接模式操作,以便中央处理器控制。在本设计中,不使用 VDMA scatter gather 功能,因为可以使用 VDMA 的更简单的寄存器直接模式充分实现系统, 从而避免实现 scatter gather 功能带来的面积成本。只有在系统需要对VDMA 进行相对复杂的软件控制时,才应启用 scatter gather。(引用正点原子教程)
2.帧缓存(正点原子教程原文)
首先我们来看下什么是帧缓存。帧缓存存储器(Frame Buffer),简称帧缓存,也常被称作显存,是为显示设备(如HDMI显示器、RGB LCD液晶屏等)提供数据缓存的一片存储区域。一般图像输入源和图像显示的传输速率不匹配(如图像输入源传输速度较快或者图像显示端传输速度较快),这个时候需要一片存储区域来缓存输入的数据,以便显示设备读取数据,同时也方便后续对视频数据做图像处理。帧缓存的每一个存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。
在实际应用中,一般选择外置的DDR3存储器作为帧缓存,而不是选择片内的BRAM,这是由于ZYNQ片内的BRAM存储容量非常小。我们知道,ZYNQ7010的BRAM存储容量为2.1Mbit,ZYNQ7020的存储容量为4.9Mbit,假设RGB LCD屏的分辨率为800*480,那么存储一帧图像需要的存储容量为(如:RGB888数据格式=24位)800*480*24= 9216000bit≈8.79Mbit,整个BRAM的存储容量不足以存储单帧图像,更何况有时需要更多个显存的情况,因此使用外置的DDR3存储器作为帧缓存。
对于使用帧缓存存储器来缓存图像数据来说,可以采用单帧缓存或者多帧缓存的方案。单帧缓存是指图像的输入和图像的显示都是通过读写同一片存储区域来实现的。虽然也可以实现显示设备显示图像的功能,但这会带来一个问题,即读出的单帧图像是输入的两帧图像或者更多帧图像数据叠加在一起的结果,可能会导致显示设备显示的图像出现割裂的现象。单帧缓存方案的读写示意图如下图所示:
鉴于如上图所示的单帧读取会有图像撕裂的缺点,因此我们使用双(多)缓存就能避免这种情况的出现。多数情况用3个显示缓存是最佳的选择。双缓存模式如下图:
Dynamic Genlock的概念:
在很多的视频应用中,图像输入端和输出端的数据速率不匹配,通常使用帧缓存来避免因速率不匹配而导致的潜在错误。为了解决单帧缓存区域带来的图像叠加问题,通过分配多个帧缓存区域保存数据,图像输入端在写入其中一个帧缓存时,输出端读取其它的帧缓存。那么Genlock是什么呢?这个词语的意思就是同步锁的意思。
这个所谓的同步锁其实是在VDMA中运用的;所谓同步,也就是让显示的图像仅仅跟随摄像头采集的数据(仅仅是差了一帧的时间,现实世界可以视为同步;不要把这个和复位什么的同步相比较)。前人就是牛,发明了很多同步方式,Xilinx中的VDMA中就有4种同步的方式:
① Genlock Master: 当写通道( S2MM)或者读通道( MM2S)配置为 Genlock Master 时,该通道不会跳过或者重复任一帧缓存区域,按照帧缓存顺序读出数据。配置为 Genlock Slave 的通道应当紧跟 Genlock Master 通道变化,但有一定的延迟,延迟的大小在寄存器( *frmdly_stride[28:24])中配置。
② Genlock Slave: 当写通道( S2MM)或者读通道( MM2S) 配置为 Genlock Slave 时,该通道会通过跳过或者重复一些缓存区域的方式,尝试与 Genlock Master 通道同步。
图解:下图是配置成Genlock 模式,其中S2MM是Master,MM2S是Slave, Slave紧紧跟随Master操作完成的内存(红色和紫色)。Slave为了紧紧跟随Master,因此可能会出现跳帧的现象。如图中Slave的红与紫色的交界处,出现了0到2的跳变,并没有读取缓存1;除了这个现象,在蓝色标记处还出现了Master和Slave共同操作一个缓存的现象。
(个人猜测,究竟谁配置成master,谁配置成slave,要看写得快还是读的快,你品一下)
③ Dynamic Genlock Master: 当写通道( S2MM)或者读通道( MM2S) 配置为 Dynamic Genlock Master 时, 该通道会跳过 Dynamic Genlock Slave 通道正在操作的帧缓存,通过跳过或者重复一些帧缓存区域的方式来完成。这里以分配三个帧缓存为例。当配置为 Dynamic Genlock Master 的通道访问帧缓存时,没有检测到 Slave通道访问的帧缓存,那么它会循环访问帧缓存 0 1 2 0 1 2;而如果检测到 Slave 访问的帧缓存区域,它们它会跳过该区域并开始访问下一帧缓存。因此,如果 Slave 通道长时间访问帧缓存 1,则 Master 会循环访问帧缓存 2 和 3。
④ Dynamic Genlock Slave: 当写通道( S2MM)或者读通道( MM2S) 配置为 Dynamic Genlock Slave 时,该通道会操作 Dynamic Genlock Master 通道上一周期操作的帧。
图解:红色和紫色为跟随上一步的缓存。但是当Slave正在对一个缓存区域进行操作时,Master不能夺人所爱,只能跳过这个缓存,寻找下一块缓存。这样就避免了Genlock模式中Master和Slave同时爱上一块缓存的情况。虽然也会出现跳帧现象,但是相比Genlock模式,优点是明显的。另外,对于实时显示的视频来说,Master和Slave同时操作一段内存本身就是引出帧缓存的根本原因。因此在视频处理中,我们需要采用Dynamic Genlock的模式。
以上就是VDMA种同步锁的概念。S2MM(个人理解谁是数据流写到内存映射中,因此是写入)。MM2S(个人理解就是从内存映射中读取到数据流的总线上,因此是读出)。内存映射(memory map),数据流(stream)。
如果想要用Zynq驱动OV5640并通过HDMI传输到显示屏上。你以为简简单单的做个DVP(OV5640)转RGB(HDMI INPUT)转HDMI OUTPUT就行了?说真的你是不是想省去AXI4协议?如果你真的这么想也未必太天真了。因为OV5640与HDMI的时钟不统一,如果时钟差别不大,即使显示也是在短时间内没有影响。另外摄像头没有clkx5这个时钟(clk_x5 for HDMI),因此你懂了吧,不能简简单单直接进行转换。其实真正的框架如下(图中有个错误,与DDR3交互的应该是VDMA模块):
这里引用正点原子教程的整体框架图(截图有点模糊),在这个框架图中微黄色部分就是我们需要应用到的IP。这些IP是如何设计的,我们做为新手就不需要深究了,我们只需要知道怎么用就好了。
从图中不难看出,我们需要用到7个主要的IP(这里不讲解这些模块的配置,很多博客都提到了这些IP的配置,可参见其他博客):
猛地一看,这么多线怎么连啊?其实是有内在规律的。
GP的ACLK和HP的ACLK这里没有保持相同的频率,也就是Lite和Stream用的不是一个时钟,附上连好信号的系统图(下载链接附件的pdf中,放大更清晰哦)。
按正常约束引脚的方法约束即可。但是在一个地方需要注意:因为pclk是一个输入的时钟信号,因此在生成bit流时会出现错误。因此需要让Vivado在生成bit流时知道这个端口是时钟信号,因此在约束文件中需要加上:
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets cam_pclk_IBUF]
这样就能顺利生成bitstream了。如果不加也可以,生成bitstream时,根据提示也能按照上述方法修改错误。
我的工程其余引脚的约束:
采用正点原子的例程、ALINX的板子.当然也可以像米联所提供的教程那样配置寄存器(篇幅原因只给出主程序)。
#include
#include
#include
#include "xil_types.h"
#include "xil_cache.h"
#include "xparameters.h"
#include "xaxivdma.h"
#include "xaxivdma_i.h"
#include "display_ctrl_hdmi/display_ctrl.h"
#include "vdma_api/vdma_api.h"
#include "emio_sccb_cfg/emio_sccb_cfg.h"
#include "ov5640_sccb.h"
//宏定义
#define DYNCLK_BASEADDR XPAR_AXI_DYNCLK_0_BASEADDR //动态时钟基地址
#define VDMA_ID XPAR_AXIVDMA_0_DEVICE_ID //VDMA器件ID
#define DISP_VTC_ID XPAR_VTC_0_DEVICE_ID //VTC器件ID
//全局变量
//frame buffer的起始地址
unsigned int const frame_buffer_addr = (XPAR_PS7_DDR_0_S_AXI_BASEADDR
+ 0x1000000);
XAxiVdma vdma;
DisplayCtrl dispCtrl;
VideoMode vd_mode;
int main(void)
{
u8 ov5640_id;
vd_mode=VMODE_1280x720;
print("Hello World\n\r");
OV5640_SCCB_Init();
OV5640_Init();
print("Hello World\n\r");
ov5640_id=SCCB_RecvByteFReg(SlaveADDR,0x3100);
if(ov5640_id!=0x78){
xil_printf("初始化失败,ID:%d",ov5640_id);
return 0;
}
else{
xil_printf("OV5640 正常,ID:%d",ov5640_id);
}
vd_mode = VMODE_1280x720;
//配置VDMA
run_vdma_frame_buffer(&vdma, VDMA_ID, vd_mode.width, vd_mode.height,
frame_buffer_addr,0,0,BOTH);
//初始化Display controller
DisplayInitialize(&dispCtrl, DISP_VTC_ID, DYNCLK_BASEADDR);
//设置VideoMode
DisplaySetMode(&dispCtrl, &vd_mode);
DisplayStart(&dispCtrl);
while(1)
{
PL_LED(1,0);
sleep(1);
PL_LED(1,1);
sleep(1);
}
return 0;
}