简介这份资源面向FPGA初学者与数字电路课程实践者围绕“用ROM存储图片并通过VGA接口显示”这一典型项目展开帮助读者打通从图像数据生成到硬件显示的完整链路。压缩包共245个文件约1.12MB以Quartus工程文件为主包含hdb、cdb、tdf、vwf等编译与仿真中间文件以及v、qsf、qpf等Verilog源码与工程配置另有mif存储器初始化文件、sof与pof下载文件、rpt报告和readme说明覆盖综合、布局布线与下载全流程。已有1056人学习下载。资源核心价值在于提供可直接复现的工程实例读者可参考ROM模块与VGA控制器的Verilog实现理解HS、VS同步信号与RGB像素数据的时序配合并借助MATLAB生成的mif文件掌握64×64像素图片的存储与读取方法适合作为课程设计、实验报告或自学练手的参考模板。1. 从一块 FPGA 开发板说起图片怎么从 ROM 里“流”到 VGA 屏幕上手头有一块 FPGA 开发板想让它不依赖电脑、不接摄像头直接把自己预先存好的图片显示在 VGA 显示器上这件事听起来简单做起来却卡住过不少人。我最初的想法也很朴素把图片数据写进 ROM然后按 VGA 的时序把像素一个个读出来送出去不就行了真正动手才发现ROM 的地址怎么算、VGA 的行场同步怎么对齐、像素时钟从哪来、图片格式怎么转成 Verilog 能读的.mif或.coe每一步都有细节。这个项目就是围绕“ROM 存储图片 VGA 显示”这条链路展开的用 Verilog 把整个数据通路串起来适合刚接触 FPGA 图像显示、想找一个完整可跑例程的开发者。它不涉及复杂的图像处理算法核心就是存储、寻址、时序三件事但恰恰是这三件事把 FPGA 做显示的基本功全练了一遍。2. 系统架构拆解ROM、VGA 时序与时钟三块怎么咬合2.1 整体数据通路从地址计数器到 RGB 输出这个项目的结构不复杂但每一级都有明确的职责。图片数据预先存在片内 ROM 里ROM 的地址由一组行列计数器生成计数器本身又必须严格跟随 VGA 的行场同步信号走。换句话说VGA 时序不只是输出给显示器的它同时充当了 ROM 的“读地址发生器”。整个通路可以拆成四段时钟分频与像素时钟生成、行场同步计数器、ROM 地址映射、RGB 数据输出。常见做法是让 ROM 输出位宽等于一个像素的颜色深度比如 RGB565 就是 16 位RGB332 就是 8 位。地址位宽则由图片分辨率决定例如 640×480 的图片总像素数是 307200地址至少需要 19 位2^19 524288。这里有个容易忽略的点VGA 的可见区域只是行场周期的一部分消隐区同样占用时钟周期所以地址计数器不能简单地从 0 数到 307199而要在可见区域内才递增消隐区保持或复位。// 行场计数器核心逻辑简化示意 always (posedge pixel_clk or negedge rst_n) begin if (!rst_n) begin h_cnt 0; v_cnt 0; end else begin if (h_cnt H_TOTAL - 1) begin h_cnt 0; if (v_cnt V_TOTAL - 1) v_cnt 0; else v_cnt v_cnt 1; end else begin h_cnt h_cnt 1; end end end // 可见区域判断只有在这里才输出有效像素 wire visible (h_cnt H_ACTIVE) (v_cnt V_ACTIVE); wire [18:0] rom_addr visible ? (v_cnt * H_ACTIVE h_cnt) : 19d0;这段代码里H_TOTAL和V_TOTAL是包含消隐区的总周期数H_ACTIVE和V_ACTIVE是实际可见分辨率。rom_addr只在可见区域才计算真实地址消隐区给 0 或者保持都可以关键是不要让 ROM 在无效区域输出干扰数据。参数上如果你的图片是 800×600就把H_ACTIVE改成 800V_ACTIVE改成 600同时 ROM 深度要相应加大。2.2 ROM 初始化图片怎么变成 .mif 文件FPGA 的片内 ROM 通常用 IP 核生成Quartus 里叫 ROM: 1-PORTVivado 里可以用 Block Memory Generator。不管哪个平台都需要一个初始化文件常见格式是.mifMemory Initialization File或.coeCoefficient File。图片不能直接丢进去得先转成文本格式的像素数据。我一般会写一个简单的 Python 脚本做转换把 BMP 或 PNG 的每个像素提取出来按 RGB565 或 RGB332 编码成十六进制再按.mif的格式写出来。这一步看起来是“杂活”但格式错了后面全白搭。# 图片转 .mif 的简化脚本以 RGB565 为例 from PIL import Image img Image.open(test.bmp).convert(RGB) w, h img.size pixels list(img.getdata()) with open(rom_init.mif, w) as f: f.write(fWIDTH16;\nDEPTH{w*h};\n\n) f.write(ADDRESS_RADIXHEX;\nDATA_RADIXHEX;\n\nCONTENT BEGIN\n) for i, (r, g, b) in enumerate(pixels): # RGB565: R占5位G占6位B占5位 rgb565 ((r 3) 11) | ((g 2) 5) | (b 3) f.write(f{i:05X} : {rgb565:04X};\n) f.write(END;\n)这段脚本的关键在rgb565的拼接红色右移 3 位取高 5 位绿色右移 2 位取高 6 位蓝色右移 3 位取低 5 位然后按位或起来。生成的.mif文件里WIDTH16对应 ROM 输出位宽DEPTH是总像素数。如果你的图片是 640×480DEPTH就是 307200。注意地址用十六进制写数据也用十六进制Quartus 的 ROM IP 核能直接识别这个格式。2.3 VGA 时序参数行场同步到底怎么算VGA 时序是很多新手翻车的地方。行同步和场同步都有四个区间同步脉冲、后沿、可见区域、前沿。以 640×48060Hz 为例像素时钟是 25.175MHz行总周期 800 个像素时钟其中同步脉冲 96 个后沿 48 个可见区域 640 个前沿 16 个。场总周期 525 行同步脉冲 2 行后沿 33 行可见区域 480 行前沿 10 行。这些数字不是随便定的是 VGA 标准里规定的。写代码时我习惯把参数定义成parameter方便换分辨率时统一改。// 640x48060Hz 时序参数 parameter H_ACTIVE 640; parameter H_FRONT 16; parameter H_SYNC 96; parameter H_BACK 48; parameter H_TOTAL H_ACTIVE H_FRONT H_SYNC H_BACK; // 800 parameter V_ACTIVE 480; parameter V_FRONT 10; parameter V_SYNC 2; parameter V_BACK 33; parameter V_TOTAL V_ACTIVE V_FRONT V_SYNC V_BACK; // 525同步脉冲的极性也要注意VGA 通常是行同步负极性、场同步负极性也就是在同步区间输出低电平。如果你发现显示器提示“无信号”或者画面偏移先检查这两个极性再检查像素时钟是不是准确。像素时钟一般用 PLL 从板载晶振倍频或分频得到50MHz 晶振分频到 25MHz 是最常见的做法。3. 从零跑通工程搭建、引脚分配与上板验证3.1 工程目录与模块划分一个干净的工程目录能让后面改起来省心。我一般会这样组织vga_rom_display/ ├── rtl/ │ ├── top.v // 顶层模块例化所有子模块 │ ├── vga_timing.v // 行场同步与像素计数 │ ├── rom_reader.v // ROM 地址生成与数据读取 │ └── pll_25m.v // 像素时钟生成IP 核 ├── ip/ │ └── rom_1port.v // ROM IP 核 ├── sim/ │ └── tb_top.v // 仿真测试文件 ├── mif/ │ └── rom_init.mif // 图片初始化文件 └── quartus_prj/ // 工程文件顶层模块只做例化和连线不写具体逻辑。vga_timing.v负责产生hsync、vsync和当前像素坐标。rom_reader.v根据坐标算出 ROM 地址并把 ROM 输出的像素数据打一拍送到 VGA 端口。这样分的好处是换图片只改.mif换分辨率只改vga_timing.v里的参数互不影响。3.2 顶层模块连线与引脚分配顶层模块的写法很直接把 PLL、ROM、时序模块和输出端口连起来就行。module top ( input wire clk_50m, // 板载 50MHz 晶振 input wire rst_n, // 复位按键低有效 output wire vga_hsync, output wire vga_vsync, output wire [4:0] vga_r, output wire [5:0] vga_g, output wire [4:0] vga_b ); wire pixel_clk; wire [9:0] h_cnt, v_cnt; wire [18:0] rom_addr; wire [15:0] rom_data; wire visible; // PLL 生成 25MHz 像素时钟 pll_25m u_pll ( .inclk0(clk_50m), .c0(pixel_clk) ); // VGA 时序 vga_timing u_timing ( .pixel_clk(pixel_clk), .rst_n(rst_n), .h_cnt(h_cnt), .v_cnt(v_cnt), .hsync(vga_hsync), .vsync(vga_vsync), .visible(visible) ); // ROM 地址生成 rom_reader u_reader ( .h_cnt(h_cnt), .v_cnt(v_cnt), .visible(visible), .rom_addr(rom_addr) ); // ROM IP 核 rom_1port u_rom ( .address(rom_addr), .clock(pixel_clk), .q(rom_data) ); // RGB565 拆成 R/G/B 三路输出 assign vga_r visible ? rom_data[15:11] : 5d0; assign vga_g visible ? rom_data[10:5] : 6d0; assign vga_b visible ? rom_data[4:0] : 5d0; endmodule引脚分配是上板前最后一道坎。不同开发板的 VGA 接口引脚不同常见的是电阻网络 DAC 接在 GPIO 上。你需要查开发板原理图把vga_hsync、vga_vsync、vga_r[4:0]、vga_g[5:0]、vga_b[4:0]一一对应到实际引脚。Quartus 里在 Pin Planner 里填Vivado 里在 XDC 文件里写。分配错了不会烧板子但画面会花屏或者完全没显示。3.3 仿真验证与上板调试上板之前我强烈建议先跑一遍仿真。写一个简单的 testbench给 50MHz 时钟和复位观察hsync、vsync和vga_r/g/b的波形。重点看三个地方行同步周期是不是 800 个像素时钟场同步周期是不是 525 行可见区域的 RGB 输出是不是和.mif里的数据对得上。// 简化 testbench initial begin clk_50m 0; rst_n 0; #100 rst_n 1; #1000000 $stop; end always #10 clk_50m ~clk_50m; // 50MHz仿真通过后再上板。上板时如果画面偏了调H_BACK和V_BACK如果颜色不对检查 RGB565 的位拼接顺序如果画面闪烁检查像素时钟是不是稳定在 25MHz。这些排查思路在下一章会展开。4. 避坑与排查ROM 初始化、时序偏差、颜色错位怎么查4.1 现象显示器提示“无信号”但 FPGA 已经烧录成功原因通常是行场同步信号没有输出或者极性反了。先确认hsync和vsync在仿真里有没有正常翻转再检查引脚分配是不是接反了。有些开发板的 VGA 接口把行场同步标成HS和VS别和H_SYNC、V_SYNC搞混。解决方法是拿示波器或者逻辑分析仪量一下实际引脚看有没有 31.5kHz 左右的行频和 60Hz 左右的场频。4.2 现象画面显示出来了但图片是斜的或者错位这是 ROM 地址计算和 VGA 可见区域没对齐。常见错误是地址计数器在消隐区也递增导致可见区域的第一行读到了上一行末尾的数据。检查rom_addr是不是只在visible为高时才用v_cnt * H_ACTIVE h_cnt计算消隐区是不是给了固定值。另外确认H_ACTIVE和图片宽度一致图片是 640 宽就不能写成 800。4.3 现象颜色整体偏红或偏蓝RGB565 的位拼接顺序错了。rom_data[15:11]是红色[10:5]是绿色[4:0]是蓝色这个顺序不能乱。如果你在 Python 脚本里拼成了 BGR显示出来就是红蓝互换。解决方法是拿一张纯红、纯绿、纯蓝的测试图分别跑一遍看输出对不对。4.4 现象画面有轻微抖动或撕裂像素时钟和 VGA 时序不匹配。检查 PLL 输出的时钟是不是准确的 25.175MHz有些板子用 25MHz 也能显示但长时间看会有轻微抖动。另外确认pixel_clk是不是只驱动了 VGA 相关逻辑没有混入其他高频逻辑。如果 ROM 读数据有延迟记得在 RGB 输出前打一拍保证时序收敛。4.5 现象换了一张图片后 ROM 编译报错.mif文件的DEPTH和 ROM IP 核配置的深度不一致。比如图片是 640×480DEPTH应该是 307200但 IP 核里可能还留着上一次的 800×600 的 480000。改 IP 核配置重新生成或者直接改.mif里的DEPTH值两边必须一致。另外地址位宽也要够307200 需要 19 位地址IP 核里地址位宽设成 18 位就会溢出。5. 二次开发换分辨率、加多图切换与动态效果的改造思路5.1 换分辨率从 640×480 到 800×600 要改哪些地方换分辨率不是改一个参数就完事涉及四个地方联动。第一vga_timing.v里的H_ACTIVE、V_ACTIVE和对应的前后沿、同步脉冲参数要按新标准改。800×60060Hz 的像素时钟是 40MHz行总周期 1056场总周期 628。第二PLL 要重新配置输出 40MHz 而不是 25MHz。第三ROM 的深度要改成 800×600480000地址位宽至少 19 位2^19524288。第四.mif文件要重新生成图片也要缩放到 800×600。我一般会把这些参数集中到一个vga_config.vh头文件里用define定义换分辨率时只改这一个文件。这样比散落在各个模块里改要靠谱得多。5.2 多图切换用按键或状态机控制 ROM 地址偏移如果 ROM 里存了多张图片比如四张 640×480 的图总深度就是 4×3072001228800地址需要 21 位。切换图片的本质就是给 ROM 地址加一个基址偏移。// 多图切换按键选择图片基址 reg [1:0] img_sel; always (posedge pixel_clk or negedge rst_n) begin if (!rst_n) img_sel 0; else if (btn_pressed) img_sel img_sel 1; end wire [20:0] base_addr img_sel * 307200; wire [20:0] rom_addr_full visible ? (base_addr v_cnt * H_ACTIVE h_cnt) : 21d0;这里btn_pressed需要做按键消抖否则按一次会跳好几张。消抖可以用计数器实现检测到按键稳定 20ms 后再产生一个时钟周期的脉冲。img_sel从 0 到 3 循环对应四张图的基址。注意 ROM 深度要够地址位宽也要相应加大。5.3 动态效果用计数器做图片滚动或淡入淡出静态图片显示跑通后可以加一点动态效果。最简单的滚动效果就是让 ROM 地址在行方向加一个偏移量偏移量随时间递增。// 水平滚动每帧偏移量加 1 reg [9:0] scroll_x; always (posedge vsync or negedge rst_n) begin if (!rst_n) scroll_x 0; else scroll_x scroll_x 1; end wire [9:0] h_shift (h_cnt scroll_x) % H_ACTIVE; wire [18:0] rom_addr visible ? (v_cnt * H_ACTIVE h_shift) : 19d0;scroll_x在每个场同步到来时加 1h_shift是加上偏移后对宽度取模保证地址不越界。这样画面就会水平滚动。淡入淡出稍微复杂一点需要把 RGB 值乘以一个衰减系数系数随时间变化。可以用乘法器或者查表实现但要注意时序乘法器会引入延迟需要在 RGB 输出前对齐。5.4 验证方法用仿真波形和上板对比每次改完我都会先跑仿真看波形。重点看三个信号rom_addr在可见区域是不是连续递增vga_r/g/b是不是和.mif里的数据一致hsync和vsync的周期是不是符合新分辨率的参数。仿真通过后再上板上板时先显示测试图确认颜色和位置都对再换正式图片。从那以后我每次改 VGA 时序参数都强制走一遍“仿真看波形 → 上板看测试图 → 换正式图”的流程不再直接烧板子碰运气。希望帮到你。本文还有配套的精品资源点击获取