用 OSVVM 掌握约束随机验证
约束随机验证是一种测试平台策略,依靠为被测设备 (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 视为黑盒,不假设任何实现知识。毕竟本文重点是测试平台,非环形缓冲区 FIFO。
测试平台用实体实例化法实例化 DUT。实例化平淡无奇,暂省略代码,稍后可在文中下载。
DUT 泛型映射为:
- RAM_WIDTH: 16
- RAM_DEPTH: 256
测试平台策略
实现前先梳理测试策略。下图展示我们将创建的测试平台核心概念。
我们在 DUT 输入侧执行随机写事务。输入数据每时钟周期设为随机值,写使能脉冲持续随机周期数。
类似地,随机执行读操作。读使能信号以随机时钟周期数的突发方式置位。
并行放置行为模型。这是接口相同但实现不同于 DUT 环形缓冲区的 FIFO。行为模型无需可综合,允许用高级 VHDL 编程特性创建。
在单独进程中将 DUT 输出与行为模型输出每时钟周期用断言语句比对。若两 FIFO 实现任何时刻行为不同,断言失败终止仿真并报错。
最后通过观测进出 DUT 事务收集功能覆盖数据。功能覆盖点如同时读写、FIFO 至少填满一次。主定序器进程监控这些事件。所有监控覆盖事件发生后停止仿真。
导入 OSVVM 库
OSVVM 库兼容任何支持 VHDL-2008 的仿真器。可能已随仿真器默认库提供。ModelSim PE 学生版含 OSVVM,可从 Mentor Graphics 免费下载。
ModelSim 附带较旧 OSVVM 版本,但够用,含所需一切。直接导入随机和覆盖包:
library osvvm; use osvvm.RandomPkg.all; use osvvm.CoveragePkg.all;
最新 OSVVM 版本随时可从 GitHub 下载。若仿真器未含或需最新特性请下载。
声明 OSVVM 变量
OSVVM 包含保护类型包。这些类型创建的变量作用域限于定义进程。因此我们在测试平台架构声明区将其声明为共享变量:
-- OSVVM variables shared variable rv : RandomPType; shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType;
rv 变量类型 RandomPType 用于生成随机值。仅需一个,因可在所有需随机值的进程复用。最后一行声明六个 CovPType 变量。
声明六个覆盖变量因我们有六个覆盖目标,称作「箱」。共享变量使用前须初始化。调用 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 第一参数传覆盖箱字符串描述。该字符串在仿真结束打印各箱统计时重现。从文本描述可见,我们用箱检查特定角落案例是否发生。
AddBins 为重载过程,可在箱变量内创建多记分板。但我们每箱仅一个记分板。故向 AddBins 传便利常量 ONE_BIN。这将 CovPType 变量初始化为各一箱。箱代表的记分板在监控事件至少发生一次时视为覆盖。
生成随机输入
先创建生成 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_en 为 '1' 时才理会 wr_data 端口。循环后程序回到进程开头,触发另一随机写事务。
执行随机读
读取 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;
上方代码展示链表 FIFO 包声明。为保护类型,有 Push、Pop、IsEmpty 三函数。分别用于向 FIFO 添加/移除元素、检查是否为空。
-- Testbench FIFO that emulates the DUT shared variable fifo : LinkedList;
保护类型是 VHDL 类式构造。在测试平台声明区声明共享变量创建链表对象,如上所示。
行为模型
为完全仿真环形缓冲区 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 置位且 full 为 '0' 时,行为模型向测试平台 FIFO 推新值。类似地,读逻辑监听 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 下游逻辑也应同步时钟。故在上升沿检查输出合理。
第二代码块检查 DUT empty 信号仅在测试平台 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 初始化覆盖箱对象,如前文讨论。释放复位后收集覆盖数据。每时钟上升沿循环内代码运行。
循环内第一代码块收集覆盖数据。调用箱 ICover 过程记录覆盖点命中。整数参数 0 无效果;1 计为命中。
每覆盖箱对象内仅单「箱」,因用 ONE_BIN 常量初始化。此单箱可由 ICover(1) 触达。用 to_integer 函数将布尔表达式转整数 1 或 0 记录命中或未命中。
记录覆盖数据后,调用所有箱 IsCovered 函数检查所有覆盖目标是否达成。全部达成则退出循环。
终止测试前确保 DUT 为空。通过强制 wr_en 为 '0'、rd_en 为 '1' 从写/读进程接管控制权。
最后调用各覆盖箱 WriteBin 函数打印每目标命中次数统计。进程末尾 finish 关键字停止仿真。
运行测试平台
下方表单可下载含所有 VHDL 文件的完整 ModelSim 工程。
加载工程执行 Zip 含 do-file 后,在 ModelSim 控制台输入「runtb」运行测试平台。运行时间随机,因覆盖目标随机命中。但结果可复现,因用伪随机序列。
代码未初始化种子,使用伪随机生成器默认种子。可调用 RandomPType 对象 InitSeed 过程设不同种子,产生不同随机序列。
控制台输出
输入「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 高级 VHDL 测试平台与验证课程,我无关联。但我参加过其 5 天实体课程。课程由 VHDL 分析与标准化组 (VASG) 主席 Jim Lewis 讲授。总体而言,是任何想将 VHDL 测试平台推向更高层次公司的绝佳投资。
VHDL
- 维护的不可预测挑战:专家 Jason Afara 的洞见
- 集成二维码、RFID与体温验证的门禁控制系统
- 高速PCB设计:差分信号处理与仿真验证
- Hurco TM8 斜床身车床:紧凑设计、精密控制、多功能刀具
- CAGI旋转压缩机性能验证计划:权威背书可靠高效运行
- Cadence发布面向十亿门级SoC验证的高性能仿真与原型平台
- Siemens推出四款新品强化Veloce系统,驱动无缝硬件辅助验证
- VE-VIDES倡议发布值得信赖的IoT安全设计与验证框架
- 先进验证技术:铺平下一代AI芯片之路
- Synopsys发布完整HBM3 IP套件,面向多芯片系统将带宽提升至921 GB/s
- Java 生成随机数实战:Random 与 Math.random 详解
- Java 8 Stream API:声明式数据处理与并行计算完全指南