当情况变得诡异时
总体而言,FPGA 是非常可靠的元器件;不过,和任何技术产品一样,如果使用不当,它们也不会表现得好。
遗憾的是,现有的 FPGA 设计工具在“引导我们这些人类避免犯错”这方面相当不帮忙,这常常导致不可靠、不可预测的结果。更准确地说,FPGA 工具只有在遵循某些设计规范时才能保证可靠运行,而这些工具的隐含前提是:我们知道自己在做什么。软件编译器只要发现源代码里有错误,就会拒绝生成可执行文件;相比之下,FPGA 工具只会说:“这是你的比特流(bitstream),想试就试吧。哦对了,这里有 200 条警告;如果你能明白第 143 条警告是什么意思,你就会知道这里面有一个必须修复的严重问题。”
从事 FPGA 相关工作的人,有时会觉得电子器件被某种神秘力量附身了,一切都不合逻辑。一个故障可能因为一个毫不相干的改动而出现或消失,而且找不到任何合理的解释。许多受过正规教育的工程师,为了解释这种怪象,也会去接受一些关于 FPGA 的毫无道理的理论。
这就是“黑魔法模式”:当理智的人不再相信自己的问题有合乎逻辑的解释,转而根据自己的经验去寻找解决办法。FPGA 在很冷的时候一切正常?好,给它装一个大散热器。问题只出现在某些板子上?行,那就逐块测试,把不行的板子扔掉。诸如此类。
故障看上去是什么样子
下面列出一些常见情况,它们足以把工程师推向非理性思维。这类情况总是:“一切正常,除非……”
- ……它突然就不正常了,而且没有任何明显原因。
- ……有人对 Verilog 代码(或 VHDL)、FPGA 工具的设置或版本做了一个随意的、看起来毫无关系的改动。
- ……空调开着 / 关着。
- ……系统已经运行了一段时间 / 是冷机刚上电。
- ……换用了另一个生产批次的 FPGA。
- ……换用了另一个生产批次的其他元器件。
- ……我把手指放到这里 / 把手盖到那里 / 按下那个毫不相关的按钮。
随之而来的,常常是一种信念:电子器件本身有缺陷,根本没有达到数据手册承诺的水平。关于某家大型公司大量出货有缺陷芯片的阴谋论并不少见。
所以,这不只是一个 bug。没错,bug 也能把人逼疯,但 bug 总有一定程度的重现性,而且绝不可能因为空调开关而出现或消失。我从来没听过软件工程师把某个 bug 归咎于 PC 电脑本身(虽然极少数情况下确实如此)。相比之下,FPGA 设计中的错误确实可能造成硬件层面的故障。从承认这一点,到开始怪罪整个世界,只有一步之遥。
其实存在合乎逻辑的解释。真的。
而且这个解释并非遥不可及,只是它不一定容易找到。
我在这个领域做自由职业者已经有相当长一段时间,也偶尔修过这类问题。请相信我:除了极少数特例,FPGA 本身没有问题,问题极可能出在比特流上。
坏消息是,FPGA 设计中的缺陷往往不止一个,而且这些缺陷可能就是表面问题的原因。因此,需要修复的东西可能很多。我曾经好几次被请去修复一个“几乎能工作”的 FPGA 设计,随后很快意识到对方抱有不切实际的期望:认为“这个小问题”很快就能解决。让设计稳定下来,往往意味着要做大量工作,却看不到任何明显的进展。
无论如何,你别无选择。在这一页里,我会尝试把理性思考带回来,包括列出一些可能的原因,解释为什么电子器件看起来像在闹鬼。
当然,最好的办法是从一开始就避免出现这种局面。先遵循我在另一个页面列出的黄金法则,就是一个很好的起点。
事情为什么会变得诡异
简短的回答是:FPGA 设计出了问题。如果是这样,原则上只有两种可能。比较幸运的一种是:FPGA 应该完成的功能出现了明显、稳定的故障。这就像软件 bug:找到它,修复它,修复后确认能工作,结束。
不那么幸运的情况是,FPGA 虽然能工作,但大多是碰巧。于是,当某些条件改变时,FPGA 突然无法正常工作,之后也许又恢复正常。但为什么会这样?
重点在于:FPGA 是一种电子器件,因此制造过程中存在误差。更重要的是,当硅片温度变化时,晶体管改变状态的速度也会变化,信号在逻辑结构(logic fabric)上传播的速度同样会变化。供电电压的变化,也会影响 FPGA 内部事件发生的速度。
因此,如果 FPGA 变热或变冷一些,相对于时钟,信号到达触发器(flip-flop)的时间就可能提前或推迟一点点。仅凭这一点,就可能让触发器没有采到它本该采到的信号,或者采到一个它通常采不到的信号。即使只是硅片局部发热,也可能造成这种现象:芯片上相邻(而且可能毫不相关)的逻辑变得不那么活跃或更加活跃。
同样,FPGA 在制造过程中也存在误差。虽然所有出厂 FPGA 都通过了测试,符合规格,但不同芯片的硅片速度仍可能有快有慢。这也是为什么一个有问题的逻辑设计可以在一块 FPGA 上工作,在另一块上却不能。
所有这些随机参数,都会影响逻辑结构中众多微小部件改变状态的时机;而这种时序上的差异,足以让设计在“完美运行”与“彻底灾难”之间产生天壤之别。因此,制造、温度或电压的微小偏差,都可能带来肉眼可见的差异。
那么,FPGA 怎样才能做到可靠? 当 FPGA 设计正确时,FPGA 工具会保证一切都按照定义工作。或者更准确地说:保证在任何一个通过了出厂测试、并且在数据手册规定范围内使用的 FPGA 上,一切都能正常工作。归根结底,就是要让环境温度保持在要求范围内(必要时做好散热),并确保 FPGA 引脚上的电压正确。
然而,如果没有遵循 FPGA 设计要求,工具也就不能保证正常工作。这意味着,那些本不应造成任何区别的参数开始变得至关重要;整块板子时好时坏,取决于一些本应无关紧要的事情。这种情况能诡异到什么程度,是没有上限的。
虽然这话可能有点重复,但我还是再举几个例子:
- 例如,整个设计可能一直工作得非常完美;之后你做了一个微不足道的改动,重新跑一遍实现,加载新的比特流,然后“砰”的一声,彻底不工作了。这通常是因为逻辑在 FPGA 逻辑结构中的布局发生了变化,逻辑单元之间的布线也随之改变。换言之,布局布线(place and route)的结果变了,传播延迟(propagation delay)也变了;某些之前能按时到达目的地的信号,现在不能按时到达了。
- 你手上有三块完全相同的板子:第一块工作得很好,第二块只在上午能工作,第三块从来没正常过。这可能是因为 FPGA 硅片之间存在细微差异。于是,设计中某处有一个信号,在第一块板上能按正确的时序到达;第二块板上的时序裕量本来就小一些,环境温度一变,就可能让 FPGA 越过“工作 / 不工作”的门槛;而第三块 FPGA 则一直处在这个门槛错误的一侧。
- “我一打开这个设备,另一个东西就停止工作了,可它们明明毫无关系。”这通常与温度有关。即使一个功能的逻辑与另一个功能完全无关,一个活跃的逻辑单元也会加热它旁边的单元。
这一点值得反复强调:FPGA 设计做得规范时,上述情况根本不会发生,至少也是极为罕见。很少有人意识到,当人们认真阅读并遵守数据手册,并且正确使用 FPGA 时,电子器件能有多可靠。
不过我想,对于正在读这一页的读者来说,这种说教已经有点晚了——问题已经存在了。所以,根据我自己的经验,我列出了一些导致 FPGA 看似闹鬼的常见原因。如果你正面临这样的问题,那么很可能就是其中之一。
原因 #1:时序问题
在很大程度上,FPGA 工具是通过满足你给出的时序约束(timing constraints)来保证 FPGA 稳定运行的。这是你和工具之间的约定:你准确地表达时序要求,工具则确保在任何一个你使用的 FPGA 上都能满足这些要求——只要该 FPGA 在其允许的温度和电压范围内运行。
常见的情况是,整个时序约束只有一条,内容仅包含参考时钟(reference clock)的频率。这可能已经足够;但如果你只是从别的设计里复制了这一行,而且嘿,它居然也能工作,那你就很有理由重新审视一下了。
实际上,关键是要把时序做对,而这不是一件简单的事,哪怕对最有经验的 FPGA 设计师也是如此。它意味着,你必须确保设计中的每一条信号路径(path)都受到一个约束的控制,使得路径终点的触发器总能正确收到信号——至于那些本来就不需要约束的路径,当然可以除外。
所以要检查的第一件事是:设计是否满足了时序约束?这本来是最基本的,但因为大多数 FPGA 工具无论有没有满足时序,都会照样生成比特流,所以 FPGA 新手很容易犯这个错误。
接下来要检查时序约束。关于这种检查,有专门一页讨论。不过长话短说:你确切理解这些时序约束的含义吗?它们的含义是否与实际需要完全一致?如果存在选择性的时序约束——也就是带过滤条件、专门覆盖某些路径的约束——它们是否确实作用在了正确的路径上?
接下来,还应该仔细阅读时序报告。再说一次,这一专门页面对这个话题有更详细的说明。
另一个需要注意的地方是跨时钟域(clock domain crossing)。是否存在信号不安全地从某个时钟域(clock domain)跑到另一个时钟域的情况?这可能是因为你没有留意哪些逻辑属于哪个时钟。跨时钟域是否只通过 FPGA 工具生成的 FIFO 来完成?如果不是,这些跨越是否都正确且安全?
原因 #2:复位使用不当
这可能看起来和前面的问题没有关系,但如果逻辑的初始状态没有得到保证,那就很容易引发黑魔法式的行为。
规则很简单:如果你没有认真考虑过复位和逻辑上电后的初始状态,那么你很可能已经做错了。
具体来说,看下面这个例子:
always @(posedge clk or negedge resetn)
if (!resetn)
the_reg <= 0;
else
the_reg <= [ ... ] ;
如果你对复位的理解就是写出上面这样的代码,却没有明确处理 @resetn 被撤销(也就是在本例中变为高电平)时会发生什么,那你一定要看看这一页。
不管怎样,最好确认一下:该有复位的地方是否都有复位,并且它们是否正确地发挥了作用。关于这具体意味着什么,在关于这个话题的一组简短页面中有讨论。
原因 #3:时钟处理
时钟质量也许是数字设计中最被低估的话题。很多人的态度是:“没错,这个信号会在高低电平之间变化,那就把它当时钟用吧。”
用于 FPGA 逻辑的时钟必须稳定,而且抖动(jitter)性能要足够好。同样重要的是,时钟到 FPGA 的物理连接也必须稳定可靠。
所以,对于下面这样的语句,
always @(posedge clk)
无论你把什么用作 @clk,都必须特别小心。最理想的情况是,这个时钟来自专用的时钟发生器(oscillator,即振荡器),它可以保证时钟稳定、抖动低。一般来说,把外部时钟用作锁相环(PLL)的参考时钟,比把它直接接到逻辑上更好。即使 PLL 并没有改变时钟频率,这一点也成立。
这样做的原因是,使用 PLL 后,就可以监视它的锁定检测(lock detector)输出。因此,当 PLL 未锁定时,所有依赖该时钟的逻辑都可以保持在复位状态。如果参考时钟存在稳定性的问题(尤其是刚上电之后那段时间),这样做能大大降低出问题的概率。
PLL 本身也可能被误认为是捣乱者:因为它会偶尔失锁,造成一些看似没有必要的复位。一种常见的“修复”办法,是把 PLL 拿掉,让外部时钟直接接到逻辑上,然后一切似乎又正常了。在这种情况下,真正的问题很可能出在参考时钟上。移除 PLL 并没有解决问题,只是把问题推进了逻辑结构中,从而可能制造出黑魔法现象。
以上讨论的都是来自专用振荡器的时钟,这其实是最简单的情况。如果时钟来自其他来源,情况就更糟了:由处理器或其外设产生的时钟,即使非用不可,也必须非常谨慎。这类时钟可能会被处理器短暂停住,或者偶尔产生非法波形。这通常是因为软件写入了相关硬件寄存器,而且这种写入可能是某个无关任务的一部分。用示波器观察时,这类短暂事件不一定看得见,但它们仍然会造成奇怪的故障。
另一个常见的问题来源,是对源同步时钟(source-synchronous clock)处理不当。换句话说,外部器件同时提供一个时钟信号和一个或多个数据信号,数据与这个时钟同步。通常,数据信号只能在时钟的上升沿(或只在下降沿)附近改变数值。
一种常见但相当危险的做法,是把源同步时钟直接接到 FPGA 内部的应用逻辑上。问题之一在于,源同步时钟本来往往并不是设计成持续运行的时钟,因此它可能短暂停摆,或者出现毛刺脉冲。
另一个可能的问题是,源同步接口通常要经过物理连接器才能连到 FPGA。例如,数据源是相机,用线缆连接到主板。连接器一般都很可靠,但即使只是因振动而失去纳秒级的物理接触,也足以在时钟信号上产生一个非法脉冲。数据信号当然也可能出现这种情况,但通常影响没那么大,尤其是数据源是相机的时候。然而,如果这样的时钟信号被直接接入应用逻辑,一个纳秒宽的脉冲就足以惹出大麻烦。
因此,对于带时钟和数据的源同步接口,最佳做法是把时钟和数据都当作普通信号对待。也就是说,用一个明显更快、稳定且安全的时钟,同时对这些源同步时钟和数据信号进行采样(sampling),最好使用 I/O 引脚旁边的专用触发器来完成。
当源同步时钟从低电平变为高电平时,采样这个时钟信号的触发器的输出也会发生类似变化。因此,只要看到这个触发器的输出从低变高,就可以用同步逻辑检测到源同步时钟的上升沿。当然,这套逻辑要基于那个更快且稳定的时钟。当逻辑检测到这样一个上升沿时,就把数据标记为有效。换句话说,保存数据输入值的那些触发器输出,被标记为有效数据。
这种对 01 信号进行采样(01-signal sampling)的方法有一个明显优点:无论时钟信号发生什么情况,FPGA 逻辑仍然可以依赖一个安全的时钟运行。如果源同步时钟出了问题,就由检测边沿的逻辑去做出适当响应。
这种技术只适用于频率相对较低的源同步时钟(通常最高到 200–300 MHz,具体取决于 FPGA 的速度,以及是否使用 DDR 采样)。
对于速度更快的源,更好的做法是:把源端时钟送给PLL,并用 PLL 的输出驱动应用逻辑。如前所述,当 PLL 指示未锁定时,逻辑应处于复位状态。这个做法之所以正确,可能还有另一个原因:当频率高到无法采用刚才说的 01 信号采样方法时,要保证正确采样,往往只能通过对时钟进行移相来找到正确的时序。也就是说,逻辑会不断自动调整时序,直到被采样的信号不再出现错误。这种技术无论如何都需要使用 PLL。
原因 #4:违反 RTL 设计规则
用于综合的 Verilog 代码(或 VHDL)必须遵循一些严格的规则,尤其是寄存器传输级(RTL,Register Transfer Level)范式。这通常意味着,任何具有记忆功能的逻辑单元(例如触发器)都只能因时钟边沿而改变状态。唯一的例外是异步复位(asynchronous reset),但异步复位信号不能随随便便选一个就用。
当综合器(synthesizer)遇到违反这些规则的 Verilog 代码时,它通常会试图配合你,生成可能与仿真结果不一致的逻辑。另一种可能是,综合结果大多数时候符合预期行为,但偶尔会随机出错。
例如,下面这个用于在 0 到 14 之间计数的设计,就是错误的:
reg [3:0] counter;
wire reset_cnt;
assign reset_cnt = (counter == 15); // This is so wrong!
always @(posedge clk or posedge reset_cnt)
if (reset_cnt)
counter <= 0;
else
counter <= counter + 1;
最可怕的错误,是把 @reset_cnt 用作异步复位。
先解释一下仿真中的行为:@counter 在 @clk 的上升沿递增。但当 @counter 达到 15 时,@reset_cnt 变为 '1',并把 @counter 异步复位到 0。因此,用 @clk 去采样 @counter 时,它会像预期一样只呈现 0 到 14。
在硬件上,这就不一定灵了。问题在于 @reset_cnt 是 @counter 的组合逻辑(combinatorial logic)函数。因此,当 @counter 的值从 7 跳到 8 时,计算 @reset_cnt 的逻辑可能短暂地看到 @counter 等于 15。这是因为 7 的二进制是 0111,而 8 的二进制是 1000。如果 bit 3 到达计算 @reset_cnt 的逻辑所用的传播延迟(propagation delay)最短,@reset_cnt 就可能会短暂地变成 '1'。结果,@counter 有时从 0 数到 14,有时从 0 数到 7。至于你会看到哪一种,温度和其他无关因素都可能产生影响。
不过,上面关于这个例子为何错误的解释,已经被大幅简化了。工具可以用各种极富创意的方式实现组合逻辑,所以在时钟边沿之间,几乎什么都有可能发生。工具唯一能保证的是:信号到达终点触发器时,能满足其时序要求——即建立时间(setup time)和保持时间(hold time)。
所以,只要逻辑设计没有严格遵守 RTL 设计规则,各种奇怪的事情就一定会发生。
原因 #5:温度与供电
这不是一个很常见的故障原因,而且检查起来很容易。尽管如此,温度和供电确实可能成为各种奇怪问题的根源。
很自然,如果硅片温度超出允许范围,那什么都不能保证正常工作。最常见的原因是散热设计不足导致的过热,或者风扇被灰尘困扰。
至于电源,它可能因为各种原因产生错误的输出。用示波器简单测一下,通常就能知道电压是否在规格范围内。但要注意,电压必须在所有时刻都保持在这个范围内,仅仅平均电压正确是不够的。开关电源总会产生噪声,偶尔还会有尖峰,这两者都不能超出限值。
注意,即使一个不超过 1 μs 的尖峰看上去似乎无害,在 FPGA 内部也相当于几十到几百个时钟周期;也就是说,FPGA 会在相当长一段时间内被施加不正确的电压。最好在靠近 FPGA 的去耦电容处测量电压,这样看到的是真正到达 FPGA 的电压。还要把示波器的触发(trigger)设置在上限和下限电压上,并确保示波器确实能对越限事件作出反应。短暂的尖峰在示波器屏幕上很难被注意到,但触发可以抓住它们。
有时电源问题直接源于板卡设计不良。许多电源模块都有一个经常被忽略的最小负载电流要求。如果没有从电源模块抽取这个最小电流,电源可能会变得不稳定,输出电压不达标,甚至更糟:偶尔发生振荡。
另一个常见错误,是在需要稳压器的地方放了开关电源。特别是有些低抖动时钟振荡器需要非常干净的电源输入。如果这样的振荡器由噪声很大的电源供电,这种噪声就会转化为时钟输出上的抖动。只要用该时钟的千兆收发器(Gigabit transceiver,例如 PCIe、USB 3.x、光纤等)工作,往往就会导致数据链路不可靠。
同样,当设计中包含 DDR 存储器时,需要一路参考电压供电。FPGA 和 DDR 存储器都会用这个电压作为两者之间连线上 '0' 和 '1' 的判定阈值。如果这路电压由开关电源产生,那么电源噪声很可能会让 FPGA 与 DDR 存储器之间无差错传输数据变得更难,甚至不可能。
原因 #6:你在逗我吗?
有时候,所谓黑魔法现象的背后,是一个大到让人想问“这东西怎么还能动起来”的缺陷。例如,PCB 上的走线与 FPGA 的相应引脚完全断开,但正确的信号仍然能通过串扰或寄生电容到达 FPGA。
这种情况尤其容易发生在时钟信号上,因为时钟走线常常布满整块电路板;而且由于它们是周期信号,所以即使只是以寄生方式到达 FPGA,也很可能足以让它看起来正常。
所以务必用示波器检查所有时钟,测量点要尽可能靠近 FPGA。如果时钟路径上有交流耦合电容,那里就是一个很好的测试点,尤其是你可能会发现电容根本没有焊上。
原因 #7:纯粹的 bug
或者更准确地说:这个设计从一开始就没打算真正工作。从来没有人坐下来认真推导过,这套逻辑怎样才能保证完成它该做的事。代码是在反复试错中拼凑出来的,一部分靠仿真,一部分靠硬件调试。当现象上看起来一切正常时,这个过程就算结束了。可是回头看代码,简直像奇迹:因为代码被打了无数次补丁,每次都只是为了修那个“小问题”,现在已经没人能看懂它在干什么,更不用说去改它了。
我把这个原因放在最后,因为它并不是真正的黑魔法行为,只是一个非常令人恼火的 bug。然而,这正是 FPGA 项目陷入困境的最常见原因。
如果你仍然认为这是 FPGA 的错
有时候确实不是你的错。FPGA 本身或厂商软件里可能有 bug。这种情况发生的频率,比人们习惯性指责 FPGA 厂商的频率要低得多,但在极少数情况下,确实如此。
因为人们天然倾向于把责任推给别人,所以请帮自己一个忙:除非你满足下面两条之一(或同时满足),否则不要以“都是 FPGA 的错”来结束这场驱魔仪式:
- 第一条:厂商发布了一份与你情况完全吻合的勘误记录(errata),既包括问题原因,也包括问题结果。判断这一点可能有点难,因为勘误记录往往有意写得含糊,尤其是为了让问题显得极其特定和罕见,同时淡化后果。不要因为看到某份勘误记录与你的情况“有点像”,就草率认为它匹配。总会有一份勘误记录在某种程度上与你的问题相似。
- 第二条:无可辩驳的确凿证据。也就是说,你能反复复现一个具体 bug,而且这个 bug 恰好能解释你的问题为什么会发生。仅仅证明工具或 FPGA 本身行为不合理是不够的;你需要把问题缩小到一个具体的、可复现的错误逻辑模式。
如果上述两条都不具备,你还是结束了排查,并且只是想办法绕过了问题,那么你以后很可能会再次遇见它。
我亲身经历过的、最能说明 FPGA 本身 bug 的例子,是很久以前 Xilinx Virtex-4 的硬件 FIFO。那是一个双时钟 FIFO,它的控制逻辑直接做在硅片上(也就是说,不是在逻辑结构中实现)。
那个 FIFO 的数据流时不时会被卡住。经过一番调查后发现,它在正常运作一段时间后,会同时保持 empty(空)和 full(满)信号有效。这是一个非法状态,除非 FIFO 被保持在复位中——但它当时并没有。所以,在确认自己观察到的信号完全无误之后,我判定问题出在 FPGA 的 FIFO 上。之后,我改用由逻辑结构实现的 FIFO。
几个月后,我找到了关于这些 FIFO 的勘误记录。如果不是事先知道这个问题,我恐怕看不懂那条记录。但仔细阅读描述后,我可以断定它证实了我观察到的情况。
这个例子只是想说明:在把问题定性为“不是我的错”之前,FPGA 的 bug 必须明显到什么程度。
总结
当 FPGA 看起来似乎违反了自然规律时,人们很容易接受一些偏离常识的解释。但重要的是,仍然要去寻找一个合乎逻辑的解释——而且很多时候,并不需要超能力就能找到它。
不过,找出原因很可能需要对设计做一次彻底审查,而这并不一定是坏事。这种排查虽说可能让人沮丧,但它无论如何都可能对设计质量有显著贡献。