Bootstrap

路科验证MCDF_svlab4笔记

学前提示

0、建议自己写代码,可以加深印象和理解。如果你想要下载已经更新比较全而且已经跑通的代码,那就点下方链接~(需要花个零食钱噢)
mcdf_svlab4完整代码资源

1、实验3到4为什么多了那么多package?
因为实验4的接口、模块变多了,所以需要针对他们有各自的package
2、mcdf的文件夹中编译有先后,先编译arbiter、formater、reg、salve_fifo,再编译mcdf模块。
3、可回顾lab0,各部分功能
4、看tb文件的顺序:
1、DUT接口
2、环境中例化的接口,FMT_IF,REG_IF,CH_IF
3、各个pkg中drv、mon等都是啥
4、顶层盒子env的结构,组件如何连接
5、test是如何协调各个gen来工作的

5、chnl和reg的driver是initiator,主动发起请求;而fmt的driver是responder,主动发起请求的是DUT里的formatter,信号是req。
responder需要模拟从端接受、消化数据的功能。而消化数据有快慢,需要设立一个大小不同的fifo的模拟。

6、为何formatter里只有一个fifo,而模仿formatter的mcdf_refmod里的对应部分却有3个?

  • 实际上,目前的mcdf_refmod的功能是不完整的,它模拟了reg的配置功能以及对chnl数据的打包,但是没有模拟REG让哪些chnl开关的功能以及arbiter的仲裁功能。
  • 所以mcdf_refmod目前假定arbiter没有丢数,而且优先级的功能正常。这样的mcdf_refmod只能检查打包数据的完整性,但无法检查数据的顺序(优先级不同,顺序不同)。如何检查跟着实验要求之后说。
  • 比较的逻辑:fmt_mb里的数据是监测formatter的,只有一个fifo(fifo的特点是先进先出),放着全部的数据。而各个out_mbs放着对应自己chnl的数据。比较时,先从fmt_mb里拿一个数据,知道对应的id后就直接去对应的out_mbs里取数,如果正常的话,取出来的就是和fmt_mb里的数据一样。

验证框图

在这里插入图片描述
在这里插入图片描述

代码细品

A.fmt_grant可否像通道从端接口时序里的ready一样设为1,等没准备好时再设为0?**

在这里插入图片描述

在这里插入图片描述
不行。
1、首先要知道,通道从端的部分chnl_agent是属于initiator,主动发起请求。我们在写它的时候侧重点在于发送的数据是否有效,相比fmt_agent多了一个valid信号;而fmt_agent的driver是responder,属于响应方,侧重点在于去模仿formatter的下行,需要关注因为下行的差异而导致grant信号发送速度的快慢。

2、fmt_agent是来模仿formatter的下行的,那么下行的FIFO的空间不是固定的,有大有小;FIFO空间大,可以更快地拉高grant信号;而下行对数据的消化速度也是不一样的,有快有慢,较快的消化速度有助于及时拉高grant;
因此,下行准备好的时间不是固定的,grant信号的拉高时间也是有快有慢的。如果直接将grant信号默认设为1,就无法达到模拟效果。

举个例子:

在这里插入图片描述
①FIFO的空间比较小,消化数据比较慢,那么grant信号的拉高就比较慢;就像上图①中的管道一样,上面(formatter)的流量很大,但是下半段空间小,那么水也只能慢慢流出;
②FIFO的空间比较大,消化数据比较快,那么grant的回复就比较快,也就是可以在req发出来后的第二个时钟周期,grant就拉高;就像上图②中的管道一样,上面(formatter)的流量很小,而下半段管道很粗,那么从上面来的水就可以马上流出。

B.在fmt_driver里,既然在do_config里会对fifo例化,那么为什么还要在new函数里先例化?而且还给fifo的容量设置了4096这么大的空间?

function new(string name = "fmt_driver");
      this.name = name;
      this.fifo = new();      //例化
      this.fifo_bound = 4096; 
      this.data_consum_peroid = 1;
    endfunction
task do_config();
      fmt_trans req, rsp;
      forever begin
        this.req_mb.get(req);
        case (req.fifo)
          SHORT_FIFO: this.fifo_bound = 64;
          MED_FIFO: this.fifo_bound = 256;
          LONG_FIFO: this.fifo_bound = 512;
          ULTRA_FIFO: this.fifo_bound = 2048;
        endcase
        this.fifo = new(this.fifo_bound);   //重新例化,开辟空间

        case(req.bandwidth)
          LOW_WIDTH: this.data_consum_peroid = 8;
          MED_WIDTH: this.data_consum_peroid = 4;
          HIGH_WIDTH: this.data_consum_peroid = 2;
          ULTRA_WIDTH: this.data_consum_peroid = 1;
        endcase

        rsp = req.clone();
        rsp.rsp = 1;
        this.rsp_mb.put(rsp);
      end
    endtask

B1. 在fmt_driver里,既然在do_config里会对fifo例化,那么为什么还要在new函数里先例化fifo?

  • 这个问题有个假设,就是do_config需要存在,也就是你需要一开始就进行配置。但是这个假设并不是一直成立的。有时候是没有do_config的,比如有一些组件是作为slave,他不可能每次接收master的数据都事先配置。
  • 这种情况下,你就没有办法通过req_mb得到req,也就不知道req.fifo,没法得到fifo_bound,因此也就无法根据fifo_bound的大小来对fifo进行new,那么fmt_pkg也就不工作了,没法模拟下行。
  • 事先例化fifo,其实就相当于一个初始值,让fmt_pkg在没有配置的时候也能工作。

B2. 为什么初始化时还给fifo的容量设置了4096这么大的空间?

  • fifo_bound需要给初始值,否则默认为0,那么下面语句给fifo设置空间也会出问题。
 this.fifo = new(this.fifo_bound);
  • 此处fifo_bound设置为4096,以及data_consum_peroid设置为1,只是为了让FIFO的空间比较大,消化数据比较快,那么当fmt发送req时,fmt_pkg模拟的下行能够更快地让grant拉高。
  • 也就是说,这里将fifo_bound设置成4096并不是一个硬性的要求,也可以是其他的数值,比如64、256等等,目的都是为了更快地让grant拉高。
  • 不探究fifo_bound和data_consum_peroid的影响时,就选一个最好的值就行了。等需要探究了,再特地改变他们的值。(比如下行从端低带宽测试就是探究低带宽带来的影响)

C. 如何理解reg_pkg中reg_driver里的reg_write的这两句:repeat(2) @(negedge intf.clk); t.data = intf.cmd_data_s2m;

task reg_write(reg_trans t);
      @(posedge intf.clk iff intf.rstn);
      case(t.cmd)
        `WRITE:begin
                intf.drv_ck.cmd_addr <= t.addr;
                intf.drv_ck.cmd <= t.cmd;
                intf.drv_ck.cmd_data_m2s <= t.data;
               end
        `READ:begin
                intf.drv_ck.cmd_addr <= t.addr;
                intf.drv_ck.cmd <= t.cmd;     
                repeat(2) @(negedge intf.clk);
                t.data = intf.cmd_data_s2m; //是=,没有drv_ck
               end
        `IDLE:begin
                this.reg_idle();
              end
        default: $error("command %b is illegal", t.cmd);
      endcase
      $display("%0t reg_driver [%s] sent addr %2x, cmd %2b, data %8x", $time, name, t.addr, t.cmd, t.data);
    endtask

解析:
其实也是为了避免采样的竞争问题。
若cmd为READ,DUT会在下一个周期将数据驱动到接口cmd_data_s2m处;如下图所示:
在这里插入图片描述
①如果TB进行采样时,直接选择在下个周期的上升沿采,可能采不到正确的值。因为此处用的是阻塞赋值“=”,存在竞争冒险情况,此时采样可能采不到要读的数据。
②而如果在当前周期再过两个下降沿去采数据,就能避免上述问题。第一个下降沿还在当前周期,第二个下降沿就在下一个周期了。此时数据已经驱动到接口cmd_data_s2m处了,这时去采样接口处的数据就一定是要读的数据。

C1. 为什么reg_write里的赋值有时用“=”有时用“<=”?

要给接口时钟块内的信号传递数据,就要用非阻塞<=,来模拟时序逻辑;
如果不用时钟块,那就是组合逻辑,就用阻塞=。

C2. 为何在reg_monitor里,对于READ命令,就选择在下一拍的clk上升沿进行采样,而不像reg_driver里等下一拍的下降沿才去采样?

task mon_trans();
      reg_trans m;
      forever begin
        @(posedge intf.clk iff(intf.rstn && intf.mon_ck.cmd != `IDLE));//为IDLE无意义
        m = new();
        m.addr = intf.mon_ck.cmd_addr;
        m.cmd  = intf.mon_ck.cmd;
        if(intf.mon_ck.cmd == `WRITE) begin
          m.data = intf.mon_ck.cmd_data_m2s;
        end
        else if(intf.mon_ck.cmd == `READ) begin
          @(posedge intf.clk);          //  漏了,注意是下一拍写的数据才会给
          m.data = intf.mon_ck.cmd_data_s2m;//注意,此处是通过mon_ck时钟块采的,和driver里不同
        end
        mon_mb.put(m);
        $display("%0t %s monitored addr %2x, cmd %2b, data %8x", $time,this.name, m.addr, m.cmd, m.data);
      end
    endtask 

其实这两种方式应该都可以,只是都是为了正确地采集到下一周期才输出到cmd_data_out的数据。区别在于有没有用时钟块。
在这里插入图片描述

1、reg_driver里是不通过时钟块采样,就需要避免竞争问题。下图中0x04里的数据是D2。我们在READ信号来之后的第二个下降沿去采样cmd_data_out,是可以准确采到D2的。
在这里插入图片描述
2、reg_monitor是通过时钟块进行采样的,所以就不用再考虑竞争的问题。mon_trans里是通过mon_ck时钟块采的信号,会默认在上升沿后1ns才去采样,这样也可以准确地采样到D2稳定后的值。
在这里插入图片描述
ps.
有同学说mon_ck里的信号是input的,应该是在上升沿的1ns前采样而不是1ns后。确实是,所以我把解释给删除了。
知道如何解释的同学也可以帮我看看哈。

(暂时先忽略这个D问题哈)

D. (待解决)mcdf_pkg的do_reg_update为什么只写了

task do_reg_update(); //对读写寄存器进行读操作没必要更新,因为此处最终是为了更新
      reg_trans t;        //你才把值写到读写寄存器里,所以你读它的数据,肯定是同一个
      forever begin       //既然是同一个了你还更新干嘛,所以没必要
        this.reg_mb.get(t);
        if(t.addr[7:4] == 0 && t.cmd == `WRITE) begin//读写寄存器&写操作
          this.regs[t.addr[3:2]].en = t.data[0];      
          this.regs[t.addr[3:2]].prio = t.data[2:1];  
          this.regs[t.addr[3:2]].len = t.data[5:3];
        end
        else if(t.addr[7:4] == 1 && t.cmd == `READ) begin//只读寄存器&读操作
          this.regs[t.addr[3:2]].avail = t.data[7:0];
        end
      end
    endtask
  • 首先要知道下面几点 我们根据addr[7:4]来区分读写/只读寄存器,如果是0就是读写寄存器,是1就表示只读寄存器。
  • 根据addr[3:2]来区分寄存器的编号。
  • regs[]是mcdf_refmod里用来模拟DUT里的寄存器的,会先读取DUT寄存器里的配置信息存储在regs里,然后就把这些信息更新到refmod里
  • 对于只读寄存器,那么就只需要考虑READ命令就行。

那么问题来了,为什么对于读写寄存器,我们没有考虑读的情况呢?

if(t.addr[7:4] == 0 && t.cmd == `WRITE) 

测试

在这里插入图片描述
在这里插入图片描述

完整性测试

代码

class mcdf_data_consistence_basic_test extends mcdf_base_test;
  function new(string name = "mcdf_data_consistence_basic_test");
    super.new(name);
  endfunction

  task do_reg();
    bit[31:0] wr_val, rd_val;
    // slv0 with len=8,  prio=0, en=1
    wr_val = (1<<3)+(0<<1)+1;
    this.write_reg(`SLV0_RW_ADDR, wr_val);
    this.read_reg(`SLV0_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));

    // slv1 with len=16, prio=1, en=1
    wr_val = (2<<3)+(1<<1)+1;
    this.write_reg(`SLV1_RW_ADDR, wr_val);
    this.read_reg(`SLV1_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));

    // slv2 with len=32, prio=2, en=1
    wr_val = (3<<3)+(2<<1)+1;
    this.write_reg(`SLV2_RW_ADDR, wr_val);
    this.read_reg(`SLV2_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));

    // send IDLE command
    this.idle_reg();
  endtask

  task do_formatter();
    void'(fmt_gen.randomize() with {fifo == LONG_FIFO; bandwidth == HIGH_WIDTH;});
    fmt_gen.start();
  endtask

  task do_data();
    void'(chnl_gens[0].randomize() with {ntrans==100; ch_id==0; data_nidles==0; pkt_nidles==1; data_size==8; });
    void'(chnl_gens[1].randomize() with {ntrans==100; ch_id==1; data_nidles==1; pkt_nidles==4; data_size==16;});
    void'(chnl_gens[2].randomize() with {ntrans==100; ch_id==2; data_nidles==2; pkt_nidles==8; data_size==32;});
    fork
      chnl_gens[0].start();
      chnl_gens[1].start();
      chnl_gens[2].start();
    join
    #10us; // wait until all data haven been transfered through MCDF
  endtask
endclass

分析

1、这里解释一下wr_val的意思,以slv2为例。

// slv2 with len=32, prio=2, en=1
    wr_val = (3<<3)+(2<<1)+1;

bit(5:3)对应数据包长度,序号和对应的长度解码表如下:

序号对应长度
04
18
216
332
4-7暂时为32
因为bit(5:3)、bit(2:1)、bit(0)分别为数据包长度、优先级、通道使能信号,可以通过移位的方式来表示wr_val。
这里3<<3中的第一个3可以通过解码表知道其表示数据包长度为32,通过左移3位的方式移动到bit(5:3)。

2、在看波形的时候还发现了有趣的地方。从下面的波形上看,好像只是一开始的时候3个通道同时发送,但是过了某个时间之后,你会发现已经按通道0、1、2的顺序依次发送了。这是为啥?
在这里插入图片描述
不是明明在class mcdf_env里写的是同时发送吗?

virtual task run();
  $display($sformatf("*****************%s started********************", this.name));
  this.do_config();
  fork
    this.chnl_agts[0].run();
    this.chnl_agts[1].run();
    this.chnl_agts[2].run();
	……
  join
endtask

在这里插入图片描述
其实答案在于三个通道的优先级不同。

  • 首先,三个chnl_agts同时向slave-fifo发送数据,而发送到通道0、1、2的data_size不一样,分别为8、16、32,这些数据暂时存在了FIFO0、FIFO1、FIFO2里。
  • 由于我们在mcdf_data_consistence_basic_test 里定义了优先级排序是0>1>2,所以当三个通道同时申请仲裁时,先通过0的,再通过1的,最后才是2。因此就出现了上面的波形。
  • 如果你在mcdf_data_consistence_basic_test 把优先级排序改成2>1>0,那么情况就反了,如下图
    在这里插入图片描述

寄存器读写测试

这个测试目的很简单,就是将把写进寄存器的配置读回来,然后比较一下看对不对。
在这里插入图片描述

代码

class mcdf_reg_stability_test extends mcdf_base_test;
    function new(string name = "mcdf_data_consistence_basic_test");
      super.new(name);
    endfunction

    task do_reg();
      bit[7:0] chnl_rw_addrs[] = '{`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_RW_ADDR};
      bit[7:0] chnl_ro_addrs[] = '{`SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};
      int pwidth = `PAC_LEN_WIDTH + `PRIO_WIDTH + 1;
      bit[31:0] check_pattern[] = '{((1<<pwidth)-1), 0, ((1<<pwidth)-1)};
      bit[31:0] wr_val, rd_val;

      // RW register access and bits toggle
      foreach(chnl_rw_addrs[i]) begin
        foreach(check_pattern[i]) begin
          wr_val = check_pattern[i];
          this.write_reg(chnl_rw_addrs[i], wr_val);
          this.read_reg(chnl_rw_addrs[i], rd_val);
          void'(this.diff_value(wr_val, rd_val));
        end
      end

      // RO register read access
      foreach(chnl_ro_addrs[i]) begin
          this.read_reg(chnl_ro_addrs[i], rd_val);
      end

      // send IDLE command
      this.idle_reg();
    endtask
  endclass

代码解析

bit[31:0] check_pattern[] = '{((1<<pwidth)-1), 0, ((1<<pwidth)-1)};
  • check_pattern[]也就是要写进寄存器里的数,(1<<pwidth)-1是32’b111111,
  • 所以此处的do_reg就是先把32’b111111写进reg,再读回来看是否一致;同样地,分别写进32’h0和32’b111111并读回来。

寄存器稳定性测试

spec里提到了读写寄存器的bit(31:6)是无法写入的,所以我们要测试一下是否真的无法写入。
在这里插入图片描述
在这里插入图片描述

代码

class mcdf_reg_illegal_access_test extends mcdf_base_test;
  function new(string name = "mcdf_reg_illegal_access_test");
    super.new(name);
  endfunction

  task do_reg();
    bit[7:0] chnl_rw_addrs[] = '{`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_RW_ADDR};
    bit[7:0] chnl_ro_addrs[] = '{`SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};
    int pwidth = `PAC_LEN_WIDTH + `PRIO_WIDTH + 1;  //=6
    bit[31:0] check_pattern[] = '{32'h0000_FFC0, 32'hFFFF_0000};
    bit[31:0] wr_val, rd_val;

    // RW register write reserved field and check
    foreach(chnl_rw_addrs[i]) begin
      foreach(check_pattern[j]) begin
        wr_val = check_pattern[j];
        this.write_reg(chnl_rw_addrs[i], wr_val);
        this.read_reg(chnl_rw_addrs[i], rd_val);
        void'(this.diff_value(wr_val & ((1<<pwidth)-1), rd_val));//将期望值和读出来的值对比
      end                                                       
    end

    // RO register write reserved field and check (no care readable field
    // value)
    foreach(chnl_ro_addrs[i]) begin
        wr_val = 32'hFFFF_FF00;
        this.write_reg(chnl_ro_addrs[i], wr_val);
        this.read_reg(chnl_ro_addrs[i], rd_val);
        void'(this.diff_value(0 , rd_val & 32'hFFFFFF00));
    end

    // send IDLE command
    this.idle_reg();
  endtask
endclass

代码解析

1、check_pattern[]怎么理解?

  • pwidth = `PAC_LEN_WIDTH + `PRIO_WIDTH + 1 = 6
  • (1<<pwidth)-1='b1000000-1='b0111111
  • wr_val & ((1<<pwidth)-1)也就是wr_val &'b0111111,表示只取wr_val的低6位,这是我们期望读回来的值
  • diff_value(wr_val & ((1<<pwidth)-1), rd_val)将期望值与读回来的值rd_val进行比较

2、为什么在写入时要分两次(也就是32’h0000_FFC0, 32’hFFFF_0000)来写bit[31:6],直接用32’hFFFF_FFC0一次性进行操作不就行了?

  • 如果只是要检查保留域的话,用32’hFFFF_FFC0一次性写入没有问题
  • 如果想要避免数据的粘连问题,分两次可能更佳。举个例子来加深对粘连问题的理解:
    如果你想要对低6位进行写入,来测试每个bit位是否可以正常翻转,有A和B两种写法:
    在这里插入图片描述
  • 如果bit位之间不会相互干扰,那么可以直接通过操作A给数据进行测试。
  • 但有时候可能设计疏忽了,导致对某一位进行操作时会影响到其他位,也就是说数据位之间存在粘连。那么就无法保证每一位都从0->1->0,此时操作B就可以避免这种问题。(其实还没有完全理解)
  • UVM的bit_bash做的更精细,会单独地对每一位进行0->1->0的翻转,虽然测试更长,但是更准。

数据通道开关检查

基本判断:
数据通道关闭时,mcdf_checker不会收到输入/出端的检测数据,因此也没有数据比较的信息。

测试通过标准:
1、此测试在最后的report中comparison count以及error count信息统计为0;
2、时序检查。当slave channel被关闭时,valid如果拉高,ready不应该出现拉高的情况,因为通道关闭,此时便不能接受数据,也就不应该给出可以接受数据的信号(ready)。

测试出现问题的可能原因:

  • 数据可能没有被真正写入FIFO(?)
  • slave channel没有被真正关闭

测试实现的思路:

  • 用接口mcdf_intf来监测DUT里的通道使能信号en,将其传入mcdf_checker;
    在这里插入图片描述

  • 将chnl_intf中的valid、ready信号也传入mcdf_checker中。

  • 通过观测valid、ready和en信号来完成此检查

代码实现:

task do_channel_disable_check(int id);
  forever begin
    @(posedge this.mcdf_vif.clk iff (this.mcdf_vif.rstn && this.mcdf_vif.mon_ck.chnl_en[id]===0));
    if(this.chnl_vifs[id].mon_ck.ch_valid===1 && this.chnl_vifs[id].mon_ck.ch_ready===1)
      rpt_pkg::rpt_msg("[CHKERR]", 
        $sformatf("ERROR! %0t when channel disabled, ready signal raised when valid high",$time), 
        rpt_pkg::ERROR, 
        rpt_pkg::TOP);
  end
endtask
interface mcdf_intf(input clk, input rstn); 
  // USER TODO
  // To define those signals which do not exsit in reg_if, chnl_if, arb_if or fmt_if
  logic chnl_en[3];

  clocking mon_ck @(posedge clk);
    default input #1ns output #1ns;
    input chnl_en;
  endclocking
endinterface
  //mcdf 接口抓取MCDF内部的en信号
  assign mcdf_if.chnl_en[0] = tb.dut.ctrl_regs_inst.slv0_en_o;
  assign mcdf_if.chnl_en[0] = tb.dut.ctrl_regs_inst.slv1_en_o;
  assign mcdf_if.chnl_en[0] = tb.dut.ctrl_regs_inst.slv2_en_o;

其中,ctrl_regs_inst是DUT寄存器ctrl_regs的实例,里面有发送给各channel的通道使能信号slv0_en_o、slv1_en_o、slv2_en_o。将这些信号给到mcdf_if,配合valid、ready信号进行测试。

除了监测DUT内部的en信号,还可以调用mcdf_refmod里面的get_field_value()得到通道使能信号RW_EN。因为他们的数据都是一样的,都来自reg_agent。
只需将

@(posedge this.mcdf_vif.clk iff (this.mcdf_vif.rstn && this.mcdf_vif.mon_ck.chnl_en[id]===0));

改为下面这句即可:

@(posedge this.mcdf_vif.clk iff (this.mcdf_vif.rstn && refmod.get_field_value(id, RW_EN)===0);

当然,这样写是有前提的:

  • 你的refmod的监测功能需要是正常的,也就是说refmod寄存器里更新的值要和监测到DUT寄存器里的信号是一样的。

注意:
不要轻易监测DUT内部信号,往往在你监测DUT内部信号时,一定存在着假设,也就是你要监测的内部信号的产生是合理的。
上述代码存在着假设:寄存器的配置、发送没有问题。也就是:

  • 假设1:外部的reg_agent对寄存器的配置信息正常送到了寄存器中。这个假设可以通过寄存器读写测试来覆盖到,也就是检测寄存器的读写值是否正确,对应的测试类名是mcdf_reg_write_read_test。
  • 假设2:在假设1的基础上,DUT中寄存器的en信号可以准确送到3个channel,也就是寄存器和3个channel的连接正常。比如,如果寄存器和channel 0 的连接出了问题,没有把寄存器的en信号为0(关闭)传过去,那么当valid为1时,ch_ready有可能还为1。

分析
我们在mcdf_data_consistence_basic_test里把通道0和1关闭,只打开通道2,观察实验结果:

task do_reg();
  bit[31:0] wr_val, rd_val;
  // slv0 with len=8,  prio=0, en=1
  wr_val = (1<<3)+(0<<1)+0;
  this.write_reg(`SLV0_RW_ADDR, wr_val);
  this.read_reg(`SLV0_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));

  // slv1 with len=16, prio=1, en=1
  wr_val = (2<<3)+(1<<1)+0;
  this.write_reg(`SLV1_RW_ADDR, wr_val);
  this.read_reg(`SLV1_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));

  // slv2 with len=32, prio=2, en=1
  wr_val = (3<<3)+(2<<1)+1;
  this.write_reg(`SLV2_RW_ADDR, wr_val);
  this.read_reg(`SLV2_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));
  // send IDLE command
  this.idle_reg();
endtask

可以从打印消息看出通道0和1被关闭了,没有比较信息,只有2有。
在这里插入图片描述
而从波形中也可以看出数据没有经过通道0和1,只有2有。
在这里插入图片描述
再放大一点看,可以看到通道0和1的valid信号为1,而ready信号一直没有拉高,说明我们的通道开关功能是正常的,测试通过。
在这里插入图片描述

优先级测试

测试功能点测试内容测试通过标准测试类名
优先级测试将不同数据通道配置为相同或者不同的优先级,在数据通道使能的情况下进行测试如果优先级相同,那么arbiter应该采取轮询机制从各个通道接收数据;如果优先级不同,那么arbiter应该先接收高优先级通道的数据,同时,最终所有的数据都应该从MCDF发送出来mcdf_arbiter_priority_test

要想实现这个优先级相同采取轮询机制的步骤,需要更新一下arbiter的代码,我们目前手头拿到的代码是没有加上轮询机制的。

在这里插入图片描述

arbiter 部分代码

只要把arbiter中的下面部分增加轮询机制部分即可。

always @ (posedge clk_i or negedge rstn_i)
begin : CHANEL_SELECT
    if (!rstn_i) id_sel_r  = 2'b11;
	else if (f2a_id_req_i)
//------------------此处为新增加的轮询机制部分,其他地方几乎不变------------------------------------
		if({slv2_req_i,slv1_req_i,slv0_req_i} != 3'b000)//如果没有 Slave 申请权限,权限保持不变
			begin			
			if(slv0_prio_i == slv1_prio_i && slv1_prio_i == slv2_prio_i)// 如果权限相同,轮询调度
				begin
					case(id_sel_r)
						2'b00:	begin
									if(slv1_req_i == 1)	
										begin
											id_sel_r <= 2'b01;
											a2f_pkglen_sel_r = slv1_pkglen_i;
										end
											
									else if(slv2_req_i == 1)
										begin
											id_sel_r <= 2'b10;
											a2f_pkglen_sel_r = slv2_pkglen_i;
										end
								end
						2'b01:	begin
									if(slv2_req_i == 1)	
									begin
										id_sel_r <= 2'b10;
										a2f_pkglen_sel_r = slv2_pkglen_i;
									end
									else if(slv0_req_i == 1)
									begin
										id_sel_r <= 2'b00;
										a2f_pkglen_sel_r = slv0_pkglen_i;
									end
								end
						2'b10:	begin
									if(slv0_req_i == 1)
									begin
										id_sel_r <= 2'b00;
										a2f_pkglen_sel_r = slv0_pkglen_i;
									end
									else if(slv1_req_i == 1)
									begin
										id_sel_r <= 2'b01;
										a2f_pkglen_sel_r = slv1_pkglen_i;
									end
								end
						default: 
						begin
							id_sel_r <= 2'b00;
							a2f_pkglen_sel_r = slv0_pkglen_i;
						end
					endcase

				end 
			end   
//----------------------------------------------------------------------------------------------
		else
			case ({slv2_req_i,slv1_req_i,slv0_req_i})  // 如果权限不同,优先级调度
					3'b001: 	begin 
								id_sel_r <= 2'b00;
								a2f_pkglen_sel_r = slv0_pkglen_i;
								end 
								
					3'b010: 	begin
								id_sel_r <= 2'b01;
								a2f_pkglen_sel_r = slv1_pkglen_i;
								end 
					
					3'b011: 	begin
								if(slv1_prio_i >= slv0_prio_i) 
									begin
										id_sel_r <= 2'b00;
										a2f_pkglen_sel_r = slv0_pkglen_i;
									end 
								else 
									begin
										id_sel_r <= 2'b01;
										a2f_pkglen_sel_r = slv1_pkglen_i;
									end 
								end 
								
					3'b100: 	begin
									id_sel_r <= 2'b10;
									a2f_pkglen_sel_r = slv2_pkglen_i;
								end 
								
					3'b101: 	begin
								if(slv2_prio_i >= slv0_prio_i) 
									begin
										id_sel_r <= 2'b00;
										a2f_pkglen_sel_r = slv0_pkglen_i;
									end 
								else 
									begin
										id_sel_r <= 2'b10;
										a2f_pkglen_sel_r = slv2_pkglen_i;
									end 
								end
								
				3'b110: 	begin
								if(slv2_prio_i >= slv1_prio_i) 
									begin
										id_sel_r <= 2'b01;
										a2f_pkglen_sel_r = slv1_pkglen_i;
									end 
								else 
									begin
										id_sel_r <= 2'b10;
										a2f_pkglen_sel_r = slv2_pkglen_i;
									end 
								end
								
				3'b111: 	begin
									if(slv2_prio_i >= slv0_prio_i && slv1_prio_i >= slv0_prio_i)  
											begin 
												id_sel_r <= 2'b00;
												a2f_pkglen_sel_r = slv0_pkglen_i;
											end  																														//priority 0>1 && 0>2
									if(slv2_prio_i >= slv0_prio_i && slv1_prio_i < slv0_prio_i)   
											begin
												id_sel_r <= 2'b01;              																				//priority 1>0>2
												a2f_pkglen_sel_r = slv1_pkglen_i;
											end 
									if(slv2_prio_i < slv0_prio_i && slv2_prio_i >= slv1_prio_i)   
											begin
												id_sel_r <= 2'b01; 																								//priority 1>2>0
												a2f_pkglen_sel_r = slv1_pkglen_i;
											end 	
									if(slv2_prio_i < slv0_prio_i && slv2_prio_i < slv1_prio_i)    
											begin 
												id_sel_r <= 2'b10; 																								//priority 2>0 && 2>1
												a2f_pkglen_sel_r = slv2_pkglen_i;
											end 
									end

				default: 	begin 
									id_sel_r <= 2'b11;
									a2f_pkglen_sel_r = 3'b111;
								end 
								
			endcase 

	else 
		begin
			id_sel_r <= id_sel_r;
			a2f_pkglen_sel_r <= a2f_pkglen_sel_r;
		end 
end 

checker代码更新

task do_arbiter_priority_check();
      int id;
      forever begin
        @(posedge this.arb_vif.clk iff (this.arb_vif.rstn && this.arb_vif.mon_ck.f2a_id_req===1));
        id = this.get_slave_id_with_prio();           //id的优先级最高
        if(id >= 0) begin
          @(posedge this.arb_vif.clk);
          if(!(refmod.get_field_value(0,RW_PRIO) == refmod.get_field_value(1,RW_PRIO)
          && refmod.get_field_value(1,RW_PRIO) == refmod.get_field_value(2,RW_PRIO)))
            if(this.arb_vif.mon_ck.a2s_acks[id] !== 1)    //id的优先级最高,那么对应的a2s_acks应该为1,否则报错
              rpt_pkg::rpt_msg("[CHKERR]",
                $sformatf("ERROR! %0t arbiter received f2a_id_req===1 and channel[%0d] raising request with high priority, but is not granted by arbiter", $time, id),
                rpt_pkg::ERROR,                               
                rpt_pkg::TOP);
        end
      end
    endtask

其中,下面这一句通过get_field_value函数获取每个chnl的优先级。如果三个优先级都不一样,那么就不会执行轮询,按照原来情况执行,也就是通过id优先级的高级来通过arbiter。
如果三个优先级都一样,那么就采用轮询机制,不执行接下来信息打印部分,否则就一定会报错。因为id的优先级最高,那么对应的a2s_acks应该为1。而轮询机制并不是按照优先级来控制a2s_acks信号,所以会出现错误。

if(!(refmod.get_field_value(0,RW_PRIO) == refmod.get_field_value(1,RW_PRIO)
   && refmod.get_field_value(1,RW_PRIO) == refmod.get_field_value(2,RW_PRIO)))

测试代码1:优先级不同

可以根据mcdf_data_consistence_basic_test 的代码调整一下优先级即可,比如此处我将2的优先级调为最大。

  class mcdf_arbiter_priority_test extends mcdf_base_test;
    function new(string name = "mcdf_arbiter_priority_test");
      super.new(name);
    endfunction

    task do_reg();
      bit[31:0] wr_val, rd_val;
      // slv0 with len=8,  prio=2, en=1
      wr_val = (1<<3)+(2<<1)+1;
      this.write_reg(`SLV0_RW_ADDR, wr_val);
      this.read_reg(`SLV0_RW_ADDR, rd_val);
      void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));

      // slv1 with len=16, prio=1, en=1
      wr_val = (2<<3)+(1<<1)+1;
      this.write_reg(`SLV1_RW_ADDR, wr_val);
      this.read_reg(`SLV1_RW_ADDR, rd_val);
      void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));

      // slv2 with len=32, prio=0, en=1
      wr_val = (3<<3)+(0<<1)+1;
      this.write_reg(`SLV2_RW_ADDR, wr_val);
      this.read_reg(`SLV2_RW_ADDR, rd_val);
      void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));

      // send IDLE command
      this.idle_reg();
    endtask

    task do_formatter();
      void'(fmt_gen.randomize() with {fifo == LONG_FIFO; bandwidth == HIGH_WIDTH;});
      fmt_gen.start();
    endtask

    task do_data();
      void'(chnl_gens[0].randomize() with {ntrans==100; ch_id==0; data_nidles==0; pkt_nidles==1; data_size==8; });
      void'(chnl_gens[1].randomize() with {ntrans==100; ch_id==1; data_nidles==1; pkt_nidles==4; data_size==16;});
      void'(chnl_gens[2].randomize() with {ntrans==100; ch_id==2; data_nidles==2; pkt_nidles==8; data_size==32;});
      fork
        chnl_gens[0].start();
        chnl_gens[1].start();
        chnl_gens[2].start();
      join
      #10us; // wait until all data haven been transfered through MCDF
    endtask
  endclass

测试代码1分析

测试代码2:优先级相同

和上面的测试代码一样,只不过把优先级设为相同。

task do_reg();
  bit[31:0] wr_val, rd_val;
  // slv0 with len=8,  prio=0, en=1
  wr_val = (1<<3)+(0<<1)+1;
  this.write_reg(`SLV0_RW_ADDR, wr_val);
  this.read_reg(`SLV0_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));

  // slv1 with len=16, prio=0, en=1
  wr_val = (2<<3)+(0<<1)+1;
  this.write_reg(`SLV1_RW_ADDR, wr_val);
  this.read_reg(`SLV1_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));

  // slv2 with len=32, prio=0, en=1
  wr_val = (3<<3)+(0<<1)+1;
  this.write_reg(`SLV2_RW_ADDR, wr_val);
  this.read_reg(`SLV2_RW_ADDR, rd_val);
  void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));

  // send IDLE command
  this.idle_reg();
endtask

测试代码2分析

仿真运行,通过。
在这里插入图片描述
部分波形如下:
在这里插入图片描述
上图中,chnlx_data_i表示通过chnl发送的数据;fmt_data_o指通过fmt发送出去的数据;fmt_child_o表示当前发送的数据属于哪个通道。
可以看出三个通道的数据发送交替进行,实现轮询仲裁。

下行从端低带宽测试

测试功能点测试内容测试通过标准测试类名
下行从端低带宽测试将MCDF下行数据接收端设置为小存储量,低带宽的类型,由此使得formatter发送出数据后,下行从端有更多的机会延迟grant信号的置位,用来模拟真实场景在req拉高后,grant应该在至少两个时钟周期后拉高,以此来模拟下行从端数据余量不足的情况。当这种激励时序发生10次后,可以停止测试。mcdf_formatter_grant_test

代码

class mcdf_down_stream_low_bandwidth_test extends mcdf_base_test;
  function new(string name = "mcdf_down_stream_low_bandwidth_test");
    super.new(name);
  endfunction

  task do_reg();
    bit[31:0] wr_val, rd_val;
    // slv0 with len=8,  prio=0, en=1
    wr_val = (1<<3)+(0<<1)+1;
    this.write_reg(`SLV0_RW_ADDR, wr_val);
    this.read_reg(`SLV0_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));

    // slv1 with len=16, prio=1, en=1
    wr_val = (2<<3)+(1<<1)+1;
    this.write_reg(`SLV1_RW_ADDR, wr_val);
    this.read_reg(`SLV1_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV1_WR_REG"));

    // slv2 with len=32, prio=2, en=1
    wr_val = (3<<3)+(2<<1)+1;
    this.write_reg(`SLV2_RW_ADDR, wr_val);
    this.read_reg(`SLV2_RW_ADDR, rd_val);
    void'(this.diff_value(wr_val, rd_val, "SLV2_WR_REG"));

    // send IDLE command
    this.idle_reg();
  endtask

  // configure formatter with short/medium fifo and low/medium bandwidth to
  // mimic critical datapath
  task do_formatter();
    void'(fmt_gen.randomize() with {fifo inside {SHORT_FIFO, MED_FIFO}; bandwidth inside {LOW_WIDTH, MED_WIDTH};});
    fmt_gen.start();
  endtask

  // Burst data packet transition for data pressure
  task do_data();
    void'(chnl_gens[0].randomize() with {ntrans==300; ch_id==0; data_nidles==0; pkt_nidles==1; data_size==8; });
    void'(chnl_gens[1].randomize() with {ntrans==300; ch_id==1; data_nidles==0; pkt_nidles==1; data_size==16;});
    void'(chnl_gens[2].randomize() with {ntrans==300; ch_id==2; data_nidles==0; pkt_nidles==1; data_size==32;});
    fork
      chnl_gens[0].start();
      chnl_gens[1].start();
      chnl_gens[2].start();
    join
    #10us; // wait until all data haven been transfered through MCDF
  endtask
endclass

代码分析

我试着将不同的FIFO容量和消耗数据的速度进行搭配,跑完测试后记录下各自所花的时间,如下表:

FIFOWIDTHTime/ns
SHORT_FIFOLOW_WIDTH760205
SHORT_FIFOMED_WIDTH426685
MED_FIFOLOW_WIDTH751185
MED_FIFOMED_WIDTH421805
ULTRA_FIFOULTRA_WIDTH363275
可以看出跑完测试所花的时间关键看消耗数据所花的时间,FIFO大小影响较小。
  • SHORT_FIFO搭配LOW_WIDTH,看波形时发现前14次都是req的下一个周期就拉高grant,而在第15次就不是了,直接在req后的第18个周期才拉高grant;相比之下,ULTRA_FIFO搭配ULTRA_WIDTH,如果fifo空间又大,消耗数据又很快,那么给出grant信号就会很快,因此每一次都是req的下一个周期就拉高grant。
  • 如果fmt_agt消耗数据很快,那么就算其fifo容量小一点也无所谓,fmt_agt会较快地给formatter发送grant信号,因此阻塞的时间就会缩短,结束测试的时间也更快。
  • 但是如果消耗数据所花的时间比较长,就算你的fifo稍微大一些也无济于事,fifo很快就满了,所以fmt_agt会较慢地给出grant信号,阻塞的时间就长,结束测试的时间也更长。
;