FPGA入门项目:基于ROM存储的VGA图片显示设计
2026/9/9 18:33:24 网站建设 项目流程

简介:面向FPGA入门与进阶学习者,这份资源演示了如何用Verilog将64×64像素图片预存入ROM,并通过VGA接口实时显示。整个工程基于Quartus环境,包含完整源码与配置文件:Verilog文件(.v)实现ROM读取与VGA控制器,AHDL文件(.tdf)补充逻辑设计,存储器初始化文件(.mif)存放经MATLAB转换的图片像素RGB数据,仿真波形(.vwf)便于时序验证,.sof/.pof可用于直接下载至开发板。压缩包共245个文件,大小1.12MB,既有工程配置(.qpf/.qsf)也有综合报告,结构清晰。已有1052人学习,内容覆盖图片数据生成、ROM建模、VGA行场同步时序控制以及时钟信号产生等关键环节。借助MATLAB生成mif、Quartus综合布线以及硬件下载验证,可系统掌握从图片数据到显示屏输出的完整流程,适合电子设计与FPGA爱好者对照实践。

1. 一块入门板子最容易出成就感的小项目

最近整理实验室的FPGA开发板,翻出一个吃灰已久的ego1板卡,顺手把当年入门时写的“ROM存储图片的VGA显示”项目重新跑了一遍。这个项目可以说是FPGA图像显示方向最经典的练手题目之一,简单、直观、见效快,整个过程涉及ROM的初始化、VGA时序驱动、像素数据的读取与同步等多个基础知识点,非常适合刚学完Verilog语法、想上手一个完整项目的同学。

先说清楚这个项目是干什么的:把一张图片预先存储到FPGA内部的ROM(只读存储器)中,然后通过VGA接口将图片内容实时显示到显示器上。整个过程不需要外部存储器芯片,也不需要复杂的SDRAM控制逻辑,仅靠FPGA内部的块RAM资源和几个简单的时序模块就能完成。对于初学者来说,这是理解“存储-读取-显示”这条数据链路的最佳方式。

项目本身适合三类人:一是刚学完Verilog基础语法、想练手完整工程的初学者;二是准备找工作、想在手头积累几个拿得出手的FPGA项目的应届生;三是对图像显示方向感兴趣、想搞明白VGA时序原理的电子爱好者。

2. 整体设计思路:为什么选ROM而不是RAM或外部Flash

2.1 项目核心架构解析

整个系统的数据流其实非常简单:VGA驱动模块产生行场同步信号和像素时钟,同时生成当前像素位置对应的ROM地址;ROM根据地址输出存储的图像数据;数据经过简单的同步处理后直接送到VGA的RGB引脚上。核心就这三件事,但这三件事背后各有各的门道。

先说为什么用ROM存储图片。ROM的特点是上电后数据固定不变,适合存储静态内容。在FPGA中,ROM一般由内部的块RAM初始化而成,通过加载一个COE或MIF格式的初始化文件,在综合时直接把数据固化到存储单元里。这样设计的好处是不需要额外的写控制逻辑,读取操作简单,时序容易把控。

与之对比,如果用RAM,还得考虑上电后如何把图片数据写入RAM的问题,这就会引入串口、SD卡或者Flash控制器的设计,复杂度瞬间上了一个台阶。而如果选用外部Flash,虽然能存大图,但初学阶段涉及SPI接口、Flash读时序等额外知识,容易让人迷失在细节里。

2.2 图片分辨率与存储空间的匹配计算

选定了用内部ROM,接下来就是算存储空间。以我用的ego1开发板为例,其FPGA芯片是Xilinx Artix-7系列的XC7A35T,内部有50个Block RAM,每个36Kb(可以配置成18Kb或36Kb两种模式),总存储容量大约1800Kb。

我用了一张160x120像素的图片做实验。如果把每个像素的RGB数据都存下来,存储量就是160乘以120乘以颜色深度。我选的是RGB332格式,即红色3位、绿色3位、蓝色2位,共8位数据,这样一个像素占1个字节。总存储量就是160乘以120等于19200字节,也就是153600比特,约150Kb。这个容量对块RAM来说毫无压力,只用不到十分之一的资源。

如果想让显示效果更细腻,可以选择640x480分辨率的图片,但此时存储量会飙升到640乘以480乘以8等于2457600比特,约2.4Mb,超过了XC7A35T内部块RAM的容量上限。所以初学者想完整显示一帧图像,建议先用低分辨率图片跑通流程,后续再考虑用外部存储器扩展。

2.3 为什么需要理解VGA时序

VGA显示的底层协议是标准的逐行扫描时序,这个时序如果搞不明白,整个项目就会陷入看得到信号但画面不正常的困境。VGA接口的核心是五根信号线:行同步(HSYNC)、场同步(VSYNC)、红色(R)、绿色(G)、蓝色(B)。显示器的电子枪从左到右逐行扫描,扫描完一行后回到下一行起点,扫描完整帧后再回到左上角,这个过程中行同步和场同步信号起到“告诉显示器当前扫描位置”的作用。

很多初学者容易忽略的是,VGA时序中包含同步脉冲、后沿、有效显示区和前沿四个阶段。只有有效显示区的数据才是真正显示到屏幕上的像素,其余时间段必须输出黑色或无效数据。如果这一步处理不好,做出来的效果往往是画面偏斜、左右跳动或者上下翻滚。

以640x480分辨率60Hz刷新率为例,行周期是800个像素时钟,其中有效显示640个像素,同步脉冲96个时钟,后沿48个时钟,前沿16个时钟。帧周期是525行,其中有效显示480行,同步脉冲2行,后沿33行,前沿10行。这些参数都是有标准的,不能随意修改。

3. 图片数据准备:从PNG到COE文件的关键步骤

3.1 图片尺寸与格式的取舍

实操第一步是准备图片数据。很多人在这步就栽了跟头,因为直接用常规图片工具保存的PNG或JPEG文件,FPGA是没法直接用的,必须转换成ROM初始化文件支持的格式。

图片尺寸的选择要综合考量显示效果和存储容量。我建议初学者先用160x120或者320x240分辨率的图片。这里有个经验心得:分辨率不一定要完全贴合VGA标准的分辨率,因为我们可以通过控制VGA控制器的计数范围,只在有效显示区的一部分区域内显示图片,其余区域显示黑色背景。比如在640x480的显示区中,设定一个从左上角偏移一定距离的位置作为图片起始点,这样就能把160x120的图片在屏幕左上角显示出来,图片周围是黑色边框,效果非常干净。

颜色深度的选择同样关键。RGB332虽然只有8位颜色,看起来色彩不够丰富,但胜在存储效率高、设计简单。如果想让颜色更饱满,可以选用RGB444(12位色深)或RGB565(16位色深),代价是存储容量直接翻倍或翻三倍。我实际对比过,在160x120分辨率下,RGB444的显示效果已经相当不错,肉眼几乎看不出与原始图片的明显差异,而存储量也仅仅增加到了28800字节。

3.2 使用Python脚本完成图片到COE的转换

选择好图片后,就需要写一个小工具把图片转成COE文件。COE是Xilinx Vivado支持的一种初始化文件格式,文件内容是16进制表示的存储数据,每条数据对应一个存储地址。

我自己写了个Python脚本,用PIL图像处理库来处理图片。脚本的逻辑非常简单:打开图片并调整尺寸,遍历每个像素点,将RGB值转换为目标颜色深度格式的整数,然后按地址顺序写入COE文件。关键代码大致如下:

from PIL import Image img = Image.open("input.png").convert("RGB") img = img.resize((160, 120)) with open("image.coe", "w") as f: f.write("memory_initialization_radix=16;\n") f.write("memory_initialization_vector=\n") for y in range(120): for x in range(160): r, g, b = img.getpixel((x, y)) # RGB332: 高3位为红色,中3位为绿色,低2位为蓝色 data = ((r >> 5) << 5) | ((g >> 5) << 2) | (b >> 6) f.write(f"{data:02x}") if not (y == 119 and x == 159): f.write(",\n") else: f.write(";\n")

脚本执行完后会生成一个COE文件,这个文件里每一行对应ROM中一个地址的初始值。注意:COE文件最后必须以分号结尾,否则Vivado在加载时会报错,这是新手最容易踩的坑之一。

4. FPGA内部ROM的生成与例化

4.1 使用Vivado IP核生成单端口ROM

有了COE文件,下一步就是在Vivado中生成ROM的IP核。打开Vivado后,在IP Catalog中搜索Block Memory Generator,配置成Single Port ROM模式。这里有几个关键参数需要格外注意。

端口宽度必须与图片颜色深度一致,我用的RGB332就是8位。读深度要严格等于图片像素总数,160x120的图片就是19200。填写初始化文件路径时,选择刚才生成的COE文件。还有一点容易忽略:在Other Options页面,确保选中“Load Init File”选项,否则ROM内容不会加载。

我常用的是Standalone模式,这种模式下输出端口会带一个可选的寄存器,虽然增加了一级流水线延迟,但能有效改善时序收敛,在高主频场景下非常有用。对于本项目,我建议开启寄存器输出,因为VGA像素时钟只有25MHz,多一级延迟不会造成显示错位,只要在驱动逻辑中同步调整读取地址的时序就行。

4.2 地址换算:让图片行与VGA行对齐

ROM生成之后,最关键的一步是地址计算。VGA控制器在扫描到有效显示区时,会持续输出行列坐标。我们需要把这些坐标映射到ROM的读地址上。

先确定图片在屏幕上的显示起始位置。比如我希望图片显示在左上角,起始点设置为(80,60),即图片左上角距离屏幕左边80个像素、上边60行。那么当一个像素位于图片区域内时,图片内的相对坐标就是:相对X等于当前像素X坐标减80,相对Y等于当前像素Y坐标减60。

ROM的存储顺序是逐行扫描,即先存第一行的160个像素,再存第二行的160个像素,依次类推。因此ROM地址等于相对Y乘以图片宽度加相对X。以160x120的图片为例,第3行第5个像素的地址就是3乘以160加5等于485。

这段逻辑在Verilog中的实现非常直观:

reg [14:0] rom_addr; always @(posedge clk) begin if (in_display_region) begin rom_addr <= (pixel_y - 60) * 160 + (pixel_x - 80); end else begin rom_addr <= 15'd0; end end

这里的pixel_x和pixel_y是VGA驱动模块产生的行列计数器的当前值。超出图片区域时,直接将地址置零,同时将输出数据屏蔽为黑色背景。

5. VGA驱动模块的核心实现与同步技巧

5.1 VGA时序计数器的搭建

VGA驱动模块是整个项目的核心。标准做法是用两个计数器。一个像素计数器用来计每行的像素数,范围是0到799;一个行计数器用来计总行数,范围是0到524。像素计数器每来一个像素时钟就加一,加到799后归零,同时行计数器加一。行计数器加到524后归零,表示一帧结束。

同步信号在特定计数器范围内拉低。水平同步信号在像素计数器位于656到751之间时输出低电平,其余时间输出高电平。垂直同步信号在行计数器位于490到491之间时输出低电平,其余时间输出高电平。

有效显示区的判断逻辑稍复杂一些。水平方向像素计数器在0到639之间是有效显示区,垂直方向行计数器在0到479之间是有效行,两者的与关系就是有效显示信号。

我在实际写代码时会把参数定义成常量,方便后续修改。如果把分辨率从640x480改成800x600,只需要修改常量定义和同步信号的边界条件,其余逻辑可以保持不变。

5.2 ROM数据的时序对齐

这个项目最容易出错的地方在于ROM读出数据与VGA时序的同步。前面提到开启了ROM输出寄存器,这意味着一发出地址,要等两个时钟周期才能采到正确的数据。如果直接把ROM输出接到RGB引脚上,图像会整体向右偏移两个像素,虽然不仔细看不容易察觉,但对于讲究精确的显示项目来说是不可接受的。

解决方法是把有效显示信号和行列坐标也打两拍,让所有信号保持同步。具体做法是在显示有效信号打拍后的高电平期间,将ROM输出直接赋给RGB输出;而在打拍后的无效期间,将RGB输出全部置零。

reg [1:0] display_valid_delay; always @(posedge clk) begin display_valid_delay <= {display_valid_delay[0], in_display_region}; end assign vga_rgb = display_valid_delay[1] ? rom_data : 8'd0;

这是典型的流水线对齐操作,在图像处理项目中非常常用。很多人觉得图像显示项目难,难点其实不在逻辑复杂度,而在于这种时序对齐的细节。一个小细节没对齐,画面就是不正常,排查起来还特别费劲。

5.3 顶层模块与管脚约束

顶层模块的编写相对简单,就是把VGA驱动模块和ROM模块例化连接起来。VGA驱动模块输出像素坐标给地址计算模块,地址计算模块根据坐标生成ROM地址,ROM输出数据经过对齐处理后送给VGA接口。

管脚约束文件是另一个需要认真对待的部分。VGA接口的时钟、行同步、场同步和RGB信号都要正确分配到开发板上对应的物理引脚。常见的一种错误是把行同步和场同步信号接反,导致画面完全无法显示或者疯狂跳动。RGB信号分配错误则会导致颜色错乱,比如红色显示成蓝色。

另外,别忘了检查开发板的VGA接口是否需要额外的使能信号。部分开发板在VGA接口处集成了一颗电平转换芯片,需要额外拉高或拉低某个控制引脚才能正常输出。我用的ego1板卡上有一个VGA相关的使能引脚,如果不拉高,显示接口完全无输出。

6. 常见问题与排查技巧实录

6.1 问题速查表

我把这个项目实践中遇到的高频问题整理成了表格,方便后续排查。

现象可能原因解决方案
显示器显示“无信号”行场同步信号极性错误或频率不匹配检查行同步低电平持续时间是否在标准范围内
画面整体向右偏移ROM输出寄存器导致的数据延迟将有效显示信号打拍对齐后再输出RGB
画面只有噪点无图像ROM未加载初始化文件或COE格式错误确认IP核中已勾选Load Init File选项
图片显示位置不对起始坐标设置错误检查行列计数器的坐标与起始偏移的计算逻辑
颜色全部错乱RGB位宽或位序分配错误对照引脚约束文件和RGB332格式逐位排查
图片上下颠倒或左右镜像图片存储顺序与地址映射不一致调整地址计算中的坐标方向

6.2 调试经验分享

调试VGA项目时,最有效的工具是逻辑分析仪。用Vivado自带的ILA核,抓取像素计数器、行计数器、ROM地址和ROM数据这几个关键信号。抓完之后对照VGA标准时序图逐一比对,基本能快速定位问题所在。

不过逻辑分析仪也不是万能的,因为VGA信号变化频率较高,抓取深度有限。我的经验是先跑低频时钟验证逻辑正确,再把时钟频率切换到正常的25MHz。具体做法是把像素时钟先设置成1MHz左右的低频,这样每个像素周期变长,逻辑分析仪能轻松捕获完整时序,如果低频下显示正常,说明逻辑本身没有问题,问题大概率出在时序约束或高速信号完整性上。

还有一个值得一提的坑:某些显示器对行场同步信号的边沿非常敏感。我调试时遇到过一台老款显示器,在640x480分辨率下显示正常,但同一套代码跑到部分新款显示器上就显示“输入不支持”。排查后确认不是FPGA逻辑问题,而是显示器对同步信号参数的要求更加严格。如果遇到这种情况,可以试试调整同步脉冲的宽度,或者增加前沿和后沿的时间。

7. 更多的扩展方向

RUN通这个基础项目后,可以做的事情还有很多。最简单的扩展是改用RGB565颜色格式,用16位颜色深度显示图片,效果会明显提升。更进一步,可以把多张图片存入不同的ROM深度段,通过按键切换显示内容,这就是一个简单的电子相册。

如果想把动态图像显示也搞定,那么需要在现有架构上增加SDRAM控制器和摄像头接口,把摄像头采集的数据写入SDRAM,再从SDRAM读出显示到VGA上。整个数据通路的核心思想与本项目完全一致,只是存储介质从ROM换成了更灵活的SDRAM。

再往上层走,可以在VGA显示的基础上叠加字符叠加模块,实现字符、汉字、图片的混合显示。工业控制领域中常见的界面显示,本质上就是这些技术的组合。

根据我个人的经验,这个项目值得好好打磨,不是跑通就完了,要把每一行Verilog代码都理解透彻。VGA时序、ROM初始化、流水线对齐,这三个知识点在后续几乎所有图像相关的FPGA项目中都会反复用到。把基础打牢,后面做摄像头采集显示、实时图像处理和HDMI输出时,就不需要在底层细节上反复踩坑了。

最后再分享一个小技巧:做VGA相关项目时,在显示器上贴几张便签纸标出关键坐标区域,比如标注出(80,60)到(240,180)的图片显示区域,调试时会直观很多。别笑,这招在排查显示偏移问题时真能节省不少时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询