
1. 先把这个项目的全局摸清楚1.1 一块开发板、一根VGA线中间其实隔着一段“翻译”我做FPGA教学项目这么多年VGA彩条显示基本是每个人都要过的一关。原因很简单它同时踩中了数字电路里几个最核心的知识点——分频、计数、时序、电平转换。而基于权电阻网络的VGA彩条显示在DE0-CV、DE10-Lite这类教学板上格外典型因为这两块板子把VGA接口和权电阻网络直接做在了PCB上你不需要外接任何模块插上就能看到画面。但很多人第一次做的时候会卡住代码写完了显示器要么黑屏要么颜色乱掉要么画面歪掉一半。问题往往不在Verilog本身而在于没搞明白FPGA输出的数字信号是怎么变成VGA能识别的模拟电压的。这个项目要干的事情说穿了就三件第一用FPGA内部逻辑产生符合VGA标准的行同步信号、场同步信号和像素数据第二把像素数据送到板载的权电阻网络让3.3V的数字电平转换成0到0.7V的模拟电压第三通过VGA接口输出到显示器显示一幅经典的彩条图案。它能帮你把“数字时序”和“模拟接口”这两块拼图拼到一起是入门FPGA视频输出最好的一块敲门砖。适合谁看如果你手上正好有DE0-CV或者DE10-Lite想跑一个不依赖任何外设的完整项目这就是最合适的第一个。如果你只有其他板子本文的时序部分和电阻计算方法同样通用照着改引脚就行。我下面会从原理、计算、代码到调试一层层拆开讲尽量让你看完能直接上手。1.2 为什么第一个图案选彩条而不是别的很多人会问为什么教程清一色用彩条开头而不是显示个“Hello World”或者一张图片。这背后有很实际的考量。彩条的每一根竖条对应一个固定的RGB组合颜色区域是按水平像素坐标硬划分的逻辑极其简单几乎不需要什么算法。这样一来你调试时唯一要操心的变量就只有时序和电路不会因为图像逻辑本身出错而干扰判断。从工程角度看彩条还是一个天然的“自检画面”。标准八色彩条的顺序是白、黄、青、绿、品红、红、蓝、黑这八种颜色刚好覆盖了RGB三通道的全部高低组合任何一路颜色线没接好或者权重不对你一眼就能从颜色错位上判断出来。比如红色那条变成了暗红八成是红色通道的某一位没连上或者权重电阻算错了。这种可视化的反馈比拿示波器一个个量引脚效率高得多。还有一点彩条的像素分布是均匀的每根竖条宽度一致。如果你发现屏幕上竖条宽窄不一那基本可以断定像素时钟不稳或者行计数边界算错了。所以我一直建议新手把彩条当作调试的第一个画面它的信息密度足够高问题暴露得足够直接。1.3 开发板和工具链的取舍DE0-CV和DE10-Lite这两块板子我都用过它们的共同点是都板载了VGA接口和配套的权电阻网络主时钟都是50MHz都用Quartus作为开发工具。区别在于DE0-CV用的是Cyclone VDE10-Lite用的是MAX 10引脚分配和PLL IP核的配置界面略有不同但代码本身几乎可以通用改一下引脚约束和PLL参数就能互相移植。工具链上我推荐直接用Quartus PrimeLite版对这两块板子完全够用。为什么不推荐在代码里手写分频器而是用PLL因为50MHz除2得到25MHz虽然看起来简单但VGA的像素时钟对抖动有一定要求用PLL产生的时钟相位更干净而且后续想改分辨率时只改PLL参数就行不用动逻辑代码。这个习惯我从教学项目一直保持到实际产品属于典型的“前期多花十分钟后期少踩十个坑”。在动手写代码之前我建议先把板子的用户手册翻到VGA那一页把VGA_R、VGA_G、VGA_B各自对应的引脚号抄下来。这一步千万别省因为不同批次的板子引脚定义偶有调整照着手册或者官方引脚约束文件来比在网上抄别人的代码靠谱得多。2. 权电阻网络FPGA和VGA之间的那位翻译官2.1 VGA的RGB为什么偏偏是模拟信号要理解权电阻网络存在的意义得先搞清楚VGA接口为什么需要它。VGA是DE-15接口一共15个针脚里面真正跟图像有关的核心信号是五根红色、绿色、蓝色三路颜色信号加上行同步和场同步。关键在于这三路颜色信号不是数字的高低电平而是模拟电压范围大概是0到0.7V。电压高低直接对应颜色的明暗深浅0V对应这个颜色完全不亮0.7V对应最亮。那为什么设计成模拟信号因为VGA诞生的年代显示设备主要是CRT显示器电子束打在荧光粉上的亮度本来就是连续可调的模拟电压控制起来最自然。后来的LCD虽然内部是数字的但为了兼容显示器入口仍然保留了模拟输入和内部ADC。所以哪怕你手里是台液晶显示器VGA口的RGB针脚接收的依然是被当作模拟量处理的电压。FPGA的GPIO只能输出3.3V或者0V这种数字电平直接接上去要么全亮要么全灭中间无数种灰度根本表达不出来。这就需要一个中间器件把数字的“0/1组合”翻译成不同的模拟电压。DAC数模转换器是标准答案但专用的视频DAC芯片成本高、外围复杂。对于彩条这种对精度要求不苛刻的场景用几个电阻搭一个权电阻网络就够了。这也是教学板愿意把它做在PCB上的原因——成本几毛钱效果足够用还顺便教会你一个经典电路。2.2 从3.3V到0.7V分压电阻到底怎么算我先从最简单的一位颜色说起把账算清楚。假设FPGA某个颜色引脚输出高电平时是3.3V低电平时是0V。显示器那一端每个颜色通道内部都有一个75Ω的对地终端电阻这是VGA规范规定的用来做阻抗匹配。所以从FPGA引脚到地实际上是一个“板载电阻 显示器75Ω”的串联分压电路。我们希望FPGA输出高时75Ω上的电压接近0.7V。设板载串联电阻为R那么分压公式是V_out 3.3 × 75 / (R 75)让它等于0.7V解方程R 75 3.3 × 75 / 0.7 353.6所以R约等于278Ω。取标准值就是270Ω。这就是很多老教程里RGB各串一个270Ω电阻的来源一个电阻搞定一位颜色原理非常清爽。但270Ω这个值有个前提你得确认显示器端确实有75Ω终端。有些老式显示器或者转接头内部终端值会有偏差实测出来的亮度就不太对。我一般的做法是先用270Ω试然后拿示波器量一下高电平时的实际电压如果偏离0.7V太多就在附近的值里微调比如220Ω或者330Ω。不过对于彩条这种只看“有没有颜色”的场景270Ω和330Ω的差异肉眼几乎看不出来不用太纠结。注意如果你用的是没有内置75Ω终端的设备比如某些采集卡或者自制接口分压计算里就没有那个75Ω了电阻值要重新算。这种情况要先确认对端的输入阻抗。2.3 多位色深权重电阻怎么排一位颜色只能表达“亮”和“灭”两种状态三个通道合起来一共8种颜色就是最朴素的八色。这对彩条来说已经够用了但如果你想显示渐变、图像或者小游戏就需要更多色深。这时候就要用多位权电阻网络DE0-CV和DE10-Lite的板载VGA电路就是按每色4位设计的也就是红绿蓝各4根引脚一共12根。原理是这样的假设红色通道有4根引脚记为R3、R2、R1、R0权重分别是8、4、2、1。我们给每根引脚接一个电阻让它们的电流贡献也按这个比例变化。因为电流和电阻成反比想要8:4:2:1的权重电阻就要反过来按1:2:4:8排。也就是R3接最小的电阻R0接最大的电阻。四路电流在同一个节点上叠加再流过显示器内部的75Ω就得到一个阶梯状的模拟电压。4位能组合出16个电压台阶红绿蓝各16级合起来就是16×16×16约4000种颜色肉眼看着已经相当丰富了。具体阻值怎么定我们希望在四位全为1时输出电压约0.7V。把显示器75Ω当作负载用节点电压法列方程最后能解出主电阻R对应最高位R3的电阻大概在500到700Ω之间。取标准值一种常见的排法是R3用680ΩR2用1.3kΩR1用2.7kΩR0用5.1kΩ近似满足1:2:4:8的权重。这几个值都在E24标准系列里容易买到。因为板载电路已经做好了你不需要自己焊但理解这个计算过程很重要——万一哪天你要自己做一块转接板这套账就是现成的。实操心得权电阻网络的精度取决于电阻的匹配误差。1%精度的金属膜电阻就够用5%的碳膜电阻会出现颜色轻微偏色虽然彩条场景不明显但如果你要做图像显示建议直接上1%。另外布线时四路电阻到VGA引脚的走线尽量等长、靠近减少干扰。2.4 为什么开发板不直接上专用DAC说到这里可能有人会问既然权电阻有误差为什么不干脆用一颗专用视频DAC芯片。这其实是成本、教学和复杂度三者的平衡。专用DAC虽然精度高、支持更高色深但需要额外的配置接口、供电和外围滤波电路对入门项目来说是过度设计。权电阻网络用几个普通电阻就能实现正好把“数字到模拟”这个抽象概念变成一个看得见摸得着的电路教学价值远超过它的性能缺陷。等你以后做真正的视频产品再上专用DAC也不迟那时候你会对这个简单网络理解得更透。3. VGA时序640×48060Hz的每一个数字从哪来3.1 行场扫描的四个区间VGA的显示过程本质上是“逐行扫描”——显示器一行一行地画画完一帧再从头开始。FPGA要做的就是精确地告诉显示器“现在该画第几行第几个像素”。这个“精确”靠的是行同步和场同步信号以及一套约定好的时间参数。以最经典的640×48060Hz为例我把完整参数列成表后面写代码时直接照着填。参数行方向像素场方向行有效显示区640480前沿Front Porch1610同步脉冲Sync Pulse962后沿Back Porch4833一个完整周期800525理解这张表的关键在于“四个区间”的概念。一行的时间不是640个像素就结束了而是分成四段有效显示区负责真正输出像素其余三段都不显示内容。前段前沿是有效区结束到同步脉冲之间的空档同步脉冲是告诉显示器“这一行结束了、准备下一行”后段后沿是同步脉冲结束后到下一行有效区开始之间的缓冲。场方向同理只是单位从像素变成了行。为什么要留这些空档因为电子束或者显示器的时序需要时间回扫。虽然LCD没有物理回扫但为了兼容整个VGA生态这套消隐时间必须保留。总行周期是800像素总场周期是525行这个比例决定了刷新率。顺便说一句640×480的标准刷新率算下来是59.94Hz而不是整数60Hz原因就是25.175MHz这个非整数像素时钟后面会讲。3.2 同步极性为什么是低电平有效VGA的同步信号有极性之分640×48060Hz这个模式的行业约定是行场同步都是负极性也就是同步脉冲期间信号为低电平其余时间为高电平。这个约定从CRT时代沿袭下来显示器会自动检测极性并适配但如果你做的是标准模式就老老实实按负极性来别自己发明。写代码的时候这一点很容易搞反。我见过不少人把同步脉冲写成高电平有效结果显示器要么不认要么画面错位。判断方法很简单如果屏幕显示稳定但整体偏移多半是极性问题如果完全不显示可能是同步信号根本没出来。写极性的时候我习惯用取反的方式表达比如同步信号等于“非落在同步区间内”这样逻辑一目了然不容易绕晕。3.3 像素时钟25.175MHz还是25MHz像素时钟是VGA时序的“节拍器”每来一个时钟沿计数器往前走一个像素。640×48060Hz的标准像素时钟是25.175MHz用这个值算出来的刷新率是精确的59.94Hz。但25.175这个数很尴尬FPGA的PLL很难精确合成通常会合成25MHz或者25.2MHz。这里有个经验绝大多数现代显示器对时钟偏差的容忍度很高5%以内的偏差基本都能自动适配。25MHz相比25.175MHz偏差约0.7%25.2MHz偏差约0.1%两者实测都能稳定点亮我平时直接用25MHz省事。板子上是50MHz晶振正好是25MHz的两倍用PLL分频很干净。如果你用的是DE0-CV或者DE10-Lite在Quartus里调一个ALTPLL IP核输入50MHz输出设成25MHz占空比50%相位0就能直接用。注意别用逻辑分频器写“每来两个50MHz时钟翻转一次”来代替PLL。虽然理论上也是25MHz但组合出来的时钟抖动大、驱动能力弱长走线上容易出问题。视频这种对时序敏感的场景PLL是标配。3.4 消隐区为什么要输出黑色在有效显示区之外的那三段区间FPGA输出的RGB应该是全0也就是黑色准确说是不驱动。原因有两个一是这段区域本来就不显示输出颜色没意义二是如果在这里输出非零可能会和同步信号叠加造成画面边缘出现杂色或者亮线。很多新手画面边缘有一条彩色竖线就是因为没把消隐区清零。我的习惯是在像素生成逻辑里先判断“是否处于有效区”有效区才根据坐标取颜色否则直接给0逻辑简单又不会出问题。这个判断我一般拆成两个信号行有效和场有效两者同时成立时才是真正要输出的像素。写成组合逻辑就是“h_active v_active”然后套在颜色选择外面。这样即使以后做更复杂的图像这套骨架也能直接复用。4. 从计数器到彩条Verilog落地实现4.1 顶层模块和PLL怎么搭代码结构我按“PLL 行计数 场计数 同步生成 彩条生成”五块来组织这样每一块职责单一调试时也容易定位。PLL用Quartus的IP核生成模块叫pll_25m输入是板载50MHz时钟输出clk_pix。顶层把复位、时钟和VGA的几组输出引脚接出来。这里我把代码写完整你可以直接拿去改。module vga_colorbar ( input clk_50m, // 板载50MHz input rst_n, // 低电平复位 output [3:0] vga_r, // 红色4位 output [3:0] vga_g, // 绿色4位 output [3:0] vga_b, // 蓝色4位 output vga_hs, // 行同步 output vga_vs // 场同步 ); // ---------- 640x48060Hz 时序参数 ---------- localparam H_ACTIVE 640; localparam H_FP 16; localparam H_SYNC 96; localparam H_BP 48; localparam H_TOTAL 800; localparam V_ACTIVE 480; localparam V_FP 10; localparam V_SYNC 2; localparam V_BP 33; localparam V_TOTAL 525; // ---------- 像素时钟 ---------- wire clk_pix; pll_25m u_pll ( .inclk0 (clk_50m), .c0 (clk_pix) ); // ---------- 行场计数器 ---------- reg [9:0] h_cnt; reg [9:0] v_cnt; always (posedge clk_pix or negedge rst_n) begin if (!rst_n) h_cnt 10d0; else if (h_cnt H_TOTAL - 1) h_cnt 10d0; else h_cnt h_cnt 1b1; end always (posedge clk_pix or negedge rst_n) begin if (!rst_n) v_cnt 10d0; else if (h_cnt H_TOTAL - 1) begin if (v_cnt V_TOTAL - 1) v_cnt 10d0; else v_cnt v_cnt 1b1; end end这段代码的核心是“行计数到位后场计数加一”。注意场计数的使能条件是“h_cnt走到最后一个像素”这样两者天然同步不会出现某一帧的行数不对。计数器用10位足够覆盖800和525留了余量。4.2 同步信号的生成和极性处理同步信号是在计数器基础上比较出来的。行同步在“有效区前沿”之后拉低持续96个像素场同步在“有效区前沿”之后拉低持续2行。因为是负极性所以最后要取反。// ---------- 同步信号负极性 ---------- reg h_sync_r, v_sync_r; always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin h_sync_r 1b0; v_sync_r 1b0; end else begin h_sync_r ~((h_cnt H_ACTIVE H_FP) (h_cnt H_ACTIVE H_FP H_SYNC)); v_sync_r ~((v_cnt V_ACTIVE V_FP) (v_cnt V_ACTIVE V_FP V_SYNC)); end end assign vga_hs h_sync_r; assign vga_vs v_sync_r; // 有效显示区 wire h_active (h_cnt H_ACTIVE); wire v_active (v_cnt V_ACTIVE);这里我特意把有效区判断单独拎出来方便后面复用。同步信号用寄存器输出而不是直接组合逻辑是为了让输出更稳定避免毛刺。多一级寄存器的延迟对显示完全没有影响但能换来更干净的信号质量这个取舍我觉得很划算。4.3 彩条区域的划分逻辑彩条就是把640个有效像素平均分成8段每段80个像素每段对应一种颜色。判断当前像素落在哪一段用h_cnt除以80取商就行因为80是常数综合器会自动优化成合适的实现。颜色顺序按标准彩条白、黄、青、绿、品红、红、蓝、黑。// ---------- 彩条生成 ---------- reg [3:0] r_d, g_d, b_d; always (*) begin if (h_active v_active) begin case (h_cnt / 10d80) 3d0: begin r_d 4hF; g_d 4hF; b_d 4hF; end // 白 3d1: begin r_d 4hF; g_d 4hF; b_d 4h0; end // 黄 3d2: begin r_d 4h0; g_d 4hF; b_d 4hF; end // 青 3d3: begin r_d 4h0; g_d 4hF; b_d 4h0; end // 绿 3d4: begin r_d 4hF; g_d 4h0; b_d 4hF; end // 品红 3d5: begin r_d 4hF; g_d 4h0; b_d 4h0; end // 红 3d6: begin r_d 4h0; g_d 4h0; b_d 4hF; end // 蓝 default:begin r_d 4h0; g_d 4h0; b_d 4h0; end // 黑 endcase end else begin r_d 4h0; g_d 4h0; b_d 4h0; // 消隐区输出黑色 end end assign vga_r r_d; assign vga_g g_d; assign vga_b b_d; endmodule几位色深这里我用的是4位全亮全灭也就是每个通道要么0x0要么0xF这样8种颜色都是“纯色”看起来最鲜艳。如果你想让颜色柔和一点可以把某个通道设成中间值比如0x8但彩条场景没必要纯色对比更明显更容易检查通道是否都正常工作。4.4 引脚约束别抄错了代码写完最后一步是把信号映射到板子的物理引脚。这一步必须对着开发板的用户手册或者官方示例的QSF文件来。我给个格式示例具体的管脚号请以你手上板子的手册为准。# DE10-Lite / DE0-CV 引脚约束示例管脚号以官方手册为准 set_location_assignment PIN_xx -to clk_50m set_location_assignment PIN_xx -to rst_n set_location_assignment PIN_xx -to vga_r[3] set_location_assignment PIN_xx -to vga_r[2] # ... VGA_G、VGA_B 同理 set_location_assignment PIN_xx -to vga_hs set_location_assignment PIN_xx -to vga_vs我踩过的坑是把vga_r的位序接反了。因为权电阻网络对位序敏感R3和R0接反红色亮度等级会整个倒过来彩条看起来“红是红但亮度不对”。排查的时候我一般先确认最高位和最低位有没有接反再确认红绿蓝三组有没有整组串位。这个检查花不了几分钟但能省下大量折腾时间。5. 上板调试常见问题与排查实录5.1 黑屏、无信号从电源和时钟查起屏幕完全黑、显示器提示“无信号”这是新手遇到最多的状况。排查顺序我建议从后往前推。先确认板子确实在运行电源灯亮、下载成功的标志灯有反应。再看时钟如果你用的是PLL确认PLL有没有锁定有些设计里没接locked信号PLL没锁也不报错但输出时钟是乱的整个逻辑都不工作。我的习惯是把pll的locked信号引到一个LED上点亮就说明时钟正常这一步能排除一大类问题。确认时钟后用示波器或者逻辑分析仪量一下行场同步引脚看有没有方波输出。如果同步信号是死的那问题在计数器或者使能逻辑如果有波形但显示器还是没反应就要看同步频率对不对。一个快速判断方法是量行同步频率应该是约31.5kHz场同步约60Hz。如果频率差很多说明参数填错了回去核对H_TOTAL和V_TOTAL。还有一个容易被忽略的点有些显示器会自动切换到当前有信号的输入源如果你同时插了HDMI和VGA显示器可能默认选了HDMI。这时候在显示器菜单里手动切到VGA就行别急着怀疑代码。我至少在这上面浪费过半小时。5.2 颜色不对、偏色、缺色画面出来了但颜色不对是最常见的第二类问题。如果整个屏幕只有红绿蓝中的一种或者两种说明有通道没接上或者约束写错了重点查那一路的三个引脚。如果颜色“都在”但明显偏色比如白色看起来发黄那基本是权电阻的权重问题可能是某一位没连上导致亮度等级不对。还有一个特别隐蔽的情况颜色整体偏暗。这往往是分压计算里的终端电阻假设不成立或者板载电阻值和理论值有偏差。实测下来只要红绿蓝三路用的是同一套电阻、同样接法即使整体亮度略有偏差颜色之间的相对关系也是对的彩条看起来仍然正常。所以如果你的目标是“能看出八种颜色”偏暗一点完全不用改如果你的目标是精确色彩那就得拿示波器量电压台阶了。我整理了一张排查表遇到颜色问题的可以直接对照现象可能原因排查动作只有单色/缺色某通道引脚未约束或断路量该通道三根引脚波形白色偏黄/偏蓝某通道权重位未接通检查权重电阻焊接和位序整体偏暗终端电阻或分压比偏差示波器量高电平电压颜色顺序颠倒位序接反R3接成R0核对引脚位序5.3 画面抖动、撕裂、偏移画面能显示但抖动或者错位这类问题通常出在时序上。轻微抖动画面左右晃动几个像素多半是像素时钟不稳定如果用了逻辑分频而不是PLL换成PLL基本能解决。画面撕裂或者上下滚动往往是场同步频率不对或者场同步极性和显示器期望的不一致。画面整体偏移、左边或右边缺一条这种是消隐区参数不对。比如后沿设小了有效区起点偏了画面就会整体左移。这种情况不用慌微微调整H_BP或者H_FP一次改几个像素直到画面居中。我一般会留一个参数化的常量方便快速试值。还有一种情况是画面能显示但边缘有一条亮线或者彩色竖条。这几乎可以肯定是消隐区没有清零。回到代码里确认“非有效区输出0”那一句有没有写或者有没有被后面的逻辑覆盖。我自己就遇到过组合逻辑分支写漏消隐区输出了有效区最后一段的颜色结果屏幕右边多一条彩线找了好久才发现。实操心得调试时序时与其盯着显示器干着急不如先拿示波器把行场同步和几个颜色最高位的波形抓下来。波形对了画面基本就对了波形不对改代码比改电路快得多。这个习惯让我把大部分调试时间从“试错”变成了“验证”。5.4 从彩条延伸到下一步彩条跑通之后这个项目的骨架其实已经能支撑很多扩展。你可以把固定的颜色逻辑换成从ROM读数据用像素坐标当地址就能显示图片可以在彩条上面叠加一个由按键控制移动的方块做个小游戏也可以把色深从4位扩到更多位只要板载DAC支持。这里用的行场计数器和消隐处理是通用的换任何内容都不用重写。我自己在把这套代码搬到实际项目时最大的体会是时序参数的表格比代码本身更值得反复核对。代码结构大同小异出问题的地方往往就是某个前沿值填错了一位或者极性弄反了。所以我后来养成一个习惯把时序参数表打印出来贴在显示器旁边改代码时对着表看出错的概率直线下降。最后分享一个我觉得挺有用的小技巧在调试阶段把行有效和场有效分别接到两个LED上或者用板子上的数码管显示当前帧计数。虽然只能看到一闪一闪但能确认计数器是不是在动、帧率是不是正常比盯着黑屏猜要靠谱得多。等画面出来之后再把这些调试输出去掉代码就干净了。