一直想写一个axi-vip的理解,但是介于个人水平有限,一直没能做出很好的总结,这个系列的内容将对这方面内容做出一个阐述。另外,个人水平有限,仅参考,有什么问题希望大家能够批评指正,共同进步!
这个vip同样是参考github上的一个项目,这个vip支持如下的特性:
(1)支持axi4、axi4-lite的协议;
(2)地址位宽、数据位宽、ID位宽可以配置;
(3)支持延迟写入数据和响应;
(4)支持间隙写入数据和读取响应;
(5)响应的顺序支持乱序和保序;
(6)支持读操作的interleave
首先分析tvip_axi_item,需要注意的是在这个item中存在一些新的数据类型time。对于SV来说,time数据类型如下,四值逻辑,用来反映当前时间。
除此以外还需要注意数据类型real,用于表示浮点数。
uvm_event address_begin_event;
time address_begin_time;
uvm_event address_end_event;
time address_end_time;
uvm_event write_data_begin_event;
time write_data_begin_time;
uvm_event write_data_end_event;
time write_data_end_time;
uvm_event response_begin_event;
time response_begin_time;
uvm_event response_end_event;
time response_end_time;
和configuration相关的约束:
constraint c_valid_id {
(id >> this.configuration.id_width) == 0;
}
constraint c_valid_address {
(address >> this.configuration.address_width) == 0;
}
constraint c_valid_memory_type {
if (this.configuration.protocol == TVIP_AXI4LITE) {
memory_type == TVIP_AXI_DEVICE_NON_BUFFERABLE;
}
}
constraint c_valid_qos {
qos inside {[
this.configuration.qos_range[0]:
this.configuration.qos_range[1]
]};
}
上面的约束中需要注意区分<<的作用,<<有两部分作用,一种是在verilog中作为左移右移使用,另一种是在sv中作为流操作符使用。对于前者来说,使用的格式是b=a<<2这种形式,也就是将a左移两位后赋给b;而对于流操作来说使用的格式是h={>>{j}}的方式,也就是将j按照从左到右的打包方式打包。
目前还没有理解 (id >> this.configuration.id_width) == 0的意思,个人认为是创建这么多位的0,也就是>>是当作左移右移符号使用的。
除此以外需要注意和configuration相关的几个约束,第一个busrt传输的长度;第二个burst传输的size;第三个4k边界地址;
constraint c_valid_burst_length {
if (this.configuration.protocol == TVIP_AXI4) {
burst_length inside {[1:this.configuration.max_burst_length]};
}
else {
burst_length == 1;
}
}
constraint c_valid_burst_size {
if (this.configuration.protocol == TVIP_AXI4) {
burst_size inside {1, 2, 4, 8, 16, 32, 64, 128};
(8 * burst_size) <= this.configuration.data_width;
}
else {
(8 * burst_size) == this.configuration.data_width;
}
}
constraint c_4kb_boundary {
(
(address & `tvip_axi_4kb_boundary_mask(burst_size)) +
(burst_length * burst_size)
) <= 4096;
}
关于中间变量的约束:
constraint c_valid_write_data {
solve access_type before data;
solve burst_length before data;
(access_type == TVIP_AXI_WRITE_ACCESS) -> data.size() == burst_length;
(access_type == TVIP_AXI_READ_ACCESS ) -> data.size() == 0;
foreach (data[i]) {
(data[i] >> this.configuration.data_width) == 0;
}
}
这里的strobe是和access_type和burst_length密切相关的。对于strobe信号需要注意的是:字节选通的信号是仅仅在写操作时才会起作用,在读操作的时候时不存在的,因此读操作的时候对strobe的size直接约束为0。随后按照字节选通的方式进行创建位数。
constraint c_valid_strobe {
solve access_type before strobe;
solve burst_length before strobe;
(access_type == TVIP_AXI_WRITE_ACCESS) -> strobe.size() == burst_length;
(access_type == TVIP_AXI_READ_ACCESS ) -> strobe.size() == 0;
foreach (strobe[i]) {
(strobe[i] >> this.configuration.strobe_width) == 0;
}
}
这个约束目前没搞清楚作用。
constraint c_address_start_delay {
`tvip_delay_constraint(start_delay, this.configuration.request_start_delay)
}
对于写操作来说,实际上会出现写数据的delay,这里对delay进行约束,这里是指写数据中间可能存在延迟。
constraint c_write_data_delay {
solve access_type, burst_length before write_data_delay;
if (access_type == TVIP_AXI_WRITE_ACCESS) {
write_data_delay.size() == burst_length;
}
else {
write_data_delay.size() == 0;
}
foreach (write_data_delay[i]) {
`tvip_delay_constraint(write_data_delay[i], this.configuration.write_data_delay)
}
}
对于写操作和读操作存在ready的delay,这里对ready的delay进行约束。注意这里对于写操作和读操作的区分,对于写操作来说,其B通道中会存在slave给master回应的过程,这个response收到master给slave的ready信号的影响,这里对于B通道的ready信号制作了一个size。原因在于一个response是表示这个burst传输的响应,因此对于写操作的一个burst操作来说,其size也只为1。而对于读操作来说,其size是burst_length,原因在于这里的ready是针对读操作返回给master的每一个数据的ready。
constraint c_response_ready_delay {
solve access_type, burst_length before response_ready_delay;
if (access_type == TVIP_AXI_WRITE_ACCESS) {
response_ready_delay.size() == 1;
}
else {
response_ready_delay.size() == burst_length;
}
foreach (response_ready_delay[i]) {
if (access_type == TVIP_AXI_WRITE_ACCESS) {
`tvip_delay_constraint(response_ready_delay[i], this.configuration.bready_delay)
}
else {
`tvip_delay_constraint(response_ready_delay[i], this.configuration.rready_delay)
}
}
}
此外需要注意几个函数:
这个函数用来拿到item中的burst的长度。
function int get_burst_length();
if ((configuration != null) && (configuration.protocol == TVIP_AXI4LITE)) begin
return 1;
end
else begin
return burst_length;
end
endfunction
接下来分析tvip_axi_master_sub_driver:
driver的整体框架如下,也就是主要有三个任务,分别是address_thread、write_data_thread、response_thread,接下来具体分析这三个任务。
protected task main();
fork
address_thread();
write_data_thread();
response_thread();
join
endtask
对于address_thread来说,整体框架如下:
protected task address_thread();
tvip_axi_item item;
forever begin
get_item_from_queue(address_queue, item);
consume_delay(item.start_delay);
begin_address(item);
drive_address(1, item);
wait_for_address_ready();
drive_address(0, null);
end_address(item);
end
endtask
get_item_from_queue函数原型如下:
注意在这个函数中下面的if条件语句没有看懂。
protected task get_item_from_queue(
input tvip_axi_request_item_queue queue,
ref tvip_axi_item item
);
queue.get(item);
if (!vif.at_master_cb_edge.triggered) begin
@(vif.at_master_cb_edge);
end
endtask
consume_delay函数原型如下:
protected task consume_delay(int delay);
repeat (delay) begin
@(vif.master_cb);
end
endtask
drive_address函数原型如下:
针对写操作:
protected task drive_address(
bit valid,
tvip_axi_item item
);
vif.master_cb.awvalid <= valid;
if (valid) begin
vif.master_cb.awaddr <= item.address;
vif.master_cb.awid <= item.id;
vif.master_cb.awlen <= item.get_packed_burst_length();
vif.master_cb.awsize <= item.get_packed_burst_size();
vif.master_cb.awburst <= item.burst_type;
vif.master_cb.awcache <= item.get_cache();
vif.master_cb.awprot <= item.protection;
vif.master_cb.awqos <= item.qos;
end
endtask
针对读操作:
protected task drive_address(
bit valid,
tvip_axi_item item
);
vif.master_cb.arvalid <= valid;
if (valid) begin
vif.master_cb.araddr <= item.address;
vif.master_cb.arid <= item.id;
vif.master_cb.arlen <= item.get_packed_burst_length();
vif.master_cb.arsize <= item.get_packed_burst_size();
vif.master_cb.arburst <= item.burst_type;
vif.master_cb.arcache <= item.get_cache();
vif.master_cb.arprot <= item.protection;
vif.master_cb.arqos <= item.qos;
end
endtask
wait_for_write_data_ready函数原型:
protected task wait_for_write_data_ready();
do begin
@(vif.master_cb);
end while (!vif.master_cb.wready);
endtask
wait_for_address_ready任务:
protected task wait_for_address_ready();
do begin
@(vif.master_cb);
end while (!get_address_ready());
endtask
这个任务也就是只要get_address_ready函数返回的内容不ready,那么就会一直打拍,get_address_ready任务内容如下:
protected function bit get_address_ready();
return vif.master_cb.awready;
endfunction
protected function bit get_address_ready();
return vif.master_cb.arready;
endfunction
总的来说,整个address_thread的层次结构如下,首先通过get_item_from_queue拿到item,然后根据拿到的item中的相关设定判断是否需要延迟,随后驱动地址信息,最后等aw通道的ready信号拉高;
对于write_data_thread()任务来说,整体的框架如下:
protected task write_data_thread();
tvip_axi_item item;
if (is_read_component()) begin
return;
end
forever begin
get_item_from_queue(write_data_queue, item);
for (int i = 0;i < item.get_burst_length();++i) begin
consume_delay(item.write_data_delay[i]);
if (i == 0) begin
begin_write_data(item);
end
drive_write_data(1, item, i);
wait_for_write_data_ready();
drive_write_data(0, null, 0);
end
end_write_data(item);
end
endtask
get_item_from_queue函数和上面的函数是一样的,对于写操作来说,一个burst传输过程中每一个数据都是有可能存在延迟的,因此这里实际需要对burst传输过程中每一个数据进行delay操作,因此上面会存在for循环加consume_delay的操作。
drive_write_data函数原型如下,给数据和strobe,这里的strobe是每个数据都有相应的strobe,数据的最后一拍给wlast。
protected virtual task drive_write_data(
bit valid,
tvip_axi_item item,
int index
);
vif.master_cb.wvalid <= valid;
if (valid) begin
vif.master_cb.wdata <= item.data[index];
vif.master_cb.wstrb <= item.strobe[index];
if (configuration.protocol == TVIP_AXI4) begin
vif.master_cb.wlast <= index == (item.get_burst_length() - 1);
end
end
endtask
wait_for_write_data_ready函数原型如下:
protected task wait_for_write_data_ready();
do begin
@(vif.master_cb);
end while (!vif.master_cb.wready);
endtask
最后一部分是response_thread,在了解response_thread之前需要了解一个别的类tvip_axi_payload_store,这个类主要实现的功能如下:
*************************************tvip_axi_payload_store*********************************************
内部定义了item,data,strobe,response的队列,除此以外还存在一些任务,具体任务如下:
store_write_data,如果是写操作的化,会把这个函数输入的写数据和strobe推到内部定义的data和strobe里;
store_response,将这个函数输入的response存到自己的队列中,如果是读操作,还会将这个函数输入的数据推到相应的队列中;
接下来分析pack的几个任务,在分析之前需要需要分析item中的几个函数:主要有put_data、put_response、put_strobe三个函数,这三个函数的结构均如下(也就是将输入的response给到外面的response):
function void put_response(const ref tvip_axi_response response[$]);
this.response = new[response.size()];
foreach (response[i]) begin
this.response[i] = response[i];
end
endfunction
接下来看:pack_write_data和pack_response。对于pack_write_data任务来说,是调用item中的put_data和put_strobe任务,这个item就是tvip_axi_payload_store中定义的item。对于pack_response是调用item中的put_response,如果是读操作的化,还会调用item中的put_data任务。
最后看两个任务,分别是get_stored_write_data_count和get_stored_response_count任务,这两个任务内部定义的data和response的size。
*************************************tvip_axi_payload_store*********************************************
在分析response_thread的任务之前,先分析一个sample_response任务,这个任务
protected task sample_response(
input tvip_axi_id id,
ref bit busy
);
tvip_axi_payload_store store;
store = response_stores[id][0];
store.store_response(get_response_status(), get_response_data());
if (get_response_last()) begin
store.pack_response();
end_response(store.item);
void'(response_stores[id].pop_front());
busy = 0;
end
endtask
也就是说response_thread主要包含的内容是把外部的response_stores给到内部的store,内部的store调用store_response将相关的数据和response存储到store内部中,如果最后一个数据,需要调用store的pack_response,也就是最后一笔数据的时候,将store中华所有的response整合一下到item中去。
这个函数任务的任务如下:
protected function tvip_axi_response get_response_status();
return vif.master_cb.bresp;
endfunction
protected function tvip_axi_response get_response_status();
return vif.master_cb.rresp;
endfunction
protected function tvip_axi_data get_response_data();
return '0;
endfunction
protected function tvip_axi_data get_response_data();
return vif.master_cb.rdata;
endfunction
protected function logic get_response_last();
return '1;
endfunction
protected function logic get_response_last();
return (configuration.protocol == TVIP_AXI4LITE) || vif.master_cb.rlast;
endfunction
最后看下整个response_thread任务所执行的内容:
protected task response_thread();
bit busy;
tvip_axi_id id;
tvip_axi_id current_id;
int delay;
forever begin
wait_for_response_valid();//等待B通道的valid信号
id = get_response_id();//拿到ID信号
if ((!busy) || (id != current_id)) begin//一开始循环过程中busy为0,因此执行下面语句,下次循环的时候不会执行这个if语句
if (is_valid_response(id)) begin//response_stores是否存在对应id的response
busy = 1;//把busy变为1,id变为相同的id
current_id = id;
if (!response_stores[current_id][0].item.response_began()) begin
begin_response(response_stores[current_id][0].item);
end
end
else begin
busy = 0;
`uvm_warning("UNEXPECTED_RESPONSE", $sformatf("unexpected response: id %h", id))
continue;
end
end
delay = get_response_ready_delay(current_id);//获得delay;
if (default_response_ready) begin//一开始的一拍是ready的;
sample_response(current_id, busy);//调用sample_response;
if (delay > 0) begin//delay大于0,拉低ready,延迟几拍;
drive_response_ready(0);
consume_delay(delay);
drive_response_ready(1);
end
end
else begin
consume_delay(delay);//一开始的一拍是不ready的,直接延迟一拍,再继续考虑后续延迟情况
drive_response_ready(1);
consume_delay(1);
sample_response(current_id, busy);
drive_response_ready(0);
end
end
endtask
对于整个任务的框架就是等B通道valid,拿到id,第一次循环的时候执行上面的循环语句,变化busy状态和id号;获取delay;根据第一拍是否有delay进行延迟,最后调用sample_response任务,这个任务结束的时候会将busy置为0,为下一次循环做准备。