ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA实现ROM存储图片的VGA显示方案:从存储计算到时序对齐

FPGA实现ROM存储图片的VGA显示方案:从存储计算到时序对齐 简介本资源面向FPGA初学者与数字电路设计爱好者提供一套通过ROM存储64×64像素图片并经VGA接口显示的完整工程实践方案帮助读者理解从图像数据生成到硬件显示的完整链路。压缩包共245个文件约1.12MB以hdb、cdb、tdf、vwf等Quartus工程中间文件为主同时包含8个v文件Verilog源码、mif存储器初始化文件、qsf引脚约束、sof与pof下载文件及readme说明覆盖综合、仿真、布局布线与下载全流程。已有1056人学习下载适合作为课程设计或自学练手项目。资源核心价值在于将MATLAB生成mif数据、Verilog编写ROM与VGA控制器、时序信号生成及PLL时钟配置等知识点串联为可运行工程读者可据此掌握像素数据到RGB信号的转换方法并借助仿真波形与工程文件排查显示异常快速完成从数据准备到板级验证的闭环实践。1. 从一块 ROM 说起为什么图片显示要先解决存储问题做 VGA 显示的项目很多人第一反应是「怎么把图显示出来」于是埋头去调时序、算行场同步结果图像要么花屏、要么颜色错位折腾几天才发现根子不在显示模块而在图片数据根本没被正确读出。这个项目的核心其实是一件事把一张图片预先写进 FPGA 内部的 ROM再按 VGA 时序一个像素一个像素地读出来送到屏幕上。它适合正在做数字逻辑课程设计、FPGA 入门毕设的同学也适合想搞清楚「存储—读取—显示」这条链路的从业者。选 ROM 而不是 RAM是因为图片是静态的、只读的用 ROM 综合出来的块存储更省资源、时序更干净这就是这个方案最反直觉也最省事的地方。下面从需求、架构、实现到验收把整套方案拆开讲清楚。2. 需求拆解与技术选型分辨率、色深和存储介质怎么定2.1 先算清楚一张图要占多少存储动手写代码之前必须先把「一张图到底要多少 bit」算明白否则综合时块存储爆掉或者图像被截断都是血泪经验。假设目标分辨率是 640×480色深用 RGB332红绿蓝各占 3、3、2 位共 8 位那么一帧像素总数是 640×480 307200 个每个像素 8 位总共需要 307200 字节约 300 KB。这个数字直接决定了你能不能塞进片内 ROM。通常的做法是入门板卡片内块存储有限640×480 全彩往往放不下所以要么降分辨率比如 320×240存储量降到 1/4约 75 KB要么降色深RGB121 或单色。我一般会先查目标器件的块存储容量再反推能支持的最大分辨率而不是先定分辨率再硬塞。下面这张表是几种常见组合的存储占用方便你直接对照选型。分辨率色深单像素位宽总存储量适用场景640×480RGB3328 bit约 300 KB资源充足的器件320×240RGB3328 bit约 75 KB入门板卡首选320×240RGB1214 bit约 37.5 KB资源紧张时160×120RGB3328 bit约 18.75 KB验证流程用选型逻辑很直接先看器件块存储够不够再决定分辨率和色深。别一上来就追求 640×480 真彩翻车概率极高。2.2 为什么用 ROM 而不是 RAM以及初始化文件怎么来ROM 在这个项目里的角色是「图片仓库」。它的地址线接像素计数器数据线输出像素颜色值只要地址给对颜色就自动出来不需要写使能、不需要读写切换时序上少了一大堆麻烦。RAM 虽然可写但图片是静态的用 RAM 属于杀鸡用牛刀还容易因为读写冲突引入玄学问题。ROM 的内容需要在综合前初始化通常用.mifMemory Initialization File或.hex文件。生成方式一般是用脚本把一张 BMP 或 PNG 图片转成逐像素的颜色数据。下面是一段把图片转成 mif 的 Python 示例逻辑是读图、量化颜色、按地址逐行写出。from PIL import Image # 打开图片并统一到目标分辨率 img Image.open(source.png).convert(RGB).resize((320, 240)) WIDTH, HEIGHT 320, 240 with open(image.mif, w) as f: f.write(fWIDTH{8};\n) # 每个像素 8 位 f.write(fDEPTH{WIDTH * HEIGHT};\n) f.write(ADDRESS_RADIXDEC;\n) f.write(DATA_RADIXHEX;\n) f.write(CONTENT BEGIN\n) addr 0 for y in range(HEIGHT): for x in range(WIDTH): r, g, b img.getpixel((x, y)) # 量化成 RGB332红3位 绿3位 蓝2位 pixel ((r 5) 5) | ((g 5) 2) | (b 6) f.write(f{addr} : {pixel:02X};\n) addr 1 f.write(END;\n)这段脚本的关键在量化那一步r 5把 8 位红压成 3 位g 5压成 3 位b 6压成 2 位再按 RGB332 的位序拼成一个字节。地址从 0 递增和后面 ROM 的读地址一一对应。参数上WIDTH和DEPTH必须和 Verilog 里 ROM 的地址位宽匹配否则综合会报地址越界。图片源建议先用小图验证跑通了再换大图。3. 总体架构设计像素时钟、地址生成与 ROM 读取的配合3.1 三个模块的职责划分整套方案拆成三个模块最清晰时钟分频模块、VGA 时序控制模块、ROM 存储模块。时钟分频负责把板卡晶振常见 50 MHz分出一个 25.175 MHz 的像素时钟640×48060Hz 就靠它VGA 时序控制负责产生行同步、场同步信号以及当前扫描到的像素坐标ROM 存储模块则根据坐标换算出地址把颜色数据吐出来。这里有个容易忽略的点ROM 读数据有一个时钟周期的延迟。也就是说你这一拍给出地址下一拍数据才出来。如果直接把 ROM 输出接到 VGA 颜色线上图像会整体右移一个像素。通常的做法是在时序模块里把行场同步信号也延迟一拍让颜色和同步对齐这个细节后面避坑章节会展开。3.2 地址换算从 (x, y) 到线性地址ROM 是一维的而屏幕是二维的所以必须把像素坐标 (x, y) 映射成线性地址。公式很简单addr y * WIDTH x。在 Verilog 里如果 WIDTH 是 2 的幂比如 512可以直接用移位和拼接省资源但 320 不是 2 的幂就得老老实实做乘法或者用查找表。下面这段是地址生成的核心逻辑。// 像素坐标转 ROM 线性地址 // H_ACTIVE / V_ACTIVE 为有效显示区宽度和高度 wire [18:0] rom_addr; assign rom_addr v_cnt * H_ACTIVE h_cnt; // ROM 例化读延迟一拍 rom_image u_rom ( .address (rom_addr), .clock (pixel_clk), .q (pixel_data) // 8 位 RGB332 颜色 );v_cnt和h_cnt是当前扫描的行、列计数只在有效显示区内累加。H_ACTIVE是每行有效像素数乘出来就是行偏移。注意rom_addr的位宽要够320×240 需要 76800 个地址至少 17 位这里给 19 位留了余量。ROM 例化时clock接像素时钟保证读地址和显示节奏同步。3.3 时序参数640×48060Hz 的完整配置VGA 时序是一堆数字配错一个就黑屏或抖动。640×48060Hz 的标准参数如下这些值来自 VGA 行业规范直接照抄即可。参数行水平场垂直有效像素640480前沿1610同步脉冲962后沿4833总周期800525行同步和场同步都是低电平有效同步脉冲期间拉低。计数器从 0 数到总周期减一落在有效区间的坐标才去读 ROM。这套参数配对了屏幕才会稳定显示配错了就是满屏雪花或者图像滚动。4. 关键模块实现Verilog 代码与逐段说明4.1 时钟分频与像素时钟生成板卡晶振通常是 50 MHz而 640×48060Hz 需要 25.175 MHz 的像素时钟两者不是整数倍关系。工程上一般用锁相环PLL来生成比手写分频器稳定得多。如果器件没有 PLL或者只是做课程设计图省事也可以用计数器分频但频率会有偏差屏幕可能轻微抖动。// 用 PLL 生成 25.175MHz 像素时钟以常见 IP 核为例 // 输入 50MHz输出 pixel_clk 和 locked 信号 pll_vga u_pll ( .inclk0 (clk_50m), .c0 (pixel_clk), // 25.175MHz .locked (pll_locked) ); // 复位信号PLL 未锁定时保持复位避免时序错乱 wire sys_rst_n rst_n pll_locked;这里把pll_locked和外部复位相与是个实用习惯PLL 锁定前输出不稳定如果此时就让时序模块跑起来屏幕会先花一下。等锁定后再释放复位显示就干净了。参数上PLL 的倍频、分频系数由 IP 核向导算好不用手算。4.2 VGA 时序控制模块时序模块是整个方案的心脏它同时产生行场同步和像素坐标。下面这段是精简后的核心逻辑行场计数器各自数到总周期归零同步信号在指定区间拉低。// 行计数0 ~ 799 always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) h_cnt 0; else if (h_cnt H_TOTAL - 1) h_cnt 0; else h_cnt h_cnt 1; end // 行同步96~191 区间拉低 assign hsync ~((h_cnt H_SYNC_START) (h_cnt H_SYNC_END)); // 场计数每行结束加一0 ~ 524 always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) v_cnt 0; else if (h_cnt H_TOTAL - 1) begin if (v_cnt V_TOTAL - 1) v_cnt 0; else v_cnt v_cnt 1; end end // 场同步2~3 区间拉低 assign vsync ~((v_cnt V_SYNC_START) (v_cnt V_SYNC_END)); // 有效显示区标志 assign display_en (h_cnt H_ACTIVE) (v_cnt V_ACTIVE);H_TOTAL是 800V_TOTAL是 525H_SYNC_START是 65664016H_SYNC_END是 752。这些常量建议用parameter定义改分辨率时只改一处。display_en用来控制 ROM 地址是否累加非显示区地址保持不变避免读出无效数据。4.3 ROM 读取与颜色输出对齐前面提过 ROM 有一拍延迟所以颜色输出要和同步信号对齐。做法是把hsync、vsync、display_en都打一拍和pixel_data同步输出。// 同步信号延迟一拍与 ROM 数据对齐 reg hsync_d, vsync_d, de_d; always (posedge pixel_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin hsync_d 1b1; vsync_d 1b1; de_d 1b0; end else begin hsync_d hsync; vsync_d vsync; de_d display_en; end end // 颜色输出非显示区给黑色 assign vga_r de_d ? pixel_data[7:5] : 3b000; assign vga_g de_d ? pixel_data[4:2] : 3b000; assign vga_b de_d ? pixel_data[1:0] : 2b00;pixel_data是 ROM 输出的 8 位 RGB332拆成 3、3、2 位分别送到 VGA 的红绿蓝通道。de_d为低时输出黑色这样消隐区不会出现杂色。这一步对齐做对了图像位置才准确否则会整体偏移一个像素肉眼看得出来。5. 避坑与常见问题排查五个最容易翻车的地方5.1 图像整体右移或下移一个像素现象是显示出来的图比预期偏右或偏下。原因是 ROM 读延迟一拍而同步信号没延迟颜色和坐标错位。解决办法就是把hsync、vsync、display_en都打一拍再输出和pixel_data对齐也就是 4.3 节那段代码。这个坑几乎每个新手都会踩一次。5.2 屏幕花屏或颜色错乱现象是满屏彩色噪点或者颜色明显不对。原因通常是 mif 文件的地址位宽和 ROM 配置不匹配或者量化时位序拼错了。排查方法是先用一张纯色图比如全红生成 mif看屏幕是不是纯红如果不是就是位序问题。RGB332 的位序是{R[2:0], G[2:0], B[1:0]}拼错一位颜色就全乱。5.3 综合报块存储不足现象是综合工具提示 RAM/ROM 资源超限。原因是分辨率或色深定太高片内存储放不下。解决办法是降分辨率或降色深参考 2.1 节的表格重新选型。也可以把 ROM 改成从外部存储读取但那就超出这个方案的范围了。5.4 屏幕抖动或不同步现象是图像滚动、抖动或者显示器提示「无信号」。原因是像素时钟频率不对或者行场总周期参数配错。排查时先用示波器量hsync和vsync的频率640×48060Hz 的行频应该是 31.5 kHz场频 60 Hz。频率不对就回去检查 PLL 配置和时序常量。5.5 mif 文件生成后 ROM 读出来全是 0现象是屏幕全黑ROM 输出恒为 0。原因是 mif 文件路径没被综合工具识别或者文件格式有误。检查方法是看综合日志里有没有「initialized with mif」之类的提示没有就说明文件没加载。另外 mif 的DEPTH必须和 ROM 深度一致ADDRESS_RADIX和DATA_RADIX也要和代码里的进制对上。6. 验收与答辩怎么证明这套方案真的跑通了验收这件事很多人以为「屏幕亮了」就算完其实答辩时老师或评审最想看的是你能不能拿出可复现的证据链。我一般会准备三样东西一份时序波形截图、一组对照实验、一份资源占用报告。先说波形。用仿真工具跑一段把hsync、vsync、display_en、rom_addr、pixel_data都抓出来重点看rom_addr在有效显示区是否连续递增pixel_data是否比地址晚一拍。这张图能直接证明你理解了 ROM 读延迟比嘴上说一百遍都管用。下面是一个简单的仿真激励片段用来产生几个像素时钟观察地址和数据的关系。// 仿真激励给足时钟观察 ROM 读时序 initial begin clk_50m 0; rst_n 0; #100 rst_n 1; // 释放复位 #1000000; // 跑足够长时间覆盖多行 $stop; end always #10 clk_50m ~clk_50m; // 50MHz 时钟这段激励跑起来后在波形窗口里量rom_addr和pixel_data的相位差确认是一拍。参数上#1000000是仿真时长要足够覆盖至少一整帧否则看不到完整的行场周期。再说对照实验。准备两张 mif一张纯色、一张简单图案比如棋盘格。纯色图验证颜色通道对不对棋盘格验证地址映射对不对。如果纯色显示正确但棋盘格错位说明地址换算有问题如果纯色都不对说明颜色位序或 ROM 初始化有问题。这种二分排查法在答辩时特别有说服力评审一看就知道你是系统性地验证过而不是碰运气调通的。最后是资源占用报告。综合完成后工具会给出一份报告里面列出用了多少逻辑单元、多少块存储、多少 IO。把这份报告和 2.1 节的存储估算对照如果实际占用和估算接近说明你的设计是可控的如果差很多就要回头查是不是 ROM 被综合成了分布式存储而不是块存储。答辩时把这份报告一亮等于告诉评审这套方案我心里有数。还有个小技巧答辩现场如果屏幕效果不理想别慌着解释直接切到仿真波形用波形证明逻辑是对的问题可能出在板卡或连线。评审更看重你的分析能力而不是一次成功。从那以后我每次做显示类项目都强制先跑仿真波形再上板这个习惯帮我省了无数个熬夜的夜晚。希望这套方案能帮到你把 ROM 存储图片的 VGA 显示一次做通。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表