亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

约束随机验证

约束随机验证是一种测试平台策略,它依赖于为被测设备 (DUT) 生成伪随机事务。目标是通过与 DUT 的随机交互来实现对许多预定义事件的功能覆盖。

开源 VHDL 验证方法 (OSVVM) 是一个免费的 VHDL 库,其中包括许多用于创建受限随机测试平台的便捷包。我们对 RandomPkg 和 CoveragePck 特别感兴趣,我们将在本文中使用它们。我建议访问 OSVVM GitHub 页面以了解有关此库的更多功能。

被测设备

我将深入研究一个示例,以更好地解释受约束的随机测试平台与使用定向测试的经典测试平台有何不同。我们在本博客上一篇文章中创建了环形缓冲区 FIFO,但我们没有创建自检测试台来验证模块的正确性。

我们将为使用约束随机验证的环形缓冲区 FIFO 创建一个合适的测试平台。

entity ring_buffer is
  generic (
    RAM_WIDTH : natural;
    RAM_DEPTH : natural
  );
  port (
    clk : in std_logic;
    rst : in std_logic;

    -- Write port
    wr_en : in std_logic;
    wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Read port
    rd_en : in std_logic;
    rd_valid : out std_logic;
    rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0);

    -- Flags
    empty : out std_logic;
    empty_next : out std_logic;
    full : out std_logic;
    full_next : out std_logic;

    -- The number of elements in the FIFO
    fill_count : out integer range RAM_DEPTH - 1 downto 0
  );
end ring_buffer;

环形缓冲区模块的实体如上面的代码所示。我们将 DUT 视为一个黑匣子,这意味着我们不会假设任何关于 DUT 是如何实现的知识。毕竟,这篇文章是关于测试台的,而不是环形缓冲区 FIFO。

我们将使用实体实例化方法在测试台中实例化 DUT。实例化比较简单,代码暂时省略,本文后面可以下载。

DUT 泛型将映射到以下值:

测试台策略

在我们开始实施任何东西之前,让我们来看看测试策略。下图展示了我们即将创建的测试平台的主要概念。

我们将在 DUT 的输入端执行随机写入事务。输入数据将在每个时钟周期设置为随机值,写使能输入上的选通将具有随机持续时间。

同样,我们将随机执行读取。我们将在持续随机数量的时钟周期的突发中断言读使能信号。

将有一个与 DUT 并行的行为模型。这是一个 FIFO,其实现方式与 DUT 中使用的环形缓冲区不同,但仍具有相同的接口。与 DUT 不同,行为模型不必是可综合的。这让我们可以自由地使用高级 VHDL 编程特性来创建它。

我们将在单独的过程中将 DUT 的输出与行为模型的输出进行比较。此过程将单独负责使用断言语句在每个时钟周期进行此比较。如果两个 FIFO 实现在任何时候表现不同,断言失败将导致仿真以错误终止。

最后,我们将通过观察进出 DUT 的事务来收集功能覆盖率数据。例如,功能覆盖点可能意味着同时读取和写入,或者 FIFO 至少被填充一次。我们将在我们的主要测试台定序器进程中监控这些事件。当我们监控的所有功能覆盖事件都发生时,模拟将停止。

导入 OSVVM 库

OSVVM 库可以与任何支持 VHDL-2008 的模拟器一起使用。它可能已经包含在您的模拟器附带的默认库中。它包含在 ModelSim PE 学生版中,可以从 Mentor Graphics 免费下载。

ModelSim 附带了旧版本的 OSVVM,但没关系,它拥有我们需要的一切。我们可以继续像这样导入随机包和覆盖包:

library osvvm;
use osvvm.RandomPkg.all;
use osvvm.CoveragePkg.all;

OSVVM 库的最新版本始终可以从 GitHub 页面下载。如果您的模拟器没有包含它,或者您想使用该库的最新功能,请执行此操作。

声明 OSSVM 变量

OSVVM 库包含具有受保护类型的包。由这些创建的变量将被限制在定义它们的进程的范围内。因此,我们将在测试平台架构的声明区域中将它们声明为共享变量,如下面的代码所示。

 -- OSVVM variables
  shared variable rv : RandomPType;
  shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType;

rv RandomPType 类型的变量 用于生成随机值。我们只需要其中一个,因为我们可以在需要生成随机值的每个进程中使用相同的对象。最后一行代码声明了六个 CovPType 类型的变量 .

我们声明了六个覆盖变量,因为我们将有六个覆盖目标,我们将这些对象称为«bins»。共享变量必须先初始化,然后才能用于收集覆盖率数据。我们通过调用 AddBins 来做到这一点 每个 CovPType 的程序 垃圾箱。

    -- Set up coverage bins
    bin1.AddBins("Write while empty", ONE_BIN);
    bin2.AddBins("Read while full", ONE_BIN);
    bin3.AddBins("Read and write while almost empty", ONE_BIN);
    bin4.AddBins("Read and write while almost full", ONE_BIN);
    bin5.AddBins("Read without write when almost empty", ONE_BIN);
    bin6.AddBins("Write without read when almost full", ONE_BIN);

我们将覆盖范围的字符串描述作为 AddBins 的第一个参数提供 程序。当我们打印每个覆盖范围的统计信息时,该字符串将在模拟结束时重新出现。从文本描述中可以看出,我们将使用 bin 来检查是否发生了一些非常具体的极端情况。

AddBins 是一个重载过程,可用于在 bin 变量中创建多个记分牌。但是,我们将只有一个与每个 bin 关联的记分牌。因此,我们将提供便利常量 ONE_BIN 作为 AddBins 的参数 程序。这将初始化 CovPType 每个变量都有一个 bin。当它们监控的事件至少发生一次时,它们所代表的记分牌被认为被覆盖。

生成随机输入

让我们从创建向 DUT 生成输入数据的过程开始。环形缓冲区 FIFO 旨在忽略尝试的覆盖和过度读取。因此,我们可以简单地以随机持续时间的突发方式写入随机数据。我们不必考虑 DUT 是否真的准备好吸收数据。

  PROC_WRITE : process
  begin
    wr_en <= rv.RandSlv(1)(1) and not rst;

    for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop
      wr_data <= rv.RandSlv(RAM_WIDTH);
      wait until rising_edge(clk);
    end loop;
  end process;

此过程需要考虑的唯一因素是 DUT 未处于复位状态。我们在这个过程的第一行随机启用或禁用写使能信号,但只有rst才会启用 是 '0' .

随后的 for 循环将在随机数量的时钟周期内将随机数据写入 DUT,即使使能信号未激活也是如此。我们可以这样做,因为 DUT 应该忽略 wr_data 端口,除非 wr_en 信号是 '1' .在for循环之后,程序会循环回到进程的开始处,触发另一个随机写事务。

执行随机读取

从 DUT 读取数据的过程类似于写入过程。我们可以随机激活rd_en 随时发出信号,因为 DUT 设计为在空时忽略读取尝试。 rd_data 上出现的数据 端口实际上没有被检查。这个过程只控制读使能信号。

  PROC_READ : process
  begin
    rd_en <= rv.RandSlv(1)(1) and not rst;

    for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop
      wait until rising_edge(clk);
    end loop;
  end process;

行为验证

我们将在我们的测试平台中构建 DUT 的行为模型以验证其行为。这是一个众所周知的测试平台策略。首先,我们同时为行为模型提供与 DUT 相同的输入。然后,我们可以比较两者的输出,以检查 DUT 是否具有正确的行为。

上图显示了这种测试平台的基本布局。行为模型与 DUT 并行工作。我们使用它作为蓝图来检查 DUT 的输出。

测试台 FIFO

我们将使用链表来实现行为模型。链表无法合成,但它们非常适合测试平台。您可能还记得如何在 VHDL 中创建链接列表 如果您是此博客的常客,请阅读文章。我们将使用其中的代码来实现环形缓冲区 FIFO 的行为模型。

package DataStructures is
   type LinkedList is protected
 
      procedure Push(constant Data : in integer);
      impure function Pop return integer;
      impure function IsEmpty return boolean;
 
   end protected;
end package DataStructures;

Linked List FIFO 的包声明如上面的代码所示。它是具有三个功能的受保护类型; Push、Pop 和 IsEmpty。这些用于在 FIFO 中添加和删除元素,以及检查其中是否还有零个元素。

  -- Testbench FIFO that emulates the DUT
  shared variable fifo : LinkedList;

受保护类型是 VHDL 中的类结构。我们将通过在testbench的声明区声明一个共享变量来创建一个链表对象,如上代码所示。

行为模型

为了完全模拟环形缓冲区 FIFO 的行为,我们声明了两个反映 DUT 输出信号的新信号。第一个信号包含来自行为模型的输出数据,第二个是相关的有效信号。

  -- Testbench FIFO signals
  signal fifo_out : integer;
  signal fifo_out_valid : std_logic := '0';

上面的代码显示了来自行为模型的两个输出信号的声明。对于行为模型,我们不需要任何专用输入信号,因为它们与连接到 DUT 的输入信号相同。我们使用信号来模拟 DUT 输出,因为它使我们能够轻松收集覆盖数据,我们将在本文后面看到。

PROC_BEHAVIORAL_MODEL : process
begin
  wait until rising_edge(clk) and rst = '0';

  -- Emulate a write
  if wr_en = '1' and full = '0' then
    fifo.Push(to_integer(unsigned(wr_data)));
    report "Push " & integer'image(to_integer(unsigned(wr_data)));
  end if;
    
  -- Emulate a read
  if rd_en = '1' and empty = '0' then
    fifo_out <= fifo.Pop;
    fifo_out_valid <= '1';
  else
    fifo_out_valid <= '0';
  end if;
  
end process;

实现环形缓冲区 FIFO 行为模型的过程如上面的代码所示。如果复位信号未激活,此过程将在时钟的每个上升沿触发。

每当 wr_en 时,行为模型都会将新值推送到测试台 FIFO 信号被断言,而 full 信号是 '0' .同样,行为模型过程中的读取逻辑通过监听rd_en来工作 和 empty 信号。后者由 DUT 控制,但我们将验证它是否在我们将创建的下一个过程中工作。

检查输出

负责检查 DUT 输出的所有逻辑都收集在一个名为 «PROC_VERIFY» 的进程中。我们使用断言语句来检查 DUT 的输出是否与行为模型中的输出相匹配。我们还检查 DUT 和行为模型在 FIFO 为空时是否一致。

验证过程的代码如下所示。

PROC_VERIFY : process
begin
  wait until rising_edge(clk) and rst = '0';
  
  -- Check that DUT and TB FIFO are reporting empty simultaneously
  assert (empty = '1' and fifo.IsEmpty) or
         (empty = '0' and not fifo.IsEmpty)
    report "empty=" & std_logic'image(empty) 
      & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty)
    severity failure;

  -- Check that the valid signals are matching
  assert rd_valid = fifo_out_valid
    report "rd_valid=" & std_logic'image(rd_valid) 
      & " while fifo_out_valid=" & std_logic'image(fifo_out_valid)
    severity failure;

  -- Check that the output from the DUT matches the TB FIFO
  if rd_valid then
    assert fifo_out = to_integer(unsigned(rd_data))
      report "rd_data=" & integer'image(to_integer(unsigned(rd_data)))
        & " while fifo_out=" & integer'image(fifo_out)
      severity failure;
      report "Pop " & integer'image(fifo_out);
  end if;

end process;

该过程由时钟的上升沿触发,从第一行代码可以看出。 DUT 是一个时钟过程,连接到 DUT 的下游逻辑也有望与时钟信号同步。因此,在时钟上升沿检查输出是有意义的。

第二个代码块检查 empty 来自 DUT 的信号仅在测试台 FIFO 为空时才有效。 DUT 和行为模型都必须同意 FIFO 是否为空,此测试才能通过。

然后是读取数据有效信号的比较。 DUT和行为模型应该同时输出数据,否则有问题。

最后,我们检查 DUT 的输出数据是否与我们从测试台 FIFO 中弹出的下一个元素匹配。当然,这只有在 rd_valid 信号被断言,这意味着 rd_data 可以对信号进行采样。

收集覆盖率数据

为了控制测试台的主要流程,我们将创建一个排序器进程。此过程将初始化覆盖箱,运行测试,并在满足所有覆盖目标时停止测试平台。下面的代码显示了完整的«PROC_SEQUENCER»过程。

PROC_SEQUENCER : process
begin

  -- Set up coverage bins
  bin1.AddBins("Write while empty", ONE_BIN);
  bin2.AddBins("Read while full", ONE_BIN);
  bin3.AddBins("Read and write while almost empty", ONE_BIN);
  bin4.AddBins("Read and write while almost full", ONE_BIN);
  bin5.AddBins("Read without write when almost empty", ONE_BIN);
  bin6.AddBins("Write without read when almost full", ONE_BIN);

  wait until rising_edge(clk);
  wait until rising_edge(clk);
  rst <= '0';
  wait until rising_edge(clk);

  loop
    wait until rising_edge(clk);

    -- Collect coverage data
    bin1.ICover(to_integer(wr_en = '1' and empty = '1'));
    bin2.ICover(to_integer(rd_en = '1' and full = '1'));
    bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and
                           empty = '0' and empty_next = '1'));
    bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and
                           full = '0' and full_next = '1'));
    bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and
                           empty = '0' and empty_next = '1'));
    bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and
                           full = '0' and full_next = '1'));

    -- Stop the test when all coverage goals have been met
    exit when
      bin1.IsCovered and
      bin2.IsCovered and
      bin3.IsCovered and
      bin4.IsCovered and
      bin5.IsCovered and
      bin6.IsCovered;
  end loop;
  
  report("Coverage goals met");

  -- Make sure that the DUT is empty before terminating the test
  wr_en <= force '0';
  rd_en <= force '1';
  loop
    wait until rising_edge(clk);
    exit when empty = '1';
  end loop;

  -- Print coverage data
  bin1.WriteBin;
  bin2.WriteBin;
  bin3.WriteBin;
  bin4.WriteBin;
  bin5.WriteBin;
  bin6.WriteBin;
  
  finish;
end process;

首先,我们通过调用AddBins来初始化coverage bin对象 正如我们在本文前面已经讨论过的那样。然后,在重置释放后,我们继续收集覆盖率数据。在时钟的每个上升沿,循环结构内的代码都会运行。

循环内的第一个代码块用于收集覆盖率数据。我们可以调用ICover bin 上的程序来记录对它所代表的覆盖点的命中。如果我们提供整数参数 0 ,调用将无效。如果我们使用整数参数1 ,它将被视为命中。

每个覆盖 bin 对象内只有一个 «bin»,因为我们使用 ONE_BIN 对其进行了初始化 持续的。可以通过调用 ICover(1) 来访问这个单一的 bin .我们可以通过将布尔表达式转换为整数 1 来记录覆盖点上的命中或未命中 或 0 使用 to_integer 功能

记录覆盖率数据后,我们通过调用 IsCovered 检查是否已满足所有覆盖率目标 在所有垃圾箱上运行。然后,如果所有覆盖目标都已满足,我们将退出循环。

在终止测试之前,我们将确保 DUT 是空的。为了实现这一点,我们通过强制 wr_en 来接管写入器和读取器进程的控制权 向 '0' 发出信号 和 rd_en'1' 发出信号 .

最后,我们通过调用 WriteBin 打印出每个覆盖率目标已达到多少次的统计信息 在每个覆盖箱上运行。 finish 进程结束时的关键字会导致模拟器停止模拟。

运行测试台

您可以使用下面的表格下载整个 ModelSim 项目以及所有 VHDL 文件。

在我们通过执行包含在 Zip 中的 do-file 加载项目后,我们可以通过在 ModelSim 控制台中键入 «runtb» 来运行测试台。测试台的运行时间将是随机的,因为覆盖目标是随机命中的。但是,测试结果是可重现的,因为它实际上是一个伪随机序列。

我们没有在代码中初始化种子,这意味着默认种子值将用于伪随机生成器。可以通过调用 InitSeed 来设置不同的种子 RandomPType 上的程序 对象,这将产生不同的随机序列。

控制台输出

在我们给出 «runtb» 命令后打印到 ModelSim 控制台的输出如下所示。仿真运行时,测试台 FIFO 会有很多随机推送和弹出。

VSIM 2> runtb
# ** Warning: Design size of 15929 statements or 2 leaf instances exceeds
#             ModelSim PE Student Edition recommended capacity.
# Expect performance to be quite adversely affected.
# ** Note: Push 34910
#    Time: 790 ns  Iteration: 0  Instance: /ring_buffer_tb
...
# ** Note: Pop 37937
#    Time: 83100 ns  Iteration: 0  Instance: /ring_buffer_tb
# ** Note: Pop 13898
#    Time: 83110 ns  Iteration: 0  Instance: /ring_buffer_tb
# %% WriteBin: 
# %% Write while empty  Bin:(1)   Count = 2  AtLeast = 1
# 
# %% WriteBin: 
# %% Read while full  Bin:(1)   Count = 3  AtLeast = 1
# 
# %% WriteBin: 
# %% Read and write while almost empty  Bin:(1)   Count = 106  AtLeast = 1
# 
# %% WriteBin: 
# %% Read and write while almost full  Bin:(1)   Count = 1  AtLeast = 1
# 
# %% WriteBin: 
# %% Read without write when almost empty  Bin:(1)   Count = 1  AtLeast = 1
# 
# %% WriteBin: 
# %% Write without read when almost full  Bin:(1)   Count = 3  AtLeast = 1
#
# Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127

当满足所有覆盖目标时,打印出所有覆盖区域的统计信息。一些垃圾箱只被击中一次,而一个垃圾箱被击中 106 次。但最后,每个垃圾箱都至少被击中过一次。因此,我们可以知道,我们定义覆盖范围的所有事件都已经过测试和验证。

波形

让我们检查波形以了解测试台一直在做什么。下图显示了带有 fill_count 的波形 信号表示为模拟值。此信号的迹线在顶部时 FIFO 已满,在底部时为空。

从波形中我们可以看出,环形缓冲区是随机填充和清空的。然而,我们没有明确地对填充水平的这些倾斜和下降进行编程。尽管如此,我们还是看到了与 DUT 交互的有机外观模式。

关于约束随机验证的更多信息

当测试向量有太多排列而无法进行详尽的测试时,约束随机验证是一种很好的测试平台策略。与有向极端情况测试相比,随机交互表现出更自然的行为,而不会牺牲准确性。

只要我们正确设置了覆盖数据收集,我们就可以确定所有的极端情况都已得到满足。额外的好处是随机测试更有可能暴露 DUT 中您没有专门测试的弱点。只要您知道所有极端情况,就可以为它们创建定向测试。但极端情况很容易被忽视,此时您可以从受约束的随机验证方法中受益。

本文只触及了约束随机验证可以做什么的皮毛。我建议阅读 OSVVM GitHub 页面上的文档以深入了解该主题。

我还推荐 SynthWorks 的 Advanced VHDL Testbenches and Verification 课程,我不隶属于该课程。但是,我参加了这个物理课程的 5 天版本。该课程由 VHDL 分析和标准化小组 (VASG) 主席 Jim Lewis 教授。总体而言,对于任何希望将 VHDL 测试平台提升到新水平的公司来说,这都是一笔巨大的投资。


VHDL

  1. Cadence 加速十亿门 SoC 验证
  2. Siemens 添加到 Veloce 以实现无缝硬件辅助验证
  3. 项目探索物联网安全的可信设计和验证流程
  4. 高级验证:开启人工智能芯片新时代的大门
  5. Synopsys 通过 HBM3 IP 和验证实现多芯片设计
  6. 带二维码、RFID 和温度验证的访问控制
  7. 维护的不舒服、不可预测和随机的一面
  8. 如何在 Java 中生成随机数
  9. Java 8 - 流
  10. 带有验证图形的斜床身车床特征控制
  11. 高速PCB设计的差分等距处理和仿真验证
  12. 旋转式压缩机的 CAGI 性能验证程序