简介这份资源面向FPGA初学者与数字电路课程实践者围绕“用ROM存储图片并通过VGA接口显示”这一典型项目展开帮助读者打通从图像数据生成到硬件显示的完整链路。压缩包共245个文件约1.12MB以Verilog源码.v、Quartus工程与综合报告.qpf、.rpt、.cdb、.hdb、仿真波形.vwf、引脚与配置文件.pin、.sof、.pof以及初始化文件.mif为主覆盖工程建立、综合、布局布线与下载各阶段。已有1056人学习下载说明该案例在入门群体中具有较高参考价值。资源内含ROM模块与VGA控制器的可运行代码、MATLAB生成的.mif图像数据、时序约束与仿真文件读者可据此理解像素数据如何映射为RGB信号、同步信号如何按VGA时序产生并借助现成工程快速复现64×64图片显示效果为后续扩展色彩深度与分辨率打下基础。1. 从一块 FPGA 开发板说起ROM 存图 VGA 显示到底能跑出什么效果手头有一块 FPGA 开发板想让它不依赖电脑、不接 HDMI 盒子直接往显示器上输出一张固定图片——这个需求听起来简单但真动手会发现两个绕不开的问题图片数据放哪以及怎么按 VGA 时序把像素一个不差地吐出去。这份资源给的就是一套 Verilog 实现的完整方案把图片转成 ROM 初始化文件综合进片内 Block RAM再用 VGA 时序控制器逐像素扫描输出。它解决的是「无 CPU、无操作系统、纯硬件逻辑驱动显示」这个场景适合正在学 FPGA 图像通路、需要一份能直接上板验证的工程参考的人。关键词 verilog 存储显示说的就是这条从存储到显示的硬连线。2. ROM 初始化与图片格式转换mif 文件怎么生成、参数怎么算2.1 为什么选 ROM 而不是 RAM 或外部 Flash先说选型。图片是静态的上电后内容不变ROM 是最自然的映射——综合工具会把$readmemh或 mif 初始化数据直接烧进 Block RAM不占逻辑资源也不需要写时序。用 RAM 的话你得额外写一套初始化状态机纯属给自己找事用外部 Flash 又要多一层 SPI 读取逻辑对「显示一张固定图」这个目标来说完全是过度设计。真正要算清楚的是容量。一张 640×480 的图片如果按 RGB565 每像素 16 bit 存总数据量是 640×480×16 4,915,200 bit也就是 600 KB。这个数字很关键——大多数入门级 FPGA 的片内 Block RAM 总量在几百 KB 到 1 MB 出头600 KB 已经吃掉相当比例。所以常见做法是降分辨率或降色深320×240 配 RGB565 只要 150 KB或者 640×480 配 8 位索引色调色板另存只要 300 KB。我一般会先查目标器件的 Block RAM 手册确认单块容量和总块数再倒推能接受的图片规格。提示ROM 深度必须是 2 的幂次附近地址位宽由深度决定。深度 76800320×240不是 2 的幂综合工具通常会自动补齐到 131072别被「浪费了一半」吓到这是正常行为。2.2 用脚本把图片转成 mifPython 转换代码手工生成 mif 不现实写个脚本最省事。下面这段把一张图片缩放到目标分辨率再按 RGB565 打包成 mif 格式from PIL import Image def img_to_mif(img_path, mif_path, width320, height240): img Image.open(img_path).convert(RGB) img img.resize((width, height)) # 强制缩放到 VGA 分辨率 pixels list(img.getdata()) with open(mif_path, w) as f: f.write(fWIDTH16;\nDEPTH{width * height};\n\n) f.write(ADDRESS_RADIXUNS;\nDATA_RADIXHEX;\n\nCONTENT BEGIN\n) for addr, (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{addr:06X} : {rgb565:04X};\n) f.write(END;\n) img_to_mif(input.png, rom_image.mif, 320, 240)逻辑说明resize保证像素数和 ROM 深度严格对应否则地址会越界或显示错位。RGB565 的位拼接顺序是 R 在高位、B 在低位这个顺序必须和后面 VGA 模块里拆分的顺序一致两边对不上就会出现「红蓝互换」的经典翻车。ADDRESS_RADIXUNS表示地址用无符号十进制DATA_RADIXHEX表示数据用十六进制这两个声明 Quartus 的 mif 解析器认Vivado 则更常用.coe格式转换思路一样只是文件头写法不同。参数方面width和height必须和 VGA 时序模块里的行、场参数完全一致改一个就要同步改另一个。色深如果从 16 位降到 8 位WIDTH改成 8打包逻辑也要换成调色板索引不能只改一个数字。2.3 在 Verilog 里例化 ROMIP 核配置与读时序mif 生成后用厂商 IP 核向导生成 ROM。以常见流程为例深度填 76800或工具自动补齐后的值位宽 16初始化文件指向刚才的 mif。生成后会得到一个带地址输入和数据输出的模块例化时注意读延迟// ROM 例化1 周期读延迟 rom_image u_rom ( .address (rom_addr), // 像素地址位宽由深度决定 .clock (pix_clk), // 像素时钟和 VGA 扫描同步 .q (rom_data) // 输出 16 位 RGB565 );关键点是address必须在pix_clk上升沿稳定q会在下一个周期才有效。这意味着 VGA 模块取像素时地址要提前一个时钟周期给出否则显示会整体偏移一列。这个「提前一拍」是新手最容易忽略的地方现象是画面左边多一条竖线或整体错位一格。3. VGA 时序控制器行场同步参数与像素时钟的配合3.1 640×48060Hz 的时序参数表VGA 显示的本质是「按固定节奏扫像素」。640×48060Hz 这个模式行业里有标准参数但不同资料给的数值略有出入下面这组是我实际跑通过的参数行水平场垂直有效像素640480同步脉冲962前肩1610后肩4833总周期800525像素时钟 800 × 525 × 60 ≈ 25.175 MHz实际取 25.2 MHz 或 25 MHz 都能点亮差别是刷新率微偏显示器一般能容忍。行计数从 0 数到 799场计数从 0 数到 524只有落在有效区间行 9616 到 9616640场 210 到 210480时才输出真实像素其余时间输出消隐通常给黑色。3.2 行场计数与同步信号生成代码// 行场计数与同步信号 always (posedge pix_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; v_cnt (v_cnt V_TOTAL - 1) ? 0 : v_cnt 1; end else h_cnt h_cnt 1; end end assign hsync (h_cnt H_SYNC) ? 1b0 : 1b1; // 低有效 assign vsync (v_cnt V_SYNC) ? 1b0 : 1b1; // 有效显示区判断 assign display_on (h_cnt H_SYNC H_BACK h_cnt H_SYNC H_BACK 640) (v_cnt V_SYNC V_BACK v_cnt V_SYNC V_BACK 480);逻辑说明行计数到 799 归零并推动场计数这是「一行结束进下一行」的机制。同步信号在计数值小于同步脉冲宽度时拉低注意多数 VGA 同步是低有效写反了显示器直接黑屏无信号。display_on用组合逻辑判断当前是否在有效区只有它为高时才把 ROM 数据送到 RGB 输出否则给 0。参数修改时H_TOTAL、V_TOTAL要和上面的表对应H_BACK、V_BACK是后肩值。如果换分辨率比如 800×600整张表都要重算像素时钟也要跟着变不能只改有效像素数。3.3 把 ROM 输出接到 VGA地址生成与一拍延迟对齐地址生成要和display_on配合。有效区内像素坐标是(h_cnt - H_SYNC - H_BACK, v_cnt - V_SYNC - V_BACK)转成一维地址就是y * 640 x。但前面说过 ROM 有一拍读延迟所以地址要在display_on有效的前一拍就给出// 地址提前一拍生成补偿 ROM 读延迟 always (posedge pix_clk or negedge rst_n) begin if (!rst_n) rom_addr 0; else if (display_on_next) // 下一拍将进入有效区 rom_addr (v_cnt_next - V_SYNC - V_BACK) * 640 (h_cnt_next - H_SYNC - H_BACK); end assign vga_r display_on ? rom_data[15:11] : 5b0; assign vga_g display_on ? rom_data[10:5] : 6b0; assign vga_b display_on ? rom_data[4:0] : 5b0;这里的display_on_next、h_cnt_next、v_cnt_next是当前计数值的「下一拍预测」可以用组合逻辑提前算出来。如果懒得做预测另一个常见做法是把 ROM 地址直接用当前计数值算但输出时整体延后一拍判断display_on效果等价。两种方式选一种别混用混用会出现画面撕裂或偏移。RGB 拆分顺序[15:11]是红、[10:5]是绿、[4:0]是蓝必须和 mif 打包时的顺序严格对应。我见过有人打包时 R 放低位拆分时按高位取结果整张图颜色全反排查半天才发现是位序问题。4. 上板联调避坑黑屏、偏色、错位、闪烁的排查清单4.1 黑屏无信号先查时钟和同步极性现象显示器提示「无信号」或一直黑屏。原因通常是像素时钟没起来或者同步极性写反。解决先用示波器或逻辑分析仪量pix_clk有没有波形PLL 有没有锁定再确认hsync、vsync在非同步期是不是高电平。如果极性反了把判断条件从改成即可。还有一种情况是复位没释放计数器一直停在 0同步信号恒定显示器也认不到。4.2 画面偏色或红蓝互换位序和打包顺序不一致现象图片能显示但颜色明显不对蓝天变红、红字变蓝。原因就是 mif 打包和 Verilog 拆分的位序对不上。解决回到转换脚本确认rgb565的拼接是(r11)|(g5)|b再确认 Verilog 里[15:11]给红、[4:0]给蓝。两边对齐后颜色就正常。如果只是整体偏暗检查是不是把 5 位色值直接当 8 位用了需要左移补低位。4.3 图像整体偏移或左边多一条竖线ROM 读延迟没补偿现象画面整体右移或下移一格或者最左边有一条重复的竖线。原因就是第 3.3 节说的读延迟没对齐地址给晚了。解决把地址生成提前一拍或者把display_on判断延后一拍。判断方法很简单——如果偏移量恰好是一列像素基本就是这个问题。4.4 画面闪烁或滚动场同步参数或时钟频率偏差现象图像能显示但不停滚动或者轻微闪烁。原因可能是场同步脉冲宽度、前后肩和显示器不匹配或者像素时钟频率偏差太大导致刷新率超出显示器容忍范围。解决先核对时序表确认V_SYNC、V_BACK数值再用 PLL 把像素时钟调到 25.175 MHz 附近。如果显示器支持自动同步轻微偏差能自动纠正但偏差超过 1% 就可能滚屏。4.5 综合报 ROM 容量不足深度补齐与位宽取舍现象综合时报 Block RAM 不够或者布局失败。原因是图片数据量超过了器件可用 Block RAM。解决优先降分辨率320×240 比 640×480 省 75% 空间其次降色深RGB565 降到 RGB332 或 8 位索引色最后才考虑换更大容量的器件。别硬扛Block RAM 用满会导致布线困难时序也容易不收敛。5. 进阶技巧用参数化写法让同一套代码适配多种分辨率写到这儿工程基本能跑了。但如果你像我一样今天想显示 320×240明天想换 640×480每次都手改一堆常数很容易出错。我的习惯是把分辨率、时序参数全部做成parameter顶层只改几个数字就能切换。module vga_display #( parameter H_ACTIVE 640, parameter V_ACTIVE 480, parameter H_SYNC 96, parameter H_BACK 48, parameter V_SYNC 2, parameter V_BACK 33 )( input wire pix_clk, input wire rst_n, output wire [4:0] vga_r, output wire [5:0] vga_g, output wire [4:0] vga_b, output wire hsync, output wire vsync ); localparam H_TOTAL H_ACTIVE H_SYNC H_BACK 16; // 前肩固定 16 localparam V_TOTAL V_ACTIVE V_SYNC V_BACK 10; // 前肩固定 10 // ... 其余逻辑用 localparam 替代硬编码 endmodule这样改分辨率时只需在例化处覆盖H_ACTIVE、V_ACTIVE和对应的肩宽H_TOTAL、V_TOTAL自动算出来。但要注意ROM 的深度和 mif 文件必须同步改参数化解决不了数据文件的问题——这是两套东西别指望一个 parameter 全搞定。验证方法上我一般会先跑行为仿真用 testbench 把h_cnt、v_cnt打出来确认计数范围和有效区判断正确再上板。仿真能过、上板黑屏九成是时钟或引脚约束问题这时候查约束文件比改逻辑有用。还有一个实用技巧在 ROM 里存一张测试图比如四角不同颜色、中间画十字的图案。上板后一眼就能看出偏移、偏色、翻转这些问题比拿一张风景图排查快得多。从那以后我每次做 VGA 显示都强制先跑这张测试图确认时序和位序全对了再换正式图片。希望帮到你。本文还有配套的精品资源点击获取