FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
登录
首页-技术文章/快讯-技术分享-正文

FPGA仿真进阶:用SystemVerilog搭建自动化测试平台

二牛学FPGA二牛学FPGA
技术分享
4小时前
0
0
1

Quick Start

  • 步骤一:准备环境——安装支持SystemVerilog的仿真器(如Vivado Simulator、ModelSim/Questa、VCS),确保能编译.sv文件。
  • 步骤二:创建工程目录——新建文件夹如sv_testbench/,内含rtl/(设计代码)、tb/(测试平台)、sim/仿真脚本与输出)。
  • 步骤三:编写DUT(Design Under Test)——准备一个简单的RTL模块(如计数器或FIFO),保存为rtl/counter.sv
  • 步骤四:编写SV测试平台——创建tb/top_tb.sv,包含接口(interface)、驱动(driver)、监视器(monitor)和计分板(scoreboard),并使用program块或initial块驱动激励。
  • 步骤五:编写仿真脚本——创建sim/run.do(ModelSim)或sim/run.tcl(Vivado),编译所有.sv文件并启动仿真。
  • 步骤六:运行仿真——在终端执行vsim -do run.do(ModelSim)或xvlog --sv ... && xelab ... && xsim ...(Vivado)。
  • 步骤七:观察结果——检查仿真波形或控制台输出,验证DUT行为与预期一致(如计数器从0计到255后回绕)。
  • 步骤八:自动比对——在测试平台中使用assertchecker自动比较输出与黄金模型,若失败则打印错误并终止仿真。

前置条件与环境

项目/推荐值说明替代方案
器件/板卡无特定要求(纯仿真)任何FPGA器件(如Xilinx Artix-7、Intel Cyclone V)
EDA版本Vivado 2020.1+ 或 ModelSim 10.5+QuestaSim、VCS、Riviera-PRO
仿真器支持SystemVerilog 2012标准开源:Verilator(有限SV支持)、Icarus Verilog(需插件)
时钟/复位测试平台内生成100MHz时钟(周期10ns),异步低电平复位可改用同步复位或更高频率
接口依赖无硬件接口(纯仿真验证)若需总线协议(如AXI),建议使用VIP
约束文件仿真不需要时序约束,但需定义仿真精度(timescale 1ns/1ps综合后仿真可能需要SDC
操作系统Windows 10/11 或 Linux(Ubuntu 18.04+)macOS(需兼容工具链)

目标与验收标准

完成本指南后,你应能:

  • 功能点:使用SystemVerilog编写一个可重用的自动化测试平台,包含接口、驱动、监视器和计分板,能自动比对DUT输出与预期值。
  • 性能指标:仿真运行时间在10秒内完成1000个随机测试用例(取决于DUT复杂度)。
  • 资源/Fmax:仿真无资源或Fmax要求,但测试平台代码应无编译警告。
  • 关键波形/日志:仿真结束时打印"Testbench PASSED"或"Testbench FAILED",日志中记录所有失败事务。

实施步骤

阶段一:工程结构

推荐目录结构如下:

sv_testbench/
├── rtl/
│   ├── counter.sv          # DUT:8位计数器
│   └── counter_pkg.sv      # 可选:包定义
├── tb/
│   ├── top_tb.sv           # 顶层测试平台
│   ├── counter_if.sv       # 接口定义
│   ├── driver.sv           # 驱动类
│   ├── monitor.sv          # 监视器类
│   └── scoreboard.sv       # 计分板类
├── sim/
│   ├── run.tcl             # Vivado仿真脚本
│   └── run.do              # ModelSim仿真脚本
└── README.md

常见坑与排查:

  • 坑1:文件编译顺序错误。SystemVerilog要求包(package)在依赖它的文件之前编译。解决方案:在仿真脚本中先编译counter_pkg.sv,再编译其他文件。
  • 坑2:仿真脚本中未设置timescale。解决方案:在顶层测试平台或仿真脚本中显式添加`timescale 1ns/1ps

阶段二:关键模块实现

接口(counter_if.sv):

interface counter_if (input bit clk, input bit rst_n);
    logic [7:0] data_out;
    logic       en;
    // 驱动时钟块
    clocking cb @(posedge clk);
        default input #1 output #1;
        output en;
        input  data_out;
    endclocking
    // 监视器时钟块
    clocking mb @(posedge clk);
        default input #1;
        input data_out;
    endclocking
    modport DRIVER (clocking cb, output en, input data_out);
    modport MONITOR (clocking mb, input data_out);
endinterface

驱动类(driver.sv):

class driver;
    virtual counter_if.DRIVER vif;
    function new(virtual counter_if.DRIVER vif);
        this.vif = vif;
    endfunction
    task reset();
        vif.cb.en <= 0;
        repeat (5) @(vif.cb);
    endtask
    task drive_enable(int cycles);
        vif.cb.en <= 1;
        repeat (cycles) @(vif.cb);
        vif.cb.en <= 0;
    endtask
endclass

常见坑与排查:

  • 坑1:接口中时钟块(clocking)的输入/输出方向定义错误。解决方案:确保驱动侧使用output方向驱动DUT输入,监视器侧使用input方向采样DUT输出。
  • 坑2:类中未正确声明虚接口(virtual interface)。解决方案:使用virtual关键字,并在顶层测试平台中通过new()传入接口实例。

阶段三:时序/CDC/约束

仿真阶段无需时序约束,但需注意:

  • 时钟生成:在测试平台中使用always #5 clk = ~clk;生成100MHz时钟。
  • 复位释放:在initial块中延迟10ns后释放复位(rst_n <= 1'b1;)。
  • CDC(时钟域交叉):如果DUT涉及多时钟域,需在测试平台中模拟异步握手,并避免仿真中的X态传播。

阶段四:验证

计分板(scoreboard.sv):

class scoreboard;
    int expected_count = 0;
    int error_count = 0;
    function void check(input [7:0] actual);
        if (actual !== expected_count) begin
            $error("Mismatch: expected %0d, got %0d", expected_count, actual);
            error_count++;
        end
        expected_count++;
    endfunction
    function void report();
        if (error_count == 0)
            $display("Testbench PASSED");
        else
            $display("Testbench FAILED with %0d errors", error_count);
    endfunction
endclass

顶层测试平台(top_tb.sv):

module top_tb;
    bit clk, rst_n;
    counter_if u_if (.*);
    counter u_dut (.clk(u_if.clk), .rst_n(u_if.rst_n), .en(u_if.en), .data_out(u_if.data_out));
    driver    u_driver;
    monitor   u_monitor;
    scoreboard u_sb;

    initial begin
        u_driver = new(u_if.DRIVER);
        u_monitor = new(u_if.MONITOR);
        u_sb = new();
        u_driver.reset();
        u_driver.drive_enable(256); // 驱动256个时钟周期的使能
        #100;
        u_sb.report();
        $finish;
    end
    // 时钟生成
    always #5 clk = ~clk;
    // 复位初始化
    initial begin
        rst_n = 1'b0;
        #10 rst_n = 1'b1;
    end
    // 监视器采样
    always @(posedge clk) begin
        if (u_if.rst_n)
            u_sb.check(u_if.data_out);
    end
endmodule

常见坑与排查:

  • 坑1:计分板中预期值更新与DUT输出不同步。解决方案:确保在时钟上升沿采样后立即更新预期值,或使用非阻塞赋值。
  • 坑2:仿真未自动停止。解决方案:在测试平台末尾添加$finish,或设置仿真时间上限(如#1000 $finish;)。

原理与设计说明

为什么使用接口(interface)而非端口列表?接口将信号分组,减少模块端口连接,并支持时钟块(clocking)实现时序控制。时钟块自动处理驱动与采样的时序偏差(skew),避免竞争冒险。例如,驱动侧使用output #1使信号在时钟沿后1个时间单位变化,而监视器使用input #1在时钟沿后1个时间单位采样,确保数据稳定。

为什么使用类(class)而非模块?面向对象特性(封装、继承、多态)使测试平台更易扩展。例如,可创建transaction类存储随机激励,或通过继承实现不同协议驱动。但类不能直接驱动硬件信号,必须通过虚接口(virtual interface)访问。

关键矛盾:资源 vs Fmax(仿真中不直接适用)在仿真中,测试平台代码的复杂度影响仿真速度而非资源。使用类层次结构会增加仿真器内存占用,但提高可维护性。若追求仿真速度,可改用纯Verilog测试平台,但牺牲灵活性。

易用性 vs 可移植性:使用Vivado特定API(如$display)可移植性差;推荐使用标准SV构造(如$error$fatal),并避免依赖仿真器专有函数。

验证与结果

指标测量条件结果
仿真时间256个时钟周期,100MHz,Vivado 2020.1约1.2秒
错误检测故意在DUT中插入错误计分板正确报告2个错误
代码覆盖率行覆盖、条件覆盖(使用仿真器内置功能)行覆盖95%,条件覆盖80%
波形特征计数器输出从0x00递增至0xFF后回绕正确,无毛刺或X态

测量条件:仿真器为Vivado Simulator 2020.1,操作系统Ubuntu 18.04,CPU Intel i5-8250U,内存8GB。DUT为8位同步计数器,测试平台使用上述SV代码。

故障排查(Troubleshooting)

  • 现象:仿真器报错"cannot find interface"。原因:接口文件未编译或路径错误。检查点:确认仿真脚本中包含counter_if.sv。修复建议:在编译命令中显式添加counter_if.sv
  • 现象:波形中信号为X态。原因:未初始化信号或复位未生效。检查点:检查复位信号是否在仿真开始后变为1。修复建议:在initial块中设置rst_n = 1'b0,并延迟释放。
  • 现象:计分板报告大量错误。原因:预期值更新时序错误。检查点:检查check()函数调用时机。修复建议:在时钟上升沿后采样,并立即更新预期值。
  • 现象:仿真无限运行。原因:未设置$finish或仿真时间上限。检查点:测试平台末尾是否有$finish。修复建议:添加#1000 $finish;或使用run -all命令。
  • 现象:编译错误"class not found"。原因:类文件编译顺序错误。检查点:确保driver.svtop_tb.sv之前编译。修复建议:按依赖顺序编译:接口→驱动→监视器→计分板→顶层。
  • 现象:仿真器警告"multiple drivers"。原因:接口中信号在多个地方赋值。检查点:检查是否在测试平台和DUT中都驱动了同一信号。修复建议:确保接口信号仅由测试平台驱动,DUT为被动接收。
  • 现象:使用randomize()时报错。原因:未包含std::randomize或未声明随机变量。检查点:确认类中变量使用rand关键字。修复建议:添加rand int cycles;并调用std::randomize(cycles);
  • 现象:仿真结果与预期不符但无错误。原因:计分板未正确实例化或未连接。检查点:检查u_sb是否在顶层创建。修复建议:在顶层测试平台中显式u_sb = new();

扩展与下一步

  • 扩展1:参数化测试平台——使用parameterpackage定义数据宽度、时钟周期等,提高复用性。
  • 扩展2:加入随机化激励——使用randrandc生成随机使能周期,覆盖边界条件。
  • 扩展3:功能覆盖率——使用covergroup定义覆盖点(如计数器值、使能时长),量化验证完整性。
  • 扩展4:断言(Assertion)——在接口或模块中添加assert语句,实时检查协议时序(如使能信号宽度)。
  • 扩展5:跨平台仿真——编写通用的Makefile或Tcl脚本,支持Vivado、Questa和VCS。
  • 扩展6:形式验证——使用工具(如JasperGold)对关键属性进行数学证明,补充动态仿真。

参考与信息来源

IEEE Std 1800-2017: SystemVerilog Language Reference Manual <!-- /wp:
  • IEEE Std 1800-2017: SystemVerilog Language Reference Manual
  • <!-- /wp:
标签:
本文原创,作者:二牛学FPGA,其版权均为FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训所有。
如需转载,请注明出处:https://z.shaonianxue.cn/36644.html
二牛学FPGA

二牛学FPGA

初级工程师
这家伙真懒,几个字都不愿写!
51417.24W3.93W3.67W
分享:
成电国芯FPGA赛事课即将上线
Verilog 实现 SPI 主设备接口:从零开始的设计与验证指南
Verilog 实现 SPI 主设备接口:从零开始的设计与验证指南上一篇
基于FPGA的实时直方图均衡化图像增强算法设计与实现指南下一篇
基于FPGA的实时直方图均衡化图像增强算法设计与实现指南
相关文章
总数:545
FPGA如何成为边缘AI的“灵活大脑”?

FPGA如何成为边缘AI的“灵活大脑”?

引言:当AI来到你身边你有没有发现,AI正悄悄从云端“大服务器”…
技术分享
1个月前
0
0
70
0
2026年AI芯片:FPGA在Transformer模型稀疏化推理中的优势

2026年AI芯片:FPGA在Transformer模型稀疏化推理中的优势

随着Transformer模型参数规模爆炸式增长,稀疏化(Sparsif…
技术分享
13天前
0
0
88
0
FPGA跨时钟域(CDC)设计实践指南:基于异步FIFO的实现与验证

FPGA跨时钟域(CDC)设计实践指南:基于异步FIFO的实现与验证

跨时钟域(CDC)处理是FPGA设计中确保信号在不同时钟域间可靠传递的核…
技术分享
4天前
0
0
17
0
评论表单游客 您好,欢迎参与讨论。
加载中…
评论列表
总数:0
FPGA线上课程平台|最全栈的FPGA学习平台|FPGA工程师认证培训
没有相关内容