FPGA实现ROM存储图片的VGA显示
在数字系统教学和嵌入式开发实践中,如何让一块FPGA“画出”一张图片,始终是一个既基础又富有启发性的课题。不同于依赖操作系统或处理器干预的传统方案,纯硬件逻辑驱动图像显示的过程,能让人真正触摸到“像素是如何被点亮”的底层机制。
设想这样一个场景:你手中没有SD卡、也不接摄像头,甚至连CPU都不启用——仅靠FPGA内部资源,就能稳定输出一幅预存的Logo图像到显示器上。这正是本文所探讨的核心: 利用片内ROM存储图像数据,结合精确的VGA时序控制,构建一个完全自主运行的图像显示系统 。
这个项目看似简单,实则涵盖了从图像编码、存储建模、地址生成到模拟信号同步的完整链条。它不仅是理解视频生成原理的绝佳入口,也为后续实现更复杂的图形叠加、动态刷新等功能打下坚实基础。
要实现静态图像显示,首要问题就是—— 图像数据存在哪?怎么读?
答案是:ROM(只读存储器)。但在FPGA中,ROM并非独立芯片,而是由可编程逻辑单元或专用Block RAM(BRAM)构成的功能模块。它的本质是一张查找表,每个地址对应一个像素值。上电后无需加载,直接按地址读取即可输出颜色信息。
实际设计中,我们通常使用Xilinx的Block Memory Generator IP核,或通过Verilog描述一个带初始化文件的单端口RAM结构。关键在于,必须提前将图片转换为
.coe
(Coefficient File)格式,例如:
memory_initialization_radix = 16;
memory_initialization_vector =
ff, fe, fd, fc,
fb, fa, f9, f8,
...;
这个文件定义了所有像素的初始值,综合工具会将其“烧录”进生成的ROM模块中。以640×480灰度图为例,共需307,200个字节。若采用BRAM实现,Xilinx Artix-7等主流器件完全可以容纳,且访问延迟仅为一个时钟周期。
这里有个工程经验:当图像尺寸超过256×256时,务必优先使用BRAM而非LUT搭建ROM。否则不仅消耗大量逻辑资源,还可能因布线延迟导致时序违例。此外,为了节省空间,可对原始图像进行量化处理——比如将8位灰度压缩为3位(8级灰阶),视觉效果依然清晰可辨。
有了图像数据,下一步就是把它“按时按点”送出去。这就是VGA时序的核心任务。
尽管HDMI已成为主流,但VGA因其协议简洁、无需训练序列、兼容性强,在实验平台中仍不可替代。其基本分辨率640×480@60Hz要求像素时钟为25.175MHz,采用逐行扫描方式工作。每一帧画面分为有效区与消隐区,而HSYNC(行同步)和VSYNC(场同步)信号则用于告知显示器当前扫描位置。
具体参数如下:
| 参数 | 数值(像素/行) |
|---|---|
| 行总长 H_Total | 800 |
| 有效行 H_Active | 640 |
| 水平同步脉冲 | 96 |
| 水平前沿 | 16 |
| 水平后沿 | 48 |
| 帧总高 V_Total | 525 |
| 有效列 V_Active | 480 |
| 垂直同步脉冲 | 2 |
| 垂直前缘 | 10 |
| 垂直后缘 | 33 |
这些数值并非随意设定,而是源自CRT时代的电子束回扫时间需求。虽然现代LCD已无此物理限制,但仍需严格遵循,否则显示器无法锁定信号。
在FPGA中,我们用两个计数器
h_cnt
和
v_cnt
跟踪当前坐标。每当
h_cnt
达到800,就归零并递增
v_cnt
;当
v_cnt
达到525,也归零重置。基于这两个计数器,可以实时判断是否处于可视区域,并生成低电平有效的同步信号。
下面是典型的Verilog实现片段:
module vga_timing(
input clk_25m,
input rst_n,
output reg [9:0] h_cnt,
output reg [9:0] v_cnt,
output reg hsync,
output reg vsync,
output reg de
);
parameter H_ACTIVE = 640;
parameter H_FRONT = 16;
parameter H_SYNC = 96;
parameter H_BACK = 48;
parameter H_TOTAL = 800;
parameter V_ACTIVE = 480;
parameter V_FRONT = 10;
parameter V_SYNC = 2;
parameter V_BACK = 33;
parameter V_TOTAL = 525;
always @(posedge clk_25m or negedge rst_n) begin
if (!rst_n) begin
h_cnt <= 0;
v_cnt <= 0;
end else begin
h_cnt <= h_cnt + 1;
if (h_cnt == H_TOTAL - 1) begin
h_cnt <= 0;
v_cnt <= (v_cnt == V_TOTAL - 1) ? 0 : v_cnt + 1;
end
end
end
always @(*) begin
hsync = (h_cnt >= H_ACTIVE + H_FRONT) && (h_cnt < H_ACTIVE + H_FRONT + H_SYNC) ? 1'b0 : 1'b1;
vsync = (v_cnt >= V_ACTIVE + V_FRONT) && (v_cnt < V_ACTIVE + V_FRONT + V_SYNC) ? 1'b0 : 1'b1;
end
assign de = (h_cnt < H_ACTIVE) && (v_cnt < V_ACTIVE);
endmodule
其中
de
(Data Enable)信号尤为关键——它标记了哪些时刻输出的是有效像素。只有当
de == 1
时,才应使能ROM读取和RGB输出。否则,在消隐期内继续驱动色彩信号可能导致显示器误判或出现边缘噪点。
整个系统的运作流程其实非常直观: 每来一个像素时钟,就计算一次当前位置,查一次ROM,输出一个颜色值 。
系统架构可简化为以下数据流路径:
VGA Timing → (h_cnt, v_cnt)
↓
Address Generator → ROM Addr
↓
ROM Data ← 初始化.coe文件
↓
RGB Driver → VGA_R/G/B
地址生成是最容易被忽视却极其关键的一环。对于一幅按行主序排列的图像,其地址公式为:
addr = v_cnt * 640 + h_cnt;
注意这里的乘法操作。虽然640是常数,但在硬件中仍需考虑实现方式。一种优化做法是改用移位加法:
// 640 = 512 + 128 → 左移9位 + 左移7位
addr = (v_cnt << 9) + (v_cnt << 7) + h_cnt;
这样可以避免调用乘法器,减少逻辑层级与时序压力。
至于色彩输出部分,如果是3位灰度图,则可直接将3-bit数据接入电阻网络形成阶梯电压。典型R-2R网络或简单的分压电阻即可满足VGA电平要求(约0.7V满幅)。若想支持彩色,有两种常见升级路径:
- 扩展ROM位宽 :将每个像素存储为12位(如4:4:4 RGB),直接输出三通道;
- 引入调色板机制 :ROM只存颜色索引(如4位),再通过另一块小型LUT映射成真实RGB值。
后者尤其适合图标、菜单等有限色图像,能在极小资源开销下呈现丰富色彩。
在实际调试过程中,有几个“坑”几乎每位开发者都会踩到:
- 图像偏移或错位 :通常是地址计算错误,尤其是跨行边界时未正确处理。
- 屏幕闪烁或抖动 :可能是同步信号极性反了,或者时钟不稳定。
- 全屏绿色/红色条纹 :常见于RGB引脚接反,或位宽匹配错误。
-
只显示半幅图像
:检查ROM深度是否足够,以及
de是否过早失效。
建议初学者先用程序生成一张渐变色块测试图(如水平方向从黑到白),验证地址与输出关系正确后再加载真实图片。
另一个实用技巧是添加LED指示灯监控
de
信号状态:当LED亮起时代表正在输出有效像素,灭时表示进入消隐期。这种“可视化调试”手段能快速定位时序异常。
回到最初的问题:为什么选择FPGA来做这件事?
因为它提供了一种 极致可控的显示路径 。整个过程没有任何中断、缓存交换或多任务调度,完全是确定性的硬件流水线。每一个像素都在精确的时间窗口内被取出并送出,从根本上杜绝了画面撕裂或延迟波动。
更重要的是,这种设计思路具有很强的延展性。一旦掌握了基础框架,就可以轻松拓展出更多功能:
- 多图切换:通过增加地址偏移或选择信号,实现多张Logo轮播;
- 按键交互:接入按键控制图像滚动或局部放大;
- 动态内容:配合外部SPI Flash,实现图像动态加载;
- 更高分辨率:迁移到800×600甚至1024×768,只需调整时序参数与ROM规模;
- 视频叠加:在同一画面上叠加字符OSD或状态指示符。
这些进阶应用的本质,仍然是“地址+时序+数据流”的组合变化。
如今,尽管GPU和嵌入式Linux方案在多媒体处理中占据主导地位,但FPGA在特定场景下的价值依然无可替代——尤其是在需要超低延迟、硬实时响应或高度定制化的工业控制、医疗设备和航空航天领域。
而这个看似简单的“图片显示”项目,恰恰是通往这些复杂系统的起点。它教会我们的不只是代码和电路,更是一种思维方式: 如何把抽象的信息,一步步转化为肉眼可见的光与影 。
当你第一次看到那幅亲手编码的图像稳稳地出现在屏幕上时,那种成就感,或许正是每一位硬件工程师最初的热爱所在。
2339

被折叠的 条评论
为什么被折叠?



