第一次接触FPGA的时候我特别自信Verilog不比C难多少一个always块、两条赋值语句感觉三天就能上手。真正动手后才发现能让代码综合成电路的人很多能让电路在板子上稳定跑起来的人少一大半。这中间的差距三分之一靠经验三分之二靠记性——而记性靠不住的时候就需要笔记。我开始系统整理FPGA笔记大概是在第二块开发板之后。当时仿真都过了、约束也都加了上板后却怎么都不出波形查了三个晚上最后发现是IO电平标准配置错了。那一刻我才意识到FPGA开发拼的不是写代码的速度而是你手里有多少可复用的“认知缓存”。这篇笔记记录不打算罗列Verilog语法也不准备讲某个工具的菜单操作而是想聊聊这套我沉淀了很久的FPGA知识组织方式以及那些反复出现在我笔记里的核心知识点、时序约束方法、调试套路和上板问题排查案例。正在入门的人可以照着搭自己的笔记框架被时序和上板问题折磨的人可以直接跳到对应章节。1. 先把笔记分好类FPGA笔记的信息架构1.1 五种分类别让笔记变成流水账FPGA相关的知识来源特别杂芯片手册、EDA工具文档、应用笔记、论坛帖子、自己写的代码、编译报错、上板现象……如果不分类直接往一个文档里塞三个月之后连自己都搜不到想要的东西。我给自己的FPGA笔记做了五个一级分类语言与设计、工程与架构、时序与约束、仿真与调试、板卡与工具。语言与设计专门记Verilog/SystemVerilog的语法坑、可综合风格、状态机模板等高复用代码工程与架构用来沉淀模块划分原则、参数化设计方法、接口协议约定时序与约束记录时钟创建、输入输出延迟、跨时钟域处理仿真与调试放testbench骨架、仿真陷阱、在线调试方法板卡与工具则记录具体板子的资源分布、引脚约束、上电顺序和工具版本的注意事项。分类粒度很重要。我见过有人把“时序约束”单独开一个文件夹结果里面塞满了仿真波形截图这就不行了。分类的本质是给未来的自己一个明确的检索入口你按什么维度去查就按什么维度去分。另外每个分类下面还要设置一个索引页只写条目名称和一句话摘要翻起来比全文搜索快得多。1.2 两条状态通道暂存和归档比分类更重要的是给笔记标记“状态”。我刚开始记FPGA笔记时吃了大亏把论坛上学到的一段疑似正确的代码记进笔记结果两个月后直接复制到工程里综合报出一堆错浪费了大半天。后来我规定所有笔记必须处于两个状态之一暂存区或者归档区。暂存区就是随手记看到某个思路、某个报错、某个技巧先记下来不加任何可靠性背书状态标记为“待验证”。归档区的内容必须经过至少一次仿真或上板验证状态标记为“已验证”。从暂存区升到归档区需要走一个小流程复现一遍、记录复现条件、补充自己的理解。这套机制看起来麻烦实际用起来非常值它能保证你的核心笔记库永远是靠谱的。归档时还要记录环境信息。FPGA笔记里最大的坑就是“脱离环境谈结论”。同样一段代码在不同厂商、不同芯片系列、不同EDA工具版本下的行为可能完全不一样。我见过有人把串口调试的结论记成“通用结论”结果换了芯片平台之后怎么也调不通。所以我的归档模板里有一栏叫“验证环境”必须写清楚工具版本、芯片型号、时钟频率、约束条件缺一不可。1.3 一套可复现模板每个技巧都配最小工程FPGA笔记和软件笔记有一个很大区别光看代码片段往往不够必须跑一下综合仿真才知道这条经验在真实环境里成立不成立。所以我给自己定了一条规矩凡是记入归档区的技巧必须附带一个最小可运行工程。所谓最小就是20行以内的代码加约束脚本能独立完成综合或仿真不依赖业务逻辑。最小工程的管理我采用“一笔记一目录”的方式目录名直接用笔记编号。里面放三个子目录rtl、sim、constr。rtl放源码sim放testbenchconstr放约束文件。这样以后想回看任何一条经验直接打开对应目录跑一遍仿真就能复现。如果某个知识点没有对应工程说明它还不够清晰那就留在暂存区继续沉淀。记笔记的目录结构大概是这样的组织结构我自己一直沿用fpga_notes/ 01_language_design/ 001_three_segment_fsm/ rtl/ sim/ constr/ 002_nonblocking_assignment/ ...2. 语法与RTL设计笔记高频易错点一次理清2.1 阻塞赋值与非阻塞赋值这一页放在所有语法笔记最前面阻塞赋值和非阻塞赋值的区别几乎每本FPGA教材都会讲但工作中真的还会有人在这上面翻车。我自己写过的坑版本大概长这样// 错误示范在时序逻辑里用阻塞赋值 always (posedge clk) begin a b; c a; end这段代码的本意是把b的值经过a再传到c但综合结果和你想象的完全不是一回事。因为阻塞赋值的本质是“立即生效”在同一个always块里c拿到的是更新后的a最终c和b变成了同一个信号a那一拍寄存器等于被优化掉了。换成非阻塞赋值always (posedge clk) begin a b; c a; end这才是真正意义上的两拍移位寄存器。非阻塞赋值采用“先采样、后更新”的语义所有右值都在时钟沿来临时被同时采样时钟沿之后再统一更新左值所以c拿到的是旧a也就是b再往前一拍的版本。我在笔记里画了一张表每次写代码前都会扫一眼对比项阻塞赋值 非阻塞赋值 执行顺序立即更新后边语句能看到新值等时钟沿后在时间步末尾一起更新适用场景组合逻辑、变量临时存储时序逻辑、寄存器输出使用风险在always里混合用时序逻辑容易出错在组合逻辑里用会产生意外锁存器仿真与综合一致性低容易仿真通过综合错误高符合硬件真实行为最重要的一条经验不要在多个always块里对同一个变量赋值。我刚开始学的时候觉得这样写很省事结果变成了多驱动源仿真波形乱七八糟综合工具直接报错。后来我把这条写进笔记的置顶位置一个信号只能由一个进程驱动这是FPGA设计的铁律。2.2 状态机写法三段式为什么稳状态机是FPGA设计的常客串口、SPI、I2C、以太网控制、总线仲裁全都在跟状态机打交道。我见过很多初学者最爱的“一段式”写法把状态转移和输出逻辑全塞进一个always块。代码短的时候跑得挺好一旦状态多了输出逻辑一变整个时序就一团糟。后来我彻底转到三段式并在笔记里固化了模板。三段式把状态机拆成三个部分现态寄存器、次态组合判断、输出逻辑。核心结构长这样// 三段式状态机模板 reg [1:0] state_reg; reg [1:0] next_state; // 第一段状态寄存器同步复位 always (posedge clk) begin if (!rst_n) state_reg IDLE; else state_reg next_state; end // 第二段组合逻辑决定次态 always (*) begin next_state state_reg; case (state_reg) IDLE: if (start) next_state DATA; DATA: if (done) next_state IDLE; default: next_state IDLE; endcase end // 第三段输出逻辑寄存器输出 always (posedge clk) begin if (!rst_n) data_valid 1b0; else data_valid (state_reg DATA); end写状态机笔记时我额外记了三件事复位状态必须明确不能悬空进不定态状态编码要根据场景选——状态少用独热码状态多用二进制或格雷码case里的default分支一定要写没有default的case在综合时容易产生锁存器这是最容易被忽略、又最致命的问题。三段式最大的好处是把状态转移和输出逻辑物理分离代码可读性高时序收敛也更容易掌控。回头再看那些调了三天都分不清到底是状态跑飞还是输出错的工程几乎全部出在输出逻辑和状态转移混在一起的情况。2.3 复位策略从“能用”到“稳”复位看似简单实际上牵扯到整个系统的启动和故障恢复逻辑。FPGA复位策略一般有三种纯同步复位、纯异步复位、异步复位同步释放。我笔记里对比得很清楚复位策略优点缺点适用场景同步复位电路简单、时序容易分析复位脉冲太短时采不到属复位依赖时钟内部逻辑模块异步复位响应快、不依赖时钟复位释放时容易产生亚稳态全局复位控制异步复位同步释放兼顾两者抗亚稳态能力强需要额外同步寄存器系统级复位推荐异步复位同步释放是我在工程里最常用的做法。核心思想是复位信号拉高时立即生效复位信号释放时用时钟把它同步两级确保所有触发器在同一个时钟沿稳定退出复位。reg rst_sync1, rst_sync2; always (posedge clk or posedge rst_async) begin if (rst_async) begin rst_sync1 1b1; rst_sync2 1b1; end else begin rst_sync1 1b0; rst_sync2 rst_sync1; end end assign rst_n rst_sync2;这个技巧在上板调试时救过我很多次。如果你发现系统偶尔启动正常偶尔死机先检查复位释放是不是没有同步。复位释放太随机真是一查一个准。2.4 参数化设计与接口约定让笔记里的代码能复用写了两年FPGA之后我发现自己最缺的不是新代码而是能直接拖进工程复用的老代码。为了做到这一点我很早就在笔记里约定了一套设计习惯所有模块尽量参数化。位宽、深度、计数最大值这些常量全部用parameter定义绝不在RTL里写魔法数字。module fifo_ctrl #( parameter DATA_WIDTH 8, parameter ADDR_WIDTH 4 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] din, output reg [DATA_WIDTH-1:0] dout );模块之间的接口也要定规矩。我后来统一用valid-ready握手替代孤立的电平信号。电平信号的问题在于它不携带“什么时候有效”的信息模块一多调用方根本不知道该在哪个时刻采样。握手协议是两个信号配合发送方拉高valid表示数据有效接收方拉高ready表示可以接收当valid和ready同时为高时这一拍数据才算真正传完。这套协议写起来多两行代码但换来的模块复用性完全是另外一个量级。每个参数化模块在笔记里都对应一个“使用说明”包含端口含义、参数范围、时序图说明、例化模板。这样半年后再看到这段代码不需要重新读RTL就能直接拿起来用。3. 时序约束笔记把“为什么跑不快”写清楚3.1 建立时间、保持时间和时序违例的本质FPGA新手最容易陷入的误区是把综合工具当编译器以为只要语法对、功能对跑得慢就加频率。实际上FPGA里的每一个寄存器采样都有硬性时间窗口数据必须在时钟沿到来前满足建立时间setup time在时钟沿之后还必须保持一段保持时间hold time。时序违例的核心公式很简单时钟周期要大于“触发器输出延迟 组合逻辑延迟 建立时间”。组合逻辑路径太长数据就赶不上时钟沿。用生活里的话说快递必须在收件人离开之前送达收件人的离开时刻由时钟决定而快递的送达时间就是组合逻辑链的长度。所以代码写得花里胡哨、逻辑层数叠了七八层时序报告必然给你颜色看。我在笔记里这部分写得最细。每次综合后打开时序报告先看最差负slackWNS和最差保持时间裕量到底在哪个模块。负slack越大说明这条路径离时钟要求越远。有时候只需要在关键路径中间打两拍把组合逻辑拆成两段WNS立刻就能变正。3.2 时钟约束和输入输出延迟约束的记法不写时序约束综合器就不知道你期望的频率目标。我见过一些人跑综合只看功能和资源占用完全不看频率约束最后上板发现数据错乱。正确做法是在第一版综合之前就把时钟约束写好哪怕频率先定的比较低也要让工具知道你的时钟长什么样。最常用的约束命令其实就几条创建主时钟、设定输入延迟、设定输出延迟、标记伪路径、标记多周期路径。create_clock -name sys_clk -period 10.000 [get_ports clk] set_input_delay -max 3.000 -clock sys_clk [get_ports din] set_output_delay -max 2.500 -clock sys_clk [get_ports dout] set_false_path -from [get_clocks clk_a] -to [get_clocks clk_b] set_multicycle_path -setup 2 -from [get_pins ...]周期10ns对应100MHz输入延迟3ns表示上游数据在时钟沿后3ns才真正稳定输出延迟2.5ns表示下游器件在时钟沿后2.5ns内必须采到数据。这些数值从哪来主要看芯片手册和接口协议不是拍脑袋定的。SDC文件在我笔记里有一个固定模板每次新建工程先复制一份再改时钟名和频率效率非常高。这里有个重要的知识点false_path不是乱加的。它告诉工具这条路径上的时序关系不需要分析一般只用于跨时钟域的同步信号、测试信号等。如果把真正需要约束的路径设成伪路径工具不检查上板必出问题。我笔记里专门用红色标了一行set_false_path会把问题藏起来使用前先想清楚这条路径是否需要真实时序保证。3.3 跨时钟域笔记单bit同步与异步FIFO多时钟域在FPGA里太常见了。ADC给一个时钟处理器接口给另一个时钟DDR又给一个时钟。不同时钟域之间的信号如果直接连很容易采到正在变化的中间态也就是亚稳态。笔记里我先记住了单bit信号的经典做法两级同步器。把异步信号在接收时钟域里打两拍虽然不能完全消除亚稳态但能把亚稳态传播的概率降到可以接受的范围。reg sync1, sync2; always (posedge clk_receiver) begin sync1 async_signal; sync2 sync1; end assign synced_signal sync2;关键点在于不能用接收时钟域的寄存器对异步信号直接采样必须先同步而同步器只能用于单bit控制信号不能用于总线数据。多bit数据跨时钟域的常规方案是异步FIFO通过格雷码同步读写指针保证指针跨时钟域时任一时刻只有一位变化。我在笔记里记了一句话设计任何模块前先问自己这个信号属于哪个时钟域。跨时钟域的问题不是调试调出来的而是在设计阶段就要避免掉的。真等到上板才出现数据偶尔出错复现难度极高那真是查到头秃。3.4 PLL和时钟资源的常见坑FPGA里的PLL锁相环是时钟管理的一等公民。大多数带高速接口的工程都会把外部晶振时钟先送进PLL倍频或分频出多个不同频率的时钟供各模块使用。我在笔记里专门留了PLL的注意清单输入时钟引脚必须连接到专用时钟输入引脚不能随便找个普通IO接进去否则PLL可能锁不住PLL输出频率不能超过芯片手册给的最大范围超频之后即使locked信号为高输出抖动也会明显变大复位PLL时必须先等locked稳定为高再释放下游逻辑复位否则下游模块可能在时钟还没稳定的情况下就开始工作尽量使用官方IP核生成时钟不要自己在RTL里用计数器分频。计数器分频得到的时钟边缘不够干净全局时钟网络也不会把你这个信号当作真正的时钟来布。我踩过一个典型坑换了一块板卡后沿用旧工程的PLL配置结果locked一直不拉高。最后查数据手册才发现新板卡的外接晶振频率变了而我在PLL配置里还写着旧频率。配置界面上那一格看似无关紧要实际会让整个系统起不来。4. 仿真与在线调试笔记让问题暴露在上板之前4.1 testbench骨架时钟、复位和自动检测FPGA开发流程里仿真和上板是两条腿缺一不可。仿真阶段的核心产出就是testbench。我记了一套通用的testbench骨架新建工程时直接复制改端口省掉大量重复劳动。reg clk; reg rst_n; initial begin clk 0; forever #10 clk ~clk; // 20ns周期50MHz end initial begin rst_n 0; #50 rst_n 1; // 先复位再释放 end这个骨架非常简单但工作得很好。forever的语义是无限循环#10表示半个时钟周期的时间#50就是复位保持5个10ns。更好的做法是再加上自动比对在testbench里通过$display打印关键信号或者写一个检测模块当输出不符合预期时直接报错。这样修改设计后重新跑仿真一眼就能看出有没有引入新的问题。testbench里我还会刻意制造边界条件连续读写、空FIFO时读、满FIFO时写、突然拉高复位、中断数据流等等。这些操作在上板环境里很难手工复现但在仿真里只需要几行代码就能覆盖。仿真多花一小时上板就能少查一整天。4.2 仿真里经常翻车的细节仿真工具和现实电路之间存在语义差异很多在仿真里看起来正常的代码上板后行为完全不同。我笔记里记录了三个最常踩的坑第一个是initial块里的赋值和时钟竞争。如果在时钟沿到来的同一时刻你用阻塞赋值去改变一个本来应该由时钟驱动的信号仿真结果可能和你预期不一致。解决方法是避免在initial块里对时序逻辑信号使用阻塞赋值除非你明确知道自己在做什么。第二个是寄存器初值问题。仿真里reg默认是X态如果你依赖某个寄存器在复位前就是0仿真会给出X传播波形看起来一塌糊涂。这其实是在提醒你信号没有可靠的复位逻辑必须补上复位语句或者在上电阶段对它赋值。第三个是#延时滥用。有些同学喜欢在testbench里写一堆#100、#50强行等信号变化而不是用事件触发。测试平台变成了一系列魔法数字改一个参数就要重新对齐全部延时。更好的方式是使用(posedge clk)来同步测试过程或者用WAIT等待特定条件成立。这些写法让仿真更贴近真实硬件的行为也更健壮。4.3 在线调试逻辑分析仪IP的正确打开方式上板调试和仿真调试完全是两种节奏。仿真时你可以随时暂停、查看任意信号但上板后你只能靠芯片内部残留的观测点。这时候逻辑分析仪IP就是最重要的调试工具。所谓逻辑分析仪IP就是把一部分BRAM用作采样存储按设定的触发条件抓取内部信号再通过调试接口把波形上传到电脑显示。使用时的第一原则调试信号必须在综合前就预留出来。不要在综合完发现抓不到信号再想着把内部信号引出来综合器会把中间信号优化掉你根本找不到它的名字。我的习惯是在设计里把想观测的内部信号统一打包成一个debug_bus接到逻辑分析仪IP上用的时候直接配置触发条件就行。触发条件设置也很讲究。先确认时钟再看数据。如果波形抓出来一片空白先怀疑采样时钟有没有接到建议把locked、rst_n这些全局控制信号一并接进采集。触发位置可以选择前置触发、中间触发、后置触发一般从中间触发开始能看到触发点前后的数据。采样深度和位宽会互相挤占BRAM资源数据位宽宽的时候可以降低采样深度优先看清楚一段关键波形。4.4 调试笔记要记什么在线调试最容易白费力气是因为你会反复尝试同样的配置。后来我给调试过程专门建了一个笔记分类每条记录包含复现步骤、硬件环境、触发条件、抓到的波形特征、最终结论。比如我曾经抓过一个偶发数据错误的波形第二次复现足足花了三天后来把第一次的触发条件翻出来才找到规律。如果没有这份记录那三天基本等于白干。调试笔记还有一个额外价值它会把你的“调试直觉”显性化。记录多了之后你会发现很多问题呈现出相似的pattern比如时钟相位、毛刺位置、信号建立窗口。下一次碰到类似波形你能直接跳过一大半排查路径。这种经验积累才是FPGA工程师真正的护城河。5. 工程落地问题排查实录五个让我查了几天的问题5.1 仿真没问题上板不工作这是FPGA新手几乎必遇的经典场景。我在第五块板子时才彻底理顺了排查顺序先查时钟再查复位然后查引脚约束最后查IO电平标准。为什么是这个顺序因为时钟是系统的心跳复位是系统的起点引脚约束决定了信号有没有真正连出去而IO标准决定了信号电平能不能被识别。仿真仿真通过只代表代码逻辑在理想模型下正确不代表物理世界正确。比如复位信号低有效还是高有效仿真里可能因为testbench把复位拉高就绕过了问题上板后复位控制信号恰恰是反的系统自然一动都不动。再比如引脚约束里忘了配置某个bank的电压标准导致IO无法正常工作。这类问题在仿真里百分之百发现不了。我给自己做了一张上板检查清单每次上板异常就按顺序勾检查项具体操作时钟输出用示波器或逻辑分析仪确认时钟引脚有波形、频率正确复位路径确认复位信号电平与设计一致释放后locked正常引脚约束核对每个引脚在约束文件与原理图中的对应关系IO电平标准确认每个bank的VCCIO电压与所选IO标准匹配上下拉配置确认按键、拨码、中断等信号是否配置了正确的上下拉这张表帮我省下过无数个熬夜查问题的夜晚。5.2 PLL锁定失败PLL不锁定的症状很直接locked信号一直为低系统时钟没有输出。我遇到过几次原因各不相同。最常见的是输入时钟频率配置错误输到PLL的时钟频率和期望值差了一个数量级PLL根本不在锁定范围内。其次是电源纹波过大PLL对供电噪声非常敏感板卡电源设计不好锁定后也会掉锁。第三个是复位信号的释放时机不对PLL在被持续复位的情况下当然不会输出稳定时钟。排查PLL问题我只会做三件事先用万用表或示波器测输入时钟确认波形频率正常然后看PLL配置界面里输入频率、倍频系数、分频系数是否匹配目标最后检查电源引脚附近的滤波电容是否足够、有没有明显纹波。这三步查完绝大多数PLL问题都能定位。5.3 IO电平标准与Bank电压不匹配FPGA的IO是按bank分组的同一bank的所有IO共用同一个VCCIO参考电压。如果你在电压为3.3V的bank上配置了1.8V电平标准输出逻辑根本无法达到外部器件要求的高电平阈值数据传出去对方不识反过来内部信号输入时也有可能采集不到正确的高电平。这个问题最阴险的地方在于仿真完全正常因为仿真模型里没有电压domain的概念上板后你看到的是“数据错乱”“芯片工作异常”之类非常模糊的现象。我第一次遇到时甚至怀疑是时钟问题来回折腾了很久。解决方式也很直接打开原理图逐一核对每个bank的供电电压然后检查约束文件中的电平标准设置确保每一组IO的电平标准和它所在bank的电压完全对应。不要试图在同一个bank里混用不同电平标准。5.4 综合后时序违例反复出现时序违例的优化思路有几种拆分组合逻辑打拍、降低扇出、并行化、调整约束、调整布线策略。重点是先看最差负slack的路径在哪里。工具会在时序报告里标注出路径的起点、终点、逻辑级数和每段的延迟。先抓逻辑级数特别高的路径想办法把组合逻辑链拆短。比如你要计算一个很长的数据加和一次性把32个数全加起来那一大串加法器会形成极长的组合路径。更好的做法是分两级甚至三级流水每级只做一小部分加法。代码多几行时序立刻松动。还有一种情况是某个信号的高扇出特别是复位信号和时钟使能信号它们会驱动几百上千个寄存器布线资源不够时也会导致时序变差。这时就需要手动复制寄存器或者调整约束的权重。我发现连续调用综合工具十几次都过不了时序时最有效的策略不是继续调布线参数而是回头打开代码审视那条关键路径上的逻辑级数。代码层面优化一次比工具层面折腾十次更有效。5.5 问题排查记录表最后分享一个很好用的习惯把每次问题排查的过程整理成表格。表格的每一行都是同一个问题曾经出现过的完整档案包括问题现象、发生环境、根因分析、最终解决手段。长期累积下来这本表格就是个人版“FPGA疑难杂症手册”。比如问题现象根因解决手段检查点上板无输出复位信号极性错误修正复位逻辑原理图复位电平PLL locked一直拉不高输入时钟频率配置错误重新配置PLL输入频率晶振频率与IP核参数数据错乱bank电压与IO标准不匹配修正约束中的IO标准bank供电电压偶发数据错误跨时钟域信号未同步加两级同步器信号时钟域归属综合时序违例组合逻辑级数过多拆分流水WNS最差路径这张表每增加一行你对FPGA的理解就更深一层。遇到新问题时先翻这张表有一半概率能找到相似场景剩下的再上板排查效率能翻好几倍。整理FPGA笔记这几年我最深的体会是笔记的价值不是记下来的那一刻而是复利效应。每一条归档经验都在无形中降低你下一次调试的成本。我现在写代码前会先翻自己的笔记找到对应的模块模板综合完时序不过会先看自己的时序约束笔记上板出问题会先翻问题排查表。这个过程听起来很笨但实际非常高效因为FPGA的世界里绝大多数问题都不是新问题而是旧问题换了个皮。如果你还没开始系统整理FPGA笔记我建议就从今天遇到的第一个报错开始。不要只抄报错信息把上下文、原因、解决手段一起记下来。一年之后再回头看你会感谢自己这个微不足道的习惯。