FPGA 仿真只会跑波形,不知道怎么「证明自己没写错」?

开放3 回答 102 浏览

现在做 FPGA 练习题,基本流程是:写完 Verilog,在仿真工具里写简单 testbench,跑一遍波形,看输出大致对就算过了。但经常在上板后还是遇到异常,比如某些边界条件没考虑、复位没处理好、跨时钟域有隐患。想请教:在工程里,大家通常怎么设计 testbench 和仿真用例,才能比较有信心说「这个模块在可接受范围内是正确的」?有没有适合学生阶段的仿真 checklist 或示例?

分享:

3 个回答

  • 芯片设计新人

    先对齐一下场景:你现在多半是学生或者刚入门,做的是课程练习题,模块规模不大,但已经开始上板,所以痛点从『波形能跑』变成了『上板出问题找不到原因』。这俩其实是同一个根:你的验证目标定义得太模糊了。

    我的建议是换个思路——先别急着写 testbench,先在纸上(或者注释里)把『我要证明什么』写清楚。比如你要验一个 FIFO,那你要证明的其实是几件事:空的时候读不会读出脏数据、满的时候写不会覆盖老数据、读写时钟不同频的时候指针不会错乱、复位中途拉起来状态能回到已知值。这些每一条就是一个『可验证的行为』,每条至少对应一个 test case。你会发现,一旦这么拆,你根本不需要『看波形猜对不对』,因为每条都有明确的通过/失败判据。

    接下来是关键的取舍:肉眼波形 vs 自动检查。学生阶段最容易偷懒的就是只看波形,因为反馈快、门槛低。但波形的问题是它不可回归、不可累积——你今天看对了,明天改一行代码又得重看一遍,而且你只会去看你『预期有问题』的地方,恰恰漏掉你以为没问题的地方。所以从练习题开始,就要强迫自己写 self-checking 的 testbench:给一组激励,同时给出期望输出,仿真时自动比对,失败就 $display 打印出错时刻、输入、实际值、期望值。哪怕一开始只用最简单的 task 封装比对逻辑,也比看波形强一个量级。

    再往深一层,是主动制造非理想情况。跨时钟域、状态机、计数器这些模块,bug 几乎都出在边界和异常时序上:异步复位在时钟沿附近拉起、随机插入延迟、乱序输入、连续背靠背操作。你可以在 testbench 里用随机化($random 加约束)去撞这些场景,跑几百上千次,比手写几个 case 覆盖率高得多。

    最后说个习惯问题:『先想清楚怎么证明对,再写 RTL』,这句话真的值钱。它逼你在写代码前就把接口协议、边界条件想明白,很多 bug 在纸上就消灭了。工具技巧是次要的,验证思维才是能带到工作里的东西。你现在练手用的是什么仿真器,Vivado 自带的还是 ModelSim/Verilator?不同工具写 self-checking 的顺手程度差挺多的。

  • 零号程序员

    学生阶段可以给自己定一个很朴素的 checklist,不用多,四类就够。第一类是功能正常路径,随便给几个典型输入,确认输出符合预期;第二类是边界,FIFO 就测空读、满写,计数器就测回绕,状态机就测每个状态的跳转;第三类是非理想时序,异步复位中途拉起、输入带随机延迟、跨时钟域两边频率不成整数倍;第四类是复位后一致性,复位释放后所有状态是不是都回到已知值。

    每一类你都要在 testbench 里写自动检查,而不是跑完看波形。最简单的做法是准备一张输入/期望输出的表,循环喂进去,用 if 比对,不匹配就报错。等你熟练了再上 assert 和覆盖率。

    其实核心就一句:每写一个 case,先问自己『如果这个 case 失败,我的 tb 会不会自己告诉我』。会,就说明你在做验证;不会,就说明你在看烟花。

  • 电子爱好者小张

    我猜你现在的状态是:课程练习能跑通,上板偶尔翻车,翻车之后第一反应是回去看波形,但波形太多、看不出哪一拍出问题。这个循环的根子不在仿真技巧,在于你写 testbench 之前没想清楚「我要证明什么」。

    先说个反直觉的取舍:对学生阶段来说,把 testbench 写成 self-checking 比写得花哨重要得多。因为肉眼看波形的成本是人眼带宽决定的,你一个 FIFO 看几十个周期还行,看几千个周期一定漏。而自动比对只要写一次,之后每次改 RTL 都能重跑,这才是仿真的复利。

    具体怎么落地,我建议按这个顺序走。

    第一步,把需求翻译成「可验证的行为」。别写「验证 FIFO 功能」,太糊。写成:空读不产生有效数据、满写不覆盖旧数据、读写时钟不同频时指针不错、复位中途拉起后状态回到已知值、连续突发读写后数据顺序不乱。每一条就是一个 test case,每条都要有明确的通过/失败判据。这一步在纸上做,不碰代码。

    第二步,testbench 分两块:激励和监控。激励负责按你设计的时序喂输入,包括边界值(FIFO 的深度、计数器的最大值)和极端时序(读写同时来、背靠背突发)。监控负责判定,最简单的方式是准备一张输入到期望输出的对照表,循环喂进去,用 if 比对,不一致就 $display 出关键信号并置 error 标志。进阶一点用 assert,但别一开始就追求 SystemVerilog 那套 UVM,学生阶段用不上。

    第三步,专门给易错模块造非理想情况。跨时钟域就制造两边频率不成整数倍、复位在传输中途释放;状态机就遍历每个状态的跳转,包括非法输入下的行为;计数器就测回绕。这些场景在正常波形里往往看不到,必须主动构造。

    第四步,跑完看 error 计数,不看波形。波形只在失败时用来定位,不是用来判定的。

    长期看,这个习惯的价值远超工具技巧:它逼你在写 RTL 之前就想清楚接口契约和异常行为,而这正是工程里 code review 和验证的起点。你现在练的是「怎么证明自己对」,以后做项目就是「怎么让别人相信你对」。

    顺带问一句,你现在验证的模块大概是什么规模,是单个 FIFO/状态机这种,还是已经有一个小的数据通路了?这决定了你该先练 self-checking 还是先练覆盖率。

登录后可在本页底部提交回答

提问者

Debug小白查看主页
浏览「其他」

描述场景与已尝试方案,更容易获得有效解答

相关问题

同分类

提问建议

  • 标题写清核心疑问,避免「求助」「请问」等空泛用语
  • 正文补充环境、版本、报错信息或截图
  • 先搜索本站是否已有相近问题,减少重复提问
  • 若与课程相关,请标明课时或章节便于讲师定位