01signal.com

FPGA 设计师需要的实用技能

本页是关于如何成为专业 FPGA 设计师的系列五篇文章中的第三篇。在本页中,我会尽量梳理成为一名 FPGA 设计师最重要的那些实用技能,也尽量说清楚每项技能为什么重要。Verilog 那部分别跳过,哪怕它看起来显而易见——关于它,我可能有一些不那么显而易见的看法。

在开始之前,先解释一下我为什么只提 Verilog 而不提 VHDL:纯粹是因为推荐 Verilog,它看起来会是未来占主导地位的 HDL 语言。原因之一是,在中国很少听到有人用 VHDL。不过下面说的一切对 VHDL 同样成立。

Verilog 的两副面孔

为什么必须懂 Verilog,几乎不需要解释。事实上,很多人认为懂 Verilog 就等于会做 FPGA 设计。有那么一两位项目经理把手下初级程序员送去上了个 Verilog 课程,结果人家没变成 FPGA 高手,他们还挺失望。

所以首先要说清楚:知道 Verilog 的语法,离会用这门语言差得远。这对任何语言都成立,但在 Verilog 上,这道鸿沟要宽得多。其次,要理解 Verilog 代码不是计算机程序,它描述的是硬件。因此,原则上一切都是并行发生的。这个思维转变让很多软件开发者犯了难,尤其是那些不习惯多线程编程的人。

“它到底算不算编程语言”这个问题其实还要更复杂一点,因为 Verilog 有两大用途:仿真硬件,以及为 FPGA 或 ASIC 生成逻辑(综合)。

仿真的目的,通常是在把 Verilog 代码综合成 FPGA 里的逻辑之前先检查一遍。为此我们要写一个 testbench(测试平台),它本质上就是一个计算机程序,人为地产生激励信号,并对被测逻辑输出的信号做点处理:通常是把信号值写到文件里,或者检查它们是否符合预期。

当 Verilog 用于仿真时,它确实是一门编程语言,只不过完全不像 C、Python 或 JavaScript。不过,计算机做的正是我们让它做的事:仿真运行时,testbench 以及我们打算综合的那段代码,都会严格按语法定义的方式行事。

接下来就是那个巨大的坑:当被测过的同一段 Verilog 代码拿去综合时,Verilog 就不再是编程语言了。它描述的是逻辑,就像 HTML 描述网页上该显示什么一样。更糟的是,和 HTML 不同,Verilog 代码的解释可以相当模糊。

重要的是要明白,综合 Verilog 代码有点像变魔术。我们用人类语言描述想要的行为,综合器(synthesizer)则借助逻辑资源把它实现出来。因此,我们必须用特定的编码模式来定义这些行为——这些模式是综合器能够识别的,并且能据此生成我们想要的逻辑。和普通编程不同,我们不能想怎么写就怎么写。我们必须考虑综合器会根据我们的要求生成哪些逻辑单元。Verilog 代码不过是给综合器的一点提示,告诉它该生成什么逻辑。

在 AI 时代,我还得补一句:Verilog 不是凭感觉的 vibe coding。它是一门形式化语言,而综合器不是那种我们说几句好话就乖乖照办的 LLM。综合器在 LLM 出现之前早就存在了,它按照严格的规则解释我们的代码,而不是靠一个训练出来的神经网络。

最重要的是:如果我们把 Verilog 写得不好,逻辑的行为就不会和仿真一致。仿真器会完全照我们想要的去做,因为仿真器执行的是代码说的东西。而综合器则可能悄悄生成行为不同的逻辑——只要我们不小心把 Verilog 写对。

这一点至关重要,值得重复一遍:Verilog 用于仿真时,它就是一门编程语言,哪怕水平不怎么样。你只要语法写对,仿真器就会完全照你想要的执行。而综合器则可能生成与仿真完全不同的逻辑,即使 Verilog 语法完全正确。运气好的话,综合器会给出一条警告,提示你可能拿不到预期的结果。但愿你能看到那条警告,尽管它被埋在几百条别的警告里。运气再好一点,综合器会直接报错停下。但太多时候,你得到的是一个悄无声息的 bug。要避免这种情况,你就必须知道哪些编码模式适合综合,别的什么都不要试。

那么问题来了:到底该怎么学正确的 Verilog?

Verilog 基础技能

第一步当然是学语法。市面上有很多 Verilog 书和教程。可惜的是,其中相当多会把所有东西统统讲一遍,这不仅没必要,还可能引诱你去用那些会被综合器搞砸的编码模式。

我建议下面这份最小主题清单。从任何能找到的来源学会它们,语法层面你就够用了。如果你在看到的 Verilog 代码里碰到不懂的东西,就问你常用的 AI,让它解释是什么意思。也就这么难。以下是我的清单:

到这里,都还是容易的部分。真正的难题在于:靠正确地写 Verilog 代码,让综合器生成我们真正想要的逻辑。其实,是让世界上任何一款综合器都生成完全相同的逻辑。

我遵循一条简单的原则,也就是我自己那份黄金法则清单里的第 4 条:务必使用非常常见的编码模式。道理是:如果综合器误解了我的代码,那它也会把很多别人的代码同样理解错。而这种事情不会发生,因为这些人早就换用别的综合器了。所以我会看着自己写的代码问一句:“如果综合器把这个搞砸了,会有多少人受影响?”如果答案是“很多”,那我大概就站在安全的一边。强调一下“大概”。

但这些人到底是怎么写 Verilog 的呢?这还真是个难啃的核桃,因为绝大多数 Verilog 都写在公司内部,从不会公开。而且不同的人有不同的编码风格。更糟的是,网上的例子常常出自没经验的人之手。就算他们的代码能跑,也不一定值得模仿:换一款综合器可能就把它搞砸了,也许因为其中的综合技巧已经被推到了极限。OpenCores 上的 Verilog 代码质量参差不齐,从极好的到只能在仿真里跑通的都有。

那怎么才能知道呢?我能想出的最好建议是去读 AMD 的 IP 工具生成的 Verilog 代码。其中有些 IP,比如 DDR 存储器控制器,会生成可综合的 Verilog,而且是懂行的人写的。不过要注意,自动生成的 Verilog 代码里往往有大量无意义、没用到的代码。这是脚本(script)生成代码的典型特征。里面常常有那种只是实例化别的模块、而那些模块又实例化别的模块的层层包装,一直下去。这种无限套娃式的包装不值得模仿;它只是为了在需要支持同一份代码的不同配置时,把层次结构组织起来。自动生成的 Verilog 代码还往往带一大堆参数,这一点也不该照搬。记住,你的目标是模仿他们表达功能的方式,而不是那堆乱七八糟的层次结构。

顺便说说 SystemVerilog:它挺流行,但我个人从没用这种语言方言写过代码。主要是因为我想让自己的 Verilog 尽可能简单。需要信任综合器的地方越少越好。当我需要复杂结构时,我会用 Perl 写一个脚本(script)来生成简单的 Verilog。喂给综合器要用小勺子,不然它会吐你一身。

实现流程所用的工具

掌握这项技能,意味着能高效地使用工具、对工具有很好的掌控、看得懂它们的警告信息、让比特流(bitstream)生成过程可复现,并在项目变大时依然能管得住。一句话,掌控你在项目上的工作。

Vivado 是首选工具。不仅因为 AMD 占主导地位,还因为其他厂商——尤其是新兴的中国 FPGA 厂商——往往把工具设计成与之兼容。除了会把这些工具当 IDE 用,你还应该理解把 Verilog 代码和 IP 变成比特流(bitstream)的这一过程。它从综合开始,后面跟着若干与厂商相关的步骤,但原理上做的事都一样:逐步把综合器的输出转换成可以加载进 FPGA 的比特流。这一串执行步骤不是黑盒,也不该被当成黑盒。

理解工具如何工作,主要目的是为了正确应对错误和警告。项目实现过程中会产生大量警告,能区分哪些重要、哪些可以忽略很关键。而一旦出现错误,流程就会失败,必须解决某个问题。解决这类问题的本事,来自对工具行为背后理论的理解,以及积累的经验。

试着问 AI 怎么解决问题,有时候管用,但 AI 常常会带着你踏上一条漫长而无用的调试之旅。如果你毫无自己的判断、光听 AI 的,最后可能做出一个看似解决了问题、实际上只是消掉了一条错误信息、却制造出真正问题的改动。总之:什么都替代不了你自己的脑子,永远如此。

另一个要点是,每款工具都有自己的怪脾气,比如会误导人的错误信息。更糟的情况是:工具会悄无声息地忽略代码、设置或约束。学这些东西只能靠经验,而积累经验的一部分,就是真的去读报告,弄清那些信息是什么意思,哪怕它们跟你眼下的问题没什么直接关系。

下面是我建议作为清单的一份概念和流程列表。这些只涉及综合和得到比特流(bitstream)。仿真、验证和调试我留到后面讲。

如果你拿一个示例项目来试,你很可能把这个清单里提到的所有东西都走一遍,只有最后一项除外。在别人已经把示例项目给你准备好的情况下用工具,当然容易。现实中,项目很少组织得这么整齐,你得负责让工具正确工作,并尽可能拿到最好的结果。如果你不理解这套机器是怎么运转的,一旦它卡住或者不按你的意思来,你就很难修好它。记住,怪事随时会发生,哪怕你什么都做对了。

文件的汪洋大海

开发工具会生成文件,而且是大量文件。工具跑的每一步,从综合一直到最终定稿的比特流(bitstream),都像是一个读入一些文件、再把结果输出成另一些文件的计算机程序。

更麻烦的是,工具常常会复制源文件,然后依赖这些副本而不是原件。工具还会生成中间文件,并且依赖它们,而不是任何看起来像源文件的东西。这一点要记住,尤其是当你改了源文件、结果却毫无变化的时候。

作为学习的第一步,我不会把“搞清楚每一个文件干什么、有什么用”排在优先位置。试图把它们全都弄明白没有意义,但知道哪些文件该算“源文件”、哪些是“生成文件”很重要。你在这片文件的汪洋里游得越好,越有可能把头保持在水面之上。等你第一次为了往另一个方向开发而试着给整个项目创建一个独立副本,或者把它搬到另一台电脑上时,就会明白我的意思。

最好的办法,是把定义这个 FPGA 项目所需的最小文件集合保存在一个 Git 仓库里。隔一段时间就把其他文件全部删掉,然后从这个最小集合重新构建项目。想看看一个项目如何从最小文件集合引导起来,可以下载并尝试实现 Xillybus 的演示包(demo bundle)之一(Vivado 和 Quartus 版本都有)。注意,起步时你不是按常规把源码导入 Vivado,而是运行一个 Tcl 脚本(script)。听起来可能有点吓人,但即使你完全不懂 Tcl,这个脚本也很容易改。

关于 Vivado,有件事值得知道:DCP 是一个压缩文件,里面包含 edif 格式的网表、约束以及其他信息。比如,当 DCP 是布局布线或更后阶段的结果时,它里面含有逻辑单元的确切位置。它是设计走完流程中某个特定阶段后的一张快照。我建议你把一个 DCP 解压开来看看,就当图个乐子。

验证与仿真

在完美的世界里,逻辑设计第一次就成功,根本没有 bug 要修。当然,现实几乎总是另一回事。

在每一个 Verilog 入门课程里,都会告诉你:先写出你希望变成 FPGA 里逻辑的那部分 Verilog 代码,我们称之为“用于综合的代码”或“可综合代码”。然后用 Verilog 写一个 testbench(测试平台),放到仿真里跑,这样就能检查可综合代码是否正确工作。或者更实际地说,看看它是怎么不正确的,然后把 bug 修掉。testbench 是只为仿真目的而写的 Verilog 代码,永远不会靠近综合器。

确实,通常的工作方式是先写一个或几个 Verilog 模块,然后为它们写一个 testbench 来验证是否工作正常。随着项目变大,这个过程不断重复,最后可能整个项目都会被仿真。对于这种大规模仿真,通常会有好几个不同的 testbench,各自负责检查不同的功能。

但并不是所有仿真的做法都一样。仿真主要有三种思路。

第一种是为了看波形而仿真。这种思路下,testbench 只产生送进被测模块输入端的信号,包括时钟、复位,以及其他模拟真实物理信号或由其他模块产生的信号的波形。仿真结束后,用图形界面查看被测逻辑产生的波形,看它工作是否正确,或者为什么不正确。这个方法适合比较简单的逻辑。比如,实现某种视频输出图案的状态机(state machine),就可以这样仿真,因为只要看波形就能轻易验证那个重复出现的输出图案是否正确。

第二种思路是从文件读入、向文件写出。这里 testbench 只产生少量简单信号,重要的信号改为从文件读取。被测模块输出的值,或者选定的几个输出值,会被写进另一个文件。常见的做法是写一个计算机程序或脚本(script),用来生成 testbench 要读的文件,以及含有预期输出的文件。仿真跑完之后,把预期输出和 testbench 实际写出的内容做个简单的文本 diff 就行。当然,这有无数种变体:可以让 testbench 自己跟预期输出比较,也可以反过来:让计算机软件读 testbench 的输出并加以分析。

这种仿真方法特别适合我所说的“处理逻辑”,也就是实现某种数据处理的逻辑。它对于回归测试(regression test)也很有用,也就是用来确认代码从一版到另一版在功能上没有变化的仿真。

第三种思路是让 testbench 自行检查正确性,一旦出现坏情况就报错停下。这种方法需要用 Verilog 写出有意义的测试激励,而这通常比用脚本(script)语言来做更难。但另一方面,有了一个自带通过/失败指示的 testbench,FPGA 项目会更容易维护。跑回归测试套件的人会喜欢这种做法。

以上就是三种主要思路。我这么讲,好像只有我们人写的 Verilog 会被仿真,其实并不是:

综合后仿真

正如我前面提到的,综合器可能生成与 Verilog 代码所描述的行为不一致的逻辑。应对这个问题的一种办法,是对综合器产生的输出(也就是网表)进行仿真。这叫做综合后仿真。

要跑综合后仿真,我们让开发工具为综合后的代码生成一个 Verilog 模型。这是一个巨大的 Verilog 模块,端口和我们为综合而写的那个模块一样。但内部是由许多小模块(称为仿真原语(primitive))构成的,它们代表目标 FPGA 里实际的逻辑单元。所以这是一个非常杂乱的 Verilog 文件,但我们可以在 testbench 里引用它,替代我们原来写的那个 Verilog 模块。

经典做法是:先对人写的 Verilog 代码跑一遍仿真,再对综合后模型跑一遍,然后比较输出。上面提到的第二种方法(用文件作为输入输出)最合适,因为我们期望逻辑设计在综合之后行为完全一致。如果不一致,那就该把它视为你的 Verilog 编码风格遭受的重创。其实,如果你 Verilog 写对了,本来就不需要做综合后仿真。这类仿真在 ASIC 行业更常见,因为那里的人偏执地担心最终芯片里留下 bug。还值得一提的是:即使你的综合后仿真输出与原来完全一致,也仍然不能保证综合器做了你期望的事。

还有布局布线后仿真。顾名思义,它仿真的是逻辑单元在 FPGA 内部摆放和连接起来之后的样子。它会把 FPGA 内部的传播延迟(propagation delay)考虑进来,但方式非常有限。仿真和现实中发生的事之间仍有巨大差距。这是因为 FPGA 内部的实际延迟取决于很多物理因素,比如温度和供电电压。这些延迟还带有一定的随机性,因为芯片硅片中有散布各处的杂质。这些杂质改变了物理特性,所以电气延迟会略有偏差。因此每一颗实体 FPGA 的延迟都不同,不过仍在规格范围之内。仿真运行时,每条路径只施加一个特定的延迟值,通常取允许的最大延迟。即使你的设计在布局布线后仿真中表现完美,也仍然不意味着它能在真实的 FPGA 上工作。

所以总的来说,仿真有相当严重的局限。其一,仿真会顺从你的意愿,而综合器不会。而且即使是综合后仿真,也无法涵盖 FPGA 内部的许多真实现象:毛刺、物理逻辑单元因温度、供电电压或制造公差带来的容差。仿真也无法重现时序违规导致的逻辑行为,比如不安全地跨时钟域(clock domain)时出现的情形。

如果设计写得对、约束也给得对,仿真和现实的表现就是一致的。否则,仿真毫无价值。记住这一点很重要。

另一个局限是,仿真能覆盖的时间片段非常短。如果仿真跑一百万个时钟周期——这本身就要跑很久——而时钟是 100 MHz,那也只相当于现实中的 10 ms。罕见问题和极端情况很容易被漏掉,纯粹因为它们没恰好落进仿真的时间窗里。

我对仿真的建议

对新手我建议什么?会用仿真工具是必须的,这件事要和其他 Verilog 相关技能一起学。尤其是要熟练掌握上面说的第二种仿真方法,用文件做输入和输出。这被认为是正经的做法,原因之一就是它在回归测试中很常用。也可以拿它去跑一跑综合后和布局布线后仿真,至少为了应付求职面试也该试试。

但是:不要对仿真上瘾。每当你靠仿真找出了一个 bug,就该打自己手板一下。如果你是用第一种方法(看仿真波形)找出的 bug,那就该打得更重些。最终目标是第一次就把代码写对。仿真能抓住的是简单 bug,抓不住那些每十亿个时钟周期才闹一次鬼的问题——而那些恰恰是你不想永远追下去的。

最后,说个关于我自己的小秘密:我很少跑仿真。我会确保把 Verilog 代码写得细致、周密,让它直接就能正确工作,至少 bug 少到可以直接在 FPGA 上修。但我不知道还有没有别人这么做,我也不确定是否该把它作为通用做法推荐给你。

其实我还想补一条进阶的额外提示,供你以后留意:人们跑仿真时,常常会往逻辑里加复位,因为仿真中所有寄存器一开始都处于未知状态,标记为 "X"。结果是什么有用的事都干不了,因为整个系统都停在 "X" 状态。常见的错误是为了消掉这些 X,赶紧到处塞上异步复位(asynchronous reset)。然后忘了这可能是个非常糟糕的主意,正如另一篇文章里解释的那样。所以,当然要解决 X 的问题,但要明智地解决:也许用同步复位(synchronous reset),也许在可综合代码里用一个 "initial" 块?大多数综合器都会实现这个块,尽管它最初只为仿真而设。别让仿真左右你写代码的方式。

测试与调试

折腾到最后,你还是要去测试自己的设计,找出它为什么不像预期那样工作。有句话说得好:调试就像侦探故事,而受害者、侦探和凶手都是同一个人。

事实是,在这个侦探故事里,没有哪一种破解方法是绝对最好的。关键在于每次都能创造性地找到合适的思路、工具和方法,一步步逼近 bug 的源头。有些人每次都死守同样的调试手段,有时问题找得挺快,有时则要耗上半天。

针对某个具体项目开发一套专门的调试工具和方法,是很常见的事。大型软件项目里也会这样,但在 FPGA 设计里往往不可避免。这有点像为了测试而开发一个 testbench(测试平台),只不过是在硬件层面。

不过,尽管找 bug 没有唯一最优的方法,还是有些工具被普遍使用。我提几个。

大多数人喜欢首先上手的工具是片上逻辑分析仪。取决于你用的开发套件,这个工具叫 ILA、ChipScope、SignalTap 或类似的名字,做的事都一样:把你的电脑变成一台逻辑分析仪。用它可以看到在 FPGA 内部抓取的波形,就像看仿真波形一样。你可以选择监视哪些信号,以及触发抓取这些信号的条件。

电脑与 FPGA 之间的通信走的是 JTAG 接口,也就是用来把比特流(bitstream)文件送进 FPGA 的那个接口。所以不需要额外硬件,很多不喜欢碰电子的 FPGA 设计师很喜欢这一点。FPGA 和它本来就有的 JTAG 链路,同时也就是调试工具。

这个方法太方便了,以至于很多 FPGA 设计师对它上了瘾,忘了在某些场合可能更合适的其他办法。

这个方法的主要缺点是 JTAG 的数据带宽相对较低,所以当需要大量数据来排查时,ILA 之类工具就不合适。而且数据经 JTAG 链路传过来也要花一点时间。当我们希望对事件做出即时反应、以便把它和同时发生的其他事情关联起来时,这就成问题了。

当 JTAG 链路的局限成为问题时,就需要更快的接口。如果 FPGA 开发板有 PCIe 接口,就可以用它以很高的数据率在电脑和板子之间传输数据。实现 PCIe 接口和电脑端驱动本身可能就是个大工程,但用 Xillybus 就能快速轻松地把这样一条链路搭起来并跑通。对于配合 Xillinux 使用的 Zynq-7000 开发板,也有基于 Xillybus 的类似方案。

当某个很精准的问题藏在海量数据里时,往往就需要高带宽连接。比如图像处理中,可以把图像通过 Xillybus 传到电脑,再用专门的计算机程序在显示器上查看。

不管你选片上逻辑分析仪还是 Xillybus,这些都是“不沾手”的调试工具,完全不碰硬件。但有时候,我们就是需要硬件给出那种简单而即时的响应,才能弄清到底发生了什么。

所以首先,请允许我推荐最简单的调试工具:LED。意外地常见,找 bug 最快的办法就是定义几个不同的逻辑条件,然后确保条件成立时某个 LED 会闪。尤其是把“本不该发生的事”变成闪烁的 LED。LED 越多越好。这有点像在 C 代码里插 printf 来调试。同理,加一点调试用的 Verilog 代码,把比特流(bitstream)加载进 FPGA,胡乱试些傻办法,也许这些 LED 就能告诉你 bug 跟什么有关。

用这个方法时有一件事要记住:如果条件成立,一定要保证 LED 点亮至少 20 ms。否则人眼根本看不见。

LED 法的进阶版本是用示波器。这也是我在本系列第一页里建议买一台示波器并熟悉它的原因。思路是把 FPGA 内部的若干信号引到能用示波器探头够到的输出引脚上。在真实项目里,PCB 上专门留一个连接器并不罕见——板子设计师把 FPGA 上大量没用的引脚汇集到那里,就是拿来调试用的。

和片上逻辑分析仪相比,示波器显得有点落伍:一次只能看两个信号,更贵的示波器也许能多看几个。带宽有限,所以真正快的信号可能看不到。但有时候,它就是手头最有力的工具。尤其是当信号呈现重复图案时,用示波器看着它们,能帮你抓住出问题的地方。

有时候,还需要示波器来弄清在最朴素的硬件层面发生了什么。电压对吗?数字信号看起来该是那样吗?电压信号上有没有什么离谱的噪声?

说到这里,就引出了最接地气的一种调试:问题往往是个傻得不能再傻的硬件毛病,尤其是在为特定项目开发的 PCB 上。某路供电电压可能不稳定,偶尔冒个尖峰或者塌一下。时钟振荡器也许不按预期工作,产生的时钟信号不达标。在展开各种高深理论之前,先看看这些总是个好主意。

所以,如果你想在 FPGA 设计上做得出色,两样都得会点:“不沾手”地从 FPGA 内部取数据的方法,以及亲自上手摆弄硬件。

永远别忘了:真正高效的调试工具只有一个,前提是你用得对——你自己的脑子。

编程语言

虽然编程跟 FPGA 设计没有直接关系,但要把活干完,还是需要一点最基本的编程能力。比如我前面提到,仿真用的测试数据常常要用专门写的程序或脚本(script)生成。

下面提几种也许值得掌握的编程语言。在我建议学的东西里,这其实算是比较容易的部分,而且完全没必要从第一天起就都会。甚至一辈子不会也行。

还有其他几种语言,比如 MATLAB,也可能有用,尤其是用来为仿真生成测试数据。

懂编程语言还有另一层意义:它们常常能帮你和软件团队建立共同语言。对于涉及嵌入式处理器甚至整台计算机的 FPGA 项目来说,这很相关。所以自己多少会点编程,有助于和软件那边的人沟通。而且,写一个与你的 FPGA 设计通信的底层程序,往往比让别人照着一份规格文档去做更省事。

另外作为通用建议,学会用 Git,并且真的用它,哪怕你是独自做项目。版本控制不只是为了让管理者高兴,它还是个很有价值的工具:让你可以试试某个想法,然后退回,再后悔自己退回了。等你折腾完,那一堆乱糟糟的东西还一点都看不见。

我用 gitk 来看版本树的图形化展示。

本系列的第三页到此结束。下一页同样讨论实用技能,不过那些在入门阶段没那么重要。重点在于解释它们何时、为何会变得重要。

本页由机器从英文翻译而来。如有疑问,请参阅原文。
Copyright © 2021-2026. All rights reserved. (852e87d4)