设计组件可以用事务处理器来代替。嵌入式RAM块可以用从动性事务处理起来代替。程序存储器接口的总线功能模型就是从动性事务处理器。DUT发出一个写命令,然后总线功能模型则把数据写到块内RAM的关联数组内;DUT发出一个读命令,则从动性事务处理器把相应地址的数据发送出去。上图中的RAM块用从动性事务处理器来代替。
第一步:接口
第一步要做的就是定义用于主设备和从设备之间交换信息的物理信号。一次单独的信息交换(读或写操作)称为一个事务。在APB总线上可能有多个从设备,但只有一个主设备,可根据分配给从设备的不同的地址来区分不同的从设备。
这些物理信号在一个接口内声明。接口的名字加前缀“app_”来表明它与APB协议有关。整个接口的声明文件包含在`ifndef/`define/`endif内。这个C语言技巧,目的是为了避免当多次包含源文件时带来的重定义错误。
`ifndef APB_IF_SV
`define APB_IF_SV
interface apb_if;
endinterface:apb_fi
`endif
将信号定义为wire类型
`ifndef APB_IF_SV
`define APB_IF_SV
interface apb_if(input bit pclk);
wire [31:0] paddr;
wire psel;
wire penable;
wire pwrite;
wire [31:0] prdata;
wire [31:0] pwdata;
…
endinterface:apb_if
`endif
因为APB是一个同步的协议,要用时钟块来定义信号的方向和采样,这样可以防止设计和验证环境之间的竞争冒险,而且可使验证环境可重用于设计的不同层次(RTL级,门级),而不会违反时序要求。
`ifndef APB_IF_SV
`define APB_IF_SV
interface apb_if(input bit pclk);
wire [31:0] paddr;
wire psel;
wire penable;
wire pwrite;
wire [31:0] prdata;
wire [31:0] pwdata;
clocking sck @(posedge pclk);
input paddr,psel,penable,pwrite,pwdata;
output prdata;
...
endinterface:apb_if
`endif
端口(modport)列表中必须包含定义同步信号的时钟块。时钟信号不用指明,因为它隐式地包含在时钟块内。
`ifdef APB_IF_SV
`define APB_IF_SV
interface apb_if(input bit clk);
wire [31:0] paddr;
wire psel;
wire penable;
wire pwrite;
wire [31:0] prdata;
wire [31:0] pwdata;
clocking sck @(posedge clk);
input paddr,psel,penable,pwrite,pwdata;
output prdata;
endclocking:sck
modport slave(clocking sck);
endinterface: apb_if
`endif
第二步:连接DUT
这一步将接口连接到DUT,通过在顶层模块中例化接口和DUT来实现(必须在一个没有端口的模块内,对设计和所有必需的接口和信号进行实例化RULE 4-13)。利用层次化引用接口实例的wire类型信号来连接DUT。
module tb_top;
...
apb_if apb0(...);
...
master_ip dut_mst(...,
.apb_addr (apb0.paddr[7:0]),
.apb_sel (apb0.psel),
.apb_enable (apb0.penable),
.apb_write (apb0.pwrite),
.apb_rdata (apb0.prdata[15:0]),
.apb_wdata (apb0.pwdata),
);
endmodule:tb_top
必须在顶层模块内包含时钟发生器,使用bit来定义时钟。要确保在零时刻不会出现时钟沿。实现时钟发生器的initial和always块与实现设计的initial和always块,在最开始的调度会有竞争冒险。
module tb_top;
bit clk=0;
apb_if apb0(clk);
always #10 clk=~clk;
....
endmodule:tb_top
第三步:事务描述符
下一步就是定义APB事务描述符。事务描述符由vmm_data基类扩展而来。它需要有一个静态的vmm_log属性实例来发出本事务描述符的信息。这个消息服务接口的实例通过vmm_data的构造函数传递。
`ifndef APB_RW_SV
`define APB_RW_SV
`include "vmm.sv"
class apb_rw extends vmm_data;
static vmm_log log=new("apb_rw","class");
enum {READ,WRITE}kind;
bit [31:0] addr;
bit[31:0] data;
function new();
super.new(this.log);
endfunction:apb_rw
endclass:apb_rw
....
`endif
尽管APB总线有不同的读写数据总线,但"data"用一个属性表示。因为APB不支持读写并行操作事务,所以在一个时间只能有一个数据是有效的。"data"类属性根据kind的值来解释是读还是写数据。
第四步:从动性事务处理器
从动性事务处理器现在可以编写了,呵呵。It is a class derived from the vmm_xactor class.
`ifndef APB_ALAVE_SV
`define APB_SLAVE_SV
...
class apa_slave extends vmm_xactor;
...
endclass:apb_slave
`endif
本事务处理器需要一个物理级接口观察和响应事务。物理级接口是由一个作为构造函数的参数而传递的虚拟端口来实现的。
`ifndef APB_SLAVE_SV
`define APB_SLAVE_SV
`inluce "apb_if.sv"
`include "apb_rw.sv"
...
class apb_slave extends vmm_xactor;
virtual apb_if.slave sigs;
...
function new (string name,
int unsigned stream_id,
virtual apb_if.slave sigs,
);
super.new("APB Slave",name,stream_id);
this.sigs=sigs;
...
endfunction:new
...
endclass:apb_slave
`endif
事务描述符是从观察到的数据来填充类内的属性的,在main()任务中返回响应。对读写事务的观察的编码和verilog差不多。采样输入信号并驱动合适的响应在恰当的时间是很简单的一件事情。两者之间的不同是它是通过虚拟端口的时钟信号而不是模块的引脚来实现的。上升沿是通过本时钟块本身,而不是输入信号的边沿定义的。
virtual protected task main();
super.main();
forever begin
apb_rw tr;
this.sigs.sck.prdata<='z;
...
//wait for a SETUP cycle
do @(this.sigs.sck);
while (this.sigs.sck.psel!==1'b1||
this.sigs.sck.penable!==1'b0
);
tr=new;
tr.kind=(this.sigs.sck.pwrite)?
apb_rw::WRITE:apb_rw::READ;
tr.addr=this.sigs.sck.paddr;
if(tr.kind==apb_rw::READ)begin
...
this.sigs.sck.prdata<=tr.data;
@(this.sigs.sck.prdata);
end
else begin
@(this.sigs.sck);
一旦从动性事务处理器检测到事务的开始,直到APB事务结束,它都不能被停止。否则,如果事务处理器在事务流中间停止,它将违反协议。所以,在事务开始时,必须调用
vmm_xactor:wait_if_stopped()方法。
virtual protected tasd main();
super.main();
forever begin
apb_rw tr;
this.sigs.sck.prdata<='z;
this.wait_if_stopped();
...
//wait for a SETUP cycle
do @(this.sigs.sck);
while (this.sigs.sck.psel!==1'b1||
this.sigs.sck.penable!==1'b0
);
tr=new;
从动性事务处理器是怎样处理数据的呢?最简单就是RAM响应。在写周期的数据被写入指定的地址。在读周期,返回指定地址里的数据。把一个32位的RAM建模为一个固定大小的数组是不现实的,将占用很多内存。不过,可以用关联数据来建模。
class apb_slave extends vmm_xactor;
virtual apb_if.slave sigs;
...
local bit [31:0] ram[*];
function new(string name,
int unsigned stream_id,
virtual apb_if.slave sigs
);
......
if(tr.kind==apb_rw::READ)begin
if(!this.ram.exists(tr.addr))tr.data='x;
else tr.data=this.ram[tr.addr];
...
this.sigs.sck.prdata<=tr.data;
@(this.sigs.sck);
end
第五步:报告事务
从动性事务处理器必须能通知测试平台,一个特定的事务被检测到。被检测的事务通过在vmm_xactor::notity属性发出通知。被检测的事务(由vmm_data基类派生)有一个“RESPONSE”通知。
第六步:第一个测试
第七步:标准方法
第八步:调试信息
用`vmm_trace(),`vmm_debug(),`vmm_verbose()来发出调试信息
this.sigs.sck.prdata<='z;
this.wait_if_stopped();
`vmm_trace(log,"waiting for start of transaction");
//log为vmm_xactor类的属性,所以这里可用哦
....
`vmm_trace(log,{"Responding to transaction.../n",tr.psdisplay("")});
if(tr.kind==apb_rw::READ)begin
....
`vmm_trace(log,{responding to tranaction.../n,tr.psdisplay("")});
this.notify.indicate(RESPONSE,tr);
.....
vcs -sverilog -ntb_opts vmm +vmm_log_default=trace...
第九步:从动性事务处理器配置
以上编写的从动性事务处理器能够反映APB总线上所有事务,但是它不能和APB总线上其他的从动性事务处理器协同工作。所以必须配置从动性事务处理器使其地址限定在一个特定的范围内。必须用随机化的配置描述符来配置从动性事务处理器。这个配置描述符被指定为构造函数的可选参数。
...
class apb_slave_cfg;
rand bit [31:0] start_addr=32'h0000_0000;
rand bit [31:0] end_addr=32'hffff_ffff;
constraint apb_slave_cfg_valid
{end_addr>=start_addr;
}
....
endclass:apb_slave_cfg
...
class apb_slave extends vmm_xactor;
virtual apb_if.slave sigs;
local apb_slave_cfg cfg;
....
function neew(string name,
int unsigned stream_id,
virtual apb_if.slave sigs,
apb_slave_cfg cfg=null,
....
);
super.new("APB Slave",name,stream_id);
this.sigs=sigs;
if(cfg==null)cfg=new;
this.cfg=cfg;
...
this.notify.configure(RESPONSE);
endfunction:new
....
do @(this.sigs.sck);
while(this.sigs.sck.psel!==1'b1||
this.sigs.sck.penable!=1'b0||
this.sigs.sck.paddr<this.cfg.start_addr||
this.sigs.sck.paddr>this.cfg.end_addr);
tr=new;
配置描述符应该声明为local类型,以防止没外部修改。要提供一个reconfigure方法,以动态地配置事务处理器。
class apb_slave extends vmm_xactor;
virtual apb_if.slave sigs;
local apb_slave_cfg cfg;
…
virtual function void reconfigure(apb_slave_cfg cfg);
this.cfg=cfg;
endfunction:reconfigure
这个配置描述符不必要是vmm_data的派生类,因为它不是事务描述符,不用通过vmm_channel传输,和vmm_notify没有联系。但是,应该提供vmm::psdispla来报告仿真时当前事务处理器配置。
class apb_slave_cfg;
rand bit [31:0] start_addr=32'h0000_0000;
rand bit [31:0] end_addr =32'hFFFF_FFFF;
constraint apb_slave_cfg_valid{
end_addr>=start_addr;
}
virtual function string psdisplay(string prefix="");
$sformat(psdisplay,"%sAPB Slave Config:['h%h-'h%h]",
prefix,this.start_addr,this.end_addr);
endfunction:psdisplay
endclass:apb_slave_cfg
第十步:backdoor interface
-
为了描述和控制从动性事务处理器的响应,给RAM编写一个程序接口是非常有用的。这个程序接口允许在一个特定的RAM地址访问RAM和设定这个地址的值。一个程序接口也可提供删除特定地址的数据并回收关联矩阵的内存。
- class apb_slave extends vmm_xactor;
- virtual apb_if.slave sigs;
- local apb_slave_cfg cfg;
- ……
- function new(string name
- int unsigned stream_id
- virtual apb_if.slave sigs
- apb_slave_cfg cfg=null
- );
- super.new("APB Slave",name,stream_id);
- this.sigs=sigs;
- if(cfg=null) cfg=new;
- this.cfg=cfg;
- …
- this.notify.configure(RESPONSE);
- endfunction:reconfigure
- virtual function void poke(bit [31:0] addr,
- bit [31:0 data]);
- if(addr<this.cfg.start_addr||
- addr>this.cfg.end_addr);begin
- `vmm_error(this.log,"Out-of-range poke");
- return;
- end
- this.ram[addr]=data;
- endfunction:poke//把数据保存在指定地址的RAM内
- virtual function bit[31:0] peek(bit [31:0] addr);
- if(addr<this.cfg.start_addr||
- addr>this.cfg.end_addr)begin
- `vmm_error(this.log,"Out-of-range peek");
- return 'x;
- end
- return(this.ram.exists(addr))?this.ram[addr]:'x;
- endfunction:peek//返回指定地址上的数据
-
第十一步:扩展点
-
回调方法允许不用修改事务处理器本身就能扩展从动性事务处理器的功能。回调方法定义为virtual void function ,要在vmm_xactor_callback基类的扩展类中定义它。用`vmm_callback宏调用回调方法。
- typedef class apb_slave;
- class apb_slave_cbs extends vmm_xactor_callbacks;
- virtual function void pre_response(apb_slave xact
- apb_rw cycle);
- endfunction:pre_response
- virtual function void post_response(apb_slave xactor
- apb_rw cycle);
- endfunction:post_response
- endclass:apb_slave_cbs
…
- if(tr.kind==apb_rw::READ)begin
- if(!this.ram.exists(tr.addr))tr.data='x;
- else tr.data=this.ram[tr.addr];
- `vmm_callback(apb_slave_cbs,pre_response(this,tr));
- this.sigs.sck.prdata<=tr.data;
- @(this.sigs.sck);
- end
- else begin
- @(this.sigs.sck);
- tr.data=this.sigs.sck.pwdata;
- `vmm_callback(apb_slave_cbs,pre_response(this.tr));
- this.ram[tr.addr]=tr.data;
- end
- if(this.sigs.sck.penable!==1'b1)begin
- `vmm_error(this.log,"APB protocol violation:SETUP cycle not followed by ENABLE cycle ");
- end
- `vmm_callback(apb_slave_cbs,post_response(this,tr));
- `vmm_trace(log,{"Response to transaction … /n",tr.psdisplay("")});
- this.notify.indicate(RESPONSE,tr);
- end
- endtask:main
-
第十二步:事务响应
-
从动性事务处理器是hard-coded ,像一个RAM。但是,这样不适合提供一个不同的响应,或使用它编写一个从动性事务处理器的事务模型。响应请求通道被指定为构造函数的一个可选参数。
- function new(string name
- int unsigned stream_id
- virtual apb_if.slave sigs sigs
- apb_slave_cfg cfg=null
- apb_rw_channel resp_chan=null);
- super.new("APB Slave",name,stream_id);
- this.sigs=sigs;
- if(cfg==null)cfg=new;
- this.resp_chan=resp_chan;
- …
- this.notify.configure(RESPONSE);
- endfunction:new
- …
- `vmm_trace(log,{"responding to transactor…/n",tr.psdisplay("")});
- if(tr.kind==apb_rw::READ)begin
- if(!this.ram.exists(tr.addr))tr.data='x;
- else tr.data=this.ram[tr.addr];
- end
- else begin
- …
- this.resp_chan.put(tr);
- …
- end
- `vmm_callback(apb_slave_cbs,pre_response(this,tr));
- this.sigs.sck.prdata<=tr.data;
- @(this.sigs.sck);
- end
- else begin
- @(this.sigs.sck);
- tr.data=tr.sigs.sck.pwdata;
- `vmm_callback(apb_slave_cbs,pre_response(this,tr));
- if(this.resp_chan==null)this.ram[tr.addr]=tr.data;
- else this.resp_chan.sneak(tr);
- end
- …