- 一、旗语
-
-
- 1.旗语操作
- 2.带多把钥匙的旗语
-
- 二、信箱
-
-
- 1.测试平台上的信箱
- 2、定容信箱
- 3.在异步线程中使用信箱通信
- 4.使用定容箱和探视箱(peek)实现线程同步
- 5.使用信箱和事件同步线程
- 6.使用两个信箱同步线程
- 7、其他的同步技术
-
- 三、构建带线程并实现线程间通信的测试程序
-
-
- 1.基本事务处理器
- 2、配置类
- 3、环境类
- 4、测试程序
-
- 四、结束语
一、旗语
使用旗语可以控制相同资源的访问,类似于操作系统中的互斥访问。SV将使用多个阻塞线程FIFO排队的方式。
1.旗语操作
旗语有三种操作:
- 使用new该方法可以创建个或多个钥匙创建旗语
- 使用get一个或多个钥匙可以获得
- 使用put一个或多个钥匙可以返回
如果你试图得到一个不想被阻塞的旗语,你可以使用它try_get()函数,返回1表示足够的钥匙,返回0表示钥匙不够。
用旗语控制硬件资源的访问
program automatic test(bus_ifc.TB bus); semaphore sem; //创建旗语 initial begin sem = new(1); //分配钥匙 fork sequencer(); ///产生两个总线事务线程 sequencer(); join end task sequencer; repeat($urandom%10) //随机等待0-9个周期 @bus.cb; sendTrans(); //执行总线事务 endtask task sendTrans; sem.get(1); //获取总线钥匙 @bus.cb; ///将信号驱动到总线 bus.cb.addr <= t.addr; ... sem.put(1); ///处理后返回钥匙 endtask endprogram
2.带多把钥匙的旗语
- 返回的钥匙可以比你取出的钥匙多
- 当需要获得或返回钥匙时,必须小心测试程序
- 只剩下一把钥匙的时候,有一个线程要求两把被堵塞,第二个线程要求一把,SV第二个请求请求get(1)排名第一get(2)在前面,略先进先出的规则
- 若有多个不同大小的请求混在一起,可编写一个类来决定优先权
二、信箱
通过信道交换数据,将发生器和驱动器想象成具有自治能力的事务处理器对象。每个对象从其上游对象获得事务(如果对象本身是发生器,则创建事务 ),进行一些处理,然后将其传递给夏季对象。这里的信道必须允许驱动器和接收器异步操作。从硬件的角度来看,对邮箱最简单的理解就是把它看作是一个有源端和收端的FIFO。信箱是必须调用的对象new可以选择函数进行实例化size如果参数大小为0或未指定,则默认信箱无限大。使用put任务可以把数据放入信箱,get可移除数据。信箱为空时get会堵塞,邮箱满时put会阻塞。peek任务可以在不移除的情况下复制信箱中的数据。信箱中的数据可以使单个值,如整数或任何宽度logic,可以放入句柄,但不能放入对象。 
1、测试平台里的信箱
使用信箱实现对象的交换:Generator类
class Generator;
Transaction tr;
mailbox mbx;
function new(mailbox mbx);
this.mbx = mbx;
endfunction
task run(int count);
repeat(count) begin
tr = new();
assert(tr.randomize());
mbx.put(tr);
end
endtask
endclass
使用信箱实现对象的交换:Driver类
class Driver;
Transaction tr;
mailbox mbx;
function new(mailbox mbx);
this.mbx = mbx;
endfunction
task run(int count);
repeat(count) begin
mbx.get(tr);
@(postedge bus.cb.ack);
bus.cb.kind <= tr.kind;
...
end
endtask
endclass
使用信箱实现对象的交换:程序块
program automatic mailbox_example(bus_if.TB bus, ...);
'include "transaction.sv"
'include "generator.sv"
'include "driver.sv"
mailbox mbx; //连接发生器gen和驱动器drv的信箱
Generator gen;
Driver drv;
int count;
initial begin
count = $urandom_range(50);
mbx = new(); //创建信箱
gen = new(mbx);
drv = new(mbx);
fork
gen.run(count);
drv.run(count);
join
end
endprogram
2、定容信箱
/* 定容信箱在两个线程之间扮演了一个缓冲器的角色 */
'timescale 1ns/1ns
program automatic bounded;
mailbox mbx;
initial begin
mbx = new(1); //容量为1
fork
//生产方线程
for(int i = 1; i < 4; i++) begin
$display("Producer:before put(%0d)", i);
mbx.put(i);
$display("Producer:after put(%0d)", i);
end
//消费方线程
repeat(4) begin
int j;
# 1ns mbx.get(j);
$display("Consumer:after get(%0d)", j);
end
join
end
endprogram
3、在异步线程间使用信箱通信
在没有同步信号的情况下,可能会导致消费方还没有开始取数的时候,生产方就已经把信箱填满了,这是因为线程在没有碰到阻塞语句之前会一直运行,而生产方恰好没有碰到阻塞语句的话,可能会一口气儿直接把信箱填满了,换句话说,生产方“跑”到了消费方前面,供过于求,我们想要的是生产者和消费者之间最好有一个同步信号,生产者生产了之后,信号会马上通知消费者来“取货”,或者说消费者需要“取货”时,如果信箱里面“没货”,同步信号会立即通知生产者取“生产”,这样可以维持一个动态的平衡。
4、使用定容信箱和探视(peek)来实现线程的同步
消费者使用一个内建的信箱方法peek()来探视信箱里的数据而不将其移除,当消费者处理完数据后,便使用get()移除数据,这使得生产者可以生成一个新的数据。如果消费者使用get()代替peek()来启动循环,那么事务被立刻移除信箱,这样生产者可能会在消费者完成事务的处理之前生成新的数据。
program automatic sync_peek;
mailbox mbx;
class Consumer;
task run();
int i;
repeat(3) begin
mbx.peek(i); //探视mbx信箱里的整数
$display("Consumer: after get(%0d)", i);
mbx.get(i); //从信箱里移除
end
endtask
endclass : Consumer
Producer p;
Consumer c;
initial begin
//创建信箱、生产者、消费者
mbx = new(1); //容量为1
p = new();
c = new();
fork
p.run();
c.run();
join
end
endprogram
输出结果: 可以看出生产者和消费者步调是一致的,但是生产者仍然比消费者提前一个事务的时间,这是因为容量为1的信箱只有在你试图对第二个事务进行put操作时才会发生阻塞。
5、使用信箱和事件来实现线程的同步
可以在生产者把数据放入信箱后使用事件来阻塞它,消费者则在处理完数据后再触发事件。
program automatic mbx_evt;
mailbox mbx;
event handshake;
class Producer;
task run();
for(int i = 1; i < 4; i++) begin
$display("Producer: before put(%0d)", i);
mbx.put(i);
@handshake; //边沿敏感,可以确保生产者在发送完数据后便停止
$display("Producer: after put(%0d)", i);
end
endtask
endclass
class Consumer;
task run;
int i;
repeat(3) begin
mbx.get(i);
$display("Consumer: after get(%0d)", i);
-> handshake; //消费者触发事件,生产者可以继续生产
end
endtask
endclass : Consumer
Producer p;
Consumer c;
initial begin
mbx = new();
p = new();
c = new();
//使得生产方和消费方并发运行
fork
p.run();
c.run();
join
end
endprogram
输出结果:
6、使用两个信箱来实现线程的同步
可以再使用一个信箱把消费者的完成信息发回给生产者
program automatic mbx_mbx2;
mailbox mbx, rtn;
class Producer;
task run();
int k;
for(int i = 1; i < 4; i++) begin
$display("Producer: before put(%0d)", i);
mbx.put(i);
rtn.get(k); //生产者从返回的信箱取值,如果可以取得,说明消费者已经完成,如果没有取得值,说明消费者还没有完成事务的处理,生产者则会阻塞
$display("Producer: after get(%0d)", k);
end
endtask
endclass
class Consumer;
task run();
int i;
repeat(3) begin
$display("Consumer: before get");
mbx.get(i);
$display("Consumer: after get(%0d)", i);
rtn.put(-i); //返回到rtn信箱的信息仅仅是原始整数的一个相反值,可以使用任意值,只要能表示有返回值即可
end
endtask
endclass : Consumer
Producer p;
Consumer c;
initial begin
mbx = new();
rtn = new();
p = new();
c = new();
fork
p.run();
c.run();
join
end
endprogram
输出结果:
7、其他的同步技术
通过变量或者旗语来阻塞线程也同样可以实现握手。事件是最简单的结构,其次是通过变量阻塞。旗语相当于第二个信箱,但没有信息交换。SV中的定容信箱有一个缺点就是无法再生产者放入第一个事务的时候让它阻塞,会一直比消费者提前一个事务的时间。
三、构筑带线程并可实现线程间通信的测试程序
1、基本的事务处理器
分层的环境测试平台: 处于发生器和驱动器之间的代理
class Agent;
mailbox gen2agt, agt2drv;
Transaction tr;
function new(mailbox gen2agt, agt2drv);
this.gen2agt = gen2agt;
this.agt2drv = agt2drv;
endfunction
task run();
forever begin;
gen2agt.get(tr); //从上游的模块中获取事务
...
agt2drv.put(tr); //把事务发送给下游模块
end
endtask
endclass
2、配置类
配置类允许你在每次仿真时对系统的配置进行随机化
配置类
class Config;
bit[31:0] run_for_n_trans;
constraint reasonable{
run_for_n_trans inside {
[1:1000]};
}
endclass
3、环境类
环境类包含了发生器、代理、驱动器、监视器、检验器、记分板,以及它们之间的配置对象和信箱。
环境类
class Environment;
Generator gen;
Agent agt;
Driver drv;
Monitor mon;
Checker chk;
Scoreboard scb;
Config cfg;
mailbox gen2agt, agt2drv, mon2chk;
extern function new();
extern function void gen_cfg();
extern function void build();
extern task run();
extern task wrap_up();
endclass
function Environment::new();
cfg = new();
endfunction
function void Environment::gen_cfg();
assert(cfg.randomize);
endfunction
function void ENvironment::build();
//初始化信箱
gen2agt = new();
agt2drv = new();
mon2chk = new();
//初始化事务处理器
gen = new(gen2agt);
agt = new(gen2agt, agt2drv);
drv = new(agt2drv);
mon = new(mon2chk);
chk = new(mon2chk);
scb = new();
endfunction
task Environment::run();
fork
gen.run(cfg.run_for_n_trans);
agt.run();
drv.run();
mon.run();
chk.run();
scb.run(cfg.run_for_n_trans);
join
endtask
task Environment::wrap_up();
fork
gen.wrap_up();
agt.wrap_up();
drv.wrap_up();
mon.wrap_up();
chk.wrap_up();
scb.wrap_up();
join
endtask
4、测试程序
program automatic test;
Environment env;
initial begin
env = new();
env.gen_cfg();
env.build();
env.run();
env.wrap_up();
end
endprogram
四、结束语
你的设计可以用很多并发运行的独立块来建模,所以测试平台也必须能够产生很多激励流并检验并发线程的反应。fork-join、fork-join_none、fork-join_any用于动态创建线程,线程之间可以使用事件、旗语、信箱,以及@事件控制和wait语句来实现通信和同步。disable可以中止线程。