在进行CPU设计的时候,最花费时间的环节是制定设计方案和功能仿真调试。人无完人,再优秀的设计人员也无法保证其写出的代码没有错误(bug),所以bug调试是程序员的必备能力之一。在硬件设计过程中,查找bug并进行调试的工作可以在功能仿真阶段进行,也可以在实际电路测试阶段进行。由于从代码编写到进行功能仿真的周期短,而且功能仿真时观测信息丰富,因此我们通常会在功能仿真阶段尽可能多地发现错误。在本书中, 我们建议每个实践任务必须在通过功能仿真阶段之后再进入上板调试阶段 。
对于初学者,用Verilog语言进行电路编程、调试的经验比较少,且Verilog语言是描述电路的,与C、C++、Java、Python等编程语言描述的对象不一样,因此Verilog语言在调试的技巧上与一般的编程语言有较大差异,读者很难把C、C++程序的调试技巧借鉴到Verilog语言中。举个简单的例子,很多同学会采用“print大法”来调试C程序(虽然方法土了点,但大多数时候能解决问题),可是这种方法在调试一个Verilog程序的时候完全不适用。鉴于此,接下来介绍一些数字逻辑电路设计中常用的调试方法。
在介绍集成电路设计工作者的影像资料中,我们常常会看到集成电路设计工作者进行调试工作时,手边放着万用表、示波器、逻辑分析器等检测工具。这些工具确实是集成电路设计者进行实际电路调试时的“利器”,因为这些仪器所获取的信息能够为我们定位、分析错误提供客观依据。现实世界中,就是通过各种仪器来观察调试过程和结果的,所以我们在仿真时通过各种观察手段来“仿”出仪器结果这个“真”,体现出来就是仿真波形。可以想象,我们有一台功能极其强大的逻辑分析仪,可以将你所设计的电路在运行过程中每个时刻的每一个信号都抓取出来,并显示给你看。所以,在对数字逻辑电路设计进行功能仿真调试时,总是需要通过观察仿真波形来确定电路出错的位置。说到这里,有的人可能会有不同意见:不,我是通过仿真打印出的信息调试的,具体方法是通过在testbench(测试程序)里添加监控代码,对设计中的某些信号进行检查,发现异常值就打印出错信息。我不能说这种方式是错误的,但大家仔细想想就会发现两种观察方式的本质是一样的:观察的对象都是电路设计中的信号,只不过一种方式是将信号显示为图像让人用眼睛看,另一种方式是将信号表示为数据文件,然后用程序进行分析。两种方式各有其适用场景,可以结合使用。不过, 直接观察仿真波形是一种更方便的方法,大家都应该掌握 。
上一章在介绍基于Vivado的FPGA设计流程时,已经演示了如何进行功能仿真,并介绍了仿真调试界面。这些是功能仿真波形分析的基本手段,大家务必要熟练掌握。要达到这一目标别无他法,就是通过实践任务多练习。接下来,将介绍一些与功能仿真波形分析相关的进阶知识。
分析问题的时候最讲究思路,否则就是蛮干。同样是看波形定位错误,为什么不同的人之间会存在巨大的效率差异呢?我们的经验是:会看波形的人自有“套路”。接下来我们就说一说在实践中摸索出的一些“套路”。
第一步,熟悉待调试的设计。
设计都没搞清楚就去调试,就好比在没有地图的情况下寻找宝藏,其效率之低是可想而知的。大家可能会觉得这么简单的道理还用强调吗?但事实上,初学者最容易忽视的就是这一点。除非整个设计的代码都是你自己编写的,否则总会在调试过程中涉及别人写的代码,甚至有的人连自己的设计都说不清楚。我们常常遇到下面这样令人哭笑不得的场景:
学生:老师,我这儿调试不出来了,您帮我看看?
老师:好的,我来看看。(老师查看波形,发现异常,但是看代码的时候不懂了。)同学,这个地方的代码是什么意思?你的设计意图是什么?
学生:这个……这个就是……(支支吾吾一番后就没有然后了。)
老师:(老师对代码经过一番阅读理解)哦,你是不是最初想设计成……
学生:对对对,就是这个意思。哦,我知道哪里写错了。
亲爱的读者们,我们衷心希望这样的场景不会出现在你们身上。所以, 开始调试之前,一定要问问自己,是不是充分了解了设计 。如果有不清楚、不明白的地方,一定要先把设计搞清楚,磨刀不误砍柴工。
第二步,找到一个你能明确的错误点。
一旦设计功能仿真出错,一定意味着在某个时刻某些信号的值不对,这些点都是错误点(但它们并不都是问题的源头)。开始调试的时候,你必须找到一个错误点。对于简单电路设计来说,错误点很容易找到,因为一个健全的功能验证平台会在仿真过程中对设计的输出等信号进行监控,一旦出现不符合预期的输出就会报错。从报错信息中你就能知道出错的信号是什么,并发现出错点所处的仿真时刻 。然而对于像CPU这样一类复杂的设计,其功能仿真出错时的输出信息通常不会直接告诉你是哪个信号出错了。在这种情况下如何从输出信息定位到出错信号呢?我们在后面开始CPU设计实践的时候再做详细介绍。
第三步,沿着设计的逻辑链条逆向逐级查看信号,直至找到出错源头。
我们找到的出错点未必是造成错误的源头,但是修正错误时必须要从错误的源头着手。数字逻辑电路里各信号的状态变化是环环相扣、遵循严格的逻辑因果关系的,所以 从一个错误点出发,沿着这条逻辑链条向前追溯,就一定能找到错误的源头 。在进行逆向追溯的过程中,大部分人对于单纯的组合逻辑的逆向分析比较熟练,但是对于含有时序逻辑的电路显得有些“怵”。因此,我们重点说说这种情况下应该怎么看波形。
观察含有时序逻辑的电路的波形时,首先要把需要观察的电路所用的时钟信号抓取出来。这里的重点是不要把错误的时钟信号抓出来。在刚开始学习时,大家接触的设计往往只有一个全局时钟,但是,真实设计中经常会有多个时钟,例如CPU用一个时钟,外设用另一个时钟。由于时钟信号在各个模块内部一般都称为clk或clock,因此若抓错了时钟信号,在波形窗口中很难看出问题,但错误的时钟信号会给你的分析工作带来不必要的困扰。
有了时钟信号之后,我们再明确所观察的时序器件(如触发器、同步RAM)是用时钟上升沿还是下降沿来触发。如果我们从组合逻辑一路追溯到某个触发器或RAM的Q端上,那么就要把这个触发器或RAM的非时钟输入端信号都抓取出来,然后在波形上沿着时间轴向前(在波形图上就是向左查找)找到这个错误值写入的那个时钟上升(下降)沿,之后再对生成这个触发器或RAM输入的组合逻辑继续进行追溯。在沿着时间轴向前查找的过程中,一定要找到错误值写入的真正时刻。这里举几个典型的例子。
【例一】没有任何写使能信号的触发器 如果当前拍是触发器的Q端值首先出现错误的那一拍,那么就分析前一拍生成触发器D输入的组合逻辑。
【例二】有写使能信号的触发器 如果当前拍是触发器的Q端值首先出现错误的那一拍,我们要从这一拍开始沿着时间轴往前找到最近一个写使能信号有效的那一拍。然后,我们根据设计的意图来判断这一拍写使能信号是否应该置起。如果它应该置起,那么问题出在写入数据上,我们要继续追溯生成触发器D输入的组合逻辑。如果它不应该置起,那么问题就出在写使能信号上,我们要继续追溯生成触发器写使能信号的组合逻辑。但是,有时候你会发现这个时刻的写使能信号应该置起,写入的数据也是对的,那么这就是另一种出错的情形了,即在该时刻之后存在某个写使能信号该置起的时候没有置起。这时就需要基于设计的意图,从触发器Q端最早出错的那一拍开始,向前逐拍观察写使能和D输入的配合是否符合预期,直到找到写使能未能正常置起的那一拍。
【例三】单端口同步RAM 首先要从Q端值首次出错的那一拍开始沿时间轴向前找到最近的一个 有效的 读命令。再次提醒,有效的读命令意味着RAM的片选(和读使能)信号都是有效的,但是写使能是无效的 。在这个有效读命令发起的时钟周期,应先检查一下此刻RAM的地址输入是否正确。如果不正确,就要追溯生成地址输入的组合逻辑。如果此刻地址输入是正确的,那么又要分几种情况来考虑。
● 情况1 :我们怀疑最近一次对RAM这个地址的写出错了。
● 情况2 :我们怀疑最近一次对RAM的写与本次读之间有个本应该发出的写命令没有发出。
● 情况3 :我们怀疑后面某时刻本应该发出的一个读命令没有发出,或者说这个读命令发出的时机不对。
情况1又分两种情况。
● 情况1-1 :最近一次对该地址发出写命令的时刻,确实需要对RAM的这个地址写入,但是写入的数据错了。这时候我们就停在写入错误的那一拍,继续追溯生成写入数据的组合逻辑。
● 情况1-2 :最近一次对该地址发出写命令的时刻,并不需要对RAM的这个地址写入,这又有两种出错的可能性。
■ 情况1-2-1 :原本需要在这个时刻写另一个地址,但是地址错误地变成了当前地址,那么我们需要追溯生成地址的组合逻辑。
■ 情况1-2-2 :原本在这个时刻根本不应该对RAM写入,但是片选、写使能信号错误地被置起,且地址恰好是这个地址,那么此时需要追溯生成片选、写使能的组合逻辑。
对于情况2,从最近一次对该地址发出写命令的时刻开始,向后按照设计意图逐拍检查生成RAM片选、写使能的组合逻辑,找到那个你原本期望发出写命令的时刻,查看是什么原因造成它没有置起。
对于情况3,从最近的这个读命令向后,按照设计意图逐拍检查生成RAM片选、写使能的组合逻辑,找到那个你原本期望片选有效、写使能无效的时刻(也就是读的时刻),查看是什么原因造成它没有置起。
寄存器堆的追查方式与同步RAM相似,这里就不再详细讲述了。
1. 一次仿真记录所有信号的数据
采用前一节所述的“沿着设计的逻辑链条逆向逐级查看信号”的波形查看分析方法,我们经常会在分析的过程中将新的信号加入波形窗口中。但是,如果你创建Vivado工程之后没有做任何特别的处理,那么就会发现新加入的信号只有在加入后继续运行的仿真时间中才出现。这就导致在需要从当前时刻向前查看波形(这种情况其实还挺常见的)的时候,不得不重新运行一次仿真。如果出错的时刻比较靠后,那么等待仿真重新执行到出错点附近就会非常耗时。有没有仿真一次就能把所有信号都记录下来的方式呢?有!但是要特别设置。
在Vivado的工程视图下,点击左侧“PROJECT MANAGER”→“Settings”,在弹出的设置界面中选择“Project Settings”→“Simulation”,此时弹出的设置界面如图3.6所示。在其右部选择“Simulation”标签,然后在下面找到“xsim.simulate.log_all_signals”选项并勾选,点击OK保存配置。这样操作后再进行仿真时(如果已经开启仿真,请关闭仿真界面重新运行),就会一次性地将所有信号都记录下来。你在波形查看过程中添加任何新的信号,其从仿真0时刻开始的所有波形都会立刻显示出来。
图3.6 XSim一次性记录所有信号的设置界面
这是一个非常必要的设置。作者最初使用Vivado时因为不知道可以这样配置,所以在调试时基本上是放弃使用Vivado自带的XSim的。该配置对于调试效率的影响非常大,建议大家把它作为一个默认配置。
2. 给重要的时刻做标记
通过前面的描述,我们知道了在查看一个存在时序逻辑的电路的仿真波形时,免不了要沿着时间轴向前/向后查找。当我们分析到某一个出错时刻时,会发现导致此错误点的路径可能有好几条。通常,我们只能先沿着一条路径追溯下去,如果发现这条路径没有问题,就回到刚才找到的那个出错时刻,从另外一条路径接着追查。很多初学者会在“回到刚才找到的那个出错时刻”这个动作上花费大量的时间,因为面对茫茫的一片信号完全不记得是哪个时间点了。所以 我们强烈建议:在波形分析过程中,要及时给你认为重要的时刻做标记 (Marker)。
做标记的方法很简单:在波形上你关注的时刻处单击鼠标左键,此时游标(Cursor)将出现在你关注的时刻,点击如图3.7中C所示波形上方工具条中的按钮,就做好了一个标记。之后无论你是直接将波形移动到Marker处还是使用波形上方工具条中的快速移动游标定位到标记处的按钮(图3.7中的D和E所示),都会大幅度提高定位效率。
图3.7 波形控制工具条标记与缩放功能
3. 熟练使用波形缩小和放大功能
除了使用标记功能外, 熟练使用波形缩小和放大功能也能加快沿时间轴前后移动的速度 。基本的操作方法是,在准备进行大范围前后移动时,先点击波形上方工具条中的缩小按钮(如图3.7中B所示)将波形缩小至合适程度,然后拖动波形下方的滚动条至想观察的时刻附近,单击波形将游标落在这一时刻,再点击波形上方工具条中的放大按钮(如图3.7中A所示)放大波形至可以观察清楚信号的程度。
4. 对关联信号分割、分组
在调试复杂设计的时候,往往会在波形里加入很多信号,这样会导致上下翻看信号时出现混乱。我们 建议此时把相关联的信号通过分割(Divider)或者分组(Group)区分开来 。比如,分析流水线CPU的波形时,可以把属于同一级流水的信号放在同一个组里。建立分割的方法是,在你想加分割空行的位置的上一个信号处,对着信号名点击鼠标右键打开菜单栏,选择“New Divider”。删除分割的方法就是点击分割,按Del键。建立分组的方法是,选择准备放入一组的信号,然后右键打开菜单栏选择“New Group”。用户可以为分组起名字,当分组超过一个时,建议为分组起名字以便区分。分为一组的信号可以视调试的需要收起或是展开。
5. 用值查找快速定位多位宽信号
有时候需要从某一时刻开始向前或向后找到一个多位宽信号等于某个值的时刻。除非你很明确要找的结果就在该时刻前后几个时钟周期内出现,否则 强烈建议采用值查找(Find Value)的方法 ,而不是用鼠标拖着信号下面的滚动条人工查找。值查找的方法是,点击待查找信号的信号名,右键打开菜单栏选择“Find Value”,之后会在波形上方出现与搜索相关的工具栏,根据其提示输入数据就可以了。可惜的是,Vivado的XSim的值查找不支持通配符,使用者只能通过调整查找工具栏中Matching的方式来部分实现模糊查找的功能。
波形异常类错误是指不需要分析电路设计的功能,通过直接观察波形图就能判断出来的错误,比如波形中信号出现“X”。波形出错是浅层次的错误,很容易找到出错原因,但是初学者因为经验少,在面对这类错误的时候常常会无从下手。
我们将波形异常类错误细分为以下几类:
●信号为“Z”。
●信号为“X”。
●波形停止。
●越沿采样,即上升沿采样到被采样数据在上升沿后的值。
●波形怪异,即仿真波形图显示怪异,这是与设计的电路功能无关的错误。
“Z”表示高阻,比如电路断路就会显示为高阻,这种错误往往是以下两个原因导致的:
1)RTL里声明为wire型的变量从未被赋值。2)模块调用的信号未连接导致信号悬空。
图3.8显示了第2种情况的一个例子。
图3.8 信号为“Z”的错误示例
在上面的示例中,需要注意以下几点:
1)模块调用时信号未连接。未连接包括两种类型: 显式未连接 ,如图3.8a中的.c(); 隐式未连接 ,如图3.8a中模块adder调用时,a端口的未连接就是隐式未连接。显式未连接一般是人为故意设置的,只针对output类接口;隐式的未连接多是疏忽造成的,属于代码不规范造成的错误,往往也是导致信号为“Z”的主要原因。
2)在adder模块里,a端口未连接导致a为“Z”,c端口也未连接,但c是固定值。这是因为a端口是input,c端口是output。output类接口未连接是主模块里不使用该信号造成的,可能是人为故意设置的,而所有的input类接口被调用时不允许悬空。
3)在adder模块里,a信号从0时刻开始就是“Z”,而a_r信号是在100ns左右才变成“Z”。这是因为a信号为端口,被调用时就未连接,故从0时刻就为“Z”,但a_r信号是内部寄存器,从100ns时刻才使用a信号参与赋值,从而变成了“Z”。
针对以上问题,我们有以下几点建议:
●编写RT L时要注意代码规范,特别是模块调用时,要按接口顺序一一对应。
●所有input类接口被调用时不允许悬空。
●一旦发现一个信号为“Z”,应向前追溯产生该信号的因子信号,看是哪个信号为“Z”,一直追踪到该模块里的input接口,随后进行修正。
●可能“Z”只出现在向量信号里的某几位上,这时也采用同样的追溯方式。调用时某个接口存在宽度不匹配,也会造成该接口上某些位为“Z”。
“X”表示不定值,这种错误往往是以下两个原因之一导致的:
1)RTL里声明为reg型的变量从未被赋值。
2)RTL里多驱动的代码有时候也可能导致这种类型的错误。有些多驱动的代码不会导致“X”,因为有些多驱动代码可能会被Vivado自动处理,但这种情况其实是有风险的;有些多驱动代码会导致综合时失败,并且会明确报出多驱动的错误。
第1种情况如图3.9所示。在图3.9中,由于b_r信号声明后始终未赋值,导致其值为“X”,后续c信号由于使用了b_r信号,导致其值也为“X”。
图3.9 信号为“X”的错误示例
Vivado对于多驱动(2个及2个以上电路单元驱动同一信号),仿真时也会产生“X”,如图3.10所示。
图3.10 多驱动引发“X”的示例
这种情况下追溯信号为“X”的原因可能比较困难,可以尝试先进行综合,观察Critical warning的提示,此时会报出多驱动的警告,如图3.11所示。
图3.11 Vivado中多驱动报出Critical warning
针对信号为“X”的情况,我们有以下几点建议:
●一旦发现仿真错误来自某个出现“X”的信号,则向前追溯产生该信号的因子信号,看是哪个信号为“X”,一直追溯到某个信号未赋值,随后修正。
●如果因子信号都没有为“X”的,则可能是多驱动导致的。此时先进行综合,然后排查Error和Critical warning。
●寄存器信号如果没有复位值,在复位阶段其值可能也为“X”,但这种情况可能不会带来错误。
●“X”和1进行或运算结果为1,“X”和0进行与运算结果为0。
波形停止是指某一时刻开始仿真波形不再输出新内容,而工具却显示仿真仍在运行,这种错误往往是RTL里存在组合环路导致的。波形停止示例如图3.12所示。
图3.12 波形停止示例
有些波形停止错误的表现是:点击“run all”,但是波形立即停止,并提示检测到fatal。如图3.13所示,可以看到,这里是仿真模拟时的迭代达到了10000次的限制,造成这种情况的原因是模拟组合环路的计算达到次数上限后自动停止仿真了。
图3.13 波形停止的另一种表现
并不是所有的组合环路都会导致波形停止,有些复杂的组合环路(比如跨多个模块形成的组合环路)可能会被工具自动处理,但这种处理是有风险的,可能导致“仿真通过,上板不过”。
所谓组合环路,是指信号A的组合逻辑表达式中某个产生因子为B,而B的组合逻辑表达式中又用到了信号A,如图3.12的源码c_t用到了c,而c又用到了c_t。仿真器会在每个周期内计算该周期的所有表达式,组合逻辑循环嵌套会造成仿真器循环计算,导致其无法退出,最终导致波形停止的现象。
出现波形停止时,排查哪部分代码出现组合环路并不容易,我们建议按以下步骤处理:
1)一旦发现波形停止,就先对设计进行综合。
2)查看综合产生的Error和Critical warning提示,并尝试修正。比如图3.12示例中的组合环路,经过Vivado的综合后变成了一个多驱动的Critical warning提示,如图3.14所示。
图3.14 组合逻辑报出多驱动的Critical warning
另外,Vivado工程中的Tcl命令report_timing_summary会检查组合环路,并报出检查结果。遗憾的是,对于图3.14的示例,该命令并没有检查出组合环路,很有可能和综合时变成了多驱动有关。
越沿采样是指一个被采样的信号在上升沿采样到了其在上升沿后的值,一般情况下认为这是一个错误,是RT L里阻塞赋值“=”和非阻塞赋值“<=”使用不当导致的。
越沿采样是一种隐藏较深的错误,往往可能和逻辑错误混在一起。初看起来,其波形是很正常的,而且在发生越沿采样后,要再执行很长时间才会出错。因此,大家可以 先按照逻辑错误进行调试,如果发现数据采样有异常 , 就需要甄别是否出现了越沿采样的错误 。
图3.15给出了一个越沿采样的示例。
图3.15 越沿采样示例
如图3.15所示,在105ns时刻,clk上升沿到来,a_r和a_r_r同时变为1(也就是a的值)。a_r在105ns时刻前是0,在105ns时刻后是1。从源码来看,a_r_r是在上升沿采样a_r的值,结果在105ns时刻采样到a_r为1的值,也就是采样到a_r在同一上升沿后的值。这就属于越沿采样。
造成这一现象更深层的原因是Verilog里阻塞赋值“=”和非阻塞赋值“<=”混用了。在图3.15的源码中,a_r采用阻塞赋值,而a_r_r采用非阻塞赋值。每一次赋值分为两步:第一步是计算等式右侧的表达式;第二步是赋值给左侧的信号。这两步简记为计算和赋值。在一个上升沿到来时,所有由上升沿驱动的信号按以下顺序进行处理:
1)先处理阻塞赋值,即完成计算和赋值,同一信号完成计算后立刻完成赋值。同一always块里的阻塞赋值从上到下按顺序串行执行,不同always块里的阻塞赋值根据所用工具实现确定顺序的串行执行,一一完成计算和赋值。
2)进行非阻塞赋值的计算。对于所有非阻塞赋值,其等式右侧的值都同时计算好。
3)上升沿结束时,所有非阻塞赋值同时完成最终的赋值动作。
从以上描述可以看到,非阻塞赋值是在上升沿的最后一个时间步里完成处理的,晚于阻塞赋值的处理。所以在图3.15的示例中,a_r_r的赋值晚于a_r的赋值,造成了越沿采样的情况。
除非特意设计,一般认为越沿采样是一个设计错误。针对越沿采样错误,我们有以下两点建议。
●编写RTL时注意代码规范,所有always写的时序逻辑只允许采用非阻塞赋值。
●一旦发现越沿采样的情况,追溯被采样信号,直到追溯到某一个阻塞赋值的信号,随后进行修正。
我们将目前未能想到的波形出错的类型都归为波形怪异。当出现波形怪异类的错误时,需要区分是仿真工具出错还是RT L代码出错。
1)观察出错的信号,分析其生成原因。如果确认RT L没有出错,而波形显示又太怪异(比如始终为32'hxx?x0x?),则很可能是仿真工具出错。此时,可以重启Vivado或计算机,甚至重建工程,看能否解决此类问题。
2)如果实在无法从波形里区分错误的类型,可以尝试先进行综合,看综合后的Error、Critical warning和warning提示。其中,Error是必须要修正的,Critical warning是强烈建议要修正的,warning是建议尽量修正的。
3)对于某些不符合规范的代码,Vivado也不会报出warning,这就需要仔细复核代码。