Chapter 17 Interthread Communication
上一章介绍了单线程内的各组件间的传输过程,本章介绍线程间的通信,以生产者-消费者为例:
module producer(output byte shared, input bit put_it, output bit get_it);
initial //线程1
repeat(3) begin
$display("Sent %0d", ++shared); //shared共享变量的改变源头
get_it = ~get_it; //送至consumer的get_it信号,告诉consumer数据已经更新
@(put_it); //来自consumer的put_it信号,表示producer可以生产下一个数据的信号
end
endmodule : producer
module consumer(input byte shared, output bit put_it, input bit get_it);
initial //线程2
forever begin
@(get_it); //来自producer的get_it信号,consumer可以接受数据的信号
$display("Received: %0d", shared);
put_it = ~put_it; //送至producer的put_it信号,告诉producer数据已经明接收
end
endmodule : consumer
module top; //在顶层将两个模块的ports连接起来
byte shared;
producer p (shared, put_it, get_it);
consumer c (shared, put_it, get_it);
endmodule : top
17.1 使用UVM的Object完成线程间的通信
- 上面的例子中,我们用过verilog的ports,将producer和consumer链接起来。systerm verilog中没有模块端口,但提供共享对象句柄的方式,可以传输线程间的信息比如semaphor和mailboxes。这样不同的验证人员写出的代码就不一致,UVM使用通用的方法解决这个问题,解决方案共包括两个部分:
- Ports: 在uvm_component中例化,并在run_phase中与其他线程通信;使用put ports送数据,使用get ports收数据。
- TLM FIFO:Transaction Level Modeling是包含一个put export和一个get export的类,可以传输任何类型的数据。
- TLM FIFO深度为1,只能存储一个元素。这样的容量也许很鸡肋,但是在多线程通信中很有用。
17.1.1 在producer模块内使用put ports
上面的例子中,producer和consumer之间的通信需要三个信号的交互,包括数据shared,put_it 和 get_it,使用uvm_put_port之后,只需要传递一个参数,producer模块代码如下:
class producer extends uvm_component;
`uvm_component_utils(producer);
int shared;
uvm_put_port #(int) put_port_h; //定义uvm_put_port,参数化类,需要指定传输数据类型
function void build_phase(uvm_phase phase);
put_port_h = new("put_port_h", this); //在build phase中例化
endfunction : build_phase
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
task run_phase(uvm_phase phase); //不需要处理put_it和get_it信号,交给put port就好
phase.raise_objection(this);
repeat (3) begin
put_port_h.put(++shared); //在run phase中调用put port的put函数将数据送出
$display("Sent %0d", shared); //调用put后,数据进入FIFO,此时FIFO已满阻塞下次数据put,直到consumer将数据从FIFO中取出
end
phase.drop_objection(this);
endtask : run_phase
endclass : producer
17.1.2 在consumer模块内使用get ports
- 和producer相反,consumer是通过get port将数据从FIFO取出,排空FIFO的过程,consumer模块的代码如下:
class consumer extends uvm_component;
`uvm_component_utils(consumer);
uvm_get_port #(int) get_port_h; //参数化的get port,需要指定传输数据类型,和put类型一致
int shared;
function void build_phase(uvm_phase phase);
get_port_h = new("get_port_h", this); //在build phase中例化get port
endfunction : build_phase
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
task run_phase(uvm_phase phase); //在run phase中从FIFO取出数据
forever begin
get_port_h.get(shared); //调用get port端口的get函数,将数据从FIFO中取出,释放producer处的阻塞
$display("Received: %0d", shared); //forever循环中,当FIFO为空,get函数会阻塞consumer
end
endtask : run_phase
endclass : consumer
- 到目前为止,producer和consumer之间的运行状态为:
producer填满FIFO,挂起等待 -> consumer清空FIFO,挂起等待 -> producer填满FIFO,挂起等待 …
17.1.3 在test模块内使用将port连接起来
目前,producer和consumer分别包含put port和get port,但是两者还没有连接起来,我们需要在test模块,或在env模块将两个port连接起来(使用TLM FIFO):
class communication_test extends uvm_test;
`uvm_component_utils(communication_test)
producer producer_h;
consumer consumer_h;
uvm_tlm_fifo #(int) fifo_h; //除了声明producer和consumer,需要声明TLM FIFO
//TLM FIFO是参数化的类,传递的参数类型必须和put get port一致
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase); //build phase top-down,所以先例化FIFO,在调用TLM FIFO的build phase
producer_h = new("producer_h", this);
consumer_h = new("consumer_h", this);
fifo_h = new("fifo_h",this); //build phase中例化TLM FIFO
endfunction : build_phase
function void connect_phase(uvm_phase phase); //build phase结束以后开始connect phase,bottom-up
producer_h.put_port_h.connect(fifo_h.put_export); //将FIFO的put_export传递给producer中的put port的connect函数(类似于analysis export传给analysis port的connect函数)
consumer_h.get_port_h.connect(fifo_h.get_export); //将FIFO的get_export传递给consumer中的get port的connect函数
endfunction : connect_phase
endclass : communication_test
17.1.4 线程间的非阻塞通信
- 如果两个线程之间通信不管时间延迟,即producer发了一个包之后,无论等待多长时间再发下一个包都可以的话,上面的通信机制永远不会有问题。如果producer在每过两个时钟沿,都需要发包,但是consumer获取数据报的速度不一致,有时候每一个时钟沿就收包,有时候需要等多个周期,这时候producer就会因为consumer而阻塞。
- 为了解决这种场景,UVM提供非阻塞版本的put函数和get函数: try_put & try_get,这两个函数除了不阻塞之外,其他的和put&get完全一致。try函数返回一个单bit的数据,表示put&get是否成功。
- 我们将non-blocking的机制加入测试平台,此时consumer中不再调用get函数,而是调用try_get函数:
task run_phase(uvm_phase phase); //在run phase中从FIFO取出数据
forever begin
if (get_port_h.try_get(shared)) //get port的非阻塞try_get函数
$display("Received: %0d", shared);
end
endtask : run_phase
17.1.5 加入put&get port后的验证环境框图
- 使用正方形代表put/get port,仍然使用圆形作为put/get export,此时验证环境框图如下:
- 其中put port和get port之间为TLM FIFO,可以理解为数据的中转站。(在环境框图上不区分put/get和try_put/try_get函数)。通过uvm_put_ports和uvm_get_ports以及连接两种port的uvm_tlm_fifo,实现线程间的通信。