一、旗语
使用旗语可以实现对同一资源的访问控制,类似于操作系统里面的互斥访问。在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可以中止线程。