第六章 图像显示处理,经典再现
17.SD卡存放图片逐一送VGA显示
在学习实践过SD卡读写和VGA驱动显示的时序后,在下面4个例程中笔者精心选择了综合性较强的,相信大家静下心把这4个例程都独立地去实现后,FPGA的设计能力又会提高了一大步。
这几个例程更贴近于实战项目可以帮大家丰富简历内容,这里不妨去设想一个很真实的场景,如果您是面试官在看到很多简历尤其是校招中写的都是异步FIFO、UART、VGA等各种培训班或者网课的基本项目,但突然看到一份简历里写的项目内容:SD卡存储图片和音频并显示和播放、OV7725实时采集图像乒乓读写DDR3送HDMI图像边缘检测显示、和上位机端协定报文通过UART Modbus CRC校验,USB2.0 CRC校验以及千兆LAN口UDP协议3种接口通信,实现不同接口程控FPGA端控制并行DAC和运放调理输出任意波形等;再加上掌握图像视频多帧缓存、掌握高速接口串并转换Serdes、掌握数字信号处理FIR和DDS等IP核;掌握示波器、逻辑分析仪和波形发送器等的使用,当然如果是学有余力可以往下深入并展现在简历里,大家也完全可以把前面例程中学到的知识点通过不同描述表现在简历里,显然会在众多雷同相似的简历里脱颖而出。
这里想写一些题外话,也是笔者在写技术博客时和在校大学生朋友沟通后的一些体会吧,关于FPGA应该怎么学习,不同的人会有不同的回答,就好像小时候语文书上“小马过河”的故事,大家不妨可以先通过“FPGA基础知识”专栏入门,再通过“二十个经典例程”进阶,如果能够独立动手完成“二十个经典例程”其实FPGA代码编写和调试的水平已经很不错了,只是可能在一些领域缺少些深度。
笔者未来也会不断地更新实战的工程,预期推出几个实用专栏包括:高速接口设计涵盖SFP和PCIE等;数字信号处理涵盖采样、滤波、上下变频等;视频加速应用涵盖常用视频前后处理方法、多帧缓存技术等;ARM和FPGA项目涵盖报文收发解析、ARM开发相关技术等,最后画龙点睛地呈上FPGA核心内容即时序约束和时序分析,从理论模型到具体例子!
话归正传回到这个例程,我们去实现SD卡存放图片逐一送VGA显示的目的,如下图1所示是整个例程的设计示意图。
图1 SD卡存放图片逐一送VGA显示整体设计示意图
在这里SD卡中事先存储了.bmp格式的图片,FAT32文件格式中规定每一个文件都是从一个扇区第一个字节开始存储,并且文件内容是连续存储的。
因为.bmp格式的图片存在有固定的文件头,所以FPGA端可以通过遍历读取SD卡的扇区方法对该扇区进行判断,如果该扇区不是.bmp格式文件头则继续读取下一个扇区,而如果该扇区是.bmp格式文件头,则从文件头中获取关键性信息并且把.bmp图片的像素数据依次写入DDR3内存颗粒中,FPGA再通过VGA时序逻辑把图片送屏幕上显示,按下按键则触发FPGA从当前的扇区地址继续向后顺序遍历各个扇区,寻找.bmp文件头,当再找到下一幅图片则把新的像素值重新写入到DDR3内存颗粒中,屏幕上即会刷新下一张图片。
大家可以把这个例程看成是对前面所学VGA显示、DDR3读写、SD卡读写等诸多知识点的一个综合性应用项目吧,在后面4个例程中笔者也挑选了实战性较强的,一方面可以去回顾前面16个例程中所学的内容,另一方面也是非常好的过渡,本身非常接近于实战项目,不管是对需要丰富简历内容的朋友,还是承上启下对后期继续学习高速接口设计、数字信号处理、视频加速应用、时序分析约束专栏。
在编码之前,我们首先来理清楚整体思路,不管再复杂的工程,从实际工作角度出发,笔者更推荐大家把功能框图画出来,其次静下心思考数据来源、数据流向、数据缓存和数据处理等细节,再次把模块按照功能进行层层划分想好上下游模块之间的数据交互,最后也把重要的时钟标明清楚,这样做起项目就非常直观明了,做完上面准备编码只是工作量的问题,而不是东一榔头西一棒子,想到哪里代码写到哪里,这样效率非常低下,在后面的4个综合性例程中,笔者会带着大家在编码前画一画功能框图。
如下图2所示是SD卡存放图片逐一送VGA显示的功能框图,整个框图清晰明了,大家在动手写程序之前可以先对照框图再理一理思路,想清楚后就可以很快把代码设计出来了,而且在后续定位问题上对照框图也非常方便去排查。因为在一些中大型公司往往会要求大家在设计之前画出类似的框图方便工程管理,所以在后面4个例程中不妨试着去练习下。
图2 SD卡存放图片逐一送VGA显示功能框图
在““FPGA基础知识”专栏中笔者也多曾次强调了模块划分的设计思想,按照具体项目需求,根据功能层层细化模块,举个例子在这个例程中,大家可以想想看数据来源不就是SD卡扇区中所存储的.bmp图片像素值。
显然我们需要一个SD卡对外的接口模块,以实现读取SD卡各个扇区512字节的数据,但因为SD卡又存在SPI底层驱动和初始化问题,且初始配置系统时钟和读取扇区工作时钟又不一样,所以我们可以模块分层把SD卡分为SPI驱动模块、SD卡初始化模块、SD卡读取扇区模块,并在SD卡顶层模块中把相关信号都例化到一起。
在用户按下按键后,FPGA去顺序遍历SD卡以搜索带. bmp文件头的扇区,其中. bmp文件头带有一些关键性信息,包括图片宽度和高度、分辨率、压缩格式等,如图3所示是.bmp图片的位图文件头(14字节)和位图信息数据头(40字节)的数据格式。
图3 .bmp图片的位图文件头和位图信息数据头格式
大家对照图3可以看到实际上.bmp图片的文件头包含了很多信息,但其实在程序设计中我们只需要关心两个地方就可以了,即第一当前所读取的扇区是否是.bmp图片的文件头,第二如果当前扇区是.bmp图片的文件头,那么所读到的.bmp图片大小是多少。
所以在BMP图片查找模块只需要顺序查找SD卡各个扇区,判断是否是.bmp图片的文件头,并从.bmp图片的文件头中提取出图片大小信息,因为DDR3 MIG IP核读写位宽是128位,所以要先把.bmp的RGB888格式图像数据转换成RGB565格式的,再把每个像素16位的数据信息拼接成128位的数据,连同指示信号送给DDR3控制模块,通过写入FIFO IP和MIG IP把数据顺序写到DDR3内存颗粒中即可。
在例程中前端数据是由SD卡中所产生,并通过MIG IP核写入到DDR3内存颗粒中,DDR3存储了一幅完整的分辨率是640*480图片,那么后端则需要再把图片的数据再按照VGA接口的时序逻辑依次送显即可,所以我们分析到这里,整个例程中各模块的设计思路也就非常清晰了。
下面我们就来动手去一步步模块化完成整个例程的代码设计,这个例程也算是首个比较综合性的小项目,本身具有一定的设计难度,而笔者更多地想借此为大家传递这样的思想:不管再复杂的工程在得到具体需求以后,都可以先静下来把功能框图画好,想好信号流向和上下游模块的设计,把数据来源、数据缓存、数据处理等细节做好,那么代码写漂亮写简洁也就是水到渠成的事了。
当积累了一定的成功设计后,再多去思考和总结一些通用性的设计方法,以及深入底层包括时序分析约束,不同器件的底层逻辑资源等,慢慢从宏观上整个项目的设计架构到微观上器件选型的资源利用把握住,水平也就自然而然地提高了。
首先我们来完成读取SD卡扇区模块的代码设计,大家不妨去回顾下“SD卡任意扇区读写”例程,在那个例程中我们实现了对SD卡任意扇区的读写,并通过串口把从SD卡中读到的数据打印到上位机显示。相似的设计思想,只不过在这个例程中我们需要把“读写SD卡”例程的代码做一些精简,即把写入数据到SD卡扇区的状态机省略,然后把从SD卡中各个扇区读取到的数据信号sd_rd_dout和数据指示信号sd_rd_dout_vld一起送到BMP图片查找模块,如图4所示是SD卡顶层读取模块的代码设计。
图4 SD卡顶层读取模块的代码设计
BMP图片查找模块作为一个中间模块起到承上启下的作用,对上要顺序查找SD卡并在各个扇区中找到.bmp格式的图片,对下要把其像素数据通过FIFO和MIG IP核写入到DDR3内存颗粒中,也是整个例程中设计中关键性模块,如下表1所示是bmp_query模块信号列表。
在这个模块中我们需要实现几个功能:1. 顺序查找SD卡各个扇区,判断是否是.bmp图片的文件头,并从.bmp图片的文件头中提取出图片大小信息;2. 把.bmp的RGB888格式图像数据转换成RGB565格式的,即从把各个像素点的值从24位转换成16位;3.除去54字节的.bmp图片的文件头,并根据提取出的图片大小信息,依次顺序从SD卡的扇区中读取像素值;4. 把每个像素16位的数据信息拼接成128位的数据,连同指示信号送给DDR3控制模块,如图5所示是BMP图片查找模块的代码设计。
信号列表 | ||
信号名 | I/O | 位宽 |
clk | I | 1 |
rst_n | I | 1 |
sd_rd_done | I | 1 |
bmp_en | I | 1 |
sd_rd_dout | I | 8 |
sd_rd_dout_vld | I | 1 |
bmp_data | O | 128 |
bmp_data_vld | O | 1 |
sdcard_en | O | 1 |
sec_rd_addr | O | 32 |
表1 bmp_query模块信号列表
图5 BMP图片查找模块的代码设计
然后我们来考虑ddr3_control即DDR3读写控制的核心模块,在这个模块中我们需要去实现大批量像素数据的缓存读写,大家不妨想想看在这个例程中数据来源即为SD卡中读到的.bmp格式图片,数据处理即为640*480分辨率的VGA显示屏。
显然这两个模块的主时钟是不一样的且存在大批量需要跨时钟域的数据,因为在这个例程中DDR3内存颗粒只缓存一张完整的图片,所以大家在按下按键继续向后遍历SD卡时,会出现屏幕同时刷新两张不同图片的现象,但很快第二张图片会把第一张图片完全覆盖,这也就是最基础的FPGA图像处理方法即单帧缓存,实际上在后续OV7725摄像头例程中用到的乒乓处理即为双帧缓存,在后面例程中会展开详细地说明。
如表2所示是ddr3_control模块信号列表,在整个模块的代码设计中也有几个地方值得注意:1.本模块作为上下游跨时钟域数据的缓存,显然需要两个FIFO的支持,即上游写FIFO和下游读FIFO;2.为提高DDR3内存颗粒的读写效率,项目中一般使用连续性读写的方式,在这个例程中我们设计一次性读写64个连续的burst地址空间;3. 当写FIFO高于一定阈值或者读FIFO低于一定阈值时,从写FIFO中读取一定长度数据写入到DDR3或者从 DDR3读取一定长度数据写入到读FIFO中,从原理上分析这个阈值如果是W,那么选择FIFO深度为4W最佳,这样FIFO长期工作在2-3W存储深度之间;4.关于读写DDR3内存颗粒哪个优先,如果写优先,那么当读FIFO为空时有可能读不到数据但数据还在;如果读优先,那么当写FIFO为满时那么要写入的数据就丢失,所以个人认为写优先更靠谱些,如图6所示 DDR3读写控制模块的代码设计。
信号列表 | ||
信号名 | I/O | 位宽 |
clk_200m | I | 1 |
rst_n | I | 1 |
ddr3_wr_clk | I | 1 |
ddr3_wr_din | I | 128 |
ddr3_wr_din_vld | I | 1 |
ddr3_rd_rdy | I | 1 |
ddr3_rd_clk | I | 1 |
ddr3_rd_dout | O | 128 |
ddr3_rd_dout_vld | O | 1 |
ddr3_init_done | O | 1 |
表2 ddr3_control模块信号列表
图6 DDR3读写控制模块的代码设计