SV项目笔记之一——重难点:验证平台的搭建的底层逻辑和核心思想(来啃一啃这块硬骨头——如何理清AHB的复杂时序关系)

作为一个验证工程师,掌握随机化的验证手段是十分必要的,相比于只通过定向测试的方法,随机化的验证手段可以提高验证的效率,也可以发现定向测试意想不到的bug,但采用随机化的验证手段的一个重要的前提就是验证平台的搭建,在项目的前期,搭建验证平台的工作往往是最耗时的,但带来的收益却是显著的。一个完整的验证平台是层次鲜明的,关于验证平台的结构和层次,详见我的另一篇博客:

这一部分是重点也是难点,初学项目时,笔者试着自己写这一部分的代码,但总是遇到各种各样的错误,源于对AMBA—AHB总线协议的模糊以及slave-SRAM的模糊,但万事开头难,笔者经历无数次的耗时的debug和反思后,终于搞定了这块难啃的骨头。阅读本内容时,也请读者抱有耐心,因为最难懂的也是最能提高自己的。

开始之前,要对AHB-SRAMC的接口时序了然于胸。

本设计只支持单周期的SINGLE读写,暂不讨论INCR和WRAP的读写方式。具体的参考时序图如下:

我们写一个tb,要模拟master的行为,将数据输出到总线上,在本设计中,不考虑多个master的情形,只考虑单master对单独的SRAM的情形。而将时序图用代码表示出来最重要的部分就是逻辑抽象,我们来仔细分析,如何将这个SINGLE读写的时序图抽象成一个代码。

由于AHB是流水线的工作模式,总是允许总线上在一个时钟的上升沿存在一笔未完成的trans,master成功地将一笔trans传递到总线上的过程可以分为如下两个阶段:地址相位和数据相位,因为完成一笔trans的过程遵循着先寻址再传输的时序,因此数据相位总是滞后于地址相位一个时钟周期被发送到总线上

在一个具体的时钟上升沿,当hready信号为高时,判断一笔trans的完成需要具备两个条件,trans的数据包内应该包含了读写数据的地址和控制信号以及具体的数据,如果是写操作,那么数据的来源应该是master,如果是读操作,那么数据的来源应该是sramc。而地址和控制信号是上个时钟周期就传递过来的,数据则是在这个时钟周期传递过来的,他们之间遵循严格的时序关系,没有地址和控制信号,就不要发送数据到总线上。

这样的时序关系决定了我们可以将发送地址和控制信号作为一个线程,而发送数据信号作为一个线程,判断一笔trans的结束作为一个线程。三个线程可以是并发的,但要通过一定的手段在三个线程之间进行同步,使其满足具体的时序关系。

这段代码表达的含义是:通过fork join启动了三个并发线程,分别是

  1. 负责发送地址和控制信号的线程1,为了满足时序关系,让该线程滞后于线程3执行,因此在线程3内部触发事件judge_ready,在线程1的起始等待该事件的触发操作。
  2. 负责发送数据信号的线程2,这个线程应该设法滞后于第一个线程一个时钟周期,因此可以在第一个线程内生成一个控制信号haddr_send_ready,该信号在第一个线程结束时拉高(拉高的动作要到下个时钟上升沿才会得以体现)。而只有当线程2检测到该信号为高时才会发送数据,因此完成了数据相位滞后于地址和控制相位一个时钟周期的操作。值得注意的是,当发送了数据后,应该把该控制信号拉低。
  3. 第三个线程负责判断一笔trans有没有完成,这个动作我们设计在线程2执行之后线程1执行之前,且每个时钟沿都应该进行一次判断,确保没有丢掉任何trans。为此,我们在线程2的内部定义了一个事件:data_phase_ready,而在线程3开始之前等待该事件的触发。

通过事件的触发,完成了进程之间的同步,让3个进程按照时序有序进行,目前的时序如下:线程2:在当前时钟上升沿发送当前trans的写数据信号—>线程3:在当前时钟上升沿判断当前trans是否完成—>线程1:在当前时钟上升沿发送下一笔trans的地址和控制信号。接下来就是每个线程的具体控制方法。

如代码所示:

1. 判断线程1是否执行是根据队列中的地址数据数量来判断的,由于是流水线的工作模式,当一笔trans未完成时,队列中的数据可能是1个(开始和结尾)或者2个(中间传输过程),因此只要队列中的数据少于2个,就可以发送一笔trans进来,当然还需要一个控制信号来确定发送几笔trans,sim_count表示generate的trans数量,trans_count表示完成的trans数量。

 

2. 判断线程2是否执行是根据信号haddr_send_ready为高还是低判断的,如果信号为高表示线程1已经在之前的时钟周期已经执行过。这里需要注意的是需要分两种情况讨论,因为我们的HSELX信号是靠master拉高的(实际情况hselx是由解码器拉高的),直到t1时刻hselx信号才为高,从而触发sram给出hready信号,因此hready信号无法在t1时刻就为高,所以补充了一个在t1时刻的判断条件,此时不管hready信号的状态如何,我们都要发送第一笔trans的数据,否则就只能等到t2再发送,也就不符合时序关系了。

 

3. 判断线程3是否执行是根据线程2是否已经发送完毕数据,并且队列中的数据是两个时(中段)且hready为高,或者队列中的数据是1个时且(末尾)hselx信号为低并且hready为高时则判断一笔trans完成。

由此完成了master的时序控制。我们根据时序图进行一次梳理:(在本设计中,由于hsex信号是通过master在写数据时拉高的,因此t1时刻还是x态)

 

(1)在t0时刻,线程2执行(判断结果:不发送数据,原因:addr_send_ready信号为低)—>线程3执行(判断结果:没有trans完成,原因:q.size为0)—>线程1执行(A地址和控制信号发送),满足时序关系。

(2)在t1时刻,线程2执行(判断结果:A写数据发送,原因:满足第2个判断条件)—>线程3执行(判断结果:trans没有完成,原因:符合q.size() = 1但是hselx信号=1)—>线程1执行(B地址和控制信号发送),符合时序图。

(3)在t2时刻,线程2执行(判断结果:B写数据发送)—>线程3执行(判断结果:第一笔trans(A)完成,原因:符合q.size() = 2且hready信号为高。)—>线程1执行(判断结果:C地址和控制信号发送,原因:addr_q.size()<2),满足时序关系。

(4)在t3时刻,线程2执行(判断结果:不发送数据,原因:hready信号为低)—>线程3执行(判断结果:没有trans完成,原因:符合q.size() = 2但hready信号为低。)—>线程1执行(判断结果:不发送新地址和控制信号,原因:q.size()=2)满足时序关系。

(5)在t4时刻,线程2执行(判断结果:C写数据发送)—>线程3执行(判断结果:第二笔trans(B)完成。)—>线程1执行(判断结果:不发送数据,原因:trans_count=sim_count=3)满足时序关系。

(6)在t5时刻,线程2执行(判断结果:不发送新数据,原因:haddr_send_ready=0)—>线程3执行(判断结果:第三笔trans(C)完成。)—>线程1执行(判断结果:不发送新数据,原因:trans_count=sim_count),满足时序关系。

完成了模拟master发送数据到AHB总线上的行为,接下里应该模拟一下slave收数据的行为:

Slave总是滞后于master一个时钟周期才能收取到master发到总线上的数据,一旦收到了信息,slave就要根据当前自己的状态判断能否处理这笔trans,如果能,则把hready信号拉高,如果需要读取数据,则把读数据push到总线上,并在下一个时钟沿判断这笔trans的完成。我们把slave收数据的行为也分为以下几个部分,并使用并发线程实现:

  1. 在当前时钟上升沿收取总线上的地址信号和控制信号。
  2. 发送下一个时钟周期的hready信号和反馈信号
  3. 发送读数据信号到总线上
  4. 判断slave是否完成一笔trans

这段代码的含义是:通过fork join启动了4个并发线程,分别是:

  1. 在当前时钟沿,线程1接收总线上的地址和控制信号,并存入queue中,为了满足时序关系,该线程要滞后于线程4执行,因此可在线程4内定义一个事件slv_judge_ready,只有触发了该事件,线程1才会执行。
  2. 在当前时钟沿,线程2发送hready信号和反馈信号到总线上,该线程的启动需要满足队列中有trans的条件,因此要滞后于线程1,等待线程1内的事件slv_addr_acpt_ready,只有触发了该事件,线程2才会执行。
  3. 在当前时钟沿,线程3发送读数据信号到总线上,由于读数据信号需要根据总线上的HWRITE信号以及slave在当前时钟周期产生的控制信号hready(只有下一个时钟周期才会push到总线上),所以该线程要在hready信号产生后执行,所以在线程2中设置了一个事件slv_hready_ready,只有触发该线程才能执行线程
  4. 在当前时钟沿,线程4判断是否完成了一笔trans,该线程应设法第一个执行,判断每个时钟沿是否有trans完成,保证不会有数据的丢失。

  

由此完成了slv的时序控制,我们来根据时序图梳理一下时序关系:

在每一个时钟上升沿,发生如下动作:

(1)在t0时刻,线程4执行(判断结果:没有trans完成,原因:q.size()=0,hready信号未知)—>线程1执行(判断结果:不接受数据,原因:hselx = 0)—>线程2执行(判断结果:hready信号无响应,原因:q.size() = 0)—>线程3执行(判断结果:不发送读数据,原因:slv_q.size()=0)。

(2)在t1时刻,线程4执行(判断结果:没有trans完成,原因:q.size()=0,hready信号未知)—>线程1执行(判断结果:接受第一笔trans(A),原因:hselx = 1,q.size()=0)—>线程2执行(判断结果:hready信号拉高,原因:q.size() = 1)—>线程3执行(判断结果:如果hwrite为低则发送读数据,原因:slv_q.size()=1,hready信号已经拉高)。

(3)在t2时刻,线程4执行(判断结果:第一笔trans(A)完成,原因:q.size()=1,hready信号为高)—>线程1执行(判断结果:接受第二笔trans(B),原因:hselx = 1,q.size()=0)—>线程2执行(判断结果:hready信号拉低(按时序图先假定为低),原因:q.size() = 1)—>线程3执行(判断结果:不发送数据,原因:slv_q.size()=1,hready信号拉低)。

(4)在t3时刻,线程4执行(判断结果:第二笔trans(B)未完成,原因:q.size()=1,hready信号为低)—>线程1执行(判断结果:不接受新trans,原因:hselx = 1,q.size()=1)—>线程2执行(判断结果:hready信号拉高(按时序图先假定为高),原因:q.size() = 1)—>线程3执行(判断结果:发送数据,原因:slv_q.size()=1,hready信号拉高)。

(5)在t4时刻,线程4执行(判断结果:第二笔trans(B)完成,原因:q.size()=1,hready信号为高)—>线程1执行(判断结果:接受第三笔trans(C),原因:hselx = 1,q.size()=0)—>线程2执行(判断结果:hready信号拉高(按时序图先假定为高),原因:q.size() = 1)—>线程3执行(判断结果:发送数据,原因:slv_q.size()=1,hready信号拉高)。

(6)在t5时刻,线程4执行(判断结果:第三笔trans(C)完成,原因:q.size()=1,hready信号为高)—>线程1执行(判断结果:不接受新trans,原因:hselx = 0,q.size()=0)—>线程2执行(判断结果:hready信号无响应(x),原因:q.size() = 0)—>线程3执行(判断结果:不发送数据,原因:slv_q.size()=0)。

综上分析,设计如果按照设想中进行,则满足时序关系。

我们将代码补充完整:

Interface代码如下:

编译一下,跑个仿真,波形如下:

  1. hready一直为高时候:

 

Log文件打印的信息如下:

 

没有时序上的问题。

  1. 如果hready随机,结果如下:

Sim.log文件打印信息如下:

细心的读者可能会发现,slave并没有完成最后一笔trans,这其实也是由于master发送的hselx信号导致的,当master发送完最后一笔地址和控制信号时,就把hselx拉低了,如果碰巧此时slave没有准备好(hready为低),则这最后一笔trans显然也是无法完成的。显然这也是符合正常的逻辑关系的。

综上,我们分别模拟了master和slave的行为,并检验了所写的master在向总线push数据的时候,没有发生时序上的错误。我们使用了fork join并发线程完成了这样的控制,并通过握手信号同步各个线程,保证了时序的正确性的同时,还可以在各个线程开始前加入一定的延迟信息,从而能更真实地模拟硬件的行为。

(写于2021/7/2,码字不易,觉得有用请给个好评,如果您有更好的建议,希望进行更深层次的讨论,欢迎私信我。)

  • 22
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值