System verilog基础-子程序和测试平台

        

目录

任务和函数 

变量生命周期 

连接设计和测试平台 

什么是接口?

 接口中的clocking

测试的结束

调试方法

设置断点


        initial语句块和always语句块都是过程语句,initial 和 always 一样,无法被延迟执行,在仿真一开始它们都会同时执行,而不同initial和always直接在执行顺序上没有先后。always 可以搭配@(event)来实现组合逻辑和时序逻辑的描述,但是initial语句块它是不可综合的代码,不能写在rtl代码中,通常用于仿真时赋初值使用。

任务和函数 

        任务(task)和函数(function)之间有很明显的区别,其中最重要的一点是,任务可以消耗时间而函数不能。函数里面不能带有诸如#100的时延语句或诸如@ (posedge clk)、wait(ready)的阻塞语句,也不能调用任务。

        void 函数,没有返回值,它的主要用途在于调试和验证,比如在过程中调用void函数,打印一些信息而不返回信息,打印一些诸如状态机当前状态,某参数当前数值等信息。

        function,只能有一个返回值,不能带有延时语句,可以执行一些组合逻辑计算。

    任务相较于函数更加灵活,且 task 无法通过 return 返回结果,因此只能通过 output、inout 或者ref的参数来返回。task 内部可以置入耗时语句,而 function 不能。常见的耗时语句有 @event、wait event、#delay 等。

     如果要调用 function,则使用 function 和 task 均可对其调用;而如果要调用task,仅能使用task调用,因为如果被调用的task内置了耗时语句,则外部调用它的方法类型必须为task。

面试常出的题:task和function有什么区别?

        在函数和任务中,如果定义函数或任务的时候,没有给变量标明方向,那么默认其方向为输入。缺省的类型和方向是“Logic 输入“。 

变量生命周期 

在SV中,将数据的生命周期分为动态(automatic)和静态(static)

        局部变量的生命周期同其所在域共存亡,例如function/task中的临时变量,在其方法调用结束后,临时变量的生命也将终结,所以它们是动态生命周期。

        全局变量即伴随着程序执行开始到结束一直存在,例如module中的变量默认情况下全部为全局变量,用户也可理解为module中的变量在模拟硬件信号,所以它们是静态生命周期

如果数据变量被声明为automatic,那么在进入该进程/方法后,automatic变量会被创建,而在离开该进程/方法后,automatic变量会被销毁。而static变量则从仿真开始一直持续,不会被销毁且可被多个进程和方法共享。

        调用function的时候,默认function的类型是static的,实际上我们更加需要的是动态变量的函数,所以通常在定义function的时候,会在function和函数名之间加一个 automatic声明。

        对于automatic方法,其内部的所有变量默认也是automatic,即伴随automatic方法的生命周期建立和销毁。

        对于static方法,其内部的变量默认也是static类型

        在Verilog中,在子程序的开头把input 和 inout的值复制给本地变量,在子程序退出时则复制output和inout值。除了标量以外,没有任何把存储器传递给Verilog子程序的办法。

        而在System Verilog中,参数的传递方式可以指定为引用 ref 而不是复制。这种ref参数类型比input、output、inout更好用。 

        使用reg和const进行参数传递。System Verilog规定了ref参数只能被用于带自动存储的子程序中。如果你对程序或模块指明了automatic 属性,则整个子程序内部都是自动存储的。如上面的例子,function调用的变量是ref类型的,所以要把function定义成automatic,如果没定义就会报错,因为默认的function和task类型是static

仿真结果:

        上面的例子还用到了const修饰符。其结果是,虽然数组变量a指向了调用程序中的数组,但子程序不能修改数组的值。如果你试图修改数组的值,编译器将报错。

对于参数传递的方法,还可以采用名字进行参数传递: 

$time 与 $realtime的对比

        系统任务$time 的返回值是一个根据所在模块的时间精度要求进行舍入的整数,不带小数部分,而$realtime的返回值则是一个带小数部分的完整实数。 

连接设计和测试平台 

什么是接口?

        接口可以用作设计,也可以用作验证,在验证环境中,接口可以使得连接变得简洁而不易出错。interface和module的使用性质很像,它可以定义端口,也可以定义双相信号;它可以使用initial和always,也可以定义function和task。 

使用了interface之后,结构图变成:

可以看到,使用interface之后,整个测试框图简洁了很多。

        接口将有关信号封装在同一个接口中,对于设计和验证环境都便于维护和使用。如果你需要添加新的信号,只需要在接口中定义这个信号即可。由于接口既可以在硬件世界(module)和软件世界(class)中使用,interface作为SV中唯一的硬件和软件环境的媒介交互,它的地位不可取代。(class可以通过指针来取到interface中的信号)

        在interface的端口列表中只需要定义时钟、复位等公共信号,或者不定义任何端口信号,转而在变量列表中定义各个需要跟DUT和TB连接的Logic变量。interface也可以通过参数化(parameter)提高复用性。

        面试问题:模块和接口有什么区别?答:模块可以例化模块,模块也可以例化接口,但是接口只能例化接口而不能例化模块。

 接口中的clocking

        硬件和软件之间的连接可以用interface实现,也可以通过modport来进一步限定信号传输的方向,避免端口连接的错误。在接口中也可以声明clocking(时序块)采样的时钟信号,用来做信号的同步采样。clocking 块基于时钟周期对信号进行驱动或者采样的方式,通过人为的添加delay,使得testbench不再苦恼于如何准确及时地对信号驱动或者采样,消除了由于仿真器赋值引发的delta-cycle带来的信号竞争的问题。

        定义一个clocking 块,命名为bus,并由clock1的上升沿来驱动和采样: 

        第二行指出:在clocking块中所有的信号,默认情况下都会在clocking事件的前10ns来对输入进行采样,在事件的后2ns对其进行输出驱动。

        第三行:声明了要对其提前10ns采样的输入信号data、ready、enable

        第四行:声明了输出驱动的ack信号,该驱动信号的事件是时钟clock1的下降沿,覆盖了原有default设定的上升沿后2ns。

        第五行:addr的输入,会在clock上升沿到来的前1step采样数据,即在时钟clock1上升沿的前一个时间片采样,采样到上一个时钟周期的值。 

        如果TB在采样DUT送出的数据,在时钟与被驱动信号之间存在delta-cycle 时,应该考虑在时钟采样沿的更早时间段去模拟建立时间要求采样,这种方法可以避免由于delta-cycle问题带来的采样竞争问题。当我们把clocking运用到interface中用来声明各个接口与时钟的采样和驱动关系后,可以大大提高数据驱动和采样的准确性,从根本上消除采样竞争的问题。 

 仿真结果:

实际波形:

        总结:在clocking 块中,定义了采样input信号,会在时钟上升沿的提前3ns进行采样。所以才会有同时display,结果vld的值和ck.vld的值不同的原因,这就是clocking块的作用,利用这个特性,可以模拟信号的建立时间和保持时间,来解决因为同时刻数据赋值仿真器会有一个delta-cycle延迟带来的各种问题。

测试的结束

       在testbench里做测试的时候可以写program来做测试,program有一个特点就是在program里的代码都被执行后,program会自动结束,但是如果program中有forever语句会永远执行下去的话,可以用$exit()来强制退出。当系统发现所有的program都执行完毕了,就会自动结束仿真了。

面试:program和module有什么区别?

        program可以看做是软件的部分,所以program中不能出现和硬件行为相关的语句,比如always、module、interface,也不能出现其他program的例化语句。program中可以发起多个initial块,也可以定义新的变量。

        program的内部变量赋值方式,应该采用阻塞赋值(模拟软件行为),program内部在驱动外部的硬件信号时应该采用非阻塞赋值(硬件方式)。

总结:    module(硬件盒子)、program(软件盒子)、interface(软硬件接口),在为验证环境建立独立的测试盒子,可以考虑用program来帮助消除采样竞争问题,以及自动结束测试用例。也可以采用module硬盒子的方式,使用interface clocking来消除采样信号竞争问题,使用$stop()、$finish()系统方法来显示结束测试用例。

调试方法

        如果要实现对字符串string赋值,那么可以采用$sformatf()的方式来对字符串变量格式化,例如string_s = $sformatf("Hello,%s",name_s),这里name_s是名字的字符串变量。

设置断点

        设置断点是测试里面最重要的一个方法之一,作为一个软件工程师,灵活的使用断点是快速定位bug的方法。比如设置了三个断点,不断的仿真,经过了第一个、第二个断点,发现一直到不了第三个断点,那么就有可能是在第二和第三个断点之间出现了仿真挂起hang-on。

        点击代码前面的数字,就可以给代码打上断点,然后每run一次,就会进行一步,每次运行到断点的时候就会停下来,再次run时,才会运行打上断点的代码行,在左侧的objects窗口可以看到变量的数值,结合设置断点和观察变量数值,可以达到快速debug的目的。 

        更多时候,我们会在view栏中,打开Local,局部变量,来查看断点部分的变量数值,因为objects里面只能显示静态的变量,我们调试的会有很多动态变量(比如automatic 的function)没法在object中显示,所以就需要打开local窗口查看。

        如果要把变量在仿真的时候强行改变,可以右键变量→Modify → Force 

        这里强行把变量改成一个值,有三种选择,Freeze、Drive、Deposit三种,这三种有什么区别呢?

        Freeze:把变量赋值为Value,且整个仿真阶段不会改变,哪怕别处又给该变量赋值,但不会生效。

        Drive、Deposit:两者功能类似,都是给变量赋值,相当于此刻有硬件给信号赋值,两者的区别在于Deposit的赋值有更高的优先级,即如果此刻有多个信号给b_bit_vs_logic赋值,那么赋值结束后会采用Deposit的值,如果时Drive和多个信号赋值冲突了,会出现仿真问题,不确定值。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不吃葱的酸菜鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值