UVM自学笔记:项目之四(重难点)——具有AHB二级流水特性的高适用性driver的编写(考虑到hready为低情况)

 

目录

1. driver写功能编写,为了验证driver的功能我们同时需要完善sequence、sequence_item、env、testcase以及模拟DUT行为的Slave_module

1.1 interface的编写:

1.2 driver写功能编写:

1.2.1 初始main_phase代码:

1.2.2 满足AHB二级流水要求的main_phase代码:

1.2.3. do_drive任务代码:

1.2.4.send_addr_control()/send_wdata()/judge_trans_finished()任务代码

1.2.5.对照波形进行判断

1.3. 模拟slave行为具有发送hready信号功能的module的代码

1.4. seq_item的编写,代码如下:

1.5. seq的编写,代码如下:

1.6. 在testcase中启动seq,给seq配置控制参数具体值

1.7. 在tb_top中将tb与slave_module连接起来,并且产生时钟和复位信号

1.8. 写makefile跑仿真验证写信号时序正确性

1.8.1我们打开VCS看看波形

2.完善driver的读写功能时序

2.1driver的代码部分:

2.1.1 driver中的main_phase阶段代码如下:

 2.1.2 get_sim_trans的任务如下,也可以将该task写成方法

 2.1.3 do_drive任务代码如下:

 2.1.4 do_drive中的send_addr_control任务代码:

 2.1.5 do_drive中的send_wdata任务代码:

 2.1.6 do_drive中的judge_trans_finished任务代码:

2.2 接下来我们完善模拟DUT行为的Slave的module的编写

2.2.1 Slave中产生hready信号:

2.2.2 Slave中判断一笔trans是否完成:

 2.2.3 Slave接受总线上的数据:

 2.2.4 Slave发送一笔读数据: 

 2.2.5 其他组件配置:

3. 用makefile编译,VCS仿真查看结果:


1. driver写功能编写,为了验证driver的功能我们同时需要完善sequencesequence_itemenvtestcase以及模拟DUT行为的Slave_module

1.1 interface的编写

interface如下,在driver,monitor和slave_module的clocking_block中声明好各自信号的方向。 

1.2 driver写功能编写

1.2.1 初始main_phase代码:

 Driver 通过79行代码get到seq传来的req,随后执行do_drive函数将该笔trans传递到总线上,然后调用82行代码完成与seq的握手,之后通过83行代码,来到下一个时钟上升沿,这样做会有以下几个缺点

(1)首先,#10ns是一个hard code,一旦我们更改了时钟频率,就要到driver中重新修改该值,非常不便利。

(2)其次,seq中每产生一笔数据就会通过79行的握手被driver给get到,然后执行push到总线上的操作。但是ahb是二级流水,产生的req内既有地址和控制信号,又有数据信号,数据信号总是滞后于地址信号一个时钟周期被发送到总线上,直接将所有信号push到总线上显然是不满足二级流水的时序要求,并且当hready为低时可能会丢掉hready为低时候的信号。

(3)最后,假设我们将产生10笔trans,当执行其中一笔trans时,此时hready为低,do_drive函数不能将当前的trans push到总线上,随后调用82行的代码,让seq产生下一笔trans,那么就会丢掉hready为低时候的trans,并且让总的trans数目不满足10笔。

解决问题的方法如下

(1)问题一:我们将#10ns修改为@vif.drvCLK

(1)问题二:我们应该设法在driver中加入队列,队列里面按照顺序存放着由seq产生的每一笔trans,然后driver从该队列中按照先入先出的顺序取出数据,并将数据发送到总线上,这样既能保证二级流水,又不会丢失每一笔trans。

(2)问题三:我们让driver从seqr中get到trans的操作与driver将该笔数据按照时序发送到总线上的操作分开。在main_phase中开启两个并发线程,其中一个线程负责不断地通过seq与drv的握手从seq中获取trans并放入drv的队列中等待处理,而另一个线程负责将drv队列中的trans转换成pin级的信号按照AHB二级流水的时序要求发送到总线上。

1.2.2 满足AHB二级流水要求的main_phase代码:

 (1)首先,我们在main_phase中启动了两个forever的并发线程,其中一个负责通过95行方法:get_sim_trans,从seq中get到其产生的数据。并且将该数据放入到队列中等待处理。队列包含存放地址的队列sim_addr_q[$],以及存放控制信号或者写数据信号的其他队列。我们通过第92行的判断条件,保证了当队列中的待处理trans数目少于2个时候,就让seq产生数据,否则就无需让seq产生数据,这样可以避免在hready为低的时候,队列中未处理的数据数目越来越多,而占用大量内存空间。又可以保证在所有trans结束之前,sim_addr_q中都有至少一笔trans交给drive的do_drive函数去处理而不会出现丢数据或者数据断层的情况。

(2)其次,我们通过第二个线程第105行的任务do_drive,将trans中的每一笔信号按照时序要求发送到总线上

1.2.3. do_drive任务代码:

首先我们来对照ahb的时序波形:

 我们需要将该波形抽象成具体的代码,为了满足二级流水特性,地址和控制信号的相位总是提前于数据相位,并且driver需要知道什么时候可以发送下一笔trans。为了简化设计,我们先只考虑写数据的情况,代码如下:

 采用三个并发线程:其中send_addr_control()负责发送地址和控制信号,send_wdata()负责发送写数据信号,而judge_trans_finished()负责判断当前的trans是否有完成。需要进行如下控制:

1)其中数据相位总是滞后于地址相位,所以我在send_addr_control中增加一个信号,当该线程执行完毕后,把信号拉高,而send_wdata则等待该信号为高时才执行,而拉高的信号只有到下个时钟上升沿才会被感知到,因此完成了时序上的数据相位滞后于地址相位一个时钟周期,在send_wdata执行完以后,再将该信号拉低。

2)三个线程的执行顺序通过握手信号实现。让judge_ready线程最先执行,然后让send_wdata()线程执行,最后让send_addr_control执行

3)这样写还有一个优点:就是三个线程前可以通过#延时来控制具体每个信号前的延时时间。

1.2.4.send_addr_control()/send_wdata()/judge_trans_finished()任务代码

1.地址和控制信号的发送任务:send_addr_control

这里我们采用了一个队列addr_q用于存放从sim_addr_q中get到的地址信号,并且让该队列中最大只能有两笔未完成的trans,当判断完成一笔trans时,则将该trans从addr_q中pop出来,对于地址信号,只要addr_q队列中未完成的信号少于两个,我们就可以一直发送信号到总线上,否则不能发送新trans而应该对未完成的旧trans再发送一次。当发送完地址信号后,将haddr_send_ready信号拉高,这样数据信号才被授权发送。

2.写数据信号的发送任务:send_wdata()

得到了addr发送完毕的授权:haddr_send_ready>0以后,我们将数据信号从sim_wdata_q队列中get到,然后push到总线上,并push进入wdata_q队列中。该队列仅允许有一笔未完成的trans,当该trans完成时,则将该trans从wdata_q中pop出来,当该trans未完成时,我们应该将未完成的trans继续push到总线上。完成一笔读数据信号发送后,将haddr_send_ready拉低。

3.判断一笔trans是否完成的任务(只考虑写操作):judge_trans_finished()

 该任务负责判断总线上该时钟沿是否有一笔trans完成,当hready为高且wdata_q中有一笔写数据时表示有一笔trans将会在这个时钟沿完成,将完成的trans从对应的队列中pop出去。

1.2.5.对照波形进行判断

 1.在t0时刻,judge线程判断没有trans完成,执行发送旧trans操作(x);data_send线程判断不发数据,执行发送旧trans操作(x);addr_send线程判断发数据A到总线上,并将数据push进入addr_q中,将addr_send_ready信号拉高;

2.在t1时刻,judge线程判断没有trans完成,执行空操作;data_send线程判断addr_send_ready信号为高,并且addr_q中没有待完成的trans,执行发送A写数据,并将addr_send_ready拉低,将Apush进入wdata_q中;addr_send线程执行发送B地址数据,将addr_send_ready拉高,addr_q中有两个trans:A和B;

3.在t2时刻,judge线程判断有trans完成,执行将addr_q和wdata_q的第一笔trans:A pop出来,完成该trans;datas_send线程判断执行,发送数据B;addr_send线程执行发动数据C

4.在t3时刻,judge线程发现hready为低,执行发送旧数据操作(C);data_send线程发现wdata_q中有一笔未完成的wdata信号,执行发送旧数据操作(B);addr_send线程发现addr_q中有2笔未完成的trans,执行空操作;

5.在t4时刻,judge线程判断有trans完成,完成该trans(B);data_send线程判断可以发数据,发送数据C;addr_send线程判断sim_addr_q为空,不执行空操作;

6.在t5时刻,judge线程判断有trans完成,完成该trans(C);由于addr_send_ready为低,data_send线程判断不能发数据;并且addr_send线程不发送地址。

1.3. 模拟slave行为具有发送hready信号功能的module的代码

当driver中有关写功能都验证完毕后,我们试着写一个slave的module,能够模拟slave产生hready_resp信号,并以此来验证driver写的是否有问题。

Module的代码如下:

1.4. seq_item的编写,代码如下:

 

 在seq_item中,我们将各个信号包在其中,并且产生一个静态变量count,在new函数中,每新产生一笔trans,count的值就会加1,用来对每一笔trans打印一个trans_id标签,之后我们让每个信号类型是rand型的,利用contraint函数为其随机化设置范围,我们在检验driver功能的时候,让地址信号为trans_id*10,写数据信号为trans_id*100。之后我们利用automatic_field给每个信号开启copy clone print等功能。

1.5. seq的编写,代码如下:

 在body函数中采用手动的方式启动这个seq,通过start_item和finish_item实现与driver的握手。并且在两者之间对seq_item进行randomize。Seq中有一个信号trans_num,是用来控制一共产生多少笔trans的,在tb中通过config db机制配置给seqr,在seq中通过config机制和m_sequencer句柄get到该控制参数

1.6. 在testcase中启动seq,给seq配置控制参数具体值

 在main_phase阶段通过start函数在固定seqr上启动该seq,注意raise_objection和drop_objection

#1000ns是为了让最后的几笔trans能够完成。

 seq配置trans_num的值为30,共产生30笔trans

1.7. 在tb_top中将tb与slave_module连接起来,并且产生时钟和复位信号

1.8. 写makefile跑仿真验证写信号时序正确性

Makefile如下,我们跑几个不同的seed

 成功跑通了30笔trans:

 打印的log文件如下:

1.8.1我们打开VCS看看波形

 一共30笔trans,trans完成的时间和log文件完全对应的上

我们将seed换成2,trans数目换成50再试试看:

 

 结果一目了然,完全符合预期。

至此,我们完成了driver的写操作的时序设计。接下来,我们将完善driver的读操作的设计

2.完善driver的读写功能时序

这部分我们重点把driver中读写功能以及模拟DUT行为的slave module的读写功能全部完善,保证读写时不会出现任何的时序上的错误与丢数据情况,让driver的复用性提高

2.1driver的代码部分:

2.1.1 driver中的main_phase阶段代码如下

在main_phase中启动了两个并发线程,第一个forever进程负责不断地get从sequence产生的数据,为了不让sequence中产生的数据在仿真的0ns就全部传入driver的内部Queue中,我们通过sim_addr_q.size()进行判断,一旦driver中该Queue中没有足够的数据可发,我们便让driver通过seq_item_port.get_next_item语句从seuqnce中拿到数据,如果drv中的queue中的trans足够用,那么我们在该时钟沿处则不get数据。

第二个线程driver则将get到的trans通过do_drive这个任务按照ahb的二级流水的时序要求发送到总线上,同时,我们将hselx信号也引入,因为hselx信号来源于decoder,我们按照组合逻辑的方式驱动该信号即可,不用在每个时钟的上升沿处。

 2.1.2 get_sim_trans的任务如下,也可以将该task写成方法

从seq中get到的seq_item将其拆解成信号级,并将每个信号放入一个固定的sim_*_q的queue内。

 2.1.3 do_drive任务代码如下:

在每一个时钟的上升沿,三个线程的执行顺序如下:judge线程 -> send_wdata线程 -> send_addr_control线程。

send_addr_control线程负责发送地址和控制信号;

Send_wdata线程负责发送写数据信号;

Judge线程负责判断该时刻是否有trans完成,并决定后续的trans是否应该发送;

 2.1.4 do_drive中的send_addr_control任务代码:

Ahb二级流水最多允许只有两笔未完成的trans,因此让addr_q.size<2时,可以向总线上push数据,每向总线上push一笔trans,addr_q.size+1。

Haddr_send_ready用于通知线程send_wdata发送数据,因为二级流水,数据相位总是滞后于地址相位周期,只有地址相位发完了数据,数据相位才可以发送。

 2.1.5 do_drive中的send_wdata任务代码:

因为seq产生的每一笔trans中都有写数据,然而当hwrite为低时,我们进行读操作,不希望有这笔写数据被放进待完成处理的queue中,因此用hwrite信号进行控制,当发现这笔trans是读操作时,我们将trans中的写数据删掉

如果当前trans是写操作,当这笔trans的地址发送完毕后,我们在下个时钟沿将写数据发送出去,并等待hready为高时,将其处理掉。

 2.1.6 do_drive中的judge_trans_finished任务代码:

区分读操作还是写操作,如果hready为高时,完成了一笔trans,我们需要将其从对应的queue中pop出来,这样addr的queue才能继续接受sim_addr_queue中待处理的trans,wdata的queue也才能接受待处理的wdata,并发送到总线上。

Hready为低,不能完成当前trans,则不应该将该笔trans从queue中pop出来,在下个时钟沿继续处理该trans。

2.2 接下来我们完善模拟DUT行为的Slave的module的编写

Slave在每个时钟的上升沿先判断一笔trans有没有完成,如果没完成,则不从总线上get数据。如果完成了一笔trans,就将从总线上get到的数据从queue中pop出去,然后从总线上get到一笔新trans,之后根据自己产生的ready信号和当前trans的hwrite信号判断下个时钟上升沿要不要发送rdata信号。

Slave的具体代码如下:

 当slex信号选中该slave时,执行操作,否则什么也不做。

2.2.1 Slave中产生hready信号

2.2.2 Slave中判断一笔trans是否完成:

采用与driver类似方法判断一笔trans是否完成

 2.2.3 Slave接受总线上的数据:

当满足条件时,从总线上接受数据,如果发现一笔trans是读数据,将其记录下来,在执行这笔trans时,我们要发送读信号。每发现一笔读操作,就通过send_rdata_msg记录下来。

 2.2.4 Slave发送一笔读数据: 

为了简化操作,这里通过urandom随机发送读数据信号

 2.2.5 其他组件配置:

在seq_item中,将hwrite的随机化范围控制在0或者1:

 在testcase中启动20笔trans:

 至此,完成了slave module代码编写;

3. makefile编译,VCS仿真查看结果:

下面我们就不断地变更seed,并且使用VCS用makefile自动化编译仿真,查看log文件并用VCS查看波形:

 结果:一共产生了20笔trans都被drv成功地get到了,并且有7笔写操作和13笔读操作。

 打印的log信息如下:

 波形如下:

 我们仔细用log文件对照波形,完全符合规定的时序,没有一点问题。

为了确保正确性,更改seed再跑一遍

这次共有12笔读操作,8笔写操作:

 Log文件:

 这里我们发现了一个问题,第三笔trans时候,它的写数据不对,第三笔trans的trans_id是3,地址是trans_id*10为30,数据是trans_id*100为300.但此时的数据是200,原因是前两笔trans是读操作,我们没有把第二笔trans的wdata信号及时删除掉,我们打开driver看看发送数据send_wdata的代码:

 原因就出现在红色画圈部分,这样删掉的方法有些笨重,而且判断不准确,我们换个思路,在接受数据的时候就进行一个判断,如果是读操作,那我们则不要把这笔trans接收过来,这样还可以在程序一开始节省内存空间,一举两得。

找到get_sim_trans函数,进行如下修改:

 随后我们再编译仿真一次,log文件如下:

 可以看到,错误被成功纠正了。

对照波形:没有问题

 继续换seed跑一下,确保正确性:

这里有10笔读和10笔写

 Log文件如下:

 波形如下:没有任何问题

 综上,我们完成了模拟AHB二级流水的driver的编码,并用自写的模块slv_module进行了验证。

跑了多个seed,都没有任何问题

综上,完成了AHB-driver最令人头疼的部分。

在下一篇博客,我们将完成driver中其他的信号,并完成monitor的代码编写。

  • 25
    点赞
  • 119
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值