天天看点

SV验证-2建立组件

SV验证-2建立组件

​ 本文采用system verilog语言对一个router.v模块进行验证。参考文档为Synopsys公司2012年出版的《SystemVerilog Testbench Lab Guide》

其顶层文件如下图,本节中出现的组件全在test.sv文件中调用。

SV验证-2建立组件

1、Packet类

​ Packet类用来封装包信息。在test文件中,gen()到send()、send()到check()以及recv()到check()都需要发送包数据。在gen(0中使用randomization可以随机化Packet类中的源地址、目的地址和发包信息。

SV验证-2建立组件
class Packet; # 申明一个包类
  rand bit[3:0] sa, da;         // random port selection
  rand logic[7:0] payload[$];   // random payload array
  string   name;                // unique identifier

    constraint Limit { # 申明一个随机化控制块
    sa inside {[0:15]};
    da inside {[0:15]};
    payload.size() inside {[2:4]};
  }

  extern function new(string name = "Packet");
  extern virtual function bit compare(Packet pkt2cmp, ref string message);
  extern virtual function void display(string prefix = "NOTE");
  extern virtual function Packet copy();
endclass
           

​ 为了进行包的检查及两个包的比较,需要申明一个比较数据包的任务。在任务中通过比较payload[$](参考包数据)和pkt2cmp_payload[$](采用包数据)来验证接收数据是否正确。其比较过程如下:

  • 比较两者的大小是否相当,不相等则打印错误信息并返回0.
  • 比较两者的数据是否相等,不相等则打印错误信息并返回0.
  • 若数据相等则打印成功信息并返回1.
function bit Packet::compare(Packet pkt2cmp, ref string message);
  if (payload.size() != pkt2cmp.payload.size()) begin
    message = "Payload Size Mismatch:\n";
    message = { message, $sformatf("payload.size() = %0d, pkt2cmp.payload.size() = %0d\n", payload.size(), pkt2cmp.payload.size()) };
    return(0);
  end
    if (payload == pkt2cmp.payload) ;
    else begin
      message = "Payload Content Mismatch:\n";
      message = { message, $sformatf("Packet Sent:  %p\nPkt Received: %p", payload, pkt2cmp.payload) };
      return(0);
    end
  message = "Successfully Compared";
  return(1);
endfunction
           

​ 其余函数中,new函数为构造函数,新建该类时调用。display函数用来实现打印信息,在调试过程中打印包的信息是非常有帮助的,copy函数用来复制包。

2、gen类

​ 申明一个Generate类包用来生成发送的数据。

class Generator;
  string  name;                  // unique identifier
  Packet  pkt2send;              // stimulus Packet object
  pkt_mbox out_box;              // mailbox to Drivers
  int port_id = -1;              // port_id of connected Driver
  static int pkts_generated = 0; //packet count across all generators

  extern function new(string name = "Generator", int port_id);
  extern virtual task gen();
  extern virtual task start();
endclass: Generator
           

​ 该类中用到了一个新的类型叫mailbox,在SV中采用mailbox实现多个线程之间的数据通信,其用法与FIFO相似,存取采用put()和get()方法。本文通过定义pkt_mbox用来声明mailbox类型。

typedef class Packet;
typedef mailbox #(Packet) pkt_mbox;
           

在test文件中,复位任务执行后会立即执行gen()任务,在gen()任务执行过程如下:

  • 如果是调试模式则打印类的名字;
  • 为每个发送的包建立一个唯一的名称;
  • 随机化发包的信息,若随机化失败则打印错误信息
task Generator::gen();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.pkt2send.name = $sformatf("Packet[%0d]", this.pkts_generated++);
  //local refers to Generator class object in with clause
  if (!this.pkt2send.randomize() with {if (local::port_id != -1) sa == local::port_id;})
  begin
    $display("\n%m\n[ERROR]%t Randomization Failed!\n", $realtime);
    $finish;
  end
endtask: gen
           

​ 在start()任务中,循环次数由run_for_n_packets变量控制。执行this.gen()任务后,包信息中的数据被随机化,并通过out_box发送到Driver。

task Generator::start();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  fork
    while (this.pkts_generated <run_for_n_packets || run_for_n_packets <= 0)
    begin
      this.gen();
      begin
        Packet pkt = this.pkt2send.copy();
        this.out_box.put(pkt);
      end
    end
  join_none
endtask: start
           

3、Driver基类和Driver类

​ Driver基类代码如下,其构造函数中包括名字和接口信号,send()任务为transactor,其余任务均为发送信号任务。

class DriverBase;
  virtual router_io.TB rtr_io;  // interface signal
  string    name;               // unique identifier
  bit[3:0]  sa, da;             // source and destination addresses
  logic[7:0]  payload[$];       // Packet payload
  Packet    pkt2send;           // stimulus Packet object

  extern function new(string name = "DriverBase", virtual router_io.TB rtr_io);
  extern virtual task send();
  extern virtual task send_addrs();
  extern virtual task send_pad();
  extern virtual task send_payload();
endclass
           

​ 在生成包信息后,可以执行transactor任务将包发送到router上。发送一个包包含三部分:目的地址、打拍周期和数据信息。其传输过程如下图:

SV验证-2建立组件

申明一个send()任务包含send_addrs()、send_pad()、send_payload()三个任务。

  • send_addrs()任务,占4拍时钟周期,先将frame_n信号拉低表示开始传输信号,然后采用循环逐字节串口发送地址信息,
  • send_pad()任务:占4拍时钟周期,该任务直接将din、valid_n信号拉低,等待5个时钟上升沿即可。
  • send_payload任务:占payload.size()大小的时钟周期,每次循环将发送8bit的payload[$]数据信息。保持valid_n信号为低,发送最后1bit信号时拉高frame_n信号。
task DriverBase::send();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.send_addrs();
  this.send_pad();
  this.send_payload();
endtask

task DriverBase::send_addrs();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.rtr_io.cb.frame_n[this.sa] <= 1'b0;
  for(int i=0; i<4; i++) begin
    this.rtr_io.cb.din[this.sa] <= this.da[i];
    @(this.rtr_io.cb);
  end
endtask

task DriverBase::send_pad();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.rtr_io.cb.din[this.sa] <= 1'b1;
  this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
  repeat(5) @(this.rtr_io.cb);
endtask

task DriverBase::send_payload();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  foreach(this.payload[index]) begin
    for(int i=0; i<8; i++) begin
      this.rtr_io.cb.din[this.sa] <= this.payload[index][i];
      this.rtr_io.cb.valid_n[this.sa] <= 1'b0;
      this.rtr_io.cb.frame_n[this.sa] <= ((index == (this.payload.size() - 1)) && (i == 7));
      @(this.rtr_io.cb);
    end
  end
  this.rtr_io.cb.valid_n[this.sa] <= 1'b1;
endtask
           

​ Driver类来源于DriverBase类的扩展,新增了三个属性(mailbox、semaphore)和两种方法。in_box将包从Generator传向Diver,out_box将包从Dirver传向Scoreboard。sem[]数组用来做输入输出端口的仲裁判断,以确保只有一个组件来连接一个输入输出端口。

`include "DriverBase.sv"
class Driver extends DriverBase;
  pkt_mbox in_box;   // Generator mailbox
  pkt_mbox out_box;  // Scoreboard mailbox
  semaphore sem[];   // output port arbitration

  extern function new(string name = "Driver", int port_id, semaphore sem[], pkt_mbox in_box, out_box, virtual router_io.TB rtr_io);
  extern virtual task start();
endclass
           

​ 在start()函数中是一个无限循环,循环中接收来自in_box的包并赋值给pkt2send,解析出pkt2send的sa信号若不等于该类指定的sa,则跳过到下一循环,否则继续解析出pkt2send的da和payload信息,使用sem[]阵列来仲裁由da指定的输出端口,然后通过send()任务将该包发给DUT。随后通过out_box将该包发给Scoreboard,循环的最后一步取出旗语中的键。

task Driver::start();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  fork
    forever begin
      this.in_box.get(this.pkt2send);
      if (this.pkt2send.sa != this.sa) continue;
      this.da = this.pkt2send.da;
      this.payload = this.pkt2send.payload;
      this.sem[this.da].get(1);
      this.send();
      this.out_box.put(this.pkt2send);
      this.sem[this.da].put(1);
    end
  join_none

endtask: start
           

4、recv基类和recv类

​ 该类对从路由器输出的数据包信号进行采样,并发送到Scoreboard的check()r任务中使之与发包前的数据进行比较。其原理图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NlqQUO2E-1668320996043)(photos/receiver类.jpg)]

class ReceiverBase;
  virtual router_io.TB rtr_io;    // interface signals
  string   name;                  // unique identifier
  bit[3:0] da;                    // output port to monitor
  logic[7:0] pkt2cmp_payload[$];  // actual payload array
  Packet   pkt2cmp;               // actual Packet object

  extern function new(string name = "ReceiverBase", virtual router_io.TB rtr_io);
  extern virtual task recv();
  extern virtual task get_payload();
endclass
           

​ 在recv()任务中,先申明一个静态变量来表示收包的数量,再运行get_payload()任务进行收包,最后将包信息赋值给私有变量。接收信号图如下:

SV验证-2建立组件
task ReceiverBase::recv();
  static int pkt_cnt = 0;
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.get_payload();
  this.pkt2cmp.da = da;
  this.pkt2cmp.payload = this.pkt2cmp_payload;
  this.pkt2cmp.name = $sformatf("rcvdPkt[%0d]", pkt_cnt++);
endtask
           

​ 在get_payload()任务中,先将pkt2cmp_payload[ ] 中 的 数 据 删 除 。 该 任 务 中 存 在 一 个 f o r k j o i n 块 和 一 个 f o r e v e r 块 , f o r k j o i n 块 用 来 检 测 信 号 接 收 是 否 完 毕 和 是 否 出 现 超 时 错 误 , 而 f o r e v e r 块 用 来 接 收 r o u t e r 的 输 出 数 据 , 保 存 到 p k t 2 c m p p a y l o a d [ ]中的数据删除。该任务中存在一个fork join块和一个forever块,fork join块用来检测信号接收是否完毕和是否出现超时错误,而forever块用来接收router的输出数据,保存到pkt2cmp_payload[ ]中的数据删除。该任务中存在一个forkjoin块和一个forever块,forkjoin块用来检测信号接收是否完毕和是否出现超时错误,而forever块用来接收router的输出数据,保存到pkt2cmpp​ayload[]对列中。

task ReceiverBase::get_payload();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  this.pkt2cmp_payload.delete();
  fork
  begin: wd_timer_fork
    fork: frameo_wd_timer
    begin //循环直到检测到最后1bit发送完毕
      wait(rtr_io.cb.frameo_n[da] != 0);
      @(rtr_io.cb iff(rtr_io.cb.frameo_n[da] == 0 ));
    end
    begin
        repeat(1000) @(rtr_io.cb); //若在1000个周期内都没结束该进程块则结束仿真
      $display("\n%m\n[ERROR]%t Frame signal timed out!\n", $realtime);
      $finish;
    end
    join_any: frameo_wd_timer
    disable fork;
  end: wd_timer_fork
  join
      
  forever begin
    logic[7:0] datum;
      for (int i=0; i<8; i=i) begin //循环时,每次收集8bit数据
      if (!this.rtr_io.cb.valido_n[da])
        datum[i++] = this.rtr_io.cb.dout[da];
      if (this.rtr_io.cb.frameo_n[da])
        if (i == 8) begin
          this.pkt2cmp_payload.push_back(datum);
          return;
        end
        else begin //数据没对齐则打印错误信息并结束仿真
          $display("\n%m\n[ERROR]%t Packet payload not byte aligned!\n", $realtime);
          $finish;
        end
      @(this.rtr_io.cb);
    end
    this.pkt2cmp_payload.push_back(datum);
  end
endtask
           

​ Receiver类包含一个out_box,和start()任务。start()任务会执行一个非阻塞无限循环,每次循环会等待recv任务收包,一旦接收到包,该包就会被复制并通过mailbox传递到Scoreboard。

`include "ReceiverBase.sv"
class Receiver extends ReceiverBase;
  pkt_mbox out_box;  // Scoreboard mailbox

  extern function new(string name = "Receiver", int port_id, pkt_mbox out_box, virtual router_io.TB rtr_io);
  extern virtual task start();
endclass: Receiver
      
task Receiver::start();
  if (TRACE_ON) $display("[TRACE]%t %s:%m", $realtime, this.name);
  fork
  forever begin
    this.recv();
    begin
      Packet pkt = this.pkt2cmp.copy();
    this.out_box.put(pkt);
    end
  end
  join_none
endtask: start
           

继续阅读