
简介这是一份基于ZYBO开发板的FPGA彩色图片显示完整工程面向具备数字电路与Verilog HDL基础的高年级本科生与研究生重点解决在FPGA上完成彩色图像显示、像素时序控制与硬件验证的工程落地问题。资源共467个文件约42.7MB除46个Verilog源文件与14个VHDL文件外还包含11个MIF初始化文件、11个COE系数文件、10个XDC约束文件以及do/tcl/sh脚本、log/rpt报告、xml工程配置等覆盖源码、IP核、仿真、综合实现、下载调试的完整链路目录结构清晰便于按模块检索与复用。已有109人学习下载。通过这套代码可以系统掌握Vivado开发环境下的FPGA设计流程学习如何编写Verilog驱动彩色图片显示并在ZYBO板卡上实际操作与排错为数字信号处理、嵌入式系统开发及图像处理方向提供可复用的工程范式适合课程设计、毕业设计及进阶自学。1. 从一张彩色图片到 ZYBO 屏幕完整工程到底完整在哪一张 320×240 的彩色照片数据量不到 1.2Mbit刚好塞进 XC7Z010 的片上 BRAM。麻烦的不是存储而是怎么把它按时序变成屏幕上的像素图片要用 Python 转成 FPGA 能加载的格式BRAM 要配成正确的位宽verilog 要生成 VGA/HDMI 的行场同步信号最后还得把 16bit 的 RGB565 送到 TMDS 通道上。ZYBO 这板子在 FPGA 图像处理入门里很典型难点集中在数据准备、时钟关系、跨时钟域处理这三件事上。很多人拿到一个 VGA 驱动就去试屏幕上只有彩条或者雪花点根源基本都是数据侧没接对。这篇按我会做的完整方案从 COE 生成、BRAM 容量核算、像素计数器、XDC 约束到 ILA 在线调试把整条链路写清楚新手能照着复现熟手可以对比参数边界。2. 图片进 FPGA把 PNG 转成 COE 并算清 BRAM 能放多大图2.1 选 RGB565 而不是 RGB888 的理由彩色图片在 FPGA 里有三种常见表达RGB888、RGB565、RGB444。RGB888 每个像素 24bit颜色最准但 BRAM 开销直接多 50%RGB444 只有 12bit硬件上省可颜色量化痕迹明显显示自然风光照片时断层很扎眼。RGB565 是折中方案红色和蓝色各 5bit绿色 6bit正好用满一个 16bit 存储单元读写示例代码写起来也最简单。我一般默认选 RGB565。另一个实际原因是 Vivado 的 BRAM IP 核在 16bit 宽度下可以直接用一整块 36Kb BRAM 存 2048 个像素地址就是像素序号连位拼接逻辑都省了。如果坚持用 RGB888COE 文件里每个像素三字节BRAM 的读地址就要乘 3FPGA 里做乘 3 不是不行但会多占用 LUT 和布线资源对入门级工程不划算。2.2 用 Python 生成 Vivado 可直接加载的 COEBRAM 的初始化可以用 .coe 文件也可以直接写 .mem 二进制Vivado 里都能加载。COE 的好处是纯文本出错了一眼能看出来。下面这段脚本把任意 PNG 缩放后转成 RGB565 的 COEfrom PIL import Image import numpy as np def png_to_rgb565_coe(png_path, coe_path, width320, height240): img Image.open(png_path).resize((width, height)).convert(RGB) data np.asarray(img) with open(coe_path, w) as f: f.write(memory_initialization_radix16;\n) f.write(memory_initialization_vector\n) lines [] for y in range(height): row [] for x in range(width): r, g, b data[y, x] rgb565 ((r 3) 11) | ((g 2) 5) | (b 3) row.append(f{rgb565:04X}) lines.append(,.join(row)) f.write(,\n.join(lines) ;\n) if __name__ __main__: png_to_rgb565_coe(photo.png, image.coe)这里r 3是把 8bit 红色截成高 5 位g 2截成高 6 位b 3截成高 5 位。注意别写成r 0xF8效果一样但移位出来语义更直白和 verilog 里{r[7:3], g[7:2], b[7:3]}的写法能对上。输出的 COE 每行 16 个像素行末是逗号最后一行用分号结尾BRAM IP 核加载时遇到分号才认为数据结束。如果最后一行漏了分号Vivado 会报memory_initialization_vector未正确终止这个错很常见不用慌。脚本默认输出 320×240这是给 XC7Z010 留了余量的尺寸。想直接跑 640×480BRAM 大概率放不下先看完下一节容量核算再改。2.3 容量核算XC7Z010 的 BRAM 上限与两行像素拼接技巧ZC7Z010 的片上 Block RAM 官方标称 2.1Mbit也就是 270 块 36Kb 的 BRAM但实际能用的要扣掉配置、FIFO、跨时钟域缓冲占用的部分。RGB565 下不同尺寸的占用如下图片尺寸像素数数据量RGB565占 XC7Z010 BRAM 比例160×12019,2000.29 Mbit约 14%320×24076,8001.17 Mbit约 56%640×480307,2004.69 Mbit超过总容量放不下所以 640×480 全彩图在 ZYBOXC7Z010 版本上用 BRAM 方案是不现实的要么降分辨率要么走 DDR3。标题工程里如果号称“特别完整”大概率用的是 320×240 图配 BRAM或者做了 DDR3 搬运。DDR3 的读写控制本身又是一个大模块后面第 5 章会提迁移思路。容量不够但分辨率想再高一点时有个取巧做法两行像素拼成一个 32bit 写进 BRAM。因为 BRAM primitive 在 32bit 读出宽度下一个时钟能出两个像素地址线还能省一根。BRAM IP 的写端口设成 32bit读端口也设 32bitCOE 生成脚本里把rgb565两两拼接低位存偶数像素高位存奇数像素。显示时读到 32bit 后拆成pix_odd和pix_even按坐标奇偶选择。这样 BRAM 占用不变但读带宽翻倍为后续接更高速的 HDMI 输出留了余地。3. 显示驱动verilog 像素计数器、VGA 时序与数据地址映射3.1 640×48060 的行场参数别用错 25.175MHz 这个时钟显示器不是一边读数据一边刷屏的它按严格的行列节奏工作。640×48060Hz 的 VESA 标准时序如下参数像素时钟周期数说明水平有效区640真正显示图像的列水平前沿 HFP16行同步前的消隐水平同步脉冲 HSPW96hsync 拉低宽度水平后沿 HBP48行同步后的消隐一行总计800640169648垂直有效区480显示图像的行垂直前沿 VFP10帧同步前消隐垂直同步脉冲 VSPW2vsync 拉低宽度垂直后沿 VBP33帧同步后消隐一帧总计52548010233总行数 525 乘总像素 800 再乘帧率 60得到 25.2MHzVESA 标准里取 25.175MHz。这个来源于早期 NTSC 彩色副载波频率现代显示器对这个频率容差很宽实际工程里用 25MHz 也能正常显示。但 ZYBO 板载晶振是 50MHzPLL 分频出 25MHz 比出 25.175MHz 更干净所以我通常直接配 25MHz。如果后面要做 HDMI 1080p像素时钟跳到 148.5MHz那时 PLL 配置就要重新算了。3.2 用 verilog 计数器生成 hsync/vsync/de行场同步本质是两个计数器行计数器数到 799 归零场计数器在一行结束时加 1数到 524 归零。de数据有效信号由坐标直接判断module vga_timing #( parameter HPIX 640, parameter HFPR 16, parameter HSPW 96, parameter HBPR 48, parameter VLIN 480, parameter VFPR 10, parameter VSPW 2, parameter VBPR 33 )( input wire clk_pix, input wire rst_n, output reg hsync, output reg vsync, output reg de, output reg [9:0] x, output reg [9:0] y ); localparam H_TOTAL HPIX HFPR HSPW HBPR; // 800 localparam V_TOTAL VLIN VFPR VSPW VBPR; // 525 always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin x 0; y 0; end else if (x H_TOTAL - 1) begin x 0; y (y V_TOTAL - 1) ? 0 : y 1; end else begin x x 1; end end always (*) begin hsync ~(x HPIX HFPR x HPIX HFPR HSPW); vsync ~(y VLIN VFPR y VLIN VFPR VSPW); de (x HPIX) (y VLIN); end endmodulehsync 和 vsync 用组合逻辑直接由计数器值推导这段代码里x就是当前行内位置y是当前行号。de 为高时 x 在 0~639 范围恰好对应有效像素。注意x和y在有效去输出图像地址时直接可用不用额外拉时钟沿对齐因为 BRAM 读数据有一个时钟的读延迟数据总线会比地址晚一拍实际工程里要在读地址处提前一拍计算避免显示画面整体右移一像素。参数化写法意味着换分辨率只改 parameter不用动逻辑。如果你拿到一个别人写的写死 800 和 525 的代码别急着抄先改成参数化再调。3.3 BRAM 读地址怎么算图片居中显示时的坐标换算图片 320×240屏幕 640×480直接铺上去只占屏幕左上角。要让图片居中水平方向起始列是 (640-320)/2160垂直方向起始行是 (480-240)/2120。BRAM 的读地址是图片内部坐标不是屏幕坐标wire [9:0] img_x x - 160; wire [9:0] img_y y - 120; wire img_active de (x 160) (x 480) (y 120) (y 360); wire [18:0] rd_addr img_y * 320 img_x; // 320 IMG_WIDTH reg [18:0] rd_addr_r; // BRAM 读延迟打一拍 always (posedge clk_pix) rd_addr_r img_active ? rd_addr : 0; wire [15:0] pix_rgb; bram_image bram_inst ( .clka (clk_pix), .addra(rd_addr_r), .douta(pix_rgb) );img_y * 320 img_x是标准行优先寻址。BRAM IP 建议选“Always Enabled”读模式这样不用单独控制读使能输出引脚始终跟着地址走。地址打一拍的原因在上面提过BRAM 的douta在addra变化后一个时钟才更新如果直接把rd_addr接进去显示出来的图会整体右移和下移一像素边缘还会拖一条颜色错误的斜线。这个错位在静止图片上很容易被忽略但一旦切换图片或者叠加字符位置不对就藏不住了。如果图片没有居中而是从 (0,0) 开始显示rd_addr退化成y * 320 x但 ip 核的 dout 延迟依然存在所以打一拍这步不能省。3.4 TMDS 输出与 ODDR 原语彩色图片最后一步VGA 输出最直接ZYBO 上有 VGA 接口RGB 三路经过板载电阻网络变成模拟信号。但 ZYBO 更常用的是 HDMI 输出FPGA 侧只需要输出 TMDS 差分信号驱动和电平转换由板上的 HDMI TX 芯片完成。TMDS 是串行通道每个像素时钟要把 8bit 颜色编码成 10bit 然后逐 bit 发出所以数据通道的比特率是像素时钟的 10 倍25MHz 像素时钟对应 250Mbps这个速率下时序还得靠 ODDR 原语对齐上升沿和下降沿。HDMI 的 TMDS 编码完整实现有一套 DC 平衡算法不是简单异或代码量不小。工程上两种走法直接用 Xilinx 的 HDMI 1.4 Transmitter IP或者用公开的 TMDS 编码器 Verilog这里给一个最简的 ODDR 数据发送骨架体现数据通道怎么切到比特域// 以蓝色通道为例pix_b 是 8bit 并行像素tmds_b_serial 是串行输出 reg [9:0] tmds_b_shift; always (posedge clk_pix) begin if (de) tmds_b_shift tmds_b_encoded; // 10bit 编码结果 else tmds_b_shift 10b0000000000; // 消隐期控制符号 end genvar i; generate for (i 0; i 10; i i 1) begin : oddr_b_loop ODDR #( .DDR_CLK_EDGE(SAME_EDGE) ) oddr_b ( .Q (tmds_b_p), .C (clk_pix_x10), .CE(1b1), .D1(tmds_b_shift[i]), .D2(tmds_b_shift[9-i]), .R (1b0), .S (1b0) ); end endgenerateclk_pix_x10是像素时钟的 10 倍频可以用 MMCM/PLL 的第二路输出生成。ODDR 的 SAME_EDGE 模式保证了 D1 和 D2 在同一个时钟沿被采样串行输出的每一位都对齐到 10 倍时钟边沿。真正要显示彩色图片编码部分必须补全但这套“并行像素→移位→ODDR→差分输出”的结构是固定的不管编码算法多复杂最终都要落到 ODDR 上。如果屏幕上完全没有图像但 hsync/vsync 有波形先查的就是这一级有没有数据出来。4. ZYBO 板级约束与工程接线XDC、时钟和上电起不来的排查4.1 用 XDC 约束管脚和 TMDS 电平verilog 里写的hdmi_d_p[2:0]、hdmi_clk_p这些端口必须映射到 ZYBO 原理图上 FPGA 的实际引脚否则综合后布局布线根本不知道往哪走。XDC 约束的核心就两类引脚位置和电平标准# ZYBO HDMI 输出实际引脚以 PCB 原理图为准这里为示例结构 set_property PACKAGE_PIN N18 [get_ports {hdmi_d_p[0]}] set_property PACKAGE_PIN M18 [get_ports {hdmi_d_p[1]}] set_property PACKAGE_PIN J18 [get_ports {hdmi_d_p[2]}] set_property PACKAGE_PIN K17 [get_ports {hdmi_clk_p}] set_property IOSTANDARD TMDS_33 [get_ports {hdmi_d_p[*]}] set_property IOSTANDARD TMDS_33 [get_ports {hdmi_clk_p}]TMDS_33是 3.3V TMDS 电平标准ZYBO 的 HDMI 输出走的是板载 TMDS 驱动器FPGA 引脚直接输出差分对即可。注意这里必须同步约束差分负端_n通常写成set_property PACKAGE_PIN M17 [get_ports {hdmi_d_n[0]}]并保证差分对的两个引脚在 bank 内是配对的否则 PnR 会报DIFF_TERM或引脚不配对错误。VGA 就没有这个问题VGA 三路模拟信号是板级电阻分压网络FPGA 侧只输出普通 3.3V 单端信号加同步信号。4.2 Clocking Wizard 与 set_input_delay 用在哪ZYBO 板载 50MHz 晶振接到 FPGA 的 MRCC 引脚进 FPGA 后先用 IBUFG 缓冲再进 MMCM/PLL。Vivado 里的 Clocking Wizard 配置三路输出一路 25MHz 做像素时钟一路 250MHz 做 TMDS 串行时钟一路 100MHz 留作 DDR3 或其他逻辑。关键点是 250MHz 和 25MHz 必须来自同一个 MMCM保证相位关系确定ODDR 采样时不会出现时钟域乱跳。set_input_delay在这个纯输出的工程里一般用不到因为数据是 FPGA 送给显示器的没有外部输入数据需要约束。如果有 SD 卡读取模块SD 卡时钟和数据都是低速的也不涉及 input delay。等到哪天把图像源换成外部 ADC 或者摄像头才需要这样约束set_input_delay -clock clk_pix -max 4.0 [get_ports {pix_data[*]}] set_input_delay -clock clk_pix -min 1.0 [get_ports {pix_data[*]}]这是告诉 Vivado 外部数据相对clk_pix的到达窗口分析时序时用来判断内部触发器能不能稳定采样。别在这个工程里硬凑这条约束没有外部输入的时候写了反而会让时序报告出现一堆违例看着吓人。4.3 烧录起不来时先查这三处位文件生成不了或者下载后屏幕没反应八成不是 verilog 逻辑错而是下面三类低级问题。第一引脚约束写错。PACKAGE_PIN写到了不存在的引脚、差分对正负端不在同一个 bank、IOSTANDARD和原理图不一致Vivado 在 DRC 阶段就会报错。第二种常见是PROG_B或者配置模式跳线不对ZYBO 上电后 SPI Flash 里没有固件时JTAG 下载后 bit 进了 FPGA但断电就丢掉。如果配置模式跳线被拨成 QSPI 而 Flash 是空的上电后 DONE 灯不亮看起来像“烧录起不来”其实是配置源选错。第三是复位逻辑失效rst_n端口悬空或者按键按下才拉低导致状态机卡在复位屏幕上全黑但时钟和行场都有输出。逐条排查顺序先看 DONE 灯亮不亮再看 ILA 能不能采到时钟沿最后才回去翻 verilog 逻辑。5. 用 ILA 验证 RGB 链路抓一拍像素就知道数据通没通5.1 ILA 的参数与触发条件逻辑写完了屏幕如果正常显示这一步可以跳过但凡是画面错位、颜色不对、局部花屏都值得先用 ILA 把内部信号抓出来。ILA 是 Vivado 里的调试 IP 核相当于把一组探针插进 FPGA 内部实时采样。ILA 配置上采样时钟用 25MHz 像素时钟probe 宽度按需选择至少抓这三个信号组信号组位宽用途x, y各 10bit当前屏幕坐标de1bit数据有效标志rd_addr_r19bitBRAM 读地址pix_rgb16bitBRAM 输出像素深度默认 1024 够用采样窗口只要能覆盖从 de 拉高到第一个有效像素输出即可。触发条件建议设成de 1 y 120 x 160也就是图片左上角那个像素出现的前一拍。ILA 的触发比较器支持多条件与把 x 和 y 的精确值填进去运行后用 ILA 的波形视图找到触发点往后数几个时钟pix_rgb应该等于 COE 文件里第一行数据。这一步通过说明图片数据进 BRAM、地址映射、像素输出整条链路是通的。颜色不对就对比pix_rgb和 COE 的 16 进制值RGB565 字节序反了的话红色和蓝色会互换波形里一眼能看出来。5.2 像素比对与 DDR3 迁移时保留的同一套调试思路ILA 在静止图片工程里能做到像素级比对导出数据更简单右键 ILA 波形窗口导出 CSV然后在任意文本编辑器里和 COE 文件逐行对照。不要直接在 ILA 里面用十六进制肉眼看几百行CSV 导出后用脚本比对效率高一个量级。显存如果哪天换成 DDR3这套验证方法原样复用。DDR3 迁移的核心改动只在数据源侧PC 端通过 UART 或以太网把图片写进 DDR3FPGA 里的 AXI Read Burst 读到一块缓冲再经过异步 FIFO 切成像素时钟域的连续数据流。IP 核从 BRAM 换成 AXI BRAM Controller 或 MIG读侧 FIFO 从 16bit 加宽到 32bit而 VGA 时序、de 生成、坐标换算几乎不用动。这也解释了为什么工程要一开始就把时序模块和数据源模块分开写。本文还有配套的精品资源点击获取