lab4组件结构与lab3一样,但验证的DUT更大了,mcdt→mcdf。
lab4文件数目增加,为了模拟多个人验证同一个DUT,各个模块构建各自的package,假定模块验证完毕,现在子系统要集成各个模块的package,目前lab4不需要arbiter的package,最顶层环境交给mcdf的package。
与设计相关的文件:arbiter、formatter、reg、slave.fifo要先编译;然后再编译mcdf。
chnl_agent 的 stimlator 是initiator (个人看法:主动的体现在 valid ,可⾃⼰决定data是否有效) ⽽ fmt_agent被动,只能等fmt的req,看能不能放下再给出grant。
一、tb.sv
tb.sv⽂件中有各种接⼝⽂件,其端⼝都跟随DUT端⼝⽽设计,如cmd读写端⼝。
param_def.v来自设计,从复用角度考虑,保持统一。
arb.intf:arbiter接⼝暂时不⽤,各接⼝都在设计内部,不需要激励。
fmt_intf里,fmt_req拉高至少一拍后fmt_grant拉高(posedge信号保持一个周期),fmt_grant拉低后fmt_req跟着拉低(对照之前视频)。
fmt_req拉高,fmt_chid、fmt_length也变化,fmt_grant拉高后,fmt_start拉高,开始发送数据(连续发送fmt_data),发送完后fmt_end保持一拍,与最后一个数据同时变化。
目前来看lab4中mcdf_intf不用,channel、req、fmt的intf都直接与mcdf端口连接。有时想监测内部信号,会把信号给mcdf_intf,因此又可被验证盒子里的组件拿到,这意味着checker可拿到进而可以监测内部信号。
二、arb_pkg.sv、chnl_pkg.sv
不再细谈,可以回头看lab3的介绍。
三、reg_pkg.sv
class reg_trans:
数据成员:addr地址、cmd读写指令、data数据、rsp response 看读写回来的数据是否正常。
constraint cstr默认cmd为读、写、IDLE。宏仍是复用的param_def,说白了就是对一些参数赋值,设计验证两边对齐,可以通用。
为何要有这些constraint:
0x00、0x04、0x08为读写寄存器,转换为二进制分别为:0000 0000,0000 0100,0000 1000;
0x10、0x14、0x18为只读寄存器,转换为二进制分别为:0001 0000,0001 0100,0001 1000。
addr [7:4] ==0 && cmd == 'WRITE -> soft data [31:6] ==0;
第7-4位为0表示该地址是读写寄存器,命令cmd为写状态时(cmd=’WRITE),读写寄存器低6位可配置,高26位bit(31:6)为保留位无法写入,所以约束令高26位为0。
对于寄存器的描述可回看lab0:
soft addr[7:5] == 0;
//因为目前最高地址为0x10、0x14、0x18,转换为二进制分别为0001 0000、0001 0100、0001 1000,可看到7到5位均为0。
soft adddr[4] == 1 -> cmd != ‘READ;
//第4位为1表示只读寄存器(0x10、0x14、0x18转换为二进制分别为0001 0000、0001 0100、0001 1000),cmd为READ命令。
class reg_driver:
注:driver就是lab3的initiator,为了向UVM过渡改名了。
功能:发送激励。
task do_reset();
forever begin
@(negedge intf.rstn);
intf.cmd_addr <= 0;
intf.cmd <= `IDLE;
intf.cmd_data_m2s <= 0;
end
endtask
task do_reset():复位时,register读写为设为0,cmd设为IDLE,写的数据设为0。
task do_drive();
reg_trans req, rsp;
@(posedge intf.rstn);
forever begin
this.req_mb.get(req);
this.reg_write(req);
rsp = req.clone();
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
task do_drive():do_reset()复位后,从generator拿到一个req,调用req_write操作,clone后返回去rsp。
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;
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
task reg_write :
cmd为’WRITE,拿到 reg_trans 类型的req后,若cmd为写操作,把req的addr和cmd发到接口intf上,把数据data发到总线上。
cmd为IDLE,调用reg_idle等一拍,把addr设为0,cmd设为IDLE,数据data设为0。
cmd为READ,先把req的addr和cmd发到接口intf上,告诉接口我要读谁,然后等两个下降沿(当前clk的上升沿过来,第一个下降沿还在当前周期,再等到下一个周期下降沿)采样intf的数据cmd_data_s2m(读的数据,表示对应channel下行的FIFO余量)。
为什么等两个下降沿?
这里想借鉴一下@Hardworking_IC_boy的解读:
reg_driver里是不通过时钟块采样,就需要避免竞争问题。reg_monitor是通过时钟块进行采样的,所以就不用再考虑竞争的问题。mon_trans里是通过mon_ck时钟块采的信号,会默认在上升沿后1ns才去采样,这样也可以准确地采样到D2稳定后的值。
class reg_generator:
task send_trans();
reg_trans req, rsp;
req = new();
assert(req.randomize with {local::addr >= 0 -> addr == local::addr;
local::cmd >= 0 -> cmd == local::cmd;
local::data >= 0 -> data == local::data;
})
else $fatal("[RNDFAIL] register packet randomization failure!");
$display(req.sprint());
this.req_mb.put(req);
this.rsp_mb.get(rsp);
$display(rsp.sprint());
if(req.cmd == `READ)
this.data = rsp.data;
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
this.rsp_mb.get(rsp); generator从driver拿到rsp。
(reg_driver:从generator拿到req后看给的cmd是什么(write/read/idle),然后在reg_write对应操作。如果是读,就会把intf总线数据写进当前req里边,clong req给rsp交给generator。)
所以上边this.data = rsp.data; generator就可以拿到总线上的data。
class reg_monitor:
task mon_trans();
reg_trans m;
forever begin
@(posedge intf.clk iff (intf.rstn && intf.mon_ck.cmd != `IDLE));//clk上升沿监测合理数据,cmd不为idle。
m = new();
m.addr = intf.mon_ck.cmd_addr;
m.cmd = intf.mon_ck.cmd;
//新生成一个对象,先把addr和cmd交进来。
if(intf.mon_ck.cmd == `WRITE) begin
m.data = intf.mon_ck.cmd_data_m2s;//Write:把当前总线上数据放进来。
end
else if(intf.mon_ck.cmd == `READ) begin
@(posedge intf.clk);
m.data = intf.mon_ck.cmd_data_s2m;//READ:等下一个时钟周期,把总线上读回来的写进data。
end
mon_mb.put(m);//读写都会把数据放入mon_mb,然后交给checker。
$display("%0t %s monitored addr %2x, cmd %2b, data %8x", $time, this.name, m.addr, m.cmd, m.data);
end
endtask
class reg_agent:略。
脉络:
class reg_trans是reg_driver和DUT的reg之间发送的对象类型(包含addr、cmd、data、rsp成员变量),会先根据各个成员变量的特点进行约束,clone函数会新创建一个同类型的对象并把各变量值赋给其。
class reg_driver相当于之前的initiator,会声明两个mailbox,task run()会调用do_drive()和do_reset(),do_reset()就是用来复位的。do_drive()和之前的chnl_pkg很像,从reg_generator通过req_mb接收到req后,会执行reg_write,然后克隆一份req给rsp,然后通过rsp_mb发给reg_generator。
其中执行reg_write是一个难点,对于不同cmd指令操作不同:
写:把req的addr和cmd给reg_driver和reg之间接口的addr和cmd,把req的data给接口的cmd_data_m2s(对于验证环境是output);其实就是把req的数据写到接口总线上。
读:先把req的addr和cmd给reg_driver和reg之间接口的addr和cmd,然后要等待两个下降沿*,把接口的数据cmd_data_s2m(表示对应channel下行的FIFO余量,对于验证环境是input)给req的data。
在读的时候,cmd_data_s2m先给req的data,然后克隆req给rsp传到generator,rsp.data又给generator的this.data,所以generator也会拿到接口总线上的数据。
四、fmt_pkg.sv
stimulator可分为两种:①initiator主动的②responder被动的
channel和register的driver是主动发起请求的initiator。fmt的driver是被动的responder。
fmt响应的过程:fmt会发起req,fmt_agent给grant信号。
fmt_agent里的driver要模拟一个下行FIFO:
下⾏数据如果buffer⽐较⼩,消化buffer⽐较慢,⼀般 grant信号给的就⽐较慢(余量不太易满⾜);若上⾯给的数据⼩,下⾯缓存⼤,只要data一进来下一周期grant立即拉高。(如下图)
class fmt_trans:
function bit compare(fmt_trans t);
string s;
compare = 1;
s = "\n=======================================\n";
s = {s, $sformatf("COMPARING fmt_trans object at time %0d \n", $time)};
if(this.length != t.length) begin
compare = 0;
s = {s, $sformatf("sobj length %0d != tobj length %0d \n", this.length, t.length)};
end
if(this.ch_id != t.ch_id) begin
compare = 0;
s = {s, $sformatf("sobj ch_id %0d != tobj ch_id %0d\n", this.ch_id, t.ch_id)};
end
foreach(this.data[i]) begin
if(this.data[i] != t.data[i]) begin
compare = 0;
s = {s, $sformatf("sobj data[%0d] %8x != tobj data[%0d] %8x\n", i, this.data[i], i, t.data[i])};
end
end
if(compare == 1) s = {s, "COMPARED SUCCESS!\n"};
else s = {s, "COMPARED FAILURE!\n"};
s = {s, "=======================================\n"};
rpt_pkg::rpt_msg("[CMPOBJ]", s, rpt_pkg::INFO, rpt_pkg::MEDIUM);
endfunction
关注一下compare函数:
比较当前对象和另一个对象的成员(length、id、data),只要有一个不一样,compare返回0并报告错误。返回1则报告比较成功。
class fmt_driver:
class fmt_driver;
local string name;
local virtual fmt_intf intf;
mailbox #(fmt_trans) req_mb;
mailbox #(fmt_trans) rsp_mb;
两个mailbox(req_mb和rsp_mb)和generator通信。
local mailbox #(bit[31:0]) fifo;
local int fifo_bound;
local int data_consum_peroid;
第三个mailbox-fifo模拟下行数据buffer,之前的channel和register的driver没有fifo这个mailbox,数据发出去就不管了,但fmt模拟的从端,不但要接收数据消化掉,还要模拟消化数据的快慢。
fifo_bound:FIFO固定长度;
data_consum_peroid:数据消耗时间,侧面反映带宽,带宽越大时间消耗越少。
function new(string name = "fmt_driver");
this.name = name;
this.fifo = new();
this.fifo_bound = 4096;
this.data_consum_peroid = 1;
endfunction
function void set_interface(virtual fmt_intf intf);
if(intf == null)
$error("interface handle is NULL, please check if target interface has been intantiated");
else
this.intf = intf;
endfunction
task run();
fork
this.do_receive();
this.do_consume();
this.do_config();
this.do_reset();
join
endtask
四个方法:do_receive();do_consume();do_config();do_reset();
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
do_config():开始要配置当前的driver,要配置其行为表现的更像一个buffer。
配置过程:this.req_mb.get(req);driver从generator拿到激励req,根据case(req.fifo)配置driver类对象的数据长度fifo_bound,对第三个mailbox——fifo重新例化并指定为相同长度。根据case(req.bandwidth)对driver类对象的data_consum_peroid(配置消耗时长)做配置。然后把req克隆一份给rsp,再传回给generator。
注:这里插入一下mailbox的性质:[Systemverilog学习笔记] Thread Communication-Event、Semaphore、mailbox_hjd西瓜瓜瓜的博客-CSDN博客_mailbox uvm通过下文了解Event、Semaphore、mailbox三种对象的概念、使用方法及应用场景https://blog.csdn.net/qq_36917568/article/details/122153924mailbox 是一种允许不同进程相互交换数据的方法,mailbox是一个内置类,本质上类似于队列,但和queue队列的数据类型有很大不同,使用semaphore来控制存储队列中的push和pull。无法访问邮箱队列中的给定索引,只能按照fifo的顺序检索项目。
mailbox 可以被创建为两种:
mailbox mailbox_name = new(mailbox_space_number);//有界队列,只能存储有限个数据量。当一个进程试图将多个消息存入一个满的mailbox中时,将会被挂起直到mailbox中有足够的空间。
mailbox mailbox_name = new();//无界队列,可以存储无限个数据量。
task do_reset();
forever begin
@(negedge intf.rstn)
intf.fmt_grant <= 0;
end
endtask
do_reset():接口复位信号到来fmt_grant驱动为0。
task do_consume();
bit[31:0] data;
forever begin//不断消耗数据
void'(this.fifo.try_get(data));//尝试从FIFO拿一个数据(不管有没有都尝试拿)
repeat($urandom_range(1, this.data_consum_peroid)) @(posedge intf.clk);//拿到一个数据后等待若干个周期。
end
endtask
do_consume():模拟fmt_driver消耗数据,把fifo中的数据排出去,根据data_consum_peroid(消耗周期)的快慢,每隔几个上升沿就从fifo里try_get一个bit [31:0] data类型的数据。
task do_receive();
forever begin
@(posedge intf.fmt_req);
forever begin
@(posedge intf.clk);
if((this.fifo_bound-this.fifo.num()) >= intf.fmt_length)//bound不变,随着数据消耗fifo.num减小,余量增大。
break;
end
intf.drv_ck.fmt_grant <= 1;
@(posedge intf.fmt_start);
fork
begin
@(posedge intf.clk);
intf.drv_ck.fmt_grant <= 0;//当余量超过length,grant变为1
end
join_none
repeat(intf.fmt_length) begin
@(negedge intf.clk);
this.fifo.put(intf.fmt_data);//从fmt_start开始,重复fmt_length次采集数据放入fifo这个mailbox。接收数据完毕。
end
end
endtask
do_receive():模拟从fmt接收数据再消化的过程。当fmt有发出需求,在接口上fmt_req的上升沿来临时,同时还要保证fifo总容量-fifo现存数据量>=接口上给的fmt_length即要到来的数据长度,如果满足就会把接口上的fmt_grant拉高,紧接着接口上的fmt_start上升沿也到来了,等待一个时钟上升沿再把fmt_grant拉低,然后重复fmt_length次把fmt_data放入fifo的操作,等把数据接受完后又开始等待下一个fmt的req发送请求。
do_receice、do_config、do_reset、do_consume都是硬件行为,是并列的。随时接受、配置、复位、消化。
class fmt_generator:
略。
class fmt_monitor:
task mon_trans();
fmt_trans m;
string s;
forever begin
@(posedge intf.mon_ck.fmt_start);//不需等待req、grant,假定协议没问题,等待fmt_start到来
m = new();//例化数据包
m.length = intf.mon_ck.fmt_length;
m.ch_id = intf.mon_ck.fmt_chid;//把length、chid、fmt_length存放到fmt_trans对象里。
m.data = new[m.length];//给动态数组开辟空间
foreach(m.data[i]) begin
@(posedge intf.clk);
m.data[i] = intf.mon_ck.fmt_data;
end//重复length次,每次intf.clk上升沿,把fmt_data存入m.data。
mon_mb.put(m);
s = $sformatf("=======================================\n");
s = {s, $sformatf("%0t %s monitored a packet: \n", $time, this.name)};
s = {s, $sformatf("length = %0d: \n", m.length)};
s = {s, $sformatf("chid = %0d: \n", m.ch_id)};
foreach(m.data[i]) s = {s, $sformatf("data[%0d] = %8x \n", i, m.data[i])};
s = {s, $sformatf("=======================================\n")};
$display(s);//存好后把m放入mailbox,打印出来。
end
endtask
fmt_monitor类包含一个fmt_intf类型接口intf。会声明并创建fmt_trans类型的mailbox(mon_mb)与checker通信。run()会调用mon_trans():不需等待req和grant,假定协议没问题,等待fmt_start上升沿,例化一个fmt_trans对象(一个完整的数据包),把fmt_length和fmt_chid存放进该对象里,根据length对动态数组开辟空间,然后把每个fmt_data放进对象。存好后把整个对象放入mon_mb。
class fmt_agent:
一个盒子,各种agent都差不多。
五、mcdf_pkg.sv
对上图的解释:
register 把监测到的data放⼊ checker的mailbox(reg_mb),3个channel 放⼊ chl 的mailbox(chnl_mb0、chnl_mb1、chnl_mb2),fmt把data放⼊fmt的mailbox(fmt_mb)。
所有数据开始都放在自己已例化的mailbox中。
lab3中checker直接数据比较,因为input和output端的数据格式一致。
lab4中(mcdf)3个chnl监测到的数据都是单个的,每次监测到的都是位宽32bits的数,但fmt监测到的是一个数据包(包括ch_id、length),说明fmt的mailbox和3个channel的mailbox内数据格式不一样,为比较带来困难。所以比较之前,先把chnl和register进来的数据(对于mcdf是input)整型转化,做一个参考模型。
mcdf_refmod模拟真实硬件:
1.从reg_mb得到寄存器的读写,用task do_reg_updata更新内部寄存器模型一些信号的值,模拟mcdf中reg的行为。
2.从chnl_mb 0、1、2接收到channel的input,对数据打包(do_package方法)。3个channel的数据打包后分别放入refmod内的3个缓存out_mbs[ 0.2.]。模拟slave→FIFO→arbiter→fmt的行为。
do_compare从out_mbs[ 0.1.2.]拿到数据包,与fmt_mb拿到的数据包比较。与lab3相似,先看fmt_mb数据包id,再从out_mbs拿对应的数据包比较。
mcdf_pkg.sv代码:
import chnl_pkg::*;
import reg_pkg::*;
import arb_pkg::*;
import fmt_pkg::*;
import rpt_pkg::*;
typedef struct packed {
bit[2:0] len;
bit[1:0] prio;
bit en;
bit[7:0] avail;
} mcdf_reg_t;
先import各个pkg。
为了模拟硬件的寄存器,定义一个结构体mcdf_reg_t,存放len、prio、en(读写)avail(只读),寄存器的32位数据包含了上述变量的信息,mcdf_reg_t则是把这些信息拿出来分开存放。
class mcdf_refmod:
这种结构例化3个,分别模拟三个channel的reg。
task run();
fork
do_reset();
this.do_reg_update();
do_packet(0);
do_packet(1);
do_packet(2);
join
endtask
do_packet:模拟三个channel对数据打包;do_reset:模拟寄存器的复位,缓存的复位。
do_reg_update:reg_driver如果给reg写,monitor也会捕捉到,把消息给mcdf_checker—mcdf_refmod,通过reg_mb捕捉到reg_trans对象(addr、cmd、data、rsp)。
写操作:
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];
不得不说这里的代码很巧妙t.addr[3:2]恰好就是0、1、2,通过这个索引对三个regs的成员变量赋值。
读操作:
this.regs[t.addr[3:2]].avail = t.data[7:0];
如果reg_driver是mcdf的状态寄存器作读操作,就把fifo available的值更新到对应channel的regs.avail。
function get_field_value:
通过id(channel的编号)、mcdf_field_t(枚举类型),拿到对应channel和reg的field。模拟硬件环境中reg把配置给channel、arb、fmt。
do_packet:do_packet (0、1、2)分别表示对来自channel0、1、2的数据进行打包。
task do_packet(int id);
fmt_trans ot;//数据包格式,data是动态数组。
mon_data_t it;//单一数据格式,data是32位宽的数。
bit[2:0] len;
forever begin
this.in_mbs[id].peek(it);
ot = new();
len = this.get_field_value(id, RW_LEN);
ot.length = len > 3 ? 32 : 4 << len;
ot.data = new[ot.length];
ot.ch_id = id;
foreach(ot.data[m]) begin
this.in_mbs[id].get(it);
ot.data[m] = it.data;
end
this.out_mbs[id].put(ot);
end
endtask
ot里的动态数组data的内容长度取决于当前的length(通过get_field_value得到)。
先确定动态数组的数据个数,再通过in_mbs从chnl_mon拿数据再填进数据包ot的data,填满打包好后整个放入out_mbs。
这里假定arbiter不丢数且优先级功能正常,把三个channel的数据都打包到对应的mailbox里,当arbiter的仲裁功能还未检查。
class mcdf_checker:
do_compare从out_mbs[ 0.1.2.]拿到数据包,与fmt_mb拿到的数据包比较。与lab3相似,先看fmt_mb数据包id,再从out_mbs拿对应的数据包比较。
class mcdf_env:
例化agent和checker。
所有mailbox例化都在checker中,将checker内的mailbox句柄赋给各个agent,monitor内mailbox句柄,先例化再连接。
这里do_config没有存在必要,不用它去配置generator了,转到based_test配置。
env的report嵌套checker的report去报告。
最后一层:class mcdf_base_test:
例化generator和env,连接句柄。
run():
让env.run在后台run(fork-joinnone),不阻碍其他步骤。
this.do_reg();
this.do_formatter();
this.do_data();
相当于配置硬件,想让其工作要先配置寄存器功能模式,利用reg_generator通过reg_driver进去配置寄存器,接下来do_ formatter,让fmt的下行可以发送出去再do_data。