SV学习笔记(二)

本文

芯片验证系列文章主要分为三部分:验证工程师与芯片验证、SV学习笔记、UVM学习笔记。此为 SV学习笔记 部分第二篇,主要介绍接口、采样和数据驱动、测试的开始和结束、调试方法。

版本 说明
0.1 初版发布

参考

名称 作者 来源
《芯片验证漫游指南》 刘斌 书籍
《SystemVerilog验证》 克里斯.斯皮尔 书籍

专业术语与缩略语

缩写 全称 说明

接口

什么是接口?

  • 接口 主要用作验证 ,国外有些团队会使用sv进行设计,那么接口就会用作设计。
  • 验证环境中,接口可以 使连接变得简洁而不易出错
  • interface和module的使用性质很像, 可以定义端口,也可以定义双向信号,可以使用initial和always,也可以定义function和task
  • interface可以 在硬件域和软件域间传递信息 ,也就是可以作为module的端口列表,也可以作为软件方法的形式参数。
  • 对于interface的初步认识,可以看作“插排”,DUT与TB之间的数据驱动就是靠这个“插排”来完成的。

接口的定义与使用

  • interface的定义结构与module类似。
  • interface的 端口列表只需定义时钟、复位等公共信号 ,或者不定义任何端口信号,而在变量列表中定义DUT与TB连接的各个变量, 建议用logic来定义
  • interface也可以依靠参数化方式提高复用性。
  • interface在例化时, 与module例化方式相同
  • 对于有对应interface的DUT和TB组件,在例化时,传递匹配的interface变量名也就完成了interface内变量的传递,换句话说就是两者打通,对应interface的不同组件之间变量信号实时传递。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
interface arb_interface(input bit clk);
    logic [1:0] grant, request;
    logic reset_n;
endinterface

module arb(arb_interface arb_if);
    //......
    always@(posedge arb_if.clk or negedge arb_if.reset_n)begin
        if(!arb_if.reset_n)
            arb_if.grant <= 2'b00;
        else
            arb_if.grant <= next_grant;
    end
    //......
endmodule

module testbench(arb_interface arb_if);
    //......
    initial begin
        @(posedge arb_if.clk) arb_if.request <= 2'b01;
        $display("@%0t: Drove req=01", $time);
        repeat(2) @(posedge arb_if.clk);
        if(arb_if.grant != 2'b01) $display("@%0t: a1: grant != 2'b01", $time);
        $finish;
    end
endmodule

module tb_top;
    bit clk;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    arb_interface arb_if(clk);
    arb a1(arb_if);
    test t1(arb_if);
endmodule

接口的优势

  • 将有关信号封装在接口,对于设计和验证环境都便于维护,如需修改、添加、删除信号,只需修改interface文件即可。
  • 接口在硬件域(module)和软件域(class)都可以使用,是 硬件域和软件域交互的唯一媒介
  • 接口可例化,对于多组相同总线,通过例化可灵活使用,简化代码且便于维护。
  • 每一个agent使用对应的interface,简化验证平台结构,便于维护。
  • tb顶层例化时,无需定义信号连线,只需例化interface。

采样和数据驱动

竞争问题

  • 为了避免RTL仿真行为中发生信号竞争问题, 建议使用非阻塞赋值(<=) (简单来说阻塞赋值是顺序执行,非阻塞赋值是并发执行,硬件电路行为是并发执行)。
  • 在仿真行为中,为了避免时序电路中时钟和驱动信号的时序竞争,我们需要 尽量明确驱动时序和采样时序
  • 默认情况下,时钟对于组合电路的驱动会添加一个 无线最小时间(delta-cycle)的延迟 ,而该延迟无法用绝对时间单位衡量,它要比最小时间单位精度还要小。这是 仿真工具为了符合硬件电路真实行为(建立时间保持时间以及线延迟)而做出的处理 。(注意: #0并不代表0延迟,而是指延迟delta-cycle)

为了说明delta-cycle的概念,举例如下:

  • 源代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
`timescale 1ns/1ps
module top_module ();
        bit clk1,clk2;
    bit rstn;

    logic [7:0] d1;

    initial begin
        clk1 = 0;
        forever #5 clk1 <= ~clk1;
    end

    always@(clk1) clk2<=clk1;

    initial begin
        #0 rstn <= 0;
        #10 rstn <= 1;
        #20 $finish;
    end

    always@(posedge clk1 or negedge rstn)
        if(!rstn) d1 <= 0;
        else d1 <=  d1+1;

    always@(posedge clk1)
        $display("clk1: %0t ns d1 value is 0x%0x", $time, d1);
    always@(posedge clk2)
        $display("clk2: %0t ns d1 value is 0x%0x", $time, d1);
endmodule
  • 仿真结果:
Running Icarus Verilog simulator...
VCD info: dumping is suppressed.
clk1: 5000 ps d1 value is 0xxx
clk2: 5000 ps d1 value is 0x0
clk1: 15000 ps d1 value is 0x0
clk2: 15000 ps d1 value is 0x1
clk1: 25000 ps d1 value is 0x1
clk2: 25000 ps d1 value is 0x2
Hint: Total mismatched samples is 0 out of 0 samples

Simulation finished at 30000 ps
Mismatches: 0 in 0 samples

为什么同样在25000ps时刻,d1在clk1和clk2下的采样值不同?首先clk2是在clk1下驱动的,也就是clk2比clk1延迟一个delta-cycle时间,clk1驱动了d1,d1也比clk1延迟一个delta-cycle时间。25000ps,当在clk1下采样时,d1的值还未更新,所以采到的是0x1;而在clk2下采样时,d1已由clk1驱动更新,所以采到的是0x2。如果还未完全理解,可以打开波形窗口,不过要打开delta-cycle的开关,可以看到delta-cycle的存在,对电路的数据驱动和采样时序会有更直观的理解。

  • 总结:

    1. 如果处于各种原因,clk与被采样数据之间存在若干个delta-cycle的延迟,那么对数据的采样会存在问题。
    2. 采样数据的竞争问题会成为潜在困扰仿真采样准确性的问题。
    3. 避免采样的竞争问题: 1)在驱动时,添加相应的人为延迟,使clk与驱动变量之间的延迟加大,提高DUT使用驱动信号时的准确度;2)在采样时,依靠采样前某段时刻进行采样,来模拟建立时间的采样要求,确保采样的可靠性。
  • 图示:

未显示 delta-cycle
显示 delta-cycle

clocking

  • 在接口中声明clocking(时序块)和采样的时钟信号,可以用来 实现信号的同步和采样
  • clocking块基于时钟周期对信号进行驱动或采样的方式,使testbench不再苦恼于如何准确及时地对信号驱动或采样,消除了信号竞争的问题。
1
2
3
4
5
6
clocking bus @(posedge clk1);
    default input #10ns output #2ns;
    input data, ready, enable;
    output negedge ack;
    input #1step addr;
endclocking
  • 对上述clocking描述代码进行说明

    1. 第一行定义clocking块bus,使用上升沿来驱动和采样。
    2. 第二行指出输入信号在clk1上升沿之前5ns采样,输出信号在clk1上升沿之后2ns驱动(输入为采样,输出为驱动)。
    3. 第三行声明输入信号,采用默认的输入事件(clk1上升沿5ns前采样)。
    4. 第四行声明输出信号,并且指明为clk1下降沿驱动,覆盖了原有的clk1上升沿后2ns驱动。
    5. 第五行定义了输入信号addr,采用了自定义的采样事件,clk1上升沿后的1 step,覆盖了原有的clk1上升沿前5ns采样,这里1 step使得采样发生在clk1上升沿的上一个时钟片采样区域,即可以保证采样到的数据是上一个时钟周期数据。
  • clocking块的总结

    1. clocking块不仅可以定义在interface中,也可以定义在module和program中。
    2. clocking中列举的信号不是自己定义的,而是interface或其他声明clocking的模块定义的。
    3. clocking在声明完后,应该伴随着定义默认的采样事件,也就是“default input/output event”,如果没有定义,会默认使用时钟上升/下降沿前1step进行采样,时钟上升/下降沿后#0进行驱动。
    4. 除了定义默认的采样和驱动事件,定义信号方向时同样可以用新的采样/驱动事件对默认事件进行覆盖。

modport

modport本身目的就是将信号分组和指明输入输出方向,在连接时工具会进行端口方向检查,防止连接出错,不过掌握clocking之后,可以忽略modport,clocking已经对信号输入输出方向进行了声明。

结论

  • 为了避免采样竞争问题,验证工程师应该在验证环境的驱动环节添加固定延迟,使得在仿真波形中更容易体现出时钟与被驱动信号之间的时序前后关系,同时这样也便于对DUT的准确处理和TB的准确采样。
  • 如果TB在采样从DUT送出的数据,在时钟与被驱动信号之间存在delta-cycle时,应该考虑在时钟采样沿的更早时间端段去模拟建立时间要求,这种方法也可以避免由于delta-cycle问题带来的采样竞争问题。
  • 当我们把clocking运用到interface中,用来声明各个接口与时钟的采样和驱动关系后,可以大大提高数据驱动和采样的准确性,从根本上消除采样竞争的可能性。

测试的开始和结束

写在前头

  • 各个设计自身可以认为是一个大的线程,内部有包含多个并行的线程,而模块之间连接即线程的通信,主要依靠信号的变化。
  • 可以想象,对于一个设计,如果在仿真开始没有任何激励,那么仿真不具备执行条件,也可以认为已经结束,因为在设计内部没有产生任何新的事件,也不会触发组合逻辑和时序逻辑。
  • 如果仿真开始后仅提供时钟和复位信号,验证会持续下去,而对设计不会产生实质的功能影响。从设计角度来看,复位信号是为了让设计进入一个确定的初始状态,而时钟就是脉搏跳动。
  • verilog测试中,可以通过系统函数 “$finish()” 来结束仿真,也可以通过 “$stop()” 来暂停仿真。

program

  • program是作为验证而提出的,可以有效控制仿真的进程,但是目前验证平台更多基于UVM,UVM有独特的控制机制,所以program在实际项目中使用并不多。
  • program的提出, 将验证部分和设计部分进行有效隔离 ,每一个program作为一个独立测试, 当testbench中所有program中最后一个initial块完成后,结束仿真 。这是program的隐式结束。
  • 有些program内的initial块无法正常结束,这时候需要使用 显示结束,使用“$exit()” 来结束program。
  • program被看做软件域,所以不可以出现always、module、interface等硬件相关语句,并且不可以例化其他program。
  • program被看做软件域,可以在program内部定义变量和发起多个initial块,并且建议使用阻塞赋值(软件方式的顺序执行)。
  • program对于数据采样也可以消除delta-cycle竞争问题,详细内容可见红宝书“SV环境构建篇之程序和模块”。(待了解)

总结

  • 硬件域(module)、软件域(program)、中间域(interface)
  • 不仅可以使用interface clocking来消除采样竞争问题,可以使用program(建议使用clocking)。
  • program可以控制仿真的结束。
  • 使用“$stop()”和“$finish()”可以结束仿真。

调试方法

调试工具

大多工程师选择使用verdi作为调试工具,主要有三个窗口:层级列表窗口、源代码窗口、波形窗口。verdi的具体使用方法,请参考我的另一篇帖子 Verdi使用总结

打印消息

打印消息是调试循环语句、顺序执行语句等查看路径和当前变量值的简便方式,除此之外,由于验证平台更多变量是动态的,无法在调试工具查看动态变量值,所以对与验证环境的调试,更多使用打印消息。

打印消息命令“$display()”:

  • $time代表仿真时间变量。
  • 显示格式: %x(十六进制)、%d(十进制)、%b(二进制)、%s(字符串)、%t(时间)。
  • $display(消息级别)、$warning(警告级别)、$error(错误级别)、$fatal(严重错误级别)
  • 字符串变量格式化:string s = $sformatf(“Hello, %s!", name_s);

设置断点

  • 可以通过调试工具为程序设置断点。
  • 通过设置断点(breakpoint)可以查看程序执行到断点处(程序暂停)的变量数值,而设置断点要求验证工程师对程序执行顺序足够了解。
  • 设置断点可以便于查看软件程序(function、task、object)中局部变量的数值。注意:动态变量是无法添加到波形查看的。
  • 设置断点还可以方便调试程序执行的顺序,例如在顺序执行语句执行的多个位置设置断点,通过仿真执行,查看程序是否在断点处暂停,如果没有,那么程序的挂起(hang-on)原因就在上一个断点和此断点之间。通过此方法可定位可疑程序的范围。
  • 如果查看局部变量,需要使用局部变量窗口(Local Windows),继而通过断点查看变量(暂时不知verdi是否支持,待学习)。

文章原创,可能存在部分错误,欢迎指正,联系邮箱 cao_arvin@163.com。