0%

UVM白皮书之一个简单的UVM验证

本文基于《UVM实战》白皮书,记录UVM学习的过程,本章是对第二章:一个简单的UVM验证的学习与记录。

验证平台的组成

  • 一个验证平台要实现如下基本功能:

    • 验证平台要模拟DYT的各种真实使用情况,这意味着要给DUT施加各种激励。激励的功能是由driver来实现的
    • 验证平台要能够根据DUT的输出来判断DUT的行为是否与预期相符合,完成这个功能的是scoreboard
    • 验证平台要能收集DUT的输出并把它们传递给scoreboard,完成这个功能的是monitor
    • 验证平台要能够给出预期结果,在driver传递给DUT计算的同时,验证平台中也需要有一个模块能完成相应的计算结果,给出预期。完成这个功能的是reference model
  • 一个简单的验证平台框图如下:

    image-20240429011111379
  • 在UVM中,引入了agent和sequence的概念,因此UVM中验证平台的典型框图如下:

    image-20240429011144579

只有driver的验证平台

1.最简单的验证平台

  • driver是验证平台最基本的组件,是整个验证平台数据流的源泉。(本节以一个简单的DUT为例,说明一个只有driver的UVM验证平台是如何搭建的)

  • dut.sv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    module dut(clk,
    rst_n,
    rxd,
    rx_dv,
    txd,
    tx_en);
    input clk;
    input rst_n;
    input[7:0] rxd;
    input rx_dv;
    output [7:0] txd;
    output tx_en;

    reg[7:0] txd;
    reg tx_en;

    always @(posedge clk) begin
    if(!rst_n) begin
    txd <= 8'b0;
    tx_en <= 1'b0;
    end
    else begin
    txd <= rxd;
    tx_en <= rx_dv;
    end
    end

    endmodule
  • UVM是一个库,在这个库中,几乎所有的东西使用类(class)来实现。driver、monitor、reference model、scoreboard等组成部分都是类

  • 类有函数(function),另外还可以有任务(task),还有成员变量,这些成员变量可以控制类的行为,如控制driver的行为

  • 当要实现一个功能时,首先应该想到的是从UVM的某个类派生出一个新的类,在这个新的类中实现所期望的功能

  • 使用UVM的第一条原则是:验证平台中所有的组件应该派生自UVM中的类

  • UVM验证平台中的driver应该派生自uvm_driver

  • 一个简答的driver如下例所示:my_driver.sv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    `ifndef MY_DRIVER__SV
    `define MY_DRIVER__SV

    class my_driver extends uvm_driver;

    function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
    endfunction

    extern virtual task main_phase(uvm_phase phase);

    endclass

    task my_driver::main_phase(uvm_phase phase);
    top_tb.rxd <= 8'b0;
    top_tb.rx_dv <= 1'b0;
    while(!top_tb.rst_n)
    @(posedge top_tb.clk);
    for(int i = 0; i < 256; i++)begin
    @(posedge top_tb.clk);
    top_tb.rxd <= $urandom_range(0, 255);
    top_tb.rx_dv <= 1'b1;
    `uvm_info("my_driver", "data is drived", UVM_LOW)
    end
    @(posedge top_tb.clk);
    top_tb.rx_dv <= 1'b0;
    endtask
    `endif
    • 分析上述代码:

      • 所有派生自uvm_driver的类的new函数有两个参数,一个是string类型的name,一个是uvm_component类型的parent。事实上,这两个参数是由uvm_component要求的,每个派生自uvm_component或其派生的类在new函数中都要声明name和parent。uvm_driver是一个派生自uvm_component的类

        1
        function new(string name = "my_driver", uvm_component parent = null);
      • driver所做的事情几乎都在main_phase中完成。UVM由phase来管理验证平台的运行,这些phase统一以xxx.phase命名,且都有一个类型为uvm_phase、名字为phase的参数main_phase是uvm_driver中预先定义好的一个任务。因此几乎可以简单认为,实现一个driver等于实现其main_phase(感觉归根到底不就是实现了一个函数)

        1
        task my_driver::main_phase(uvm_phase phase);
      • 代码中还出现了uvm_info宏。这个宏的功能与verilog中display语句的功能类似,但是它比display语句更加强大。它有三个参数:第一个参数是字符串,用于把打印的信息归类,第二个参数也是字符串,是具体需要打印的信息,第三个参数则是冗余级别(在验证平台中,某些信息是非常关键的,这样的信息可以设置为UVM_LOW,而有些信息可有可无,就可以设置为UVM_HIGH,介于两者之间的就是UVM_MEDIUM。UVM默认只显示UVM_MEDIUM或者UVM_HIGH的信息)。在搭建验证平台时应尽量使用uvm_info宏取代display语句。

        1
        `uvm_info("my_driver", "data is drived", UVM_LOW)
  • 对my_driver实例化并且最终搭建的验证平台如下:(top_tb.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    `timescale 1ns/1ps
    `include "uvm_macros.svh"

    import uvm_pkg::*;
    `include "my_driver.sv"

    module top_tb;

    reg clk;
    reg rst_n;
    reg[7:0] rxd;
    reg rx_dv;
    wire[7:0] txd;
    wire tx_en;

    dut my_dut(.clk(clk),
    .rst_n(rst_n),
    .rxd(rxd),
    .rx_dv(rx_dv),
    .txd(txd),
    .tx_en(tx_en));

    initial begin
    my_driver drv;
    drv = new("drv", null);
    drv.main_phase(null);
    $finish();
    end

    initial begin
    clk = 0;
    forever begin
    #100 clk = ~clk;
    end
    end

    initial begin
    rst_n = 1'b0;
    #1000;
    rst_n = 1'b1;
    end

    endmodule
    • 分析上述代码:

      • uvm_macro.svh文件通过include语句包含进来。这是UVM中的一个文件,里面包含了众多的宏定义,只需要包含一次

        1
        `include "uvm_macros.svh"
      • 通过import语句将整个uvm_pkg导入验证平台中。只有导入了这个库,编译器在编译my_driver.sv文件时才会认识其中的uvm_driver等类名

        1
        import uvm_pkg::*;
      • 定义drv为my_driver的实例,并将其例化。调用new函数时,其传入的名字参数为drv(uvm_info宏的打印信息时出现的代表路径索引的drv就是在这里传入的参数drv

        1
        2
        my_driver drv;
        drv = new("drv", null);
      • 显示地调用my_driver的main_phase。在main_phase的声明中,有一个uvm_phase类型的参数phase(在真正的验证平台中,这个参数是不需要用户理会的,本节的验证平台还算不上一个完整的UVM验证平台,所以暂且传入null)

        1
        drv.main_phase(null);
    • 该例最后的仿真打印结果如下:

      image-20240428202136398

      • “data is drived”被输出了256次

      • 关于uvm_info宏打印的结果中有如下几项:

        • UVM_INFO关键字:表明这是一个uvM宏打印的结果

        • my_driver.sv(23):指明该条打印信息的来源,其中括号的数字表示原始的uvm_info打印语句在my_driver.sv中的行号

        • @后面的数字,比如51100表示此条信息的打印时间

        • drv这是driver在UVM数中的路径索引(其实就是后面实例调用时new传入的第一个参数)。UVM采用树形结构,对于树中任何一个节点,都有一个与其相应的字符串类型的路径索引。路径索引可以通过get_full_name函数来获取,把下列代码加入任何UVM数的节点中就可以得知当前节点的路径索引:

          1
          $display("the full name of current component is: %s", get_full_name());

2.加入factory机制

  • factory机制的实现被集成在了一个宏中:uvm_component_utils。这个宏所做的事情非常多,其中之一就是将my_driver登记在UVM内部的一张表中,这张表是factory功能实现的基础。

  • 只要在定义一个新的类时使用这个宏,就相当于把这个类注册到了这张表中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class my_driver extends uvm_driver;
    `uvm_component_utils(my_driver)

    function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
    `uvm_info("my_driver", "new is called", UVM_LOW);
    endfunction

    extern virtual task main_phase(uvm_phase phase);
    endclass
  • 在给driver中加入factory机制后,还需要对top_tb做一些改动:

    1
    2
    3
    initial begin
    run_test("my_driver");
    end
    • 这里使用一个run_test语句替换掉了前一小结中top_tb.sv中第23-28行的my_driver实例化以及main_phase的显示调用。
    • 一个run_test语句会创建一个my_driver的实例,并且会自动调用my_driver的main_phase
    • 给run_test传递的是一个字符串,UVM根据这个字符串创建了其所代表类的一个实例
  • 根据类名创建一个类的实例,这是uvm_component_utils宏所带来的效果,只有在类定义时声明了这个宏,才能使用这个功能。从某种程度上来说,这个宏起到了注册的作用。只有经过注册的类,才能使用这个功能,否则根本不能使用。

  • 所有派生自uvm_component及其派生类的类都应该使用uvm_component_utils宏注册(所谓注册,就是在定义一个新类时加上``uvm_component_utils(my_driver)`)

  • 在UVM验证平台中,只要一个类使用uvm_component_utils注册且此类被实例化(这里的实例化是通过run_test语句实现的)了,那么这个类的main_phase就会被自动调用

  • 经过上述修改后的代码(本书的代码开源,我就不一一粘贴了,网上随手一搜都能搜到),其仿真结果如下(并未打印256次data is driver,关于这个问题,牵扯UVM的objection机制):(仿真波形也卡住了)

    image-20240428215538079

    image-20240428222429417

3.加入objection机制

  • 在上一节中,虽然输出了“main_phase is called”,但是“data is drived”并没有输出,而main_phase是一个完整的任务,没有理由只执行第一句,而后面的代码不执行。看上去似乎main_phase在执行的过程中被外力”杀死“了,事实上也确实如此。

  • UVM中通过objection机制来控制验证平台的关闭

  • 在每个phase中,UVM会检查是否有objection被提起(raise_objection),如果有,那么等待这个objection被撤销(drop_objection)后停止仿真;如果没有,马上结束当前phase

    1
    2
    3
    phase.raise_objection(this);
    ...
    phase.drop_objection(this);
  • 在drop_objection语句之前必须先调用raise_objection语句,drop_objection和raise_objection总是成对出现的。加入objection机制后再运行验证平台,可以发现”data is drived”按照预期输出了256次。

    image-20240428222048974

  • raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。如$display语句是不消耗仿真时间的,这些语句可以放在raise_objection之前,但是类似@(posedge top_tb.clk)等语句是要消耗仿真时间的。若my_driver.sv的代码修改为:

    1
    2
    @(posedge top_tb.clk);
    phase.raise_objection(this);
    • 其结果如下:(甚至连仿真波形都会卡住)

      image-20240428222357817

      image-20240428222429417

4.加入virtual interface

  • 在前面的例子中,driver中等待时钟时间@(posedge top.clk)、给DUT中输入端口赋值(top.rx_dv<=1’b1)都是使用绝对路径,绝对路径的使用大大减弱了验证平台的可移植性。一个最简单的例子就是假设clk信号的层次从top.clk变成了top.clk_inst.clk,那么就需要对driver中的相关代码做大量修改。因此,从根本上来说,应该尽量杜绝在验证平台中使用绝对路径。

  • 避免绝对路径的一个方法是使用宏:

    1
    `define TOP top_tb
  • 这样,当路径修改时,只需要修改宏的定义即可。但是假设clk的冷酷就变为了top_tb.clk_inst.clk,而rst_n的路径变为了top_tb.rst_inst.rst_n,那么单纯地修改宏定义是无法起到作用的。

  • 避免绝对路径的另外一种方式是使用interface。在SV中使用interface来连接验证平台与DUT的端口。

  • interface的定义比较简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    `ifndef MY_IF__SV
    `define MY_IF__SV

    interface my_if(input clk, input rst_n);

    logic [7:0] data;
    logic valid;
    endinterface

    `endif
  • 定义了interface后,在top_tb中实例化DUT时,就可以直接使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    my_if input_if(clk, rst_n);
    my_if output_if(clk, rst_n);

    dut my_dut(.clk(clk),
    .rst_n(rst_n),
    .rxd(input_if.data),
    .rx_dv(input_if.valid),
    .txd(output_if.data),
    .tx_en(output_if.valid));
  • 在类中使用的是virtual interface

    1
    2
    class my_driver extends uvm_driver;
    virtual my_if vif;
  • 在声明了vif后,就可以在main_phase中使用如下方式驱动其中的信号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    task my_driver::main_phase(uvm_phase phase);
    phase.raise_objection(this);

    `uvm_info("my_driver", "main_phase is called", UVM_LOW);

    vif.data <= 8'b0;
    vif.valid <= 1'b0;
    while(!vif.rst_n)
    @(posedge vif.clk);
    for(int i = 0; i < 256; i++)begin
    @(posedge vif.clk);
    vif.data <= $urandom_range(0, 255);
    vif.valid <= 1'b1;
    `uvm_info("my_driver", "data is drived", UVM_LOW);
    end
    @(posedge vif.clk);
    vif.valid <= 1'b0;

    phase.drop_objection(this);
    endtask
  • UVM引进了config_db机制,来将top_tb中input_if和my_driver中的vif对应起来。

  • 在config_db机制中,分为set和get两步操作。所谓set操作,可以简单地理解成是”寄信“,而set则相当于是”收信“。

  • 在top_tb中执行set操作

    1
    2
    3
    initial begin
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
    end
  • 在my_driver中,执行get操作

    1
    2
    3
    4
    5
    6
    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    `uvm_info("my_driver", "build_phase is called", UVM_LOW);
    if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
    `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
    endfunction
  • 这里引入了build_phase。与main_phase一样,build_phase也是UVM中内建的一个phase。当UVM启动后,会自动执行build_phase,build_phase在new函数之后main_phase之前执行

    • 这里需要加入super.build_phase语句,因为在其父类的build_phase中执行了一些必要的操作,这里必须显式地调用并执行它
    • build_phase与main_phase不同的一点在于,build_phase是一个函数phase,而main_phase是一个任务phase,build_phase是不消耗仿真时间的,build_phase总是在仿真时间为0时执行。
    • 在build_phase中出现了uvm_fatal宏,uvm_fatal宏是一个类似于uvm_info的宏,但是它只有两个参数,这两个参数于uvm_info宏的前两个参数的意义完全一样。当uvm_fatal打印出第二个参数之后,会直接调用verilog的finish函数来结束仿真,其意味着验证平台出现了重大问题而无法继续下去,必须停止仿真并做相应的检查。
    • config_db的set和get函数都有四个参数,这两个函数的第三个参数必须完全一致
      • set函数:
        • 第一个参数:当使用 null 作为这个参数时,它意味着设置的配置项是全局可用的。
        • 第二个参数:表示路径索引,UVM通过run_test创建了一个my_driver的实例,其实例名即为uvm_test_top
        • 第三个参数:指定配置项的名称,为一个与get第三个参数连接的虚拟接口
        • 第四个参数:表示要将哪个interface通过config_db传递给my_driver(即把input_if接口给”vif”这个虚拟接口)
      • get函数:
        • 第一个参数:this。这是调用get方法的组件的句柄。它定义了查询配置数据库的上下文。通常,它指的是希望获取配置信息的组件的实例。
        • 第二个参数:""。这是一个字符串,定义了配置信息的作用域。空字符串表示全局作用域,即搜索整个UVM环境以找到匹配的配置项。如果这里指定了一个特定的层级路径,那么uvm_config_db将只在指定的层级内搜索配置信息。
        • 第三个参数:指定了要获取的配置项的名称,虚拟接口罢了
        • 第四个参数:表示把得到的interface传递给哪个my_driver的成员变量(即从”vif”这个虚拟接口收到的接口传递给vif这个成员变量)
    • set和get函数前面使用双冒号的原因是这两个函数都是静态函数
    • uvm_config_db#(virtual my_if)则是一个参数化的类,其参数就是要寄信的类型,这里是virtual my_if,假设要传递一个int类型的数据,那么久写为uvm_config_db#(int)
  • 最终,加入上述所述机制的代码如下:

    • my_driver.sv

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      `ifndef MY_DRIVER__SV
      `define MY_DRIVER__SV

      class my_driver extends uvm_driver;
      virtual my_if vif;

      `uvm_component_utils(my_driver)
      function new(string name = "my_driver", uvm_component parent = null);
      super.new(name, parent);
      `uvm_info("my_driver", "new is called", UVM_LOW);
      endfunction

      virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      `uvm_info("my_driver", "build_phase is called", UVM_LOW);
      if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
      `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
      endfunction

      extern virtual task main_phase(uvm_phase phase);
      endclass

      task my_driver::main_phase(uvm_phase phase);
      phase.raise_objection(this);

      `uvm_info("my_driver", "main_phase is called", UVM_LOW);

      vif.data <= 8'b0;
      vif.valid <= 1'b0;
      while(!vif.rst_n)
      @(posedge vif.clk);
      for(int i = 0; i < 256; i++)begin
      @(posedge vif.clk);
      vif.data <= $urandom_range(0, 255);
      vif.valid <= 1'b1;
      `uvm_info("my_driver", "data is drived", UVM_LOW);
      end
      @(posedge vif.clk);
      vif.valid <= 1'b0;

      phase.drop_objection(this);
      endtask

      `endif
    • my_if.sv

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      `ifndef MY_IF__SV
      `define MY_IF__SV

      interface my_if(input clk, input rst_n);

      logic [7:0] data;
      logic valid;
      endinterface

      `endif
    • top_tb.sv

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      `timescale 1ns/1ps
      `include "uvm_macros.svh"

      import uvm_pkg::*;
      `include "my_if.sv"
      `include "my_driver.sv"

      module top_tb;

      reg clk;
      reg rst_n;
      reg[7:0] rxd;
      reg rx_dv;
      wire[7:0] txd;
      wire tx_en;

      my_if input_if(clk, rst_n);
      my_if output_if(clk, rst_n);

      dut my_dut(.clk(input_if.clk),
      .rst_n(input_if.rst_n),
      .rxd(input_if.data),
      .rx_dv(input_if.valid),
      .txd(output_if.data),
      .tx_en(output_if.valid));

      initial begin
      clk = 0;
      forever begin
      #100 clk = ~clk;
      end
      end

      initial begin
      rst_n = 1'b0;
      #1000;
      rst_n = 1'b1;
      end

      initial begin
      run_test("my_driver");
      end

      initial begin
      uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
      end

      endmodule
    • 仿真结果如下:

      image-20240429005146960

      image-20240429005224640


为验证平台加入各个组件

1.加入transaction

  • 在UVM的各个组件之间,信息的传递时基于transaction的。一般来说,物理协议中的数据交换都是以帧或者包为单位的,通常在一帧或者一个包中要定义好各项参数,每个包的大小不一样。很少会有协议是以bit或者byte为单位来进行数据交换的,以以太网为例,每个包的大小至少是64byte,这个包中要包含源地址、目的地址、包的类型、整个包的CRC校验数据等。transaction就是用于模拟这种实际情况,一笔transaction就是一个包

  • 一个简单的transaction的定义如下:(my_transaction.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    `ifndef MY_TRANSACTION__SV
    `define MY_TRANSACTION__SV

    class my_transaction extends uvm_sequence_item;

    rand bit[47:0] dmac;
    rand bit[47:0] smac;
    rand bit[15:0] ether_type;
    rand byte pload[];
    rand bit[31:0] crc;

    constraint pload_cons{
    pload.size >= 46;
    pload.size <= 1500;
    }

    function bit[31:0] calc_crc();
    return 32'h0;
    endfunction

    function void post_randomize();
    crc = calc_crc;
    endfunction

    `uvm_object_utils(my_transaction)

    function new(string name = "my_transaction");
    super.new();
    endfunction
    endclass

    `endif
    • 其中dmac是48bit的以太网目的地址,smac是48bit的以太网源地址,ether_type是以太网类型,pload是其携带数据的大小(通过pload_cons约束可以看到,其大小被限制在46~1500byte),CRC是前面所有数据的校验值(这里只是在post_randomize中加了一个空函数calc_crc,post_randomize是SV中提供的一个函数,当某个类的实例的randomize函数被调用后,post_randomize会紧随其后无条件地被调用)
    • my_transaction的基类是uvm_sequence_item,在UVM中,所有的transaction都是从uvm_sequence_item派生,只有从uvm_sequence_item派生的transaction才可以使用后文讲述的UVM中强大的sequence机制
    • 从本质上来说,my_transaction与my_driver是有区别的,在整个仿真期间,my_driver是一直存在的,my_transaction不同,它有生命周期,它在仿真的某一个时间产生,经过driver驱动,再经过reference model处理,最终由scoreboard比较完成后,其生命周期就结束了
    • 一般来说,这种类都是派生自uvm_objection或者uvm_objection的派生类,uvm_objection_item的祖先就是uvm_objectionUVM中具有这种特征的类都要使用uvm_objection_utils宏来实现。
  • 当完成transaction的定义后,就可以在my_driver中实现基于transaction的驱动:(my_driver.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    `ifndef MY_DRIVER__SV
    `define MY_DRIVER__SV

    class my_driver extends uvm_driver;

    virtual my_if vif;

    `uvm_component_utils(my_driver)
    function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
    `uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
    endfunction

    extern task main_phase(uvm_phase phase);
    extern task drive_one_pkt(my_transaction tr);
    endclass

    task my_driver::main_phase(uvm_phase phase);
    my_transaction tr;
    phase.raise_objection(this);
    vif.data <= 8'b0;
    vif.valid <= 1'b0;
    while(!vif.rst_n)
    @(posedge vif.clk);
    for(int i = 0; i < 2; i++) begin
    tr = new("tr");
    assert(tr.randomize() with {pload.size == 200;});
    drive_one_pkt(tr);
    end
    repeat(5) @(posedge vif.clk);
    phase.drop_objection(this);
    endtask

    task my_driver::drive_one_pkt(my_transaction tr);
    bit [47:0] tmp_data;
    bit [7:0] data_q[$];

    //push dmac to data_q
    tmp_data = tr.dmac;
    for(int i = 0; i < 6; i++) begin
    data_q.push_back(tmp_data[7:0]);
    tmp_data = (tmp_data >> 8);
    end
    //push smac to data_q
    tmp_data = tr.smac;
    for(int i = 0; i < 6; i++) begin
    data_q.push_back(tmp_data[7:0]);
    tmp_data = (tmp_data >> 8);
    end
    //push ether_type to data_q
    tmp_data = tr.ether_type;
    for(int i = 0; i < 2; i++) begin
    data_q.push_back(tmp_data[7:0]);
    tmp_data = (tmp_data >> 8);
    end
    //push payload to data_q
    for(int i = 0; i < tr.pload.size; i++) begin
    data_q.push_back(tr.pload[i]);
    end
    //push crc to data_q
    tmp_data = tr.crc;
    for(int i = 0; i < 4; i++) begin
    data_q.push_back(tmp_data[7:0]);
    tmp_data = (tmp_data >> 8);
    end

    `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
    repeat(3) @(posedge vif.clk);

    while(data_q.size() > 0) begin
    @(posedge vif.clk);
    vif.valid <= 1'b1;
    vif.data <= data_q.pop_front();
    end

    @(posedge vif.clk);
    vif.valid <= 1'b0;
    `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
    endtask

    `endif
    • 在main_phase中,先使用randomize将tr随机化,之后通过drive_one_pkt任务将tr的内容驱动到DUT端口上

    • 这里定义了一个48位的临时变量tmp_data和一个动态大小的队列(使用$data_q,用于存储8位宽的数据

      1
      2
      bit [47:0] tmp_data;
      bit [7:0] data_q[$];
    • 获取随机的目的地址:这部分代码从传入的事务tr中取出目的MAC地址,将其每8位截取并放入data_q。此循环执行6次,因为MAC地址总共有48位。

      1
      2
      3
      4
      5
      6
      //push dmac to data_q
      tmp_data = tr.dmac;
      for(int i = 0; i < 6; i++) begin
      data_q.push_back(tmp_data[7:0]);
      tmp_data = (tmp_data >> 8);
      end
    • 开始传输数据:传输开始前,等待三个时钟周期,然后通过vif接口逐字节传输data_q中的数据。每个时钟上升沿,从队列前端弹出一个字节数据,直到队列为空。数据传输结束后,清除valid信号,表示数据包传输完成。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      repeat(3) @(posedge vif.clk);

      while(data_q.size() > 0) begin
      @(posedge vif.clk);
      vif.valid <= 1'b1;
      vif.data <= data_q.pop_front();
      end

      @(posedge vif.clk);
      vif.valid <= 1'b0;
    • 其实是通过push操作,先存下通过transaction生成的一个以太网格式的数据包,然后再发送

  • 最终仿真结果如下:(以太网数据包总共发送了两次)

    image-20240429151021108

    image-20240429151013179

2.加入env

  • 在验证平台中加入reference model、scoreboard等之前,思考一个问题:假设这些组件已经定义好了,那么在验证平台的什么位置对它们进行实例化呢?

  • 这个问题的解决方案是引入一个容器类,在这个容器类中实例化driver、monitor、reference model和scoreboard等。在调用run_test时,传递的参数不再是my_driver,而是这个容器类,即让UVM自动创建这个容器类的实例(my_env.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    `ifndef MY_ENV__SV
    `define MY_ENV__SV

    class my_env extends uvm_env;

    my_driver drv;

    function new(string name = "my_env", uvm_component parent);
    super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    drv = my_driver::type_id::create("drv", this);
    endfunction

    `uvm_component_utils(my_env)
    endclass

    `endif
  • 所有的env应该派生自uvm_env,且与my_driver一样,容器类在仿真中也是一直存在的,使用uvm_component_utils宏来实现factory的注册

  • 验证平台中的组件在实例化时都应该使用type_name::type_id::create的方式

  • 在drv实例化时,传递了两个参数,一个名字drv,另一个是this指针

    1
    drv = my_driver::type_id::create("drv", this); 
  • 回顾一下my_driver的new函数:

    1
    2
    3
    function new(string name = "my_driver", uvm_component parent = null);
    super.new(name, parent);
    endfunction
    • 这个new函数有两个参数,第一个参数是实例的名字,第二个则是parent。由于my_driver在uvm_env中实例化,所以my_driver的父节点(parent)就是my_env,通过parent的形式,UVM建立起了树形的组织结构。

    • 在这种树形的组织结构中,由run_test创建的实例是数根(这里是my_env),并且树根的名字是固定的,为uvm_test_top,在树根之后会生长出枝叶(这里只有my_driver),长出枝叶的过程需要在my_env的build_phase中手动实现。无论是树根还是树叶,都必须由uvm_component或者其派生类继承而来。

    • 当加入了my_env后,整个验证平台中存在两个build_phase,一个是my_env的,一个是my_driver的,其执行顺序:

      • 在UVM的树形结构中,build_phase的执行遵照从树根到树叶的顺序,即先执行my_env的build_phase,再执行my_driver的build_phase。当把整棵树的build_phase都执行完毕后,再执行后面的phase。
      image-20240429213951777
  • my_driver在验证平台中的层次结构发生了变化,所以在top_tb中使用config_db机制传递virtual my_if时,要改变相应的路径;同时,run_test的参数也要从my_driver变为my_env

    1
    2
    3
    4
    5
    6
    7
    initial begin
    run_test("my_env");
    end

    initial begin
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.drv", "vif", input_if);
    end
  • set函数的第二个参数从uvm_test_top变为了uvm_test_top.drv,其中uvm_test_top是UVM自动创建的树根的名字,而drv则是在my_env的build_phase中实例化drv(new创建时)时传递过去的名字。如果在实例化drv时传递的名字时my_drv,那么set函数的第二个参数中也应该是my_drv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class my_env extends uvm_env;
    ...
    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    drv = my_driver::type_id::create("my_drv", this);
    endfunction

    `uvm_component_utils(my_env)
    endclass

    module top_tb;
    ...
    initial begin
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.my_drv", "vif", input_if);
    end

    endmodule

  • 发现UVM不管创建的类对象赋值给了哪个变量,但其实传递的时候都是用实例的名字(new的第一个参数字符串),与赋值给的等式左边的那个变量名字无关

  • 仿真结果如下:

    image-20240429220500560

    image-20240429220520164

3.加入monitor

  • 验证平台必须检测DUT的行为,只有知道DUT的输入输出信号变化之后,才能根据这些信号变化来判断DUT的行为是否正确

  • 验证平台中实现监测DUT行为的组件是monitor

  • driver负责把transaction级别的数据转变成DUT的端口级别,并驱动给DUT,monitor的行为与其相对,用于收集DUT的端口数据,并将其转化成transaction交给后续的组件如reference model、scoreboard等处理

  • 一个monitor的定义如下:(my_monitor.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    `ifndef MY_MONITOR__SV
    `define MY_MONITOR__SV
    class my_monitor extends uvm_monitor;

    virtual my_if vif;

    `uvm_component_utils(my_monitor)
    function new(string name = "my_monitor", uvm_component parent = null);
    super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif))
    `uvm_fatal("my_monitor", "virtual interface must be set for vif!!!")
    endfunction

    extern task main_phase(uvm_phase phase);
    extern task collect_one_pkt(my_transaction tr);
    endclass

    task my_monitor::main_phase(uvm_phase phase);
    my_transaction tr;
    while(1) begin
    tr = new("tr");
    collect_one_pkt(tr);
    end
    endtask

    task my_monitor::collect_one_pkt(my_transaction tr);
    bit[7:0] data_q[$];
    int psize;
    while(1) begin
    @(posedge vif.clk);
    if(vif.valid) break;
    end

    `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
    while(vif.valid) begin
    data_q.push_back(vif.data);
    @(posedge vif.clk);
    end
    //pop dmac
    for(int i = 0; i < 6; i++) begin
    tr.dmac = {tr.dmac[39:0], data_q.pop_front()};
    end
    //pop smac
    for(int i = 0; i < 6; i++) begin
    tr.smac = {tr.smac[39:0], data_q.pop_front()};
    end
    //pop ether_type
    for(int i = 0; i < 2; i++) begin
    tr.ether_type = {tr.ether_type[7:0], data_q.pop_front()};
    end

    psize = data_q.size() - 4;
    tr.pload = new[psize];
    //pop payload
    for(int i = 0; i < psize; i++) begin
    tr.pload[i] = data_q.pop_front();
    end
    //pop crc
    for(int i = 0; i < 4; i++) begin
    tr.crc = {tr.crc[23:0], data_q.pop_front()};
    end
    `uvm_info("my_monitor", "end collect one pkt, print it:", UVM_LOW);
    tr.my_print();
    endtask


    `endif
    • 所有的monitor类应该派生自uvm_monitor

    • 与driver类似,在my_monitor中也需要有一个virtual my_if

    • uvm_monitor在整个仿真中是一直存在的,所以它是一个component,要使用uvm_component_utils宏注册

    • 由于monitor需要时刻收集数据,永不停歇,所以在main_phase中使用while(1)循环来实现这一目的

      1
      2
      3
      4
      5
      6
      7
      task my_monitor::main_phase(uvm_phase phase);
      my_transaction tr;
      while(1) begin
      tr = new("tr");
      collect_one_pkt(tr);
      end
      endtask
    • 当收集完一个transaction后,通过my_print函数将其打印出来,my_print在my_transaction中定义如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function void my_print();
      $display("dmac = %0h", dmac);
      $display("smac = %0h", smac);
      $display("ether_type = %0h", ether_type);
      for(int i = 0; i < pload.size; i++) begin
      $display("pload[%0d] = %0h", i, pload[i]);
      end
      $display("crc = %0h", crc);
      endfunction
    • 当完成monitor的定义后,可以在env中对其进行实例化

      1
      2
      3
      4
      5
      6
      virtual function void build_phase(uvm_phase phase);
      super.build_phase(phase);
      drv = my_driver::type_id::create("drv", this);
      i_mon = my_monitor::type_id::create("i_mon", this);
      o_mon = my_monitor::type_id::create("o_mon", this);
      endfunction
      • 实例了两个monitor,一个用来监测DUT的输入口,一个用来监测DUT的输出口(书中作者推荐)
  • 现在,整棵UVM数的结构如下:

    image-20240430014221095
  • 仿真结果如下:(仿真打印数据太多就不粘贴了,有发收发收共四次打印的结果)

    image-20240430012131785

    image-20240430012241270

4.封装成agent

  • UVM中通常将driver和monitor二者封装在一起,成为一个agent

  • 所有的agent都要派生自uvm_agent类,且其本身是一个component,应该使用uvm_component_utils宏来来实现factory注册

  • my_agent.sv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    `ifndef MY_AGENT__SV
    `define MY_AGENT__SV

    class my_agent extends uvm_agent ;
    my_driver drv;
    my_monitor mon;

    function new(string name, uvm_component parent);
    super.new(name, parent);
    endfunction

    extern virtual function void build_phase(uvm_phase phase);
    extern virtual function void connect_phase(uvm_phase phase);

    `uvm_component_utils(my_agent)
    endclass


    function void my_agent::build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (is_active == UVM_ACTIVE) begin
    drv = my_driver::type_id::create("drv", this);
    end
    mon = my_monitor::type_id::create("mon", this);
    endfunction

    function void my_agent::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    endfunction

    `endif
    • 在uvm_agent中,is_active的值默认为UVM_ACTIVE,在这种模式下,是需要实例化driver的(貌似也会自动实例化一个monitor),is_active=UVM_PASSIVE时只需要实例化monitor

      image-20240430134044322
  • 在完成i_agt和o_agt的声明后,在my_env的build_phase中对它们进行实例化后,需要指定各自的工作模式是active模式还是passive模式(my_env.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    `ifndef MY_ENV__SV
    `define MY_ENV__SV

    class my_env extends uvm_env;

    my_agent i_agt;
    my_agent o_agt;

    function new(string name = "my_env", uvm_component parent);
    super.new(name, parent);
    endfunction

    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    i_agt = my_agent::type_id::create("i_agt", this);
    o_agt = my_agent::type_id::create("o_agt", this);
    i_agt.is_active = UVM_ACTIVE;
    o_agt.is_active = UVM_PASSIVE;
    endfunction

    `uvm_component_utils(my_env)
    endclass
    `endif
  • 现在,整课UVM树变为:

    image-20240430134946557
    • 只有uvm_component才能作为树的结点,像my_transaction这种使用uvm_object_utils宏实现的类是不能作为UVM树的结点的
    • UVM要求UVM树最晚在build_phase时段完成,一般都在build_phase中完成实例化
  • 由于agent的加入,driver和monitor的层次结构改变了,在top_tb中使用config_db设置virtual my_if时要注意改变路径:

    1
    2
    3
    4
    5
    initial begin
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.drv", "vif", input_if);
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.i_agt.mon", "vif", input_if);
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.o_agt.mon", "vif", output_if);
    end
  • 仿真结果如下:

    image-20240430144920312

    image-20240430144943654

5.加入reference model

  • reference model的输出被scoreboard接收,用于和DUT的输出相比较。DUT如果很复杂,那么reference model也会相当复杂。本章的DUT很简单,所以reference model也很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    `ifndef MY_MODEL__SV
    `define MY_MODEL__SV

    class my_model extends uvm_component;

    uvm_blocking_get_port #(my_transaction) port;
    uvm_analysis_port #(my_transaction) ap;

    extern function new(string name, uvm_component parent);
    extern function void build_phase(uvm_phase phase);
    extern virtual task main_phase(uvm_phase phase);

    `uvm_component_utils(my_model)
    endclass

    function my_model::new(string name, uvm_component parent);
    super.new(name, parent);
    endfunction

    function void my_model::build_phase(uvm_phase phase);
    super.build_phase(phase);
    port = new("port", this);
    ap = new("ap", this);
    endfunction

    task my_model::main_phase(uvm_phase phase);
    my_transaction tr;
    my_transaction new_tr;
    super.main_phase(phase);
    while(1) begin
    port.get(tr);
    new_tr = new("new_tr");
    new_tr.my_copy(tr);
    `uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
    new_tr.my_print();
    ap.write(new_tr);
    end
    endtask
    `endif
    • 在my_model的main_phase中,只是单纯地复制一份从i_agt得到的tr,并传递给后级的scoreboard中

    • my_copy是一个在my_transaction中定义的函数(my_transaction.sv):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      function void my_copy(my_transaction tr);
      if(tr == null)
      `uvm_fatal("my_transaction", "tr is null!!!!")
      dmac = tr.dmac;
      smac = tr.smac;
      ether_type = tr.ether_type;
      pload = new[tr.pload.size()];
      for(int i = 0; i < pload.size(); i++) begin
      pload[i] = tr.pload[i];
      end
      crc = tr.crc;
      endfunction
  • 加入my_model后,整棵UVM树变成了:

    image-20240430152130099
  • my_model是从i_agt中得到my_transaction,并把my_transaction传递给my_scoreboard

  • 在UVM中,通常使用TLM(Transaction Level Modeling)实现component之间的transaction级别通信

  • 在UVM的transaction级别的通信中,数据的发送有多种方式,其中一种是使用uvm_analysis_port需要在my_monitor.sv中声明及实例化,并写入

    • 在monitor中的声明:

      1
      uvm_analysis_port #(my_transaction)  ap;
      • uvm_analysis_port是一个参数化的类,其参数就是这个analysis_port需要传递的数据类型,在本节中是my_transaction
    • 在monitor的build_phase中将其实例化:

      1
      2
      3
      4
      virtual function void build_phase(uvm_phase phase);
      ...
      ap = new("ap", this);
      endfunction
    • 在monitor的main_phase中,当收集完一个transaction后,需要将其写入ap中:

      1
      2
      3
      4
      5
      6
      7
      8
      task my_monitor::main_phase(uvm_phase phase);
      my_transaction tr;
      while(1) begin
      tr = new("tr");
      collect_one_pkt(tr);
      ap.write(tr);
      end
      endtask
      • write是uvm_analysis_port的一个内建函数
  • UVM的transaction级别通信的数据接收方式也有多种,其中一种就是使用uvm_blocking_get_port。这也是一个参数化的类,其参数是要在其中传递的transaction的类型。需要在my_model.sv中声明及实例化,并发送

    • 在model中声明:

      1
      uvm_blocking_get_port #(my_transaction)  port;
    • 在model的build_phase中将其实例化:

      1
      2
      3
      4
      5
      function void my_model::build_phase(uvm_phase phase);
      super.build_phase(phase);
      port = new("port", this);
      ...
      endfunction
    • 在monitor的main_phase中,通过port.get任务来得到从i_agt的monitor中发出的transaction:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      task my_model::main_phase(uvm_phase phase);
      my_transaction tr;
      my_transaction new_tr;
      super.main_phase(phase);
      while(1) begin
      port.get(tr);
      new_tr = new("new_tr");
      new_tr.my_copy(tr);
      `uvm_info("my_model", "get one transaction, copy and print it:", UVM_LOW)
      new_tr.my_print();
      ap.write(new_tr);
      end
      endtask
  • 在my_monitor和my_model中定义并实现了各自的端口之后,通信的功能并没有实现,还需要在my_env中使用fifo将两个端口联系在一起

    • 在my_env中定义一个fifo,并在build_phase中将其实例化:

      1
      2
      3
      4
      5
      6
      uvm_tlm_analysis_fifo #(my_transaction) agt_mdl_fifo;
      ...
      virtual function void build_phase(uvm_phase phase);
      ...
      agt_mdl_fifo = new("agt_mdl_fifo", this);
      endfunction
    • 之后,在connect_phase中将fifo分别与my_monitor中的analysis_port和my_model中的blocking_get_port相连:

      1
      2
      3
      4
      5
      function void my_env::connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      i_agt.ap.connect(agt_mdl_fifo.analysis_export);
      mdl.port.connect(agt_mdl_fifo.blocking_get_export);
      endfunction
      • 这里引入了connect_phase,与build_phase及main_phase类似,connect_phase也是UVM内建的一个phase,它在build_phase执行完成之后马上执行。但是与build_phase不同的是,它的执行顺序并不是从树根到树叶,而是从树叶到树根:先执行driver和monitor的connect_phase,再执行agent的connect_phase,最后执行env的connect_phase
  • 在如上的连接中,用到了i_agt的一个成员变量ap,

    • 它的定义与my_monitor中ap的定义完全一样:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
          uvm_analysis_port #(my_transaction)  ap;

      - 与my_monitor中的ap不同的是,**不需要对my_agent中的ap进行实例化,而只需要在my_agent的connect_phase中将monitor的值赋给它。换句话说,这相当于是一个指向my_monitor的ap的指针**

      ```systemverilog
      function void my_agent::connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      ap = mon.ap;
      endfunction

      - 仿真结果如下:

      ![image-20240430160945765](UVM%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B9%8B%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84UVM%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0/image-20240430160945765.png)

      ![image-20240430161005072](UVM%E7%99%BD%E7%9A%AE%E4%B9%A6%E4%B9%8B%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84UVM%E9%AA%8C%E8%AF%81%E5%B9%B3%E5%8F%B0/image-20240430161005072.png)

      ## 6.加入scoreboard

      - 在验证平台中加入reference model和monitor之后,最后一步是加入scoreboard,其代码如下:(my.scoreboard.sv)

      ```systemverilog
      `ifndef MY_SCOREBOARD__SV
      `define MY_SCOREBOARD__SV
      class my_scoreboard extends uvm_scoreboard;
      my_transaction expect_queue[$];
      uvm_blocking_get_port #(my_transaction) exp_port;
      uvm_blocking_get_port #(my_transaction) act_port;
      `uvm_component_utils(my_scoreboard)

      extern function new(string name, uvm_component parent = null);
      extern virtual function void build_phase(uvm_phase phase);
      extern virtual task main_phase(uvm_phase phase);
      endclass

      function my_scoreboard::new(string name, uvm_component parent = null);
      super.new(name, parent);
      endfunction

      function void my_scoreboard::build_phase(uvm_phase phase);
      super.build_phase(phase);
      exp_port = new("exp_port", this);
      act_port = new("act_port", this);
      endfunction

      task my_scoreboard::main_phase(uvm_phase phase);
      my_transaction get_expect, get_actual, tmp_tran;
      bit result;

      super.main_phase(phase);
      fork
      while (1) begin
      exp_port.get(get_expect);
      expect_queue.push_back(get_expect);
      end
      while (1) begin
      act_port.get(get_actual);
      if(expect_queue.size() > 0) begin
      tmp_tran = expect_queue.pop_front();
      result = get_actual.my_compare(tmp_tran);
      if(result) begin
      `uvm_info("my_scoreboard", "Compare SUCCESSFULLY", UVM_LOW);
      end
      else begin
      `uvm_error("my_scoreboard", "Compare FAILED");
      $display("the expect pkt is");
      tmp_tran.my_print();
      $display("the actual pkt is");
      get_actual.my_print();
      end
      end
      else begin
      `uvm_error("my_scoreboard", "Received from DUT, while Expect Queue is empty");
      $display("the unexpected pkt is");
      get_actual.my_print();
      end
      end
      join
      endtask
      `endif
    • my_scoreboard要比较的数据一是来源于reference model,二是来源于o_agt的monitor,前者通过exp_port获取,后者通过act_port获取
    • 在main_phase中通过fork建立了两个进程:
      • 一个进程处理exp_port的数据,当收到数据后,把数据放入expect_queue中
      • 另一个进程处理act_port的数据,这是DUT的输出数据
      • 当收集到这些数据后,从expect_queue中弹出之前从exp_port收到的数据,并调用my_transaction的my_compare函数
    • 采用这种比较处理方式的前提是exp_port要比act_port先收到数据
    • 由于DUT处理数据需要延时,而reference model是基于高级语言的处理,一般不需要延时,因此可以保证exp_port的数据在act_port的数据之前到来
  • o_agt和act_port的ap的连接方式及reference model和exp_port的ap的连接方式与前一节类似(my_env.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function void my_env::connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    i_agt.ap.connect(agt_mdl_fifo.analysis_export);
    mdl.port.connect(agt_mdl_fifo.blocking_get_export);
    mdl.ap.connect(mdl_scb_fifo.analysis_export);
    scb.exp_port.connect(mdl_scb_fifo.blocking_get_export);
    o_agt.ap.connect(agt_scb_fifo.analysis_export);
    scb.act_port.connect(agt_scb_fifo.blocking_get_export);
    endfunction
    • i_agt中monitor的transaction送给reference model,reference model处理后的transaction送给scoreboard的exp_port,o_agt中monitor的transaction送给scoreboard的act_port,最后比较exp_port与act_port这两个端口。
  • 完成my_scoreboard的定义后,也需要在my_env中将其实例化,此时,整棵UVM树为:

    image-20240430164929758
  • 仿真结果如下:

    image-20240430165343036

    image-20240430165409392

7.加入field_automation机制

  • 通过field_automation可以实现自动调用一些函数,在my_transaction中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    `ifndef MY_TRANSACTION__SV
    `define MY_TRANSACTION__SV

    class my_transaction extends uvm_sequence_item;

    rand bit[47:0] dmac;
    rand bit[47:0] smac;
    rand bit[15:0] ether_type;
    rand byte pload[];
    rand bit[31:0] crc;

    constraint pload_cons{
    pload.size >= 46;
    pload.size <= 1500;
    }

    function bit[31:0] calc_crc();
    return 32'h0;
    endfunction

    function void post_randomize();
    crc = calc_crc;
    endfunction

    `uvm_object_utils_begin(my_transaction)
    `uvm_field_int(dmac, UVM_ALL_ON)
    `uvm_field_int(smac, UVM_ALL_ON)
    `uvm_field_int(ether_type, UVM_ALL_ON)
    `uvm_field_array_int(pload, UVM_ALL_ON)
    `uvm_field_int(crc, UVM_ALL_ON)
    `uvm_object_utils_end

    function new(string name = "my_transaction");
    super.new();
    endfunction

    endclass
    `endif
    • 这里使用uvm_object_utils_beginuvm_object_utils_end来实现my_transaction的factory注册,在这两个宏中,使用uvm_field宏注册所有字段。
    • uvm_field系列宏随着transaction成员变量的不同而不同,如上面的定义中出现了针对bit类型的uvm_field_int及针对byte类型动态数组的uvm_field_array_int
    • 当上述宏注册之后,可以直接调用copy、compare、print等函数,无需自己定义
  • 引入field_automation机制的另外一大好处是简化了driver和monitor

  • my_driver.sv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    task my_driver::drive_one_pkt(my_transaction tr);
    byte unsigned data_q[];
    int data_size;

    data_size = tr.pack_bytes(data_q) / 8;
    `uvm_info("my_driver", "begin to drive one pkt", UVM_LOW);
    repeat(3) @(posedge vif.clk);
    for ( int i = 0; i < data_size; i++ ) begin
    @(posedge vif.clk);
    vif.valid <= 1'b1;
    vif.data <= data_q[i];
    end

    @(posedge vif.clk);
    vif.valid <= 1'b0;
    `uvm_info("my_driver", "end drive one pkt", UVM_LOW);
    endtask
    • pack_bytes将tr中所有的字段变成了byte流放入data_q中,pack_bytes极大地减少了代码量,在把所有的字段变成byte流放入data_q中时,字段按照uvm_field系列书写的顺序排列。在上述代码中是先放入dmac,再依次放入smac、ether_type、pload、crc
  • my_monitor.sv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    task my_monitor::collect_one_pkt(my_transaction tr);
    byte unsigned data_q[$];
    byte unsigned data_array[];
    logic [7:0] data;
    logic valid = 0;
    int data_size;

    while(1) begin
    @(posedge vif.clk);
    if(vif.valid) break;
    end

    `uvm_info("my_monitor", "begin to collect one pkt", UVM_LOW);
    while(vif.valid) begin
    data_q.push_back(vif.data);
    @(posedge vif.clk);
    end
    data_size = data_q.size();
    data_array = new[data_size];
    for ( int i = 0; i < data_size; i++ ) begin
    data_array[i] = data_q[i];
    end
    tr.pload = new[data_size - 18]; //da sa, e_type, crc
    data_size = tr.unpack_bytes(data_array) / 8;
    `uvm_info("my_monitor", "end collect one pkt", UVM_LOW);
    endtask
    • 这里使用unpack_bytes函数将data_q中的byte流转换成tr中的各个字段
    • 在 Ethernet 数据包中,有一部分是以太网帧头(Ethernet Frame Header),其中包含了目的MAC地址(6字节)、源MAC地址(6字节)、以太网类型(2字节)和 CRC 校验码(4字节),共计18字节。那么包数量占data_size - 18个bytes,所以有这行代码tr.pload = new[data_size - 18];
  • 仿真结果:

    image-20240430190504439

    image-20240430190530419


UVM的终极大作:sequence

1.在验证平台中加入sequencer

  • sequence机制用于产生激励,它是UVM中最重要的机制之一

  • 在一个规范化的UVM验证平台中,driver只负责驱动transaction,而不负责产生transaction

  • sequence机制有两大组成部分,一是sequence,二是sequencer

  • sequencer的定义非常简单,派生自uvm_sequencer,并使用uvm_component_utils宏来注册到factory中。uvm_sequencer是一个参数化的类,其参数是my_transaction,即此sequencer产生transaction的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    `ifndef MY_SEQUENCER__SV
    `define MY_SEQUENCER__SV

    class my_sequencer extends uvm_sequencer #(my_transaction);

    function new(string name, uvm_component parent);
    super.new(name, parent);
    endfunction

    `uvm_component_utils(my_sequencer)
    endclass

    `endif
  • sequencer产生transaction,而driver负责接收transaction

  • 由于uvm_driver也是一个参数化的类,应该在定义driver时指明此driver要驱动的transaction类型

    1
    class my_driver extends uvm_driver#(my_transaction);
  • 这样定义的好处是可以直接使用uvm_driver中的某些预先定义好的成员变量,如uvm_driver中有成员变量req,它的类型就是传递给uvm_driver的参数,在这里就是my_transaction,可以直接使用req

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    task my_driver::main_phase(uvm_phase phase);
    phase.raise_objection(this);
    vif.data <= 8'b0;
    vif.valid <= 1'b0;
    while(!vif.rst_n)
    @(posedge vif.clk);
    for(int i = 0; i < 2; i++) begin
    req = new("req");
    assert(req.randomize() with {pload.size == 200;});
    drive_one_pkt(req);
    end
    repeat(5) @(posedge vif.clk);
    phase.drop_objection(this);
    endtask
    • 这里依然在driver中产生激励,下一节中将会把激励产生的功能从driver中移除
  • 在完成sequencer的定义后,由于sequencer与driver的关系非常密切,因此要把其加入agent中

    1
    2
    3
    4
    5
    6
    7
    8
    function void my_agent::build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (is_active == UVM_ACTIVE) begin
    sqr = my_sequencer::type_id::create("sqr", this);
    drv = my_driver::type_id::create("drv", this);
    end
    mon = my_monitor::type_id::create("mon", this);
    endfunction
  • 在加入sequencer后,整个UVM树的结构变成了如下图的形式:

    image-20240515151639843
  • 验证平台的框图:

    image-20240515154658315
  • 其仿真结果与前序一致

2.sequence机制

  • sequence不属于验证平台的任何一部分,但是它与sequencer之间有密切的联系

    • 只有在sequencer的帮助下,sequence产生出的transaction才能最终送给driver
    • 没有sequence,sequencer就几乎没有任何作用
    • sequence就像是一个弹夹,里面的子弹就是transaction,而sequencer是一把枪。弹夹只有放入枪中才有意义,枪只有在放入弹夹后才能发挥威力
  • 从本质上来说,sequencer是一个uvm_component,而sequence是一个uvm_object。与my_transaction一样,sequence也有其生命周期。它的生命周期比my_transaction要更长一些,其内的transaction全部发送完毕后,它的生命周期也就结束了。因此,一个sequence应该使用uvm_object_utils宏注册到factory中(my_sequence.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class my_sequence extends uvm_sequence #(my_transaction);
    my_transaction m_trans;

    function new(string name= "my_sequence");
    super.new(name);
    endfunction

    virtual task body();
    repeat (10) begin
    `uvm_do(m_trans)
    end
    #1000;
    endtask

    `uvm_object_utils(my_sequence)
    endclass
    • 每一个sequence都有一个body任务,当一个sequence启动之后,会自动执行body中的代码
    • 在上面的例子中,用到了一个全新的宏:uvm_do,这个宏是UVM中最常见的宏之一,它用于:
      • 创建一个my_transaction的实例m_trans
      • 将其随机化
      • 最终将其送给sequencer
  • 一个sequece在向sequencer发送transaction前,要先向sequencer发送一个请求,sequencer把这个请求放在一个仲裁队列中,作为sequencer,它需要做两件事情:

    • 第一,检验仲裁队列里是否有某个sequence发送transaction的请求

    • 第二,检测driver是否申请transaction

    • 如果仲裁队列里有发送请求,但driver没有申请transaction,那么sequencer将会一直处于等待driver的状态,直到driver申请新的transaction。此时,sequencer同意sequence的发送请求,sequence在得到sequencer的批准后,产生出一个transaction并交给sequencer,后者把这个transaction交给driver

    • 如果仲裁队列中没有发送请求,但driver向sequencer申请新的transaction,那么sequencer将会处于等待sequence的状态,一直到有sequence递交发送请求,sequencer马上同意这个请求,sequence产生的transaction并交给sequencer,最终driver获得这个transaction

    • 如果仲裁队列中有发送请求,同时driver也在向sequencer申请新的transaction,那么将会同意发送请求,sequence产生transaction并交给sequencer,最终driver获得这个transaction

  • driver如何向sequencer申请transaction呢?

    • 在uvm_driver中有成员变量seq_item_port,而在uvm_sequencer中有成员变量seq_item_export,这两者之间可以建立一个“通道”

    • 在my_agent中,使用connect函数把两者联系在一起

      1
      2
      3
      4
      5
      6
      7
      function void my_agent::connect_phase(uvm_phase phase);
      super.connect_phase(phase);
      if (is_active == UVM_ACTIVE) begin
      drv.seq_item_port.connect(sqr.seq_item_export);
      end
      ap = mon.ap;
      endfunction
    • 当把二者连接好之后,就可以在driver中通过get_next_item任务向sequencer申请新的transaction

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      task my_driver::main_phase(uvm_phase phase);
      vif.data <= 8'b0;
      vif.valid <= 1'b0;
      while(!vif.rst_n)
      @(posedge vif.clk);
      while(1) begin
      seq_item_port.get_next_item(req);
      drive_one_pkt(req);
      seq_item_port.item_done();
      end
      endtask
    • 在如上的代码中,一个最显著的特征是使用了while(1)循环,因为driver只负责驱动transaction,而不负责产生,只要有transaction就驱动,所以必须做成一个无限循环的形式

    • 通过get_next_item任务来得到一个新的req,并且驱动它,驱动完成后调用item_done通知sequencer(如果在下次调用get_next_item前,item_done被调用,那么sequencer就认为driver已经得到了这个transaction)

    • sequence中uvm_do宏产生了一个transaction并交给sequencer,driver取走这transaction后,uvm_do并不会立刻返回执行下一次的uvm_do宏,而是等待在那里,直到driver返回item_done信号。此时,uvm_do宏才算执行完毕,返回后开始执行下一个uvm_do,并产生新的transaction

    • 其实,除get_next_item之外,还可以使用try_next_item。get_next_item是阻塞的,它会一直等到有新的transaction才会返回,try_next_item是非阻塞的,它尝试着询问sequencer是否有新的transaction,如果有,则得到此transaction,否则就直接返回

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      task my_driver::main_phase(uvm_phase phase);
      vif.data <= 8'b0;
      vif.valid <= 1'b0;
      while(!vif.rst_n)
      @(posedge vif.clk);
      while(1) begin
      seq_item_port.try_next_item(req);
      if(req == null)
      @(posedge vif.clk);
      else begin
      drive_one_pkt(req);
      seq_item_port.item_done();
      end
      end
      endtask
      • 相比于get_next_item,try_next_item的行为更加接近真实driver的行为,当有数据时,就驱动数据,否则总线将一直处于空闲状态
  • sequence如何向sequencer中发送transaction呢?

    • 前面已经定义sequence,只需要在某个component(如my_sequencer、my_env)的main_phase中启动这个sequence即可,以在my_env中启动为例:

      1
      2
      3
      4
      5
      6
      7
      task my_env::main_phase(uvm_phase phase);
      my_sequence seq;
      phase.raise_objection(this);
      seq = my_sequence::type_id::create("seq");
      seq.start(i_agt.sqr);
      phase.drop_objection(this);
      endtask
    • 首先常见一个my_sequence的实例seq

    • 之后调用start任务,任务的参数是一个sequencer指针,如果不指明此指针,则sequence不知道将产生的transaction交给哪个sequencer

  • 仿真结果如下:

    image-20240515170158447

    image-20240515170236610

3.default_sequence的使用

  • 在上一节的例子中,sequence是在my_env的main_phase中手工启动的,但在实际应用中,使用最多的还是通过default_sequence的方式启动sequence

  • 使用default_sequence的方式非常简单,只需要在某个component(如my_env)的build_phase中设置如下代码即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    virtual function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    ...
    uvm_config_db#(uvm_object_wrapper)::set(this,
    "i_agt.sqr.main_phase",
    "default_sequence",
    my_sequence::type_id::get());

    endfunction
    • 上述set参数的第二个参数,取决于在哪个地方启动sequence
  • 如果在top_tb中设置default_sequence,则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module top_tb;
    ...
    initial begin
    uvm_config_db#(uvm_object_wrapper)::set(null,
    "uvm_test_top.i_agt.sqr.main_phase",
    "default_sequence",
    my_sequence::type_id::get());
    end
    endmodule
  • 如果在my_agent的build_phase里:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function void my_agent::build_phase(uvm_phase phase);
    super.build_phase(phase);
    ...
    uvm_config_db#(uvm_object_wrapper)::set(this,
    "sqr.main_phase",
    "default_sequence",
    my_sequence::type_id::get());

    endfunction
  • 在sequence中使用starting_phase进行提起和撤销objection

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class my_sequence extends uvm_sequence #(my_transaction);
    my_transaction m_trans;

    function new(string name= "my_sequence");
    super.new(name);
    endfunction

    virtual task body();
    if(starting_phase != null)
    starting_phase.raise_objection(this);
    repeat (10) begin
    `uvm_do(m_trans)
    end
    #1000;
    if(starting_phase != null)
    starting_phase.drop_objection(this);
    endtask

    `uvm_object_utils(my_sequence)
    endclass
    `endif
  • 仿真结果同上一节一致


建造测试用例

1.加入base_test

  • UVM使用的是一种树形结构,最初这棵树的树根是my_driver,后来由于要放置其他component,树根变成了my_env

  • 但在一个实际应用的UVM验证平台中,my_env并不是树根,通常来说,树根是一个基于uvm_test派生的类。真正的测试用例都是基于base_test派生的一个类(base_test.sv)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    `ifndef BASE_TEST__SV
    `define BASE_TEST__SV

    class base_test extends uvm_test;

    my_env env;

    function new(string name = "base_test", uvm_component parent = null);
    super.new(name,parent);
    endfunction

    extern virtual function void build_phase(uvm_phase phase);
    extern virtual function void report_phase(uvm_phase phase);
    `uvm_component_utils(base_test)
    endclass


    function void base_test::build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
    uvm_config_db#(uvm_object_wrapper)::set(this,
    "env.i_agt.sqr.main_phase",
    "default_sequence",
    my_sequence::type_id::get());
    endfunction

    function void base_test::report_phase(uvm_phase phase);
    uvm_report_server server;
    int err_num;
    super.report_phase(phase);

    server = get_report_server();
    err_num = server.get_severity_count(UVM_ERROR);

    if (err_num != 0) begin
    $display("TEST CASE FAILED");
    end
    else begin
    $display("TEST CASE PASSED");
    end
    endfunction

    `endif
    • base_test派生自uvm_test,使用uvm_component_utils宏来注册到factory中

    • 在build_phase中实例化my_env,并设置sequencer的default_sequence

    • 上面的代码中出现了report_phase,在report_phase中根据UVM_ERROR的数量来打印不同的信息。report_phase也是UVM内建的一个phase,它在main_phase结束之后执行

    • 通常在base_test中做如下事情:

      • 第一,设置整个验证平台的超时退出时间
      • 第二,通过config_db设置验证平台中某些参数的值
  • 在把my_env放入base_test中之后,UVM树的层次结构变成:

    image-20240515200335301
  • top_tb中run_test的参数从my_env变成了base_test,并且config_db中设置virtual interface的路径参数要做如下改变:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    initial begin
    run_test("base_test");
    end

    initial begin
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.drv", "vif", input_if);
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.i_agt.mon", "vif", input_if);
    uvm_config_db#(virtual my_if)::set(null, "uvm_test_top.env.o_agt.mon", "vif", output_if);
    end
  • 仿真结果如下:

    image-20240515200749358

    image-20240515200805663

2.UVM中测试用例的启动

  • UVM会利用UVM_TESTNAME从命令行中寻找测试用例(以测试不同的sequence)的名字,创建它的实例并运行(注意+UVM_TESTNAME=my_case1

    1
    vsim -t ns -voptargs=+acc -sv_lib $UVM_DPI_DIR/uvm_dpi work_design.top_tb +UVM_TESTNAME=my_case1
  • 整个启动及执行的流程:

    image-20240515211542137
  • 启动后,整棵UVM树的结构:

    image-20240515211633841
  • 仿真结果:

    image-20240515211745672

    image-20240515211803920


附加知识点

  • modelsim下运行UVM仿真平台

    image-20240428204545137
  • run.do(每行脚本含义可参考https://ssy1938010014.github.io/2024/02/25/FPGA%E8%AE%BE%E8%AE%A1%E9%AB%98%E7%BA%A7%E6%8A%80%E5%B7%A7%E4%B9%8B%E7%8A%B6%E6%80%81%E6%9C%BA/中附加知识点部分)`UVM_HOME`、`UVM_DPI_DIR`在modelsim的安装目录下找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    vlib ./lib/
    vlib ./lib/work_design

    vmap work_design ./lib/work_design

    set UVM_HOME D:/SoftWare/ModelSim/verilog_src/uvm-1.1d/src
    set WORK_HOME D:/App_Data_File/ModelSim_project/UVM_Sim/UVM_learning_project
    set UVM_DPI_DIR D:/SoftWare/ModelSim/uvm-1.1d/win64

    vlog +incdir+$UVM_HOME -L mtiAvm -L mtiOvm -L mtiUvm -L mtiUPF $UVM_HOME/uvm_pkg.sv
    vlog -work work_design $WORK_HOME/ch2/dut/dut.sv $WORK_HOME/ch2/section2.2/2.2.1/sim/top_tb.sv

    vsim -t ns -voptargs=+acc -sv_lib $UVM_DPI_DIR/uvm_dpi work_design.top_tb

    add wave -position insertpoint sim:/top_tb/*

    run -all
    • 有关-sv_lib的解释:-sv_lib:这个选项用于指定一个SystemVerilog的DPI(Direct Programming Interface)库。DPI允许SystemVerilog代码调用C/C++函数,常用于模拟外部设备或高级功能。(来自GPT4)

    • -position:这是一个选项,用来指定信号在波形窗口中的添加位置。

    • insertpoint:这个参数的值指定了具体的插入点。在ModelSim的波形窗口中,insertpoint 指的是当前选中的波形组或信号的位置。如果你在波形窗口中选中了一个特定的信号或波形组,并执行带有 -position insertpoint 的命令,新添加的信号会被放置在选中的波形或组之后。

      • 举个例子:
        • 如果你没有选中任何波形或信号,新的信号通常会被添加到波形列表的最前面或最后面,具体取决于ModelSim的默认行为或之前的配置。
        • 如果你选中了一个信号,然后运行带有 -position insertpointadd wave 命令,新的信号将被插入到你选中的信号后面。(来自GPT4)
    • 本人能力和时间有限,有关run.do中的一些脚本命令,我也不是很清楚,但用到的都会在此记录。

  • 搭建UVM仿真平台时,一开始会一直报vlog(12110)以及vlog一行出错的命令,折腾了我很久(因为以为是vlog使用有问题,其实是modelsim自身的问题),最后参考vlog 12110错误及解决-CSDN博客,重启后解决(记得之前好像也是用脚本仿真出问题了,设置voptflow = 0才能仿真,但这次又重新设置为1才可以)

欢迎来到ssy的世界