SV 实战三
第一,验证环境按照隔离的观念,应该分为硬件DUT,软件验证环境,和处于信号媒介的接口interface;
第二,对于软件验证环境,它需要经历立阶段(build)、连接阶段(connect)、产生激励阶段(generate)和发送激励阶段(transfer),只有当所有的激励发送完毕并且比较完全之后,才可以结束该测试 。
接下来就正式从MCDT进入MCDF,也是最后一篇SV实战了,MCDF主要添加了寄存器控制和状态显示功能,同时也添加了一个重要的数据整形功能,因此,你会发现本次的验证文件多了。另外一方面,代码之所以增多是为了让整个验证环境的各个组件之间相互独立,功能清晰,可以发现不同的package之间的功能是独立的,同一个package中的各个验证组件的功能也是独立的。
激励发生器
(对照前面的实战 及波形图理解)
channel initiator
从激励发生器与DUT的连接关系来看,可以将其进一步分为两种: initiator(发起器)和responder(响应器)。以MCDF举例,与下行通道从端(channel slave)的连接或寄存器接口的连接均属于initiator。而mcdf的formatter接口连接,属于responder。
-
由于channel从端接口协议上有握手信号,我们需要遵照接口时序,确保chx_ready为低时,保证chx data和chx valid保持不变。
-
相邻数据之间没有数据包的限制,所以相邻数据之间的关系较弱,但也应该考虑数据之间是否有空闲周期,以及整体数据的传输速率设定。
-
由于每一个数据从端都有对应的FIFO缓存数据,所以也需要考虑如何使得FIFO的状态可遍历。例如,典型的FIFO状态可以分为empty、full以及中间状态即有数据存储但未写满。要使得FIFO可以触发到这些状态,我们就应该控制channel initiator的传输速率。
Register initiator
- 寄存器接口上cmd的默认状态应该为idle,但cmd addr、cmd data in并未指出应该为何值,所以可以考虑给出随机数值测试DUT的接口协议稳定性。
- 在寄存器读写传输上,可以考虑连续的写、读、或者读写交叉的方式测试寄存器模块的读写功能。
对于规定的读写控制寄存器的所有比特位都应该测试到位,同时,也需要测试只读比特位是否为不可写入的状态。 - 对于规定的只读状态寄存器需要测试是否为不可写入状态,同时,需要检测读出的数值是否为真实的硬件状态。
Formatter responder
- 作为三种接口协议中相对复杂的一个,首先要侧重DUT的formatter接口协议是否充分遍历。
同时需要详细理解协议的要求,除了按照协议给出fmt_grant的响应以外,也需要内置协议检查部分,确保协议的正确性。 - fmt grant的置高,则代表formatter的从端,即formatter responder作为行为模型,它有足够的存储空间容纳formatter要传输的长度为fmt length的数据包。为此,为了模拟真实场景,我们可以考虑让fmt_grant采取立即拉高或者缓时拉高的行为,测试DUT内formatter的行为。
监测器(Monitor)
Monitor(监测器)的主要功能是用来观察DUT的边界或者内部信号,并且经过打包整理传送给其它验证平台的组件,例如checker(比较器)。从更多的功能来划分monitor的功能,它们包括有:
1·观察DUT边界信号。对于系统信号如时钟,可以监测其频率变化;对于总线信号,可以监测输入总线的传输类型,以及检查输出总线是否符合协议。
2·观察DUT内部信号。从灰盒验证的方法来看,往往需要探视DUT内部信号,用来指导stimulator的激励发送,或者完成覆盖率收集,又或者完成内部功能的检查。
仔细看验证结构框图,Monitor是嵌套在agent里面,与激励发送器一起组成agent。
比较器(checker)
无论是从实现难度,还是从维护人力上来讲,checker(比较器)都应当是最需要时间投入的验证组件了。之所以这样评估,是因为checker肩负了几乎所有模拟设计行为和功能检查的任务。更细致来看,一个checker的功能包括:
- 缓存从各个monitor收集到的数据。
- 将DUT输入接口侧的数据汇集给内置的reference model(参考模型)。Reference model在这里扮演了模拟硬件功能的角色,也是需要较多精力维护的部分,因为验证者需要在熟读硬件功能描述的情况下实现模型,而不应该参考真实硬件。
- 通过数据比较的方法,检查实际收集到的DUT输出端接口数据是否同reference model产生的期望数据一致。
- 对于设计内部的关键功能模块,也有相对应的checker进行独立的功能检查。
- 在检查过程中,可以将检查成功的信息统一纳入到检查报告文件当中便于仿真后的追溯。
如果检查失败,也可以采取暂停仿真以及报告错误信息的方式便于验证者调试。
从checker这些细分的功能来看,我们需要记住几个关键词:数据缓存、参考模型和检查报告。而我们在后期的代码实例中也会围绕着这几个部分进行梳理。
测试功能点提取
目前MCDF仅仅是处于模块级验证。其中前五个功能点都是基于配置寄存器来设置的,第六个功能点是根据formatter接口时序特点来提取的。
针对MCDF列举以下验证内容
测试功能点 | 测试内容 | 测试通过标准 | 测试类名 |
---|---|---|---|
寄存器读写测试 | 所有控制寄存器的读写测试 所有状态寄存器的读写测试 | 读写值是否正确 | mcdf_reg_write_read_test |
寄存器稳定性测试 | 非法地址读写,对控制寄存器的保留域进行读写,对状态寄存器进行写操作 | 通过写入和读出,确定寄存器的值是预期值,而不是紊乱值,同时非法寄存器操作也不能影响MCDF的整体功能 | mcdf_reg_illegal_access_test |
数据通道开关测试 | 对每一个数据通道对应的控制寄存器域en配置为0,在关闭状态下测试数据写入是否通过 | 在数据通道关闭情况下,数据无法写入,同时ready信号应该保持为低,表示不接收数据,但又能使得数据不被丢失,因此数据只会停留在数据通道端口 | mcdf_channel_disable_test |
优先级测试 | 将不同数据通道配置为相同或者不同的优先级,在数据通道使能的情况下进行测试 | 如果优先级相同,那么arbiter应该采取轮询机制从各个通道接收数据,如果优先级不同,那么arbiter应该先接收高优先级通道的数据,同时,最终所有的数据都应该从MCDF发送出来 | mcdf_arbiter_priority_test |
发包长度测试 | 将不同数据通道随机配置为各自的长度,在数据通道使能的情况下进行测试 | 从formatter发送出来的数据包长度应该同对应通道寄存器的配置值保持一—对应,同时数据也应该保持完整 | mcdf_formatter_length test |
下行从端低带宽测试 | 将MCDF下行数据接收端设置为小存储量,低带宽的类型,由此使得在由formatter发送出数据之后,下行从端有更多的机会延迟grant信号的置位,用来模拟真实场景 | 在req拉高之后,grant应该在至少两个时钟周期以后拉高,以此来模拟下行从端数据余量不足的情况。当这种激励时序发生10次之后,可以停止测试。 | mcdf formatter_grant_test |
代码实现
回顾SV2再来看
chnl_pkg
同之前没有变化
重点:数据通过信箱进行传输(握手过程)的理解
generator和driver之间req和rsp有一个握手的过程。首先generator先例化一个trans(@1),将其指针放入req_mb中,driver马上取走并clone(@2)出rsp(req_mb为空),放入rsp_mb中,该过程req(@1)会被initiator保持。随后generator例化下一个trans(@3),置于req_mb中。因此,同一时刻最多有3个chnl_trans对象共存。
tb
tb接口变化,多加了reg_intf(寄存器接口),arb_intf(仲裁器接口),fmt_intf(整形器接口)
interface chnl_intf(input clk, input rstn);
...//同之前
endinterface
interface reg_intf(input clk, input rstn);
logic [1:0] cmd;
logic [`ADDR_WIDTH-1:0] cmd_addr;
logic [`CMD_DATA_WIDTH-1:0] cmd_data_s2m;
logic [`CMD_DATA_WIDTH-1:0] cmd_data_m2s;
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
output cmd, cmd_addr, cmd_data_m2s;
input cmd_data_s2m;
endclocking
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
input cmd, cmd_addr, cmd_data_m2s, cmd_data_s2m;
endclocking
endinterface
interface arb_intf(input clk, input rstn);
logic [1:0] slv_prios[3];
logic slv_reqs[3];
logic a2s_acks[3];
logic f2a_id_req;
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
input slv_prios, slv_reqs, a2s_acks, f2a_id_req;
endclocking
endinterface
interface fmt_intf(input clk, input rstn);
logic fmt_grant;
logic [1:0] fmt_chid;
logic fmt_req;
logic [5:0] fmt_length;
logic [31:0] fmt_data;
logic fmt_start;
logic fmt_end;
clocking drv_ck @(posedge clk);
default input #1ns output #1ns;
input fmt_chid, fmt_req, fmt_length, fmt_data, fmt_start;
output fmt_grant;
endclocking
clocking mon_ck @(posedge clk);
default input #1ns output #1ns;
input fmt_grant, fmt_chid, fmt_req, fmt_length, fmt_data, fmt_start;
endclocking
endinterface
interface mcdf_intf(input clk, input rstn);
// 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
reg_pkg
约束的数据和地址要结合
寄存器描述
(参考实战1)
constraint cstr {
soft cmd inside {
`WRITE, `READ, `IDLE};//默认读写
soft addr inside {
`SLV0_RW_ADDR, `SLV1_RW_ADDR, `SLV2_RW_ADDR, `SLV0_R_ADDR, `SLV1_R_ADDR, `SLV2_R_ADDR};//6个寄存器在设计param_def.v中定义。
addr[7:4]==0 && cmd==`WRITE -> soft data[31:6]==0;//读写寄存器
soft addr[7:5]==0;//寄存器最高是10 14 18 ; 第7位到第5就是0。
addr[4]==1 -> soft cmd == `READ;//只读寄存器
};
寄存器模型数据发送
drive 和 generator过程就是一个握手过程
。类比于chnl_pkg。(注意握手的连接在后面mcdf_env 实现)
task send_trans();
reg_trans req, rsp;
req = new();
//local::ch_id: generator域内的成员变量,local的作用是防止多个域中同名变量导致的指向模糊
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) //如果是READ,就把数据从drive 里面的rsp 拿到放到data传给monitor 最后做check。
this.data = rsp.data;
assert(rsp.rsp)
else $error("[RSPERR] %0t error response received!", $time);
endtask
task do_drive();
reg_trans req, rsp;
@(posedge intf.rstn);
forever begin
this.req_mb.get(req);
this.reg_write(req);
rsp = req.clone();//总线读回来的data,给rep。
rsp.rsp = 1;
this.rsp_mb.put(rsp);
end
endtask
根据控制寄存器接口时序理解得出reg_write。
// 当成员方法reg_write将trans的指针写入总线时,以指针内的cmd作为case的条件参数:
//
// 指令为write时,将指针t指向的成员对象发送至总线上。// 指令为read时,继续将t.addr和t.cmd发送给总线,连续等了两个时钟的下降沿。因为在第一个上升沿发送数据的时候,
// 第一个下降沿还在当前的周期里,所以要等两个时钟下降沿,在该下降沿的时候把总线数据intf.cmd_data_s2m,
// 读取入当前的t.data里面。
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);//读数据需要repeat(2)个下降沿。
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
监测器monitor
task mon_trans();
reg_trans m;
forever begin
@(posedge intf.clk iff (intf.rstn && intf.mon_ck.cmd != `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;
end
mon_mb.put(m);//打包数据交给check
$display("%0t %s monitored addr %2x, cmd %2b, data %8x", $time, this.name, m.addr, m.cmd, m.data);
end
endtask
agent 包含 driver 和 monitor
class reg_agent;
local string name;
reg_driver driver;
reg_monitor monitor;
local virtual reg_intf vif;
function new(string name = "reg_agent");
this.name = name;
this.driver = new({
name, ".driver"});
this.monitor = new({
name, ".monitor"});
endfunction
function void set_interface(virtual reg_intf vif);
this.vif = vif;
driver.set_interface(vif);
monitor.set_interface(vif);
endfunction
task run();
fork
driver.run();
monitor.run();
join
endtask
endclass
fmt_pkg
被动的发送数据,req,响应。模拟一个硬件行为(fifo 和 数据传送带宽)。
compare
数据比较函数,到时候在mcdf_checker
用到。
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