第七章 实战项目提升,完善简历
19.OV7725摄像头实时采集送HDMI显示(四)
在介绍完OV7725初始化配置和视频采集模块后,就到了整个项目的核心部分即DDR3乒乓存储图像模块,为了实现整个FPGA项目工程当中良好的实时性,乒乓操作在广泛应用在FPGA视频加速处理和数字信号处理中。
关于乒乓操作,有很多的FPGA相关书籍都多多少少做了一些介绍,但是相信和大部分朋友一样,笔者在最初学习FPGA的时候也阅读了不少相关介绍乒乓操作的书籍,可以说几乎连描述性文字都大同小异,从头到尾来回读上很多遍也没能体会到乒乓操作存在的意义和具体地实现方式,只能体会到书籍作者想要表达对于BRAM空间或者DDR3/SDRAM颗粒不同内存地址需要来回切换读写。
在这里大家可以从下面几个地方去思考,这也是实际项目工程实施落地中所必须面对的:1.为什么要做乒乓操作;2. 什么时候做乒乓操作;3. 怎么去做乒乓操作;4.什么是局部乒乓和全局乒乓;5.在FPGA视频研发当中,乒乓操作和多帧缓存有何异同。
借助这个例程来逐一解答上面的这些问题,大家可以先简单地把整个视频图像采集系统理解成:输入OV7725实时采集到的视频是640*480*30帧数据流,其时钟频率是12Mhz,而输出的HDMI实时显示的视频源是640*480*60帧数据流,其时钟频率是25Mhz。
整个设计中就存在大量跨时钟域的数据需要同步,为了方便理解我们去进一步简化模型,假设采集图像和显示图像两者同时进行,因为HDMI读取外部内存的速度近乎快于OV7725摄像头写入外部内存的速度的两倍。
那么不妨站在HDMI显示角度来看整个模型,也是假设采集和显示完完全全时钟同步,如图1所示,T0时刻结束时,因为只有1/2个图像帧数据存储写入外部内存中,屏幕去读取外部内存就显示了空白和1/2帧数据图像;在T1时刻结束,屏幕刚好显示完全第一帧数据图像;在T2时刻结束,屏幕显示了1/2第一帧图像和1/2第二帧图像;在T3时刻结束,HDMI刚好显示完全第二帧数据图像。
但是回归到现实在实际工程当中,首先不能保证采集和显示时钟是完全同步的,这只是一种理想的模型;其次项目当中使用的摄像头种类不尽相同,这也就意味者未必是刚好每秒采集30帧图像;再次两者写入和读取外部内存时,往往是以两端读写FIFO的数据数量作为判断的条件,而两个FIFO也是相互独立,所以如果我们不做任何处理,单纯使用外部内存进行单帧存储图像的话,各个时刻内HDMI显示的图像,图像之间帧与帧几乎必然会交错在一起。
图1 DDR3/SDRAM进行单帧存储示意图
通过上面的模型分析,这也就回答了第一个和第二个问题,如果我们不做乒乓操作的话,因为采集图像和显示图像之间显然存在各种不同步性,也必然将导致图像显示会时时刻刻出现帧与帧之间的交错现象,那么对于一个采集处理显示系统来说没有任何实时性可言。
当一个系统需要良好的实时性比如在视频加速处理实时显示时,在数模转换芯片DAC实时计算并输出波形时,通常便会考虑到流水线设计、乒乓操作、多帧缓存等技术,让用户完全感觉不出FPGA在做并行运算处理过程中所产生的不连贯,极大地提高了整个系统实时可靠性,这也就是FPGA设计当中经常说到的以空间换时间。
然后再来结合这个例程,回答第三个问题即怎么去做乒乓操作,如图2所示是片外DDR3/SDRAM乒乓读写示意图,豌豆开发板上板载了一颗存储大小是128Mb的镁光原装DDR3颗粒,因为DDR3颗粒的读写数据速度要远远快于12Mhz和25Mhz,所以完全可以作为乒乓操作片外读写数据的缓存。
OV7725 CMOS Sensor实时采集图像数据,再通过图像输入数据选择模块先写入DDR3颗粒Bank0地址空间,而图像输出数据选择模块在这段时间会一直读取DDR3颗粒Bank1地址空间的数据传输给下游,接着按照HDMI送显的时序显示Bank1中所存储的图像数据,在一帧640像素*480像素的数据写入Bank0地址空间完毕后,这时候切换读写内存的地址空间,即新的一帧640像素*480像素的数据会去写入Bank1地址空间,而图像输出数据选择模块在这段时间会一直读取DDR3颗粒Bank0地址空间的数据,HDMI在这段时间又在显示Bank0中所存储的图像数据,来回交替重复依次循环。
图2 片外DDR3/SDRAM乒乓读写示意图
接着我们再来回答第四个问题,这也是非常重要的概念,即什么是局部乒乓和全局乒乓,不少FPGA书籍对此隐隐约约提及到,但是不作为重点也没有展开描述,其实真正搞清楚这个概念,对于整体设计层面上的理解会有很大帮助。
“局部乒乓”就是说只站在数据写入端去看整个设计,当写完一帧完整的图像数据到数据缓存区Bank0就直接切换地址去写入另外一块Bank1;而“全局乒乓”则是说站在数据写入端和数据读取端去看整个设计,当写完一帧完整的图像数据到数据缓存区Bank0并且数据读取端也从Bank1读完了一帧完整的图像数据后,这时候再去切换读写地址,实际项目工程中通常会去使用“全局乒乓”,虽然可能代码层面上设计比较麻烦些,但是通过“全局乒乓”的操作,可以很大程度上保证画面显示的完整性,不会因为像“局部乒乓”一样,因为一帧图像写入完全而另一帧图像没有读取完全的时候,如果去强行进行乒乓切换读写DDR3内存地址,会导致画面有交错的可能性,这一点会在设计DDR3乒乓存储图像模块代码时去展开进一步说明。
最后就是第五个问题了即在FPGA视频研发当中,乒乓操作和多帧缓存有何异同,相信这是很多朋友们非常关心的问题,这也是笔者在学习实践FPGA过程中被困扰了很久的问题,乒乓操作这个思想真的非常好,也充分发挥了FPGA并行加速的硬件特点,但是大家再仔细想想看它真正从根本上完全解决了图像输入端和输出端之间不匹配或者说不同步的问题了吗。
“乒乓操作”只能说是从某种层度上缓解了这个问题,通过全局乒乓的操作很好地避免了因对外部存储器地址来回交替读写而导致图像帧与帧之间的交错现象,可以让用户看到640像素*480像素的实时图像,但是却不可避免地会造成帧与帧读写完成之间的等待,使得整个视频流尤其是当分辨率很高或者对图像前后处理过多时,图像流畅度方面就会大打折扣,因为图像分辨率高了或者图像前处理多了,跨时钟域读写数据自然而然就增多了,那么整体设计上读写等待的时间就不可避免的变长了,这时两帧缓存的乒乓操作在项目实施落地过程中就会显得力不从心,因为会存在肉眼可见的视频卡顿。
为了能更好地欺骗人眼,使得FPGA视频加速处理的项目更好实施落地,所以就引入了多帧缓存的技术,大家可以简单地理解成是两帧缓存乒乓操作的升级版,多帧缓存主要就是为了解决视频流畅度的问题,但是多帧缓存对外部存储资源的要求也很高,为了保证对外部存储器的读写速度,动则就需要多颗DDR3外部存储颗粒的硬件支持,笔者自己也曾尝试过三帧缓存的处理方案,使用外挂2颗DDR3外部存储颗粒的XC7A100T开发板作为硬件平台,程序中也有些图像前处理,实际测试在分辨率1024*768的情况下视频显示的效果非常流畅,同时6层PCB板2颗DDR3的硬件配置也比较节约,有兴趣的朋友们也可以在学习实践过该例程后尝试进行更高分辨率的三帧缓存技术。
如表1所示是ddr3_control_pingpang模块的信号列表,在这个模块中主要去实现上游cmos_capture摄像头采样和下游video_driver显示屏送显模块间大批量跨时钟域数据的缓存,其实和前面的例程中ddr3_control模块设计大同小异,主要就是带入了乒乓操作使能信号即ddr3_pingpang_en,相应地就设定了wr_addr_page写地址页和rd_addr_page读地址页,这里设定了一次性读写突发长度是BURST_NUM_128BIT是64(对一颗DDR3内存颗粒MIG IP核一次性最少读写地址是8),所以一帧640像素*480像素RGB565的图像需要640*480*16bit/64*128bit=600次突发读写才能完成,wr_addr_page在写完一副图像数据后即置返切换,而rd_addr_page在读完一副图像数据后即切换为~wr_addr_page,这样确保了该地址段的图像数据是完整的一帧图像,如图3所示是DDR3乒乓切换驱动模块的代码设计。
信号列表 | ||
信号名 | I/O | 位宽 |
clk_200m | I | 1 |
rst_n | I | 1 |
ddr3_dq | I/O | 16 |
ddr3_dqs_p | I/O | 2 |
ddr3_dqs_n | I/O | 2 |
ddr3_addr | O | 14 |
ddr3_ba | O | 3 |
ddr3_ras_n | O | 1 |
ddr3_cas_n | O | 1 |
ddr3_we_n | O | 1 |
ddr3_reset_n | O | 1 |
ddr3_ck_p | O | 1 |
ddr3_ck_n | O | 1 |
ddr3_cke | O | 1 |
ddr3_dm | O | 2 |
ddr3_odt | O | 1 |
ddr3_init_done | O | 1 |
ddr3_pingpang_en | I | 1 |
ddr3_wr_clk | I | 1 |
wr_load | I | 1 |
ddr3_wr_din | I | 128 |
ddr3_wr_din_vld | I | 1 |
ddr3_rd_rdy | I | 1 |
ddr3_rd_clk | I | 1 |
rd_load | I | 1 |
ddr3_rd_dout | O | 128 |
ddr3_rd_dout_vld | O | 1 |
表1 ddr3_control_pingpang模块信号列表
图3 DDR3乒乓切换驱动模块的代码设计
如下图4所示是OV7725摄像头实时采集送HDMI显示顶层模块的代码设计,因为video_driver模块和前面例程相同所以笔者在这里就不过度赘述了,顶层模块中把各个模块的相关信号例化到一起即可,这里同样的用一颗LED作为指示灯,当OV7725 CMOS Sensor和DDR3内存颗粒均初始化完成后被点亮,因为在两者都初始化完成后cmos_capture摄像头视频采集模块才开始工作,所以把其例化到该模块的复位信号。
当然OV7725 CMOS Sensor需要一个XCLK驱动这里采用手册推荐的24Mhz,另外还需要一个200Mhz时钟驱动MIG IP核,25Mhz作为640*480分辨率的VGA驱动时钟,125Mhz作为HDMI串并转换的驱动时钟,这里统一用PLL IP核分频或者倍频产生。
图4 OV7725摄像头实时采集送HDMI显示顶层模块的代码设计
如图5所示当我们把OV7725 CMOS Sensor摄像头插到豌豆开发板接口处,可以发现板载的LED灯点亮即初始化完成,很快通过HDMI接口,屏幕即显示出OV7725 CMOS Sensor实时采集到的视频数据。
图5 OV7725 CMOS Sensor摄像头实时采集上板现象