引言
FPGA 工程师需要多种技能,这些当然没法在一页纸里写完。但有几条普遍适用的原则,尤其是对这个行业来说。我试着把它们归纳成 FPGA 设计的五条黄金法则。
规则 1:清楚自己在做什么
这是第一条规则,也是最难做到的一条。它意味着,你要理解每一行代码、每一条约束或配置命令的含义,以及它们会带来什么后果。它同样意味着,你要理解逻辑设计背后的基本理论,以及设计工具会如何响应我们交给它的东西。
它和反复试错的工作方式正好相反。而所有高科技开发工具的厂商,通过提供调试器、仿真器,以及一大堆被宣传成“很容易上手”的工具,实际上是在鼓励这种反复试错。
在软件开发中,随手写点代码,拿到调试器里跑一下,看看效果,改改再跑,这种事并不少见。有时还会把网上例子里的代码片段直接复制粘贴进工程。对于简单软件来说,这种迭代式工作方式可能相当有成效,比如开发网站或智能手机应用:小缺陷没那么严重,出现了再修就行——至少大家普遍是这样看的。
但软件越复杂,这种方法就越容易走进死胡同。而在 FPGA 设计中,迭代式开发往往很快就会让你吃大亏。FPGA 设计工具在防止或警告严重错误方面远远谈不上有帮助。你大可以把自己彻底搞砸,工具绝不会阻拦你。
规则 2:保证它能正常工作
或者说:“能跑通”还不够。
FPGA 是一种电子器件,而不是一台计算机。但只要遵循某些设计原则(见规则 1),它同样可以是可重复、可依赖的。如果不遵循,它完全可能毫无理由地给你捣鬼。
看不到明显缺陷,并不代表没有问题——任何领域都是如此。例如,水管工完工后,水管没有漏水,并不代表他做得对。以后水压可能升高,或者有人不小心碰了一下,水管就可能开始漏。
然而,我们都会掉进这样的陷阱:做个快速测试,或者做个更全面的测试,只要一切看起来不错,就认为大功告成了。对漏水的水管来说,或是在开发简单网站、甚至一些不属于关键应用的软件时,这么做也算可以接受。但在 FPGA 设计上,这远远不够。
设计 FPGA 意味着你要保证整个设计一定工作。也就是说,要利用设计工具为实现这个目标而提供的种种技术,迫使工具生成一个万无一失的比特流(bitstream)。
它意味着像写数学证明一样向自己证明逻辑设计是正确的;而不是抱着“如果发生这个,就做那个”的念头去思考。逻辑应该在最匪夷所思的边界情况下也能工作,不是因为你对这些可能做了单独考虑,而是出于同一个原因——正如一个正确的数学表达式,无论什么情况下都为真。
最后能跑通,这个事实本身不值得庆祝。那只是你正确工作的自然结果。
规则 3:明智地仿真(或者干脆不仿真)
FPGA 新手最常见的抱怨之一就是:我的仿真结果完美,所以设计肯定没问题,一定是别处出了毛病。这当然是胡说八道。
所以首先把话说清楚:除非 Verilog 的编码风格遵守某些严格规则,否则仿真和硬件可以做完全不同的事情(VHDL 当然也一样)。参见规则 1。
但即使仿真做得再正确,它覆盖的时间和场景也是有限的。即使是中等规模的设计,要仿真 FPGA 内部在比如说 100 ms 内发生的事,也要花费极其漫长的时间。除此之外,在硬件上真实运行设计,往往会碰到设计者想象不到、因而也没有仿真过的场景。所以,一个仿真完美通过的设计,在硬件上可能彻底失败,原因就是 FPGA 实际运行的时间比仿真覆盖的范围长得多,或者出现了意料之外的情况。
更麻烦的是,硬件上的缺陷很难找,因为所有事情都是并行发生的。和软件调试不一样,很少有一条清晰的事件序列可以跟踪,更不用说单步执行了。当然,确实有工具可以追踪 FPGA 内部的信号,但要搞清楚该追踪哪些信号,以及用什么条件作为采集追踪数据的触发条件,往往并不容易。
所以,把尽量少用仿真当作目标吧。如果你是在一遍一遍地用仿真来修改设计,只为了“让它跑通”,那一旦把设计放到硬件上,你很可能会遇到麻烦。
相反,应该努力让 FPGA 设计从第一次运行时就完美工作。这要求在写第一行代码之前先想清楚,也要求你清楚自己在做什么(上面的规则 1)。仿真应该只是用来确认你没写错,或者顶多帮你发现一些愚蠢的拼写错误。达到这个目标是一个学习和进步的过程,但很值得。到最后,仿真会变成浪费时间,因为它们永远发现不了需要修改的问题。
这不仅节省时间,也让硬件上需要修复的缺陷变得更少、更容易发现。
我很清楚,FPGA 设计的入门第一课通常是“先仿真,再上硬件”。但那只是第一课的内容。
规则 4:不要心存侥幸
或者说:坚持成熟的编码风格。
当 Verilog 用于综合时,它并不是一种编程语言。最主要的区别是:如果你在任意一种编程语言里写出语法正确的代码,编译器(或解释器)都会保证按照语法含义精确执行;最坏也不过是报一个错误。
另一方面,综合器(synthesizer)基于一组有限的逻辑单元来生成逻辑。因此,很容易写出让最终逻辑不可靠的 Verilog 代码,甚至写出根本没法在 FPGA 上实现的代码。更糟的是,如果无法把 Verilog 代码实现为 FPGA 上的逻辑,综合器往往会产生行为不一样的逻辑。很多时候,综合器这么做时连警告都不发。换句话说,FPGA 上的逻辑行为与预期(也就是仿真)行为不一致,而整个过程没有任何警告。
更糟糕的是,综合器里的缺陷比编译器里的缺陷常见得多。标新立异的编码风格肯定会诱发这类缺陷。
当然,上面说的这些对 VHDL 同样成立。
所以,要远离这类麻烦,唯一的方法就是采用一种别人也都在用的编码风格。请始终记住这个问题:“如果综合器被这段代码搞糊涂了,除了我之外还有多少人也会跟着倒楣?”如果答案是“一大堆人”,那你就安全了。
坚持成熟的编码风格还有一个好处:可移植性。不管你喜欢与否,今天你用的可能是一个综合器,明天你面对的可能就是一个完全不同的 FPGA 和另一套开发工具。
要想了解成熟的编码风格长什么样,可以看看工具为自身内置的 IP 核(IP core)生成的 Verilog 代码。代码的作者不同,风格会有些差异,但总有某些编码模式出现得更多。那些模式就是值得你模仿的。
几乎不用多说,要遵循 RTL(寄存器传输级)设计范式来做。这不只是一种成熟的编码风格,同时也是 FPGA 工具所期望的做法。
关于 Verilog 的教科书和教程可能会误导人,因为它们通常力求全面,所以常常会介绍很多语法正确、却很少被使用、有时甚至不适合综合的写法。
规则 5:时序就是一切
逻辑设计不是软件编程。仅仅让 Verilog 中的 wire 和寄存器得到正确的值还不够;同样重要的是,这些值要在正确的时间出现在正确的位置上。另外,时钟也必须被正确处理。
大致上,主要涉及以下几个方面:
- 认真思考数据是怎样以及何时流过流水线(pipeline)不同阶段的,特别是流水线可能发生停顿(整体停顿或部分停顿)的时候。事实上,任何数据流都应该这样考虑。
- 要确保时序约束(timing constraints)正确。特别是,要仔细阅读外部元器件的数据手册,并把该算的数值算对。这个页面讨论了时序检查的方法。
- 注意时钟的问题:它们从被使用的那一瞬间起就是稳定的吗?它们的抖动(jitter)是否低到足以满足用途?
- 跨时钟域(clock domain crossing):是否需要重新同步逻辑?如果需要,它能否保证信号从一个时钟域(clock domain)到另一个时钟域的传输万无一失?更多内容请看这里。
总之,和计算机程序不同,逻辑设计关心的不仅是什么会发生,还有它何时发生。
总结
没人说过 FPGA 设计很容易,这五条规则也没有哪一条容易遵守。每一条都需要知识,也需要一定程度的自律。但坚持这些规则绝对值得。这样做能省去很多让人抓狂的麻烦,尤其是在项目的最后阶段——在那时,一切按理都应该直接顺利工作。