在FPGA开发的世界里,功能验证就像是给设计做“全面体检”,是确保一切运行如意的关键。当设计越来越复杂,靠人工一个个去核对输出?那简直是大海捞针,效率低还容易看花眼。这时候,基于约束的随机验证(CRV)就成了我们的主力军,而记分板(Scoreboard),就是这支军队里最公正的“智能裁判”。
它能够自动比对设计的实际输出和预期结果,让验证效率飙升,可靠性也大大增强。今天,我们就一起用SystemVerilog,动手在FPGA验证环境中搭建一个既简单又实用的记分板,感受一下自动化验证的魅力。
一、为什么我们需要一个“记分板”?
想象一下,你在验证一个数据通路(比如FIFO、DMA或者图像处理流水线)。测试平台(Testbench)会像连珠炮一样,向待测设计(DUT)发送海量的随机数据。如果每个输出都得你瞪大眼睛看波形,或者在一堆打印信息里手动查找,那得多累啊,而且保不准就会出错。
记分板就是来解放你的!它的核心价值有三点:
- 自动化比较:实时或事后自动对比“实际输出”和“预期模型输出”,完全不用你操心。
- 错误精确定位:一旦发现不匹配,立刻报告错误发生的时间、具体数据和上下文,调试效率直接拉满。
- 覆盖率驱动:它可以和功能覆盖率模型联手,指导随机测试的生成,确保你的验证能覆盖到各种边边角角。
二、用SystemVerilog打造记分板,需要哪些“零件”?
一个典型的记分板,我们可以利用SystemVerilog面向对象(OOP)的特性,像搭积木一样把它构建出来。主要包含三部分:
- 预测模型(Reference Model):你可以把它理解为一个“黄金标准”或“参考答案”。它接收和DUT一样的输入,然后用更高抽象级别的方式,计算出DUT应该输出的结果。
- 数据容器(Mailbox或Queue):用来临时存放预测模型算好的“预期数据”。SystemVerilog提供的mailbox或者queue特别好用,能轻松搞定线程之间的数据传递和同步问题。
- 比较器(Comparator):这是记分板的核心“大脑”。它从容器里取出“预期数据”,再和从DUT那里监测到的“实际数据”进行比对,然后给出“对”或“错”的判决。
三、实战开始:给一个简易AXI-Stream FIFO配个裁判
理论说再多,不如动手敲一敲。假设我们有一个带延迟的AXI-Stream接口FIFO模块,现在就来为它量身定制一个记分板,验证数据传得对不对。
步骤1:定义“事务”类——数据的基本单元
在验证环境中,数据通常被打包成“事务”来处理。我们先定义好它长什么样:
class axi_stream_transaction;
rand bit [31:0] tdata; // 数据本身
rand bit [3:0] tkeep; // 字节有效指示
rand bit tlast; // 包结束标志
int id; // 给事务加个ID,方便调试跟踪
function void display(string prefix="");
$display("%sTime=%0t, ID=%0d, DATA=0x%h, LAST=%b",
prefix, $time, id, tdata, tlast);
endfunction
endclass步骤2:构建“记分板”类——我们的智能裁判
重头戏来了,我们来搭建裁判本体:
class scoreboard;
// 用两个“邮箱”分别存预期和实际的事务。参数‘0’表示邮箱无限大。
mailbox #(axi_stream_transaction) exp_mbx;
mailbox #(axi_stream_transaction) act_mbx;
int match_count, mismatch_count; // 记录对比结果
// 构造函数,初始化一切
function new();
exp_mbx = new();
act_mbx = new();
match_count = 0;
mismatch_count = 0;
endfunction
// 任务:把预测模型算好的事务,放进“预期邮箱”
task put_expected(axi_stream_transaction exp_tr);
exp_mbx.put(exp_tr);
endtask
// 任务:把从DUT监测到的事务,放进“实际邮箱”
task put_actual(axi_stream_transaction act_tr);
act_mbx.put(act_tr);
endtask
// 核心比较任务!通常它会作为一个独立的线程一直运行
task run();
axi_stream_transaction exp_tr, act_tr;
forever begin
// 等待两个邮箱里都有数据(这里假设数据顺序一致)
// 注意:如果DUT会乱序输出,就需要更复杂的匹配机制,比如用ID来配对。
exp_mbx.get(exp_tr);
act_mbx.get(act_tr);
compare(exp_tr, act_tr);
end
endtask
// 具体的比较函数
function void compare(axi_stream_transaction exp, act);
if (exp.tdata === act.tdata && exp.tlast === act.tlast) begin
match_count++;
`uvm_info("SCOREBOARD", $sformatf("Match! ID=%0d, EXP_DATA=0x%h, ACT_DATA=0x%h",
exp.id, exp.tdata, act.tdata), UVM_LOW)
end else begin
mismatch_count++;
`uvm_error("SCOREBOARD",
$sformatf("Mismatch! Time=%0t, ID=%0dn", $time, exp.id) +
$sformatf(" Expected: DATA=0x%h, LAST=%bn", exp.tdata, exp.tlast) +
$sformatf(" Actual: DATA=0x%h, LAST=%b", act.tdata, act.tlast))
end
endfunction
// 测试结束后,打印一份漂亮的最终报告
function void report();
$display("n=== SCOREBOARD FINAL REPORT ===");
$display("Total Comparisons: %0d", match_count + mismatch_count);
$display("Matches: %0d", match_count);
$display("Mismatches: %0d", mismatch_count);
if (mismatch_count == 0) begin
$display("*** TEST PASSED ***");
end else begin
$display("*** TEST FAILED ***");
end
$display("============================n");
endfunction
endclass步骤3:在测试平台中集成——让裁判开始工作
最后,我们需要在顶层的Testbench里实例化记分板,并把它和监测器(Monitor)连接起来:
module tb_axi_fifo();
// ... 这里省略DUT实例、接口声明、时钟生成等代码 ...
scoreboard sb; // 声明我们的记分板裁判
initial begin
sb = new(); // 创建裁判实例
fork
sb.run(); // 启动比较线程(裁判开始工作)
// 启动驱动、监测器等线程
begin
input_monitor_task(); // 输入监测器,会调用 sb.put_expected(...)
output_monitor_task(); // 输出监测器,会调用 sb.put_actual(...)
end
// 主测试序列
run_test_sequence();
join
// 等待测试结束
#100ns;
sb.report(); // 打印最终裁决报告
$finish;
end
// 输入监测器任务示例
task input_monitor_task();
axi_stream_transaction tr;
forever begin
// 捕捉DUT输入接口上的事务...
@(posedge vif.clk iff (vif.tvalid && vif.tready));
tr = new();
tr.tdata = vif.tdata;
tr.tlast = vif.tlast;
tr.id = some_id++; // ID自增
// 将事务送入记分板的预期队列
sb.put_expected(tr);
end
endtask
// 输出监测器任务类似,调用 sb.put_actual(tr)
// ...
endmodule四、还想更厉害?这些进阶技巧了解一下
- 处理乱序与延迟:上面的简易记分板假设数据顺序不变。如果你的DUT可能会打乱顺序输出,就需要在事务类里加入唯一ID(比如序列号),然后记分板内部使用关联数组,按照ID来存储和匹配预期数据。
- 拥抱UVM框架:对于大型项目,强烈推荐使用UVM。它内置了强大的
uvm_scoreboard、uvm_in_order_comparator等组件,以及uvm_tlm_analysis_fifo用于通信,能让你搭记分板事半功倍,复用性也极高。 - FPGA环境特别考量:在FPGA原型验证时,你可能会想把部分验证组件(比如比较器)用可综合的代码实现,并放到FPGA里做实时检查。这时候,可一定要注意你的代码风格必须是可综合的哦!
- 关注性能与内存:如果是长时间、大数据量的测试,要注意管理邮箱或队列的深度,别把内存撑爆了。可以考虑把比较结果实时记录到文件里,而不是全部缓存在内存中。
五、总结一下
看,记分板其实就是构建自动化、自检型验证环境的基石。通过今天的学习,我们利用SystemVerilog的OOP特性,从零开始搭建了一个用于AXI-Stream FIFO的简易记分板,涵盖了事务定义、数据传递、比较逻辑和结果报告的全流程。
掌握了这个基础框架,你就可以举一反三,根据具体设计(比如要处理乱序、多通道或者更复杂的协议)去扩展和优化你的“智能裁判”了。
在成电国芯的FPGA高级验证课程中,我们会带你更深入地探索如何基于UVM搭建工业级的验证平台,涵盖覆盖率收集、断言(SVA)、寄存器模型(RAL)等高级主题。我们的目标是帮助你从一名FPGA设计者,成长为能应对复杂芯片验证挑战的验证专家。期待与你一起,在验证的领域里玩出更多花样!






