路科验证MCDF_lab4笔记

  • Post author:
  • Post category:其他


验证框图

学前提示

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

1

2

3

4

5

6

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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);

1

此处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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

解析:

其实也是为了避免采样的竞争问题。

若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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

猜测:

是不是关键在于有无用时钟块采?如果有,那么就不存在采样竞争的问题,就可以直接写 @(posedge intf.clk);

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

首先要知道下面几点 我们根据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)

1

测试

寄存器读写测试

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


代码

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

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

代码解析

bit[31:0] check_pattern[] = ‘{((1<<pwidth)-1), 0, ((1<<pwidth)-1)};

1

check_pattern[]也就是要写进寄存器里的数,(1<<pwidth)-1是32’h111111,

所以此处的do_reg就是先把32’h111111写进reg,再读回来看是否一致;同样地,分别写进32’h0和32’h111111并读回来。

寄存器稳定性测试

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

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

代码解析

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中error信息统计为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

1

2

3

4

5

6

7

8

9

10

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

1

2

3

4

5

6

7

8

9

10

//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;

1

2

3

4

其中,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));

1

改为下面这句即可:

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

1

注意:

不要轻易监测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。

有疑惑的小点:

1、在fmt_driver里的这段代码,为何fmt_grant信号通过时钟块取,其他信号不用?

task do_receive();//把fmt传来的数据放到fifo里

forever begin

@(posedge intf.fmt_req);

forever begin

@(posedge intf.clk);

if((this.fifo_bound – this.fifo.num()) >= intf.fmt_length)

break;

end

intf.drv_ck.fmt_grant <= 1;

@(posedge intf.fmt_start);

fork

begin

@(posedge intf.clk);

intf.drv_ck.fmt_grant <= 0;

end

join_none

repeat(intf.fmt_length) begin

@(negedge intf.clk);//添加延迟,避免采样时的竞争问题。

this.fifo.put(intf.fmt_data);

end

end

endtask

————————————————

版权声明:本文为CSDN博主「Hardworking_IC_boy」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/dinghj3/article/details/122312429