
VGA 这块老接口在 FPGA 圈子里一直没被淘汰原因特别简单它没有握手协议、没有编码器、没有复杂的链路训练你写好时序、接对电阻屏幕上就有画面。这次要拆解的就是最硬核也最有意思的一种点屏方式——不加任何专用视频 DAC 芯片用几个电阻搭一个权电阻网络让 DE0-CV 或者 DE10-Lite 直接把彩条怼到显示器上。整套方案的成本大概两三块钱但能让你把数字逻辑、模拟电平、时序参数这三件事一次性串起来。适合刚学完 Verilog 想找个看得见摸得着的项目练手的人也适合想给学生做实验演示的老师——毕竟彩条这种东西只要颜色一出说明行场时序、像素时钟、DAC 电平全都对了比看波形图直观一百倍。1. 先把彩条这件事拆开VGA 到底需要什么样的信号1.1 为什么在 FPGA 上点屏VGA 依然是性价比最高的选择我试过好几种屏幕方案最后发现给初学者练手VGA 几乎是唯一正确答案。HDMI 需要 TMDS 差分编码、需要处理 DDC 通道还得有专用的发送芯片或者至少一堆原语SPI 的 TFT 小屏虽然便宜但驱动 IC 有几十条初始化命令你还没开始写自己的逻辑就已经被寄存器手册劝退了。VGA 就不一样了。它本质上是五根线R、G、B 三路模拟色信号加 HSYNC、VSYNC 两根同步信号。信号速率低得可怜640×48060Hz 的像素时钟也就 25MHz 出头行同步频率 31.5kHz场同步频率 60Hz——这个量级对任何一块现代 FPGA 来说都是散步。更关键的是时序参数是完全公开的你甚至可以用一张纸把计数器的边界画出来。而且 VGA 用的是模拟电平驱动显示器内部有一个 75Ω 的端接电阻你只要往接口上灌 0V 到 0.7V 的电压屏幕就给你显示对应的亮度。这个0 到 0.7V就是整个项目的物理核心也是权电阻网络存在的全部理由。1.2 640×48060Hz 的时序账一个数都不能错很多人第一次做 VGA 失败不是代码写错了而是时序参数抄错了。我建议你别急着敲键盘先拿笔把这组数字抄一遍参数行方向水平场方向垂直可见区Visible640 像素480 行前肩Front Porch16 像素10 行同步脉冲Sync96 像素2 行后肩Back Porch48 像素33 行总计Total800 像素525 行行方向 640169648 800场方向 48010233 525这两个数是死的。像素时钟 25.175MHz那么行频就是 25.175MHz ÷ 800 ≈ 31.469kHz场频是 31.469kHz ÷ 525 ≈ 59.94Hz也就是大家常说的60Hz。那前肩、后肩这些看不见的部分是干嘛的这是从 CRT 时代留下来的遗产电子束扫完一行之后需要时间回扫到下一行开头这段回扫期就是消隐期不能输出画面。虽然现在 LCD 不需要回扫但它的采样电路依然要靠这段无信号时间来重新对齐、锁定行起始位置。你可以把它理解成一句句朗读时的停顿——没有停顿听的人就找不到句子的边界。提示所有可见像素之外的区域RGB 必须输出全 0。如果你在消隐期还往外送颜色有些显示器会在边缘出现彩边或者干脆锁不住同步。1.3 三路模拟色信号从哪来——权电阻网络的出场理由问题来了FPGA 的 IO 只会输出 3.3V 或者 0V它不会输出 0.35V、0.5V 这种中间值。而 VGA 要的是连续可变的模拟电压。行业里有三种解法。第一种是专用视频 DAC 芯片比如 ADV7123 那种三通道 10 位性能好但贵而且板子上得给它留位置。第二种是 R-2R 梯形网络只需要两种阻值精度好是工程上的主流。第三种就是这次的主角——权电阻网络每一位对应一个电阻阻值与该位权重成反比全部接到同一个求和节点上。为什么教学和入门项目偏爱权电阻网络因为它把二进制权重这件事变得肉眼可见。你看着 510Ω、1kΩ、2kΩ 三个电阻就能直接理解 MSB 的电流是 LSB 的四倍。R-2R 虽然优雅但它的等值输出阻抗特性对初学者来说是个黑盒。至于只有 3 位带来的 8 级灰度——对一个只显示 8 色彩条的项目而言完全够用因为你根本用不到中间电平。2. 权电阻网络 DAC 的原理与电阻选型计算2.1 二进制权电阻是怎么把 3 位数字变成 8 级电压的先把物理模型摆清楚。FPGA 的三个输出脚可以看成三个开关闭合时输出 3.3V。每个脚串一个电阻三个电阻的另一端拧在一起形成一个求和节点。这个节点再连到 VGA 接口的对应色针上而显示器内部那 75Ω 就是从这一针到地的负载。现在做电流分析。每一个开关闭合后都会往求和节点注入一股电流电流大小是 3.3V 除以它的串联电阻。要想让三路电流呈现 4:2:1 的权重关系电阻值就得反过来呈现 1:2:4 的关系。也就是说MSBbit2接最小的电阻注入最多的电流中间位bit1接两倍于 MSB 的电阻LSBbit0接四倍于 MSB 的电阻三个电阻并联起来它们的电导电阻的倒数相加就得到了数字码对应的总电导。再用节点电压法算一下输出节点的电压就是总注入电流 ÷ 总电导。数字码越大并联进去的电导越多输出电压越高屏幕越亮——这就是权电阻网络的全部原理没有别的魔法。2.2 手把手算一遍从 0.7V 反推电阻取值设 MSB 的电阻为 R那么三个电阻分别是 R、2R、4R对应的电导是 1/R、1/(2R)、1/(4R)。当三个位全部为 1白色时总电导为G_total 1/R 1/(2R) 1/(4R) 7/(4R)求和节点电压负载 75Ω即 G_load 1/75 ≈ 0.013333 SV_out 3.3 × G_total / (G_total G_load)我们要让白色对应 0.7V代入求解0.7 3.3 × (7/(4R)) / (7/(4R) 0.013333) 0.7 × 7/(4R) 0.7 × 0.013333 3.3 × 7/(4R) 4.9/(4R) 0.009333 23.1/(4R) 0.009333 18.2/(4R) 4R 1950 R ≈ 487.5Ω487.5Ω 落在 E24 系列的 470Ω 和 510Ω 之间。我实际用下来推荐取 510Ω因为算出来的白色电平是 0.680V落在 0.7V 的 ±5% 以内如果取 470Ω白色电平是 0.706V稍微超一点点但肉眼完全看不出。所以最终三只电阻的取值是位连接引脚示例阻值说明bit2MSBvga_r[2]510Ω电流最大决定主亮度台阶bit1vga_r[1]1kΩ中间权重bit0LSBvga_r[0]2kΩ权重最小顺手把 8 级灰度全算出来方便你后面用万用表验证基于 510Ω/1kΩ/2kΩ数字码二进制计算电压理想值00000.000V0.000V10010.119V0.100V20100.229V0.200V30110.332V0.300V41000.427V0.400V51010.516V0.500V61100.600V0.600V71110.680V0.700V你会发现台阶并不完全等距最大误差出现在最高位附近。原因是 75Ω 负载本身就挂在这个节点上它参与分流破坏了理想的比例关系。这在工程上叫负载效应。但你要记住一件事显示 8 色彩条时每个通道只用 0 和 7 两个码值中间那六级根本用不到所以这点非线性对你完全没有影响。只有当你以后要做灰度渐变图的时候才需要考虑换成 R-2R 或者真正的视频 DAC。注意这里默认 FPGA 输出脚的源阻抗为零。实际 Cyclone V 和 MAX 10 的 IO 输出阻抗大约在 20~30Ω 量级加到 510Ω 上大约带来 5% 的误差对彩条项目可以忽略但如果做高精度应用就得把它算进去。2.3 权电阻网络和 R-2R 到底该选哪个经常有人问这两种网络的区别我整理了一张对照表你按自己的位宽选就行对比项权电阻网络R-2R 梯形网络需要的阻值种类n 位需要 n 种只要 R 和 2R 两种3 位时的电阻数量3 只每通道6~8 只每通道输出阻抗随数字码变化恒定等于 R单调性一般能保证天然单调精度瓶颈电阻比例精度要求高对绝对精度不敏感适合位宽3~4 位6~8 位教学价值二进制权重一目了然结构优雅但需要推导结论很简单位宽在 4 位以下权电阻网络更省事、更直观超过 6 位别犹豫直接换 R-2R 或者上专用 DAC 芯片。做彩条用 3 位权电阻是最优解。另外提一句电阻精度。别用那种五色环的碳膜电阻凑数一致性太差。买 1% 精度的金属膜一包 100 只也就几块钱。为什么强调这个因为三个通道各有一套网络如果红色通道的 1kΩ 实际是 1050Ω绿色通道是 950Ω你的白色就会偏色调都调不回来。2.4 板载资源差异DE0-CV 和 DE10-Lite 分别怎么接两块板子的思路差别不小动手前先摸清楚。DE0-CV用的是 Cyclone V 5CEBA4F23C8板载 50MHz 晶振板上有 15 针 D-SUB 座。这里有个细节需要你自己确认先用万用表打到蜂鸣档量一下 D-SUB 的 1 脚红和排针上对应的信号脚是不是直通。如果直通说明板上没有集成电阻网络DAC 得你自己在外面搭如果不通那说明板上已经焊了电阻网络你只要把对应排针引出来就行。这个动作花不了一分钟能帮你省掉返工的麻烦。DE10-Lite用的是 MAX 10 10M50DAF484C7G同样是 50MHz 晶振但它走的是 Arduino Uno 外形没有 VGA 座只有两组 2×20 的 GPIO 排针。所以你必须准备一块 D-SUB 母座就是带焊盘的那种加上面包板或者洞洞板自己把网络搭起来。好处是这板子便宜、IO 电压是 3.3V 的 LVTTL直接怼没有任何电平问题。顺带说下引脚数量账3 位×3 通道 9 根数据线加 2 根同步信号一共 11 根。再加两根地线做回流13 根线。一组 20 针的 GPIO 完全够用甚至还有富余。3. Verilog 实现从像素时钟到彩条输出3.1 时钟方案分频还是 PLLDE0-CV 和 DE10-Lite 都给的是 50MHz 时钟而 640×48060Hz 的标称像素时钟是 25.175MHz。最省事的做法就是二分频得到 25MHz误差 -0.7%对应场频 59.52Hz。我实测过十几台显示器和采集卡这个偏差全部能锁住包括一些比较挑剔的工业屏。如果你追求精确可以用 PLL 生成 25.175MHz。但这里有个坑50 和 25.175 不是整数倍关系比值 1.986想要精确合成需要很大的 M/N 分频比抖动反而可能变大得不偿失。与其折腾 PLL不如换个思路——保持 25MHz 像素时钟不变去微调场消隐的行数把场频拉回 60Hz 附近。比如把后肩从 33 改成 29总行数变 521场频变成 25e6 ÷ (800×521) ≈ 59.98Hz。这是个很实用的调节旋钮。还有一个更重要的设计决策不要用分频出来的时钟去驱动逻辑改用时钟使能。为什么因为一旦你在设计里引入两个时钟域就得写多时钟约束、处理跨时钟域路径本来一个下午能搞定的项目能拖成三天。用使能信号的方式全部逻辑都跑在 50MHz 主时钟上综合工具一看就懂时序收敛毫无压力。3.2 行场计数器的写法与易错点计数器的骨架很简单行计数器从 0 数到 799归零时场计数器加一场计数器从 0 数到 524归零时一场结束。但有几个地方特别容易写错。第一个坑是只在像素节拍上递增。因为我们是 50MHz 主频配使能那个 pix_en 信号每两个周期才拉高一次计数器必须放在if (pix_en)里面否则你的像素时钟就变成 50MHz行频翻倍显示器直接黑屏。第二个坑是归零条件的边界。写h_cnt 799还是h_cnt 799在这个固定周期的设计里两者等价但养成用的习惯更好综合出来的比较器更小。真正要小心的是自增逻辑里别把 v_cnt 也顺便加了——我见过有人写成一拍同时加两个结果画面被压缩成一半高度。第三个坑是位宽。800 需要 10 位2^101024所以计数器声明成reg [9:0]刚好别图省事写成 8 位那样根本数不到 800。3.3 同步信号极性新手最容易翻车的地方这一条我要单独拎出来讲因为它是黑屏原因排行榜第一名。640×48060Hz 这个模式HSYNC 和 VSYNC 都是负极性。什么意思就是同步脉冲有效期间信号是低电平其他时间是高电平。你要是写反了很多显示器直接判定无信号。我整理了几个常见模式的参数注意极性那一列分辨率刷新率像素时钟行可见/前肩/同步/后肩场可见/前肩/同步/后肩HS 极性VS 极性640×4806025.175MHz640/16/96/48480/10/2/33负负800×6006040.000MHz800/40/128/88600/1/4/23正正1024×7686065.000MHz1024/24/136/160768/3/6/29负负你会看到 800×600 偏偏是正极性这就是为什么它经常被拿来当极性能不能自动识别的测试用例。做彩条项目时老老实实按 640×480 的负极性来写。如果不确定最快的验证方法是在代码里加一个参数开关一个一个试两分钟就能确认。3.4 完整彩条生成代码下面这份代码我在两块板子上都跑过直接可以抄。整个设计只有一个时钟域没有 PLL综合一次过。// vga_colorbar.v -- 640x48060Hz 8 色彩条配合权电阻网络 DAC module vga_colorbar ( input wire clk, // 50 MHz 板载晶振 input wire rst_n, // 低有效复位接 KEY[0] output wire [2:0] vga_r, // 接权电阻网络 - D-SUB pin 1 output wire [2:0] vga_g, // - D-SUB pin 2 output wire [2:0] vga_b, // - D-SUB pin 3 output wire vga_hs_n, // - D-SUB pin 13 output wire vga_vs_n // - D-SUB pin 14 ); // ---------- 640x48060Hz 时序参数 ---------- localparam H_VISIBLE 640; localparam H_FP 16; localparam H_SYNC 96; localparam H_BP 48; localparam H_TOTAL H_VISIBLE H_FP H_SYNC H_BP; // 800 localparam V_VISIBLE 480; localparam V_FP 10; localparam V_SYNC 2; localparam V_BP 33; localparam V_TOTAL V_VISIBLE V_FP V_SYNC V_BP; // 525 // ---------- 像素节拍使能50MHz - 25MHz ---------- reg pix_en; always (posedge clk or negedge rst_n) if (!rst_n) pix_en 1b0; else pix_en ~pix_en; // ---------- 行 / 场 计数器 ---------- reg [9:0] h_cnt; reg [9:0] v_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin h_cnt 10d0; v_cnt 10d0; end else if (pix_en) begin if (h_cnt H_TOTAL - 1) begin h_cnt 10d0; v_cnt (v_cnt V_TOTAL - 1) ? 10d0 : v_cnt 10d1; end else begin h_cnt h_cnt 10d1; end end end // ---------- 同步信号640x48060Hz 为负极性 ---------- assign vga_hs_n (h_cnt H_VISIBLE H_FP h_cnt H_VISIBLE H_FP H_SYNC) ? 1b0 : 1b1; assign vga_vs_n (v_cnt V_VISIBLE V_FP v_cnt V_VISIBLE V_FP V_SYNC) ? 1b0 : 1b1; // ---------- 有效显示区 ---------- wire active (h_cnt H_VISIBLE) (v_cnt V_VISIBLE); // ---------- 8 根竖条每根 80 像素 ---------- wire [2:0] bar (h_cnt 80) ? 3d0 : (h_cnt 160) ? 3d1 : (h_cnt 240) ? 3d2 : (h_cnt 320) ? 3d3 : (h_cnt 400) ? 3d4 : (h_cnt 480) ? 3d5 : (h_cnt 560) ? 3d6 : 3d7; reg [2:0] r, g, b; always (*) begin if (!active) begin r 3d0; g 3d0; b 3d0; // 消隐期送全黑 end else begin case (bar) 3d0: begin r 3d0; g 3d0; b 3d0; end // 黑 3d1: begin r 3d0; g 3d0; b 3d7; end // 蓝 3d2: begin r 3d0; g 3d7; b 3d0; end // 绿 3d3: begin r 3d0; g 3d7; b 3d7; end // 青 3d4: begin r 3d7; g 3d0; b 3d0; end // 红 3d5: begin r 3d7; g 3d0; b 3d7; end // 品红 3d6: begin r 3d7; g 3d7; b 3d0; end // 黄 default: begin r 3d7; g 3d7; b 3d7; end // 白 endcase end end assign vga_r r; assign vga_g g; assign vga_b b; endmodule代码里有几个地方值得多说一句。bar这个信号用的是比较链而不是除法因为h_cnt / 80虽然语法上能写但会综合出一大坨除法器白白浪费逻辑资源。比较链综合出来就是几个比较器级联面积小、延迟低。另外active的判断我特意在 always 块里又用了一次输出全零。这一手是为了兼容那些必须看到纯黑消隐的显示器。有些老机器如果消隐期有杂色会在边缘画出一圈噪点。4. 工程落地Quartus 建工程、引脚分配与下载4.1 新建工程与器件选择打开 Quartus走 New Project Wizard。器件型号别选错DE0-CV 选5CEBA4F23C8DE10-Lite 选10M50DAF484C7G。这两个型号一旦选错后面引脚分配表对不上编译直接报引脚不存在。把上面那份vga_colorbar.v加进来然后在 Project Navigator 里右键设为 Top-Level Entity。顶层实体名必须和模块名一致这个低级错误我见过太多次了。如果你是从厂商光盘或者官网下载的示例工程改的记得把原来的顶层文件从编译列表里移出去不然两个顶层会打架。4.2 引脚分配怎么做才不会返工引脚分配这一步我强烈建议不要凭记忆填数字。正确做法有两个一是用厂商提供的 System CD 里那份.qsf引脚模板直接 Import Assignments二是打开 Pin Planner在下面的引脚列表里按信号名搜索。DE10-Lite 的话它的 Arduino 排针在 Quartus 里的名字是GPIO_0[x]和GPIO_1[x]系列具体哪个 x 对应物理位置去翻《DE10-Lite User Manual》的 Pin Assignment 章节那里有一张非常详细的对照表。DE0-CV 类似看它的用户手册。约束语句长这样PIN 编号你自己替换成查到的值# 时钟与复位 set_location_assignment PIN_xx -to clk set_location_assignment PIN_xx -to rst_n set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to clk # VGA 数据 set_location_assignment PIN_xx -to vga_r[2] set_location_assignment PIN_xx -to vga_r[1] set_location_assignment PIN_xx -to vga_r[0] set_location_assignment PIN_xx -to vga_g[2] set_location_assignment PIN_xx -to vga_g[1] set_location_assignment PIN_xx -to vga_g[0] set_location_assignment PIN_xx -to vga_b[2] set_location_assignment PIN_xx -to vga_b[1] set_location_assignment PIN_xx -to vga_b[0] # 同步信号 set_location_assignment PIN_xx -to vga_hs_n set_location_assignment PIN_xx -to vga_vs_n # 统一 IO 标准 set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to vga_r[*] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to vga_g[*] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to vga_b[*] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to vga_hs_n set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to vga_vs_n再加一个 SDC 时序约束文件就一行搞定create_clock -name clk50 -period 20.0 [get_ports {clk}] derive_clock_uncertainty因为我们整个设计只有 50MHz 一个时钟域逻辑深度又浅时序根本不紧张。反倒是 IO 标准要盯紧——GPIO 默认可能是 2.5V 或者别的配错了输出电压不够白电平就上不去屏幕会发暗。注意不要把 3.3V 的 IO 配成 5V LVTTL虽然某些 BANK 支持但接错电压可能损伤器件也可能让显示器端接电阻上流过过大电流。4.3 编译、下载与上电验证点 Start Compilation等它跑完。这种规模的设计全编译也就一两分钟。下载用 Programmer模式选 JTAGUSB-Blaster 接上加载生成的.sof文件勾选 Program/Configure点 Start。DE10-Lite 板上有板载的 USB-Blaster II插上就能用不需要外接下载器。.sof是下载到 SRAM 的断电就丢。如果你想让它开机自动跑得在 Convert Programming Files 里把.sof转成.pofDE10-Lite 用的是 MAX 10 内部配置闪存然后下载到配置存储器。教学演示的时候我一般先用.sof快速迭代功能确认无误再烧.pof。首次上电如果一切正常你应该看到屏幕中间出现八根等宽的竖条从左到右是黑、蓝、绿、青、红、品红、黄、白。图像左右各有一点黑边是完全正常的那就是消隐区。4.4 硬件连线实录搭硬件这部分我按面包板走线的方式给你列一遍。D-SUB 15 针的引脚定义如下针脚信号说明1Red红色模拟信号权电阻汇总点2Green绿色模拟信号3Blue蓝色模拟信号5GND自测用接地6R GND红色信号地7G GND绿色信号地8B GND蓝色信号地10GND数字地13HSYNC行同步负极性14VSYNC场同步负极性三个颜色通道各搭一套MSB 电阻 510Ω、中间位 1kΩ、LSB 电阻 2kΩ它们的一端分别接三个 GPIO另一端全部拧在一起接到对应的色针上。这里有一个极其重要、但很多教程说错的点不要在板上再并一只 75Ω 电阻到地。显示器内部已经有了那个 75Ω 端接你在外面再并一个等效负载变成 37.5Ω白色电平会掉到大约 0.38V画面整体发灰发暗你会以为是代码问题其实是负载搞错了。有些教程加 75Ω 是因为他们用示波器直接测没有显示器提供端接那是另一回事。地线一定要接。5、6、7、8、10 这几根全部连到 FPGA 开发板的 GND。我见过一个案例RGB 和同步都接了唯独没接地结果画面出来了但满屏横纹、颜色发虚折腾了半天才发现问题在接地。走线方面面包板的杜邦线尽量剪短尤其是同步信号那两根。虽然行频才 31.5kHz但 RGB 在色条边界处是 25MHz 的跳变长线会有振铃。如果发现色条边缘有细亮线可以在靠 D-SUB 那端串一只 22~33Ω 的电阻做源端匹配效果立竿见影。5. 调试实录常见问题与排查技巧5.1 黑屏、滚动、花屏三大高频故障黑屏、显示器提示无信号这是最常见的。按概率排序检查同步极性写反占一半以上、时钟引脚没约束或者约束错、RGB 或者同步线接错针脚、复位信号一直有效KEY 的极性搞反了很多板子的按键是按下为低。快速验证极性的土办法在代码里给 HSYNC 和 VSYNC 各加一个反相开关编译四次试一遍总有一次能出画面。虽然笨但比你盯着手册研究半小时快。图像出来了但左右滚动说明行方向的总周期和显示器预期不匹配。检查 H_TOTAL 是不是 800以及每个分段的值有没有抄错。如果只是轻微地缓慢漂移那多半是像素时钟偏差累积导致的属于正常现象绝大多数显示器会自己锁住。上下滚动同理查场方向的 525 和那几个分段。还有一种情况是画面上下分成两半、位置错乱这通常是场计数器的归零条件写错了导致某一行数了两次。5.2 用 SignalTap 和万用表做定量排查示波器和逻辑分析仪不是人人都有但 SignalTap II 是 Quartus 自带的一定要用起来。把h_cnt、v_cnt、vga_hs_n、vga_vs_n都加进采样列表触发条件设成vga_hs_n下降沿采样深度开 4K 以上。跑起来之后你能直接看到hsync 在 656 到 751 之间是不是低电平周期是不是 800 个计数v_cnt 有没有在 490 到 491 之间拉低 vsync。所有疑问当场就能定位比猜快一百倍。没有示波器的话万用表也能干不少事。让屏幕显示全白把 bar 逻辑改成恒输出 7然后量 D-SUB 的第 1、2、3 针到地全白时应该在 0.68V 左右全黑时应该是 0V只有 LSB 为 1 时把输出改成恒定 3d1大约 0.12V三个通道量出来的值应该几乎一致如果红色比绿色明显偏低那就是电阻装错了或者某个 GPIO 没输出。还有一个高效手段纯色场测试。分别编译三版代码让屏幕输出纯红、纯绿、纯蓝。哪一版出问题问题就锁定在那个通道上。这个方法能把 9 根数据线的排查范围瞬间缩小到 3 根。5.3 常见问题速查表现象大概率原因处理方式完全黑屏无信号提示同步极性反了把 HSYNC/VSYNC 取反完全黑屏但显示器有背光时钟引脚未分配或分配错检查 Pin Planner 里 clk 的引脚画面左右滚动H_TOTAL 或分段参数错核对 640/16/96/48画面上下滚动V_TOTAL 或分段参数错核对 480/10/2/33画面上下分裂成两块场计数器归零逻辑错检查v_cnt V_TOTAL-1判断颜色偏色白不成白三通道电阻值不一致用 1% 金属膜逐只测量整体发暗板上多并了 75Ω 或 IO 电压配错去掉并联电阻IO 改 3.3V满屏横纹干扰地线未接或接触不良把 5/6/7/8/10 全部接地色条边缘有细亮线长线振铃源端串 22~33Ω位序颠倒红蓝互换引脚分配时 MSB/LSB 弄反重新核对 vga_r[2:0]只有 4 种颜色最高位那根线没接好量 MSB 对应 GPIO 是否有输出显示器偶尔闪断场频偏差大或刷新率过低微调 V_BP 把场频拉到 60Hz6. 从彩条再往外延值得一试的几个变体6.1 渐变、棋盘格和移动彩条彩条跑通之后别急着拆线顺手改几行代码就能玩出更多花样这几个我都试过效果都挺出片。灰阶渐变把行计数的高三位直接接到红通道场计数的高三位接到绿通道蓝通道保持为 0。你会得到一个从黑到红的横向渐变叠加绿色的纵向渐变能直观看到 8 位色的量化台阶。棋盘格一行代码就够assign {r,g,b} (h_cnt[6] ^ v_cnt[6]) ? 9b111111111 : 9b000000000;。这个图案对判断像素对齐特别有用如果格子不是正方形而是长方形说明像素时钟或者行场参数有问题。移动彩条加一个 30 位的帧计数器只在场同步归零时加一然后用它的一部分位去偏移 bar 的索引。这样色条就会慢慢横向移动。这个练习的价值在于它让你第一次接触到逐帧变化的显示逻辑后面写动画和游戏全靠这个思路。6.2 从 3 位扩到 4 位能多出多少颜色3 位每通道是 8 级三通道组合起来是 512 色。扩到 4 位就是每通道 16 级总共 4096 色。电阻怎么变还是同一个公式只是总电导变成 15/(8R)0.7 3.3 × (15/(8R)) / (15/(8R) 0.013333) 8R ≈ 4178 R ≈ 522Ω所以 4 位网络取 510Ω、1kΩ、2kΩ、4kΩ白色电平大约 0.719V稍微超了一点点但完全在容差内。IO 数量上4 位×3 通道 12 根数据线加 2 根同步一个 20 针排针还是够。但我要说句实话4 位带来的 4096 色对彩条项目毫无意义因为彩条只用 8 个端点色。它的价值在于你能做真正的渐变色卡、能显示带抗锯齿的字体。如果你打算往这个方向走我建议直接上 4 位甚至 6 位配合 R-2R别再堆权电阻了电阻种类太多容易装错。6.3 下一步往屏幕上打字符甚至做个小游戏掌握了彩条之后真正让人兴奋的方向是在屏幕上画出自己定义的内容。这条路的标准走法是在片内 BRAM 里开一块帧缓存存 1 bit 或者 2 bit 的像素数据读地址由v_cnt × 640 h_cnt算出来输出直接驱动 RGB。一块 640×480 的单色帧缓存只需要 640×480 307200 bit也就是 300Kb 左右MAX 10 上那 1.6Mb 的 M9K 块绰绰有余还能再放一份双缓冲。有了帧缓存你想画什么都行。要做字符显示就在 ROM 里存一套 8×16 的点阵字库按坐标把字模写进显存要做简单的乒乓小游戏就是两个矩形的位置变量加上边界碰撞检测走一遍消隐期更新坐标的逻辑。整个思路和你写纯软件游戏几乎一模一样唯一的区别是渲染这件事在每个像素时钟上并行发生。我个人的建议是彩条项目跑通之后先别急着上帧缓存。花半天时间把渐变、棋盘格、移动彩条这三个变体都实现一遍你会发现同一套行场计数器可以被复用在完全不同的图案上——这种同一份时序骨架、换一个像素函数的感觉才是 FPGA 做视频真正的爽点。等你把这层理解吃透了再回头去看帧缓存和游戏会发现它们只是在这套骨架上多挂了几块 BRAM 而已没有任何新概念。