
手上有一块 DE10-LiteGPIO 排针空着一多半除了点灯和数码管总觉得没把这块板子用起来。后来我把目标定在 VGA 上——不是因为它难而是因为它能让你第一次完整地走一遍数字时序信号如何变成人眼看到的画面这条链路。整套东西的核心其实只有两个一组精确到像素的时序计数器和一组把 FPGA 的 3.3V 数字电平翻译成显示器能识别的模拟电压的权电阻网络。前者决定画面稳不稳后者决定颜色对不对。这篇内容我会把基于权电阻网络的 VGA 彩条显示从头拆一遍VGA 接口上到底哪几根线在干活、75Ω 端接为什么决定了所有电阻的取值、640×48060Hz 的时序参数一个都不能凭感觉填、1 位/通道和 4 位/通道两种权电阻网络的阻值怎么算、完整的 Verilog 代码怎么组织、DE0-CV 和 DE10-Lite 两块板子的板级差异在哪、上板不亮时按什么顺序排查。适合刚接触 FPGA 视频输出的人也适合已经跑通过点灯和串口、想往上再走一步的人。1. 先把 VGA 的物理账算清楚五根信号线和 75Ω 端接的世界很多人第一次接 VGA 就卡在我到底要接几根线上。其实标准 15 针的 DE15 接口里真正参与成像的只有 5 根信号线加若干地线剩下的全是 DDC/EDID 识别线、5V 和几个保留针脚。搞不清这一点你就会在排针上浪费一堆 IO 去接那些根本不影响显示的东西。1.1 DE15 接口上真正干活的针脚把这 15 个针脚按功能分一下会发现只有前五个是信号其余都是供电、识别和地。下面这张表是我自己接线时贴在工作台上的速查表针脚名称作用本项目是否使用1RED红色模拟量0~0.7V使用2GREEN绿色模拟量0~0.7V使用3BLUE蓝色模拟量0~0.7V使用4ID2显示器识别位不用5 / 6 / 7 / 8 / 10GND模拟地必须与 FPGA 共地使用95V部分老显示器用来检测线缆插入一般不用11ID0显示器识别位不用12ID1 / SDADDC 数据不用13HSYNC行同步TTL 电平使用14VSYNC场同步TTL 电平使用15SCLDDC 时钟不用关键的一条5、6、7、8、10 这几个地脚必须全部接到 FPGA 的地。这不是多接一根更保险而是因为 RGB 三路是模拟信号显示器内部对每一路都有一个独立到地的 75Ω 端接电阻这些电阻的参考点就是这几个地脚。如果你的地线只接了其中一根或者干脆没接会看到画面出现亮度漂移、横纹干扰甚至完全不显示。我在早期搭飞线的时候吃过这个亏信号线接得整整齐齐地线偷懒只接一根结果屏幕上是满屏的斜纹排查了两个小时才想到地。1.2 75Ω 端接决定了你所有电阻的取值VGA 的 RGB 三根线是模拟输入标准规定显示器端提供 75Ω 的对地阻抗输入电压满量程约为 0.7V。这句话是整个权电阻网络设计的全部前提所有阻值都是从它推出来的。FPGA 的 IO 输出的是 3.3V 的方波如果直接怼到 VGA 的 RED 针脚上会怎么样3.3V 通过显示器的 75Ω 端接电阻直接灌进显示器的输入保护电路轻则亮度饱和、颜色永远是最亮的白重则把显示器前端电路打坏同时 FPGA 的 IO 也在持续输出约 44mA 的电流远超大部分 FPGA 引脚的推荐值。所以必须在 FPGA 和 VGA 针脚之间串电阻分压。设串联电阻为 R则显示器端得到的电压是Vout 3.3V × 75 / (R 75)想得到标准满量程 0.7V解出 RR 75 × (3.3 / 0.7 - 1) 75 × 3.714 ≈ 279Ω选常用的 E24 系列270Ω 或者 300Ω 都合适。270Ω 算出来的实际电压是 3.3 × 75 / 345 0.717V比 0.7V 高 2.4%对显示没有任何影响因为显示器对满量程本身也有容差稍微超一点仍然是满色输出。反过来说如果你手上只有 330Ω算出来是 0.605V屏幕仍然能认出白色只是整体的最高亮度会往下掉一档。这里有一个新手常踩的坑看到串 270Ω就照抄但那个 270Ω 是针对单一位驱动、满量程电压算出来的。如果你后面要搭多位权电阻网络每一路的阻值都不一样不能全部用 270Ω。这一点在第 3 节会展开。1.3 3.3V 的电平去驱动同步信号行不行行同步和场同步是数字 TTL 信号标准规定的逻辑高电平是 5V。现实情况是绝大多数液晶显示器和一部分较新的 CRT 对 3.3V 的高电平判定阈值在 2.0V 左右3.3V 完全够用。我用 DE10-Lite 的 GPIO 直驱过六台显示器没有一台因为同步电平不够而不亮。但有两个细节值得注意。第一同步信号线可以各串一个 33Ω 到 100Ω 的电阻作用不是分压而是抑制过冲和振铃尤其是在飞线较长的时候第二如果你手上正好有一台很老、同步阈值偏高的显示器最简单的办法是加一片 74HCT244 或 74LVC245 做缓冲把 3.3V 转成 5V 摆幅顺便把驱动能力提上去。我个人的建议是先用直连试不行再上缓冲不要一上来就把电路搞复杂。2. 640×48060Hz 的时序参数一个数都不能凭感觉填时序这块最容易被忽视因为大部分人觉得我数到 640 就归零不就行了。如果你真这么写看到的画面大概率是左右撕裂、上下滚动、或者干脆黑屏。VGA 的每一行、每一帧都不是有效像素 直接回车而是由同步脉冲、后沿、有效像素、前沿四段拼成的四段的长度都有明确的行业约定。2.1 四个区间和它们的顺序一行的完整结构按顺序是同步脉冲Sync→ 后沿Back Porch→ 有效像素Active→ 前沿Front Porch。一帧的结构完全一样只不过单位从像素变成了行。每个区间的功能不一样。同步脉冲是给显示器用来锁相和定位的发令枪它的宽度决定了显示器的 PLL 能不能锁定后沿是从同步结束到有效像素开始之间的让显示器缓一缓的时间老式 CRT 靠它控制电子束回扫液晶显示器靠它做自动对中前沿是有效像素结束到下一个同步脉冲之间的缓冲。三段加起来构成了所谓的消隐区在这个区域里 RGB 三路应该输出 0V也就是黑色。这里有个反直觉的点消隐区不是可以不输出而是必须输出黑色。因为 VGA 是模拟信号如果你在消隐期间让 RGB 引脚随便漂着显示器在自动调整和识别黑电平时可能会误判画面会出现边缘发灰或者整屏对比度异常。所以正确的做法是用一个 dedata enable信号只有在有效区才输出真实颜色其余时间强制输出全 0。2.2 行场时序的完整数字表下面这组数字是 640×48060Hz 的标准值我直接列成表接线的时候照着填就行参数行方向单位像素场方向单位行同步脉冲962后沿4833有效区640480前沿1610总长800525同步极性负极性负极性有了这组数帧率是可以反推验证的像素时钟 25.2MHz一行 800 个时钟一帧 525 行所以帧率是 25,200,000 / (800 × 525) 60.000 Hz一场不多一场不少。行频是 25,200,000 / 800 31.5kHz扫描线周期 31.75 微秒。所谓负极性同步意思是同步信号在脉冲期是低电平其余时间是高电平。640×48060Hz 的规范里 HSYNC 和 VSYNC 都是负极性这个如果搞反了表现是显示器显示信号超出范围或者画面上下颠倒并滚动。我第一次写的时候就是顺手写成高有效结果显示器直接进了省电模式。2.3 25MHz、25.175MHz 和 25.2MHz 该怎么选标准 VESA 文档给出的 640×48060Hz 像素时钟是 25.175MHz。这个数字很别扭因为它是从 800 × 525 × 59.94 反推出来的而 59.94 这个帧率又是为了让彩色副载波和音频载波的差拍干扰最小才定下来的历史遗留值。实际动手时你有三个选择直接对板上 50MHz 晶振做二分频得到 25.00MHz。这是最省事的方法偏差 0.7%。我实测过多台显示器都能正常锁定包括一些对时钟比较挑的型号因为它们内部通常允许 ±1% 以上的容差。用 PLL 生成 25.2MHz。在 50MHz 输入下只要把倍频系数设成 63、分频系数设成 125就能精确得到 25.2MHz配合 800×525 恰好是 60.000Hz。DE0-CV 和 DE10-Lite 上的 MAX 10 PLL 都支持这种整数分频配置这是我最推荐的做法。死磕 25.175MHz。只在你的显示器真的挑时钟、前两种都点不亮时才值得试因为 PLL 要实现这个非整数比值输出抖动会变大反而不一定更好。我的经验是先用 25.2MHz 试不亮再退到 25MHz两个都不亮的时候问题基本不在时钟上而是时序参数或者硬件接线错了别在时钟上继续消耗时间。2.4 计数器的复位边界和极性表达式计数器逻辑有一处必须写对行计数器归零的时候场计数器的行为。我见过不少写法是行到了 800 就归零、场到了 525 就归零两个独立的 if这种写法在绝大多数时候没问题但如果两个条件在同一拍同时成立仿真里会出现场计数器多走一格或者少走一格表现为画面每隔一段时间抖动一下。正确写法是把两者嵌套起来行计数的终点作为场计数的使能条件。极性表达同样容易写错。因为同步是负极性所以在计数器小于同步脉冲宽度的区间里输出应该是 0。用一行组合逻辑写出来就是hs ~(h_cnt 96)。写成hs (h_cnt 96)是等价的但写成hs (h_cnt 96)就反了。有效区坐标的提取也要小心。有效像素的起始位置是同步脉冲 后沿 96 48 144场方向是 2 33 35。只有在 h_cnt 大于等于 144 之后px 才等于 h_cnt 减去 144否则 px 应该给 0 而不是继续算下去否则你在消隐区拿到的坐标会绕回到很大值彩条的边界就会出现莫名其妙的色块。3. 权电阻网络从 8 色到 4096 色的阻值算法权电阻网络这个名字听起来很唬人本质其实很朴素每一位数字输出对应一个电阻电阻的阻值和这一位的权重成反比——权重越大的位配的电阻越小因为它要贡献更多的电流。所有位在同一个节点上按电流叠加输出的模拟电压就等于所有1位贡献之和。这就是一个最原始的 DAC。3.1 一位一通道的极简方案三只 270Ω 电阻搞定 8 色如果你的目标就是标题里说的彩条那么红绿蓝每路只需要一位也就是这个颜色通道要么全亮要么全灭一共 2³ 8 种颜色。硬件上只需要三只 270Ω 电阻分别从 FPGA 的三个 IO 接到 VGA 的 1、2、3 脚。这三只电阻的取值在第 1.2 节已经算过了就是让单一位导通时显示器端刚好得到 0.7V 满量程。因为彩条里每一路的取值只有 0 和满量程两种状态所以你完全不需要关心电阻的精度和通道间的匹配——用 5% 的碳膜电阻和用 1% 的金属膜电阻屏幕上看不出任何区别。这一点很实用手边有什么用什么不用特意去买精密电阻。下面对应的是标准八色彩条的编码顺序就是经典的 VGA 测试条顺序从左到右序号颜色RGB0白1111黄1102青0113绿0104洋红1015红1006蓝0017黑000640 像素宽度平均分给 8 条每条正好 80 像素这是个很舒服的数字后面写 Verilog 的时候用一个除法就能得到条序号。3.2 多位二进制加权网络的电流叠加模型与阻值推导现在把每路从 1 位扩展到 N 位。假设每路有 N 位数字输入第 i 位的电阻是 Ri权重是 2^ii 0 是最低位。所有位都通过各自电阻接到同一个输出节点显示器在这个节点上提供 75Ω 对地端接。因为整个网络是线性的可以用叠加定理直接写出节点电压。设每位电阻的电导是 Gi 1/Ri逻辑 1 对应电压 3.3V逻辑 0 对应 0V相当于该支路接地那么Vout 3.3V × (Σ G_on) / (Σ G_all 1/75)注意分母里是所有位的电导之和而不是只有导通位的电导之和。这是很多人推导时出错的地方逻辑 0 的位虽然输出 0V但它的电阻依然挂在这个节点上依然在分流。为了得到正确的二进制权重各路的电导要按 2 的幂分布也就是最低位电导为 G次低位 2G依此类推最高位 2^(N-1) 乘 G。以 4 位为例所有位电导之和是 (1248)G 15G。让全 1 时输出满量程 0.7V3.3 × 15G / (15G 1/75) 0.7设 u 15G × 75方程变成 3.3u / (u 1) 0.7解得 u 0.2692于是 15G 0.2692 / 75 0.00359G 0.0002393最低位电导对应电阻 R 1/G 4178Ω。再按 1:2:4:8 的电导比换算成电阻对照 E24 标准值取整就得到了 4 位权电阻网络的实际阻值表位理论阻值建议取值该位单独导通时电流bit3最高位522Ω560Ω5.9mAbit21045Ω1.1kΩ3.0mAbit12090Ω2.2kΩ1.5mAbit0最低位4178Ω4.3kΩ0.77mA用这组取值实际代入算一遍满量程电压总电导是 1/560 1/1100 1/2200 1/4300 0.003382满量程电压是 3.3 × 0.003382 / (0.003382 0.013333) 0.668V。比 0.7V 略低一点屏幕亮度差不到一档完全可接受。这套阻值的另一个好处是电流都在安全范围内。最高位电阻 560Ω导通时约 5.9mA全部位一起导通时总电流约 11.2mA分散在四个引脚上每个引脚都不会超过 MAX 10 的推荐输出电流。3.3 R-2R 梯形网络和二进制加权网络选哪个除了上面这种每个位配不同阻值的二进制加权方案还有一种经典结构叫 R-2R 梯形网络只使用两种阻值。两者的取舍很值得说清楚因为很多教程含混地写一句用 R-2R 更好就带过了。对比项二进制加权网络R-2R 梯形网络需要的阻值种类N 种比例跨度 2^(N-1)2 种R 和 2R4 位时的阻值跨度约 8 倍无跨度阻值匹配对线性的影响低位电阻误差影响大对匹配相对宽容是否需要精确的 2R不需要需要且 2R 必须是 R 的两倍元件采购难度需要凑齐一串阻值只需买两种容易批量配对适合的位宽1~4 位4~8 位我自己的判断是做彩条1 位就用二进制加权其实就一只电阻要做 16 级灰阶渐变这种真正需要线性度的场景位数超过 4 位就上 R-2R。原因是 R-2R 只需要两种阻值你可以买一包 1% 金属膜电阻从里面挑出一对阻值刚好是 2 倍关系的匹配度容易保证而二进制加权网络到了 8 位最低位电阻会大到 100kΩ 级别此时 FPGA 引脚的漏电流和输入偏置都会开始影响精度得不偿失。3.4 线性度、引脚电流和那一级缓冲器的取舍这里有一个我在实际调试中才真正想明白的事权电阻网络的线性度本质上是在低输出阻抗和FPGA 引脚电流之间做取舍。从 3.2 的公式可以看出来当负载电导 1/75 相对于网络总电导不可忽略时输出电压就不是理想的比例关系了。用上面那组 4 位阻值算一下最低位单独导通的电压3.3 × (1/4300) / (0.003382 0.013333) 0.058V而理想值应该是满量程的 1/15也就是 0.047V。也就是说最低位大约偏亮了 23%。这就是负载效应导致的非线性导通位数越少网络等效输出阻抗越高负载分压得越厉害。要改善线性度就得把网络总电导做大也就是整体把电阻值往小取让等效输出阻抗远小于 75Ω。但这么做的代价是电流飙升如果要把等效输出阻抗压到 20Ω 左右最高位电阻会掉到 60Ω 上下导通时电流超过 50mA直接超出 FPGA 引脚的能力范围有烧 IO 的风险。所以真正的工程做法是在电阻网络后面加一级缓冲比如用 74LVC244 或者一个简单的射极跟随器把输出阻抗从几百欧降到几欧再去驱动 75Ω 端接。加了缓冲之后电阻网络本身可以按较大的阻值设计电流小、效率高线性度还更好。不过回到我们这篇文章的主题彩条显示只用到 0 和满量程两个状态非线性完全体现不出来。所以做彩条的时候上面这一堆关于线性度的分析你不用真的去实现但值得知道——因为当你想把画面从彩条升级到渐变、图片、游戏的时候这些就是你必须面对的问题了。我建议的做法是先用简单的一只 270Ω 电阻把彩条点出来验证整条链路通了再去考虑要不要上多位网络和缓冲。4. Verilog 实现分频、计数、坐标映射与彩条生成代码部分我习惯拆成三个模块时钟生成、时序发生、图案生成。这样拆的好处是后面从彩条升级到图片或者小游戏时只需要替换图案生成模块时序部分一行都不用改。这一节我把完整的可综合代码给出来并逐段解释每个写法的意图。4.1 时钟与复位不要让 PLL 的锁定时间坑了你DE0-CV 和 DE10-Lite 上都有一颗 50MHz 的有源晶振。在 Quartus 里用 IP Catalog 调一个 PLL输入 50MHz输出 25.2MHz就得到了像素时钟。如果不想用 PLL比如想少配置一个 IP也可以直接对 50MHz 做二分频得到 25MHz代码就是always (posedge clk_50m) clk_25m ~clk_25m;。用 PLL 的时候有一个必须注意的点PLL 上电后需要一段时间锁定在锁定之前输出的时钟是不稳定的。如果你不加处理就直接用这个时钟驱动计数器可能会在锁定的瞬间产生一段畸变的同步信号某些显示器会因此进入信号异常状态而不显示即使后面时钟稳定了也不恢复。解决办法是接出 PLL 的 locked 信号用locked来作为复位释放的条件之一。复位本身我倾向于自己做一个上电复位计数器而不是直接用板上的按键。因为按键复位需要人手动按做实验的时候你希望上电就能跑。写起来很简单// 上电复位约 2.6ms 后自动释放 reg [15:0] por_cnt 16d0; wire rst_n por_cnt; always (posedge clk_pix) if (!rst_n) por_cnt por_cnt 16d1;用 16 位计数器全 1 作为释放条件在 25.2MHz 下大约是 2.6 毫秒足够让 PLL 完成锁定和内部状态稳定。4.2 行场同步计数器的完整写法这是整个项目里最关键的一段代码所有的画面偏移、滚动、撕裂问题八成都能在这里找到原因。核心思路是用两个计数器分别数行内位置和帧内行号总数分别是 800 和 525然后在同步 后沿的位置上提取有效区坐标。module vga_timing ( input wire clk_pix, // 25.2MHz 像素时钟 input wire rst_n, output reg vga_hs, output reg vga_vs, output reg de, // 有效像素区标志 output wire [9:0] px, // 0 ~ 639 output wire [9:0] py // 0 ~ 479 ); localparam H_SYNC 96; localparam H_BP 48; localparam H_ACT 640; localparam H_FP 16; localparam H_TOTAL 800; localparam V_SYNC 2; localparam V_BP 33; localparam V_ACT 480; localparam V_FP 10; localparam V_TOTAL 525; localparam H_START H_SYNC H_BP; // 144 localparam V_START V_SYNC V_BP; // 35 reg [9:0] h_cnt; reg [9:0] v_cnt; // 行场计数器行归零时才决定场是否前进 always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin h_cnt 10d0; v_cnt 10d0; end else 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 // 负极性同步输出 always (posedge clk_pix or negedge rst_n) begin if (!rst_n) begin vga_hs 1b1; vga_vs 1b1; end else begin vga_hs ~(h_cnt H_SYNC); vga_vs ~(v_cnt V_SYNC); end end assign px (h_cnt H_START) ? (h_cnt - H_START) : 10d0; assign py (v_cnt V_START) ? (v_cnt - V_START) : 10d0; always (posedge clk_pix or negedge rst_n) begin if (!rst_n) de 1b0; else de (h_cnt H_START) (h_cnt H_START H_ACT) (v_cnt V_START) (v_cnt V_START V_ACT); end endmodule这段代码里有三个细节值得单独说。第一v_cnt的递增逻辑写在h_cnt归零的分支里两者天然同步不会出现错位。第二px和py在消隐区被强制赋 0而不是让它继续做减法避免负值溢出成巨大的无符号数。第三de用的是 h_cnt 和 v_cnt 的原始值做比较而不是比较 px 和 py这样逻辑更直观也少一层延迟。4.3 有效区坐标到彩条分块的映射有了 0~639 和 0~479 的坐标彩条就非常简单了把 640 像素宽平均分成 8 份每份 80 像素用 px 除以 80 得到 0~7 的条序号再按 3.1 节的颜色表查表输出。module pattern_gen ( input wire de, input wire [9:0] px, input wire [9:0] py, output reg [3:0] vga_r, output reg [3:0] vga_g, output reg [3:0] vga_b ); wire [2:0] bar_idx px / 10d80; // 0 ~ 7 wire [3:0] gray px / 10d40; // 0 ~ 15底部灰阶用 always (*) begin vga_r 4h0; vga_g 4h0; vga_b 4h0; if (de) begin if (py 10d416) begin // 上面 416 行画彩条 case (bar_idx) 3d0: begin vga_r 4hF; vga_g 4hF; vga_b 4hF; end // 白 3d1: begin vga_r 4hF; vga_g 4hF; vga_b 4h0; end // 黄 3d2: begin vga_r 4h0; vga_g 4hF; vga_b 4hF; end // 青 3d3: begin vga_r 4h0; vga_g 4hF; vga_b 4h0; end // 绿 3d4: begin vga_r 4hF; vga_g 4h0; vga_b 4hF; end // 洋红 3d5: begin vga_r 4hF; vga_g 4h0; vga_b 4h0; end // 红 3d6: begin vga_r 4h0; vga_g 4h0; vga_b 4hF; end // 蓝 default: begin vga_r 4h0; vga_g 4h0; vga_b 4h0; end endcase end else begin // 底部 64 行画 16 级灰阶 vga_r gray; vga_g gray; vga_b gray; end end end endmodule关于px / 80这个除法有必要解释一下。很多人一看到除法就担心资源消耗其实除以常数在综合器眼里根本不是除法器它会自动转换成乘加运算。在 25.2MHz 的像素时钟下这点组合逻辑完全不会成为时序瓶颈。不过如果你确实想避开除法有一个很实用的小技巧把彩条的宽度选成 2 的幂。比如把 640 分成 10 条 64 像素宽的彩条只需要取px[9:6]就能得到条序号一位逻辑都不多花。或者分成 5 条 128 像素的宽条取px[9:7]。我早期做实验的时候就是用 5 条宽彩条先验证通路因为宽条对时序错误更宽容——即使坐标算错了几十个像素你也能看出它是彩条而不是一堆乱码。4.4 加一条灰阶测试条验证权电阻网络的线性上面代码里我特意留了底部 64 行做 16 级灰阶宽度 40 像素一级。这一条不是装饰它是用来验证权电阻网络是否按预期工作的。如果你接的是 1 位/通道的极简网络这 16 级灰阶会全部显示成同一种灰色因为你的网络只能表达亮或灭两种状态gray 值 1 到 15 全部会被最高位截断成亮。这本身就是个有效的验证它说明你的网络确实是 1 位。如果你接的是 4 位权电阻网络这 16 级灰阶应该呈现出肉眼可辨的明暗递进。判断标准不是每一级亮度差完全相同——因为负载效应低灰阶级别会偏亮这是正常的判断标准是单调递增且没有跳变。如果你看到灰阶条中间某两级亮度一样或者某两级顺序反了那就是对应位的电阻装错了阻值或者接错了引脚。我在第一次搭 4 位网络的时候就遇到过这个问题灰阶条的前一半看起来正常后一半突然跳到最亮。查了半天发现是最高位和第二高位的电阻装反了560Ω 和 1.1kΩ 的位置互换了。因为这两个阻值在肉眼下差别不大都是几百欧的小电阻焊完不测根本看不出来。后来我养成了一个习惯焊完电阻网络先用万用表把每一路的阻值量一遍和表里对一遍再上电。这个习惯至少帮我省下了三次拆焊重来的时间。5. DE0-CV 和 DE10-Lite 的板级差异与连接方式同样是跑这套代码两块板子的动手量完全不同。DE0-CV 上 VGA 接口和电阻网络是板载的你要做的只是在 Quartus 里分配引脚DE10-Lite 上什么都没有得自己搭一个小电路。这一节把两种情况的差异讲清楚。5.1 DE0-CV板载网络重点是找对引脚DE0-CV 的板子上已经焊好了 15 针的 VGA 母座和配套的权电阻网络你需要做的只有两件事。第一件事是打开官方的原理图 PDF找到 VGA 那一页看清楚 RGB 三路各是几位。Terasic 这块板子上的网络是每通道多位加权设计的具体位宽和引脚编号以你手上的原理图为准。确认位宽很重要因为它直接决定你在 Verilog 里输出几位。如果板载是 4 位/通道你就输出 4 位如果只有 1 位有些精简板子确实如此那你就只需要输出最高位把其余位悬空不分配。第二件事是在 Pin Planner 里把信号名和引脚号对应起来。命名上我建议统一用vga_r[3:0]、vga_g[3:0]、vga_b[3:0]、vga_hs、vga_vs这样的向量形式分配的时候逐个对应到原理图上标注的网络名。有一点要特别注意原理图上标注的位序和你在 Verilog 里定义的位序要一致哪个是最高位、哪个是最低位一定要对着原理图确认。位序反了的表现是灰阶条整体翻转亮的变暗、暗的变亮而彩条本身看起来却是对的——因为彩条只用 0 和满量程翻转后 0 变满、满变 0颜色确实会全反所以彩条也能看出来问题。另外DE0-CV 的 VGA 网络已经包含了必要的串联电阻你千万不要再在外面串一层电阻否则满量程电压会掉到 0.35V 左右画面会变得非常暗。5.2 DE10-LiteGPIO 外接网络的接法和走线注意DE10-Lite 没有 VGA 接口需要自己在 GPIO 扩展口上搭网络。我实际的做法是找一小块洞洞板把 VGA 母座、电阻和排针焊在上面做成一个可以反复插拔的小模块。这样做比直接在杜邦线上串电阻靠谱得多原因后面会讲。接线上的关键点有几个电阻必须紧靠 VGA 母座从 FPGA 到电阻那一段走的是纯数字信号长一点没关系从电阻到 VGA 针脚那一段是模拟信号越短越好。每个通道的地线都要接VGA 的 5、6、7、8、10 全部连到扩展口的地脚上最好用一根较粗的线或者多根并联。同步信号线可以各串一只 33Ω 电阻位置也尽量靠近 VGA 座用来抑制过冲。不要用太长的杜邦线。我在最开始图省事用 20 厘米的杜邦线从扩展口直接飞到 VGA 座结果画面上出现了明显的重影彩条边缘有一层淡淡的拖影。换成 10 厘米的线并缩短了模拟段的长度之后重影就消失了。原因是 25MHz 下信号的上升沿大约几纳秒纳秒级别长导线分布电感电容会造成反射而模拟段的反射直接表现为图像重影。DE10-Lite 的扩展口 IO 是 3.3V 电平在 Quartus 里把对应的 IO Standard 设成 3.3-V LVTTL 就行。另外要注意扩展口上有些引脚是有特殊功能的比如接到 ADC 或者加速计分配引脚的时候最好避开这些虽然只要不改配置文件它们一般不会干扰但避开能省掉很多莫名其妙的调试时间。5.3 工程设置和引脚分配的几个实务细节Quartus 工程里需要检查的几项设置我列一下都是踩过坑的第一确认芯片型号选对了。DE0-CV 是 Cyclone V 系列的 5CEBA4F23C8DE10-Lite 是 MAX 10 系列的 10M50DAF484C7G。型号选错的话前期编译都能过最后下载的时候才报错白折腾。第二时序约束基本不用手写。如果你用了 PLLQuartus 会自动为 PLL 的输出时钟生成约束你不需要在 SDC 里手动create_clock。如果没用 PLL而是直接对 50MHz 做二分频生成 25MHz那么 Quartus 有可能把生成出来的时钟当作普通数据信号来分析这时候需要在 SDC 里给它加一句生成时钟定义否则 TimeQuest 会给出无意义的时序报告。第三引脚分配的 I/O Standard 要逐个确认。VGA 相关的引脚全部设成 3.3-V LVTTL电流强度用默认的 8mA 就够我们的最大电流只有 5.9mA摆率Slew Rate建议设成 Slow可以减小高速切换时的过冲和电磁干扰。这个设置很多人会忽略但它在长线飞接的时候对画面质量有实际影响。第四编译完之后用 Programmer 下载的是 .sof 文件掉电就没了。如果想让板子一上电就自动跑需要把 .sof 转换成 .jic 或者 .pof 烧到配置芯片里。做实验阶段一般不需要下载 .sof 就够了。6. 上板不亮、画面抖动、颜色错乱按现象排查的完整链条这一节是整篇文章里我最想写清楚的部分因为前面所有工作是否做对最终都体现在屏幕到底出不出画面上。而排查的顺序有讲究乱试一通只会越来越乱。我把它整理成按现象分类的排查链路每一类都按最可能到最不可能的顺序排。6.1 显示器提示无信号或者直接黑屏这种情况占所有失败案例的一半以上而且绝大多数不是代码问题。排查顺序应该是这样的。第一步确认显示器有没有真的切到 VGA 输入源。这个听起来很傻但我第一次做实验的时候折腾了四十分钟最后发现显示器还停留在 HDMI 上它根本没去检测 VGA 那一路。很多显示器有自动检测机制但也有些需要手动切。第二步用示波器或者逻辑分析仪量同步信号。如果手边没有仪器可以用最简单的办法把 HSYNC 接到示波器上看周期应该是 31.75 微秒VSYNC 的周期是 16.67 毫秒。这两个数只要对上了说明时序逻辑是对的问题一定在硬件或者显示器那一侧。如果这两个数完全不对那就是代码里的时序参数错了回去检查 H_TOTAL 和 V_TOTAL 有没有算错。第三步量 RGB 三路的静态电压。在显示彩条的情况下三个通道不可能同时是 0。如果量出来三路都是 0V说明 de 信号没有拉高或者坐标计算有问题图案生成模块根本没有输出有效颜色。这时候可以先临时把 de 强制置 1如果画面出来了虽然会有消隐区的杂色就说明是 de 的逻辑有问题。第四步检查同步极性。如果示波器上看到同步脉冲是高电平有效而不是低电平那极性就是反的。表现是显示器显示信号超出范围然后进省电模式或者进来之后画面上下滚动不止。另外还有一个几乎所有人都忽略过的原因某些显示器对 25MHz 和 25.2MHz 的容忍度确实不一样。我在一台老款三星上遇到过25MHz 死活点不亮换成 25.2MHz 立刻正常。所以如果你的时序和接线都确认没错换一个像素时钟试试是值得的。6.2 画面滚动、撕裂、左右偏移画面能出来但在滚动或者撕裂这类问题的根源集中在计数器的复位边界和坐标偏移上。如果画面上下滚动是场同步的问题。先确认 VSYNC 的周期是不是 16.67 毫秒如果周期是对的那问题多半在场计数的复位逻辑上——具体说就是行计数归零和场计数递增之间有没有严格的从属关系。前面 4.2 节强调过场计数的递增必须写在行计数归零的分支里不能写成两个独立的判断。如果画面左右撕裂或者在中间断开是行同步的问题。最常见的原因是行计数器归零的时机和同步脉冲的边界没有对齐。用一种很好用的调试方法把整个画面填充成某种纯色然后在左侧画一条 1 像素宽的竖线看这条竖线在屏幕上的位置离左边缘有多远。多出来的那一段就是偏移量可以直接换算成后沿的数值去修正。如果画面整体偏移但尺寸是对的那是在有效区坐标提取的时候后沿的值没有减掉。有效像素的起点是同步 后沿如果你忘了减后沿画面会整体右移 48 像素同时左边 48 像素会显示成上一行的内容看起来就像被水平平移了。6.3 颜色不对通道接反和电阻装错彩条的颜色不对有两种典型表现。第一种是颜色系统性错位比如应该是白色的条显示成黄色应该是绿色的条显示成青色。这说明有一路信号接错了位置比如红色的引脚接到了绿色的电阻上。对照 3.1 节的八色表逐个检查每一路的高低电平很容易定位。第二种是灰色和黑色的边界不清或者满色不够亮。这通常是电阻值选得不对。如果所有颜色都偏暗说明串联电阻太大了实际得到的电压远低于 0.7V如果所有颜色都亮得发白、分不清黄色和白色说明电阻太小了电压接近或者超过了显示器的输入上限。这里分享一个我常用的快速验证方法只点亮一个通道其他两个通道强制输出 0屏幕上应该出现纯红、纯绿、纯蓝三条独立的彩条图样。如果红色通道点亮的时候屏幕上出现的是青色或者其他颜色那就说明通道映射错了。这个方法比盯着八色表逐一比对效率高得多。6.4 图像有噪点、重影、边缘发虚这类问题基本都是硬件层面的信号完整性问题和代码无关。重影是最常见的表现为彩条边缘有一层淡淡的拖影。原因几乎一定是模拟段走线太长或者三路信号的地线没有接好。解决办法是缩短从电阻到 VGA 座的距离把地线加粗或者用多根并联。噪点表现为画面上零星出现白色或者彩色的像素点。这种情况要先怀疑时钟质量。如果用的是 PLL检查一下输出时钟的抖动如果是直接二分频检查一下二分频逻辑有没有用组合逻辑实现组合逻辑分频会产生毛刺一定是时序逻辑。另外如果 RGB 的走线和同步信号的走线挨得太近串扰也会产生噪点分开布线能改善。边缘发虚是 VGA 模拟信号的带宽问题。25.2MHz 的像素时钟意味着像素周期只有 39.7 纳秒对应的模拟信号最高频率分量大约在 12MHz 以上。如果模拟段的走线电容太大或者串了额外的电容高频分量会被衰减表现为黑白边界变得模糊。这个在飞线搭的电路上比较难完全消除但把模拟段做短、不加多余的元件能改善很多。7. 把这套东西继续做深从静态彩条到可动画面彩条跑通之后这套框架其实还留着很大的扩展空间。因为时序部分是完全独立的你只要换掉图案生成模块就能做出完全不同的画面。这一节说说几个我自己试过的方向以及它们各自会引入什么新问题。7.1 用 ROM 存图像素时钟下的取址关系最简单的升级是把彩条换成一张固定的图片。做法是用 Quartus 的 ROM IP把图片的像素数据预先存进去然后在图案生成模块里用坐标算地址取数据。地址的算法很直接addr py × 图片宽度 px。不过这里有个必须注意的位宽问题640 × 480 307200需要 19 位地址线。如果图片宽度不是 2 的幂乘法就要占资源如果用 2 的幂宽度比如 256py × 256 px就退化成拼接几乎不占逻辑。还有一个资源占用的问题要提前算清楚。用 4 位/像素的灰度图640 × 480 就需要 640 × 480 × 4 1,228,800 比特约 1.17Mbit 的存储。DE10-Lite 上的 MAX 10 10M50 内部总共大约 1.6Mbit 的 M9K 存储块单缓冲能塞得下但基本没有余量了而且布局布线时会出现存储块不够用导致编译失败的尴尬。DE0-CV 上的 Cyclone V 片内存储更充裕一些也谈不上宽裕。所以真要做全屏图片建议先把分辨率降到 320×240存储需求立刻降到四分之一同时还可以做一个简单的 2 倍像素复制来铺满屏幕。7.2 画个动态图形帧计数器和双缓冲如果想让画面动起来比如做一个方块在屏幕上移动思路就完全不一样了——你需要引入一个帧计数器在每帧的场同步期间更新方块的位置然后图案生成模块判断当前像素是否落在方块范围内。判断像素是否在矩形内只需要四个比较px x0 px x0 w py y0 py y0 h。这四个比较在 25.2MHz 下完全够用不需要任何存储。但如果你想做的是多图层叠加或者滚动背景就会遇到读写冲突的问题——图案生成模块在不停地读画面数据同时某个逻辑在不停地改画面数据两边抢同一个存储块。解决这个问题的标准做法是双缓冲用两块存储一块用来显示一块用来写入每帧结束时交换。代价是存储需求翻倍1.17Mbit 就变成了 2.34Mbit超过了 MAX 10 10M50 的片内存储容量。这种情况下就必须用板载的 SDRAM 了DE10-Lite 上有一颗 64MB 的 SDRAM可以放很多帧缓冲但代价是你得写一个 SDRAM 控制器或者用现成的 IP。我的建议是分阶段来先用全组合逻辑画简单几何图形方块、圆、直线这部分不需要任何存储等这套玩熟了再上 ROM 图片最后如果真要做复杂画面再考虑 SDRAM。一上来就想做完整图形系统很容易在存储控制器上卡住反而把 VGA 这条主线搞丢了。7.3 加交互从按键到帧同步最后说一个体验提升很明显但实现很简单的小改动把板上的按键接进来用按键改变某些参数比如彩条的宽度、方块的速度。这里有一个坑必须提按键信号一定要做跨时钟域同步和消抖。按键是异步于像素时钟的直接把它采样进像素时钟域会引入亚稳态表现为偶尔的颜色跳变或者位置突跳。正确做法是用两级触发器做同步再加一个计数器做消抖比如连续 10 毫秒保持同一状态才认定有效。这个细节在跑静态彩条的时候体现不出来但一旦加了交互不做同步就会变成最难查的那种偶发故障——一个小时出现一次调试的时候又复现不了。把这些都做完回头再看从一块空着的 GPIO 到屏幕上稳定的彩条中间真正难的不是写代码而是把75Ω 端接 → 电阻取值 → 时序参数 → 坐标映射这条链条上的每一个环节都算清楚。彩条本身只是个验证工具它验证的是你对这条链条的理解是否完整。我自己的习惯是每换一块新板子都先跑一遍彩条因为只要彩条正常就说明时钟、时序、IO 电平、外部电路这四件事全都是对的后面做任何画面都有了可靠的地基。