资讯动态

从流水灯到彩色图像:FPGA显示时序设计与Verilog实现

发布时间:2026/9/16 10:12:37 来源:尧图企业网站定制
简介一份面向电子工程、计算机科学专业高年级本科生与研究生的FPGA综合工程包围绕ZYBO开发板上的彩色图片显示覆盖Verilog HDL编码、Vivado设计验证到板级下载的完整实现流程适合具备数字电路与Verilog基础、希望深入图像处理和嵌入式系统开发的读者研习也可用于课程设计、毕业设计或竞赛备赛。包内共467个文件约42.7MB以46个Verilog源文件为核心辅以xdc约束文件、xci IP配置、do/sh仿真脚本及rpt/log综合报告目录结构完整清晰便于按功能模块检索。资料目前已有109人学习工程代码的模块划分、像素时序、存储与接口设计均可直接对照方便复现和二次开发。针对调试与综合阶段压缩包保留了仿真工程配置、编译运行脚本和运行日志能帮助读者按Vivado流程排查问题真正获得从源码到bit文件落地的完整实践参考。1. 从流水灯到彩色图像ZYBO 上的 Verilog 显示工程到底长什么样流水灯写一百遍不如让屏幕显示一张彩色图片来得直接。这个工程在 Digilent ZYBO 上用 Verilog 读出一张 24 位彩色位图按 720p 的 VGA/HDMI 时序把 RGB 像素送到显示器。它没有停留在点灯这一层次而是铺开了完整像素链路图片预处理、BRAM 存储、像素时钟生成、行场同步、上板验证还附带可综合 RTL 与命令行仿真脚本。适合刚开始接触 FPGA 图像处理的学生也适合做显示项目的工程师参考像素时钟与调试接口的排布。下面把工程骨架、数据预处理、时序生成和调试手段拆开讲。2. Vivado 工程骨架与 BMP 转 COE 的数据预处理链路拿到工程先别急着开 Vivado先看文件结构。通常它分为 srcRTL、sim仿真脚本、xdc约束和 ipIP 核目录顶层模块负责例化时钟、timing 生成和图像数据通路三个模块各管一段。分层方式是几乎所有 FPGA 图像工程的共同套路数据源、时序产生、输出接口互不干扰出问题时能直接定位到具体模块。2.1 工程里那几个 bat 脚本是干什么的工程里出现的 compile.bat、elaborate.bat、simulate.bat是 Xilinx 仿真器 xsim 的命令行流程封装。Vivado 里点 Run Simulation 本质也是调这一组工具只是 GUI 把这些操作隐藏了。compile 阶段调用 xvlog 或 xvhdl 把源文件编译成库文件elaborate 阶段用 xelab 把顶层设计展开、连接各模块simulate 阶段用 xsim 执行仿真并产生波形。拆成三个 bat 的好处是改一个源文件后只需要重跑 compile.bat不需要重新 elaborate 整个工程。在 CLI 里跑仿真比开 Vivado GUI 轻量得多尤其适合批量回归。一个常见的 compile.bat 长这样echo off call xvlog -i ..\src top.v timing_gen.v pixel_proc.v call xvlog -i ..\sim tb_top.v call xelab tb_top -s sim_top -debug typical xsim sim_top -runall第一行把 RTL 目录下的三个源文件编译进 xsim 的默认库第二行编译 testbench第三行的-s sim_top给仿真快照命名第四行的-runall让仿真跑到$finish自动退出。注意 xvlog 的-i指定 include 目录如果工程里用了include params.vh这个路径必须指向 src。另一个坑是 Windows 下路径分隔符写反斜杠但 xsim 的-i参数两种斜杠都兼容所以不少脚本里混用..\src和正斜杠不是乱写是命令行解析的实际行为。2.2 把 BMP 拆成 BRAM 能读的 coeFPGA 的 BRAM 初始化需要一个事先写好的内存镜像文件。Vivado 的 Block Memory Generator 支持用 coe 文件初始化存储内容。彩色图片本身是文件所以第一步是写脚本把 BMP 转成 coe。这里用 Python 最简单因为 BMP 格式可以用标准库直接解析。2.2.1 Python 脚本行序翻转与通道拆分24 位 BMP 的像素存储顺序是 BGR且行数据从图像最后一行开始存放同时每行长度按 4 字节对齐。如果不做行序翻转上板后图像会上下颠倒。下面这一段可以放进工程目录的 tools/bmp2coe.pyimport struct, sys def bmp_to_coe(bmp_path, coe_path, width, height): with open(bmp_path, rb) as f: data f.read() pixel_offset struct.unpack(I, data[10:14])[0] row_padded (width * 3 3) ~3 # 4 字节对齐 pixels [] for y in range(height): row_start pixel_offset (height - 1 - y) * row_padded for x in range(width): b, g, r data[row_start x*3], data[row_start x*3 1], data[row_start x*3 2] pixels.append(f{(r 16) | (g 8) | b:06X}) with open(coe_path, w) as f: f.write(memory_initialization_radix16;\n) f.write(memory_initialization_vector\n) f.write(,\n.join(pixels) ;\n) if __name__ __main__: bmp_to_coe(sys.argv[1], sys.argv[2], 320, 240)data[10:14]是 BMP 文件头里的像素数据偏移通常为 54但保险起见还是从文件头读出来。row_padded处理了 BMP 每行按 4 字节对齐的规则否则解析大图时会出现一条斜向的错位线。像素值按照 24 位 RGB 打包红占高 8 位、绿占中间 8 位、蓝占低 8 位和 RTL 里拼接顺序保持一致。如果把像素拆开存成三个 8 位宽的 BRAM那 RTL 必须用三个端口并行读再同时拼回去否则 R、G、B 会错开。2.3 Block Memory Generator 配置要点在 Vivado IP Catalog 里搜 Block Memory Generator配置成 Simple Dual Port RAM读端口宽度设置为 24 位读深度等于图像像素数。BRAM 的存储量有限XC7Z010 的 Block RAM 总共约 2.1 Mbit所以图片尺寸要算好320x240 乘 24 位约 1.8 Mbit勉强压进片内 BRAM。想显示更大分辨率只能走 DDR 外存这个在 DMA 小节再展开。配置时把 coe 文件挂到 Load Init File 选项里。读端口的输出寄存器建议使能这样 BRAM 输出会比地址晚一个时钟周期RTL 里取数据时要用寄存器打一拍对齐像素时钟。这个延迟如果不处理画面会出现固定偏移或花边很多人第一次上板都栽在这里。3. 像素时钟与行场同步720p 显示时序的 Verilog 实现图片数据存在 BRAM 里只是第一步真正让它变成屏幕上的画面靠的是连续的像素流。显示控制器每个像素时钟从 ROM 里取一个像素再按标准输出行同步、场同步和数据有效信号。FPGA 里这个模块叫 timing generator它的输出直接决定了图像有没有滚动、偏左偏右或干脆黑屏。3.1 用 Clocking Wizard 生成 74.25 MHz 像素时钟ZYBO 的 PL 侧参考时钟是 125 MHz而 1280x72060Hz 标准要求像素时钟 74.25 MHz。这个频率不是整数倍用计数器分频做不出来必须用 MMCM。在 Vivado 里双击 Clocking Wizard输入时钟填 125 MHz输出时钟填 74.25 MHzMMCM 会自动选择 M/D 参数生成接近的频率。生成后顶层代码里例化这个 IP把 clk_pix 接到 timing 和 BRAM 读端口。MMCM 输出的 locked 信号要接到复位逻辑里。常见做法是全局复位信号取反后与 locked 做与运算这样 MMCM 没锁定时所有逻辑保持复位。如果忽略 locked上电瞬间像素时钟不稳显示器会报无信号。wire clk_pix; wire clk_locked; clk_wiz_0 u_clk ( .clk_out1(clk_pix), .locked(clk_locked), .clk_in1(sys_clk_125m) ); wire rst_n clk_locked ext_rst_n;这个接法把两个复位源合并外部按键复位和 MMCM 锁定状态。只要时钟没稳定rst_n 就是低电平锁定后才释放复位。3.2 720p 时序参数不是背下来是查表在 FPGA 里720p 就是一组精确的计数阈值。以 CEA-861 标准的 1280x720p60Hz 为例实际写 RTL 时只需要一张参数表参数值说明像素时钟74.25 MHz由 MMCM 生成行总数 H_TOTAL1650一个视频行的全部时钟数行有效 H_ACTIVE1280有效像素数行前肩 H_FP110行同步结束到有效数据开始行同步宽度 H_SYNC40hsync 低电平持续时间行后肩 H_BP220有效数据结束到行同步开始帧总数 V_TOTAL750一帧的总行数帧有效 V_ACTIVE720有效行数场前肩 V_FP5场同步结束到有效数据开始场同步宽度 V_SYNC5vsync 低电平持续时间场后肩 V_BP20有效数据结束到场同步开始1650 乘 750 再乘 60正好等于 74.25 MHz。这个等式是显示时序的核心校验像素时钟必须和总行数、总帧数、帧率严格匹配。换分辨率时去查 CEA-861 或 VESA 对应表格不要自己推算前后肩因为这是标准规定好的值。3.3 行场计数器产生 hsync、vsync、de 的核心代码显示时序模块可以写成完全参数化的计数器模块。下面这段是 timing_gen.v 的核心它维护 hcnt 和 vcnt 两个计数器并从中导出三个输出信号module timing_gen #( parameter H_ACTIVE 1280, parameter H_FP 110, parameter H_SYNC 40, parameter H_BP 220, parameter H_TOTAL 1650, parameter V_ACTIVE 720, parameter V_FP 5, parameter V_SYNC 5, parameter V_BP 20, parameter V_TOTAL 750 )( input wire clk_pix, input wire rst_n, output reg hsync, output reg vsync, output reg de, output reg [11:0] px_x, output reg [11:0] px_y ); reg [11:0] hcnt 0; reg [11:0] vcnt 0; wire h_last (hcnt H_TOTAL - 1); wire v_last (vcnt V_TOTAL - 1) h_last; always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin hcnt 0; vcnt 0; end else begin if (h_last) begin hcnt 0; vcnt v_last ? 0 : vcnt 1b1; end else hcnt hcnt 1b1; end end always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin hsync 1b1; vsync 1b1; de 1b0; end else begin hsync (hcnt H_ACTIVE H_FP) (hcnt H_ACTIVE H_FP H_SYNC); vsync (vcnt V_ACTIVE V_FP) (vcnt V_ACTIVE V_FP V_SYNC); de (hcnt H_ACTIVE) (vcnt V_ACTIVE); end end always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin px_x 0; px_y 0; end else if (de) begin px_x hcnt; px_y vcnt; end end endmodulehsync 和 vsync 的表达式直接按行计数器落在哪个区间来判断行同步在有效区加前肩之后持续 H_SYNC 个时钟场同步同理。de 信号是有效区域的使能门只有 de 为高时读出的数据才应该送到显示器的 RGB 数据线上。px_x 和 px_y 是当前有效像素的行列坐标后面做灰度化、边缘检测时都用这两个坐标判断位置。这里全部用非阻塞赋值hsync、vsync、de 在同一个时钟沿更新避免组合逻辑毛刺。ZYBO 的 HDMI 输出由 ADV7511 芯片编码 TMDS它需要的恰好是这种带 de、hsync、vsync 的并行 RGB 输入所以这个模块的输出可以直接接到 HDMI 输出接口。4. 从 ROM 到屏幕颜色通道处理与 DMA 大图搬运时序正常后屏幕显示的是 RAW 像素流。这个阶段就可以往数据通路上插各种处理模块。像素流是逐行逐像素的串行数据天然适合做点运算和窗口运算这也是 FPGA 图像处理和 CPU 处理图像最大的区别。4.1 灰度化变换用移位替代浮点乘CPU 上做灰度化直接乘浮点系数就行FPGA 上浮点代价太大。常用系数是 0.299、0.587、0.114FPGA 里的近似做法是映射成整数 77、150、29然后右移 8 位相当于除以 256。最大误差不超过 0.2%一个周期就能出结果。module rgb2gray #( parameter DW 8 )( input wire clk, input wire rst_n, input wire [7:0] r, input wire [7:0] g, input wire [7:0] b, output reg [7:0] gray ); reg [15:0] acc; always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc 0; gray 0; end else begin acc r * 8d77 g * 8d150 b * 8d29; gray acc[15:8]; end end endmodule这个模块故意加了一级输出寄存器打断乘法器的组合路径。如果不打这拍r/g/b 输入到 gray 输出之间会有三位乘法、加法再加右移的逻辑深度时序很容易跑不到 74.25 MHz。acc[15:8]等价于acc / 256Verilog 里要小心acc 必须是无符号 reg直接取高字节有符号数右移会补符号位。77、150、29 这三个系数在 Xilinx 7 系列上会映射进 DSP48E1一块 DSP48E1 的乘加结构刚好够用。4.2 边缘检测与滑动窗口三行缓存撑起 3x3 卷积灰度化做完后想再加边缘检测就需要 3x3 滑动窗口。图像行数据串行输入处理当前像素时上一行和上两行的对应像素还没到所以要先缓存两行。常见做法是分布式 RAM 或 BRAM 搭行缓存行深度等于一行有效像素数。第三行数据进来时三个行缓存同时输出同一列三个位置的像素再与卷积核做乘加。Sobel 边缘检测有两个解耦的核核系数Gx[-1, 0, 1; -2, 0, 2; -1, 0, 1]Gy[1, 2, 1; 0, 0, 0; -1, -2, -1]FPGA 实现时把模板系数写成固定常数9 个乘法和 8 个加法在一个周期完成输出梯度幅值后再和阈值比较。关键点在于行缓存的写地址和读地址必须严格差两行否则卷积结果会出现整幅图的斜向错位。仿真阶段不好暴露这种问题因为数据仍然是连续流只有上板看真实图像才能发现边缘是歪的。4.3 突破 BRAM 容量DMA 加异步 FIFO 的大图路径前面提到BRAM 放不下一整帧 720p。要显示大图数据流会改成 Zynq 的 PS 侧 DDR 存储器。PS 端软件把图片从 SD 卡读到 DDR3PL 侧用 Xilinx 的 VDMA 或 AXI DMA 把 DDR 里的帧数据搬到 AXI4-Stream再经过数据宽度转换和异步 FIFO 送到像素时钟域。DDR 控制器工作频率是 533 MHz 甚至更高像素时钟 74.25 MHz两个时钟域频率、相位都不一致中间必须放异步 FIFO。FIFO 写时钟接 DMA 输出的 stream clock读时钟接 clk_pix读侧位宽一次取回 24 位像素。问题经常出在“DMA 传输完成后画面还是花屏”根源往往不是 DMA 本身而是 FIFO 深度不够像素时钟域读空。深度至少要能缓存两行像素否则 de 有效期间拿不到数据画面上就是横条纹。这个数据通路一旦成立720p 甚至 1080p 都能流畅显示灰度化、边缘检测就插在异步 FIFO 读侧之后。BRAM 退化为行缓存不再承担整帧存储容量压力彻底释放。5. 花屏、偏移、黑屏用 ILA 和测试图案定位显示故障最后说调试。显示类工程有一个特点代码不报错但屏幕现象非常直观也骗不了人。学会根据画面现象反推信号问题比闷头看波形快得多。5.1 先对着现象找方向现象最可能的原因排查顺序黑屏无信号像素时钟没输出、de 没拉高先看 MMCM locked再看 de图像整体偏左或偏右H_FP/H_BP 与标准不符核对 hcnt 区间和参数表图像上下滚动vsync 极性或宽度错检查 V_FP/V_SYNC/V_BP彩色噪点密集像素时钟抖动、BRAM 读时序没对齐看 BRAM 输出寄存器是否打拍上下颠倒BMP 行序没翻转改 Python 脚本不要动 RTL红蓝对调通道顺序或拼接位宽错了检查 coe 打包和 {r,g,b} 顺序表中最后一项很容易被忽视。如果 RTL 拼接的是{b,g,r}屏幕上的红蓝就会对调仿真完全看不出问题因为仿真只验证数据流不验证人类视觉。5.2 ILA 的触发条件要落在像素坐标上ILA 是 Vivado 自带的在线逻辑分析仪把要抓的信号在综合前标记为(* mark_debug true *)综合实现后会自动出现在硬件管理器里。抓显示时序时把 clk_pix 作为采样时钟采集 hsync、vsync、de、BRAM 读地址和像素数据。触发条件我一般设成de 1b1 px_y 16d100 px_x 16d0这样能精确抓到第 100 行开头那一小段数据核对 BRAM 地址和像素值是否连续。ILA 采样深度默认 1024抓一两个像素行完全够用。如果抓到的 hsync 和像素数据之间相差几个周期说明数据路径上多了寄存器打拍de 信号也要同步打同样的拍数。这是显示工程里最常见的隐性 bug地址、数据、使能三者没对齐波形上每个信号都正常但合在一起画面就是乱的。5.3 用自检图案把数据问题和时序问题分开在实际显示 ROM 图像之前先让输出级用计数器直接生成彩条或棋盘格。用一个寄存器控制数据源是走测试图案还是走图像 ROM。如果测试图案正常说明时钟和同步部分没问题问题只剩 ROM 读取一侧如果连测试图案都乱先查时钟和时序不用碰数据处理逻辑。我一般会在工程里预留一个 pattern_sel 输入用拨码开关或 PS 寄存器控制。上板调试时先拨到图案档再拨到图像档几分钟就能定位是哪一级的问题。这个习惯在调试 DMA 显示时尤其管用因为它能把 DMA 搬运异常和显示时序异常这两类故障干净地分开。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价