使用 cocotb 的 UVM 的 Python 实现
项目描述
描述
pyuvm是用 Python 而不是 SystemVerilog 实现的通用验证方法。pyuvm使用 cocotb 与模拟器交互并安排模拟事件。
pyuvm实现了 UVM 最常用的部分,同时利用了 Python 没有严格类型且不需要参数化类的事实。该项目重构了由于键入或遗留代码而过于复杂的 UVM 部分。
该代码基于 IEEE 1800.2 规范,大多数类和方法在注释中都有规范参考。
已实现以下 IEEE 1800.2 部分:
部分 | 姓名 | 描述 |
---|---|---|
5 | 基类 | uvm_object 不捕获事务时间信息 |
6 | 报告类 | 利用日志记录,使用 UVM 层次结构进行控制 |
8 | 工厂班 | 所有 uvm_void 类自动注册 |
9 | 分阶段 | 简化为仅常见的阶段。支持异议系统 |
12 | UVM TLM 接口 | 全面实施 |
13 | 预定义的组件类 | 实现具有层次结构的 uvm_component、uvm_root 单例、run_test()、简化的 ConfigDB、uvm_driver 等 |
14 & 15 | 序列,定序器,sequence_item | 利用 Python 语言功能重构的定序器功能。更简单直接的实现 |
安装
您可以使用. pip
这还将安装cocotb作为 pyuvm 的要求。
% pip install pyuvm
然后你可以运行一个简单的测试:
% python
>>> from pyuvm import *
>>> my_object = uvm_object("my_object")
>>> type(my_object)
<class 's05_base_classes.uvm_object'>
>>> print("object name:", my_object.get_name())
object name: my_object
从存储库运行
您可以通过使用 pip 安装克隆的存储库来从克隆的存储库运行 pyuvm。
% cd <pyuvm repo directory>
% pip install -e .
用法
本节演示运行示例模拟,然后展示如何将示例组合在一起,展示 Python 中 UVM 的外观。
运行模拟
顾名思义,TinyALU 是一个微型 ALU。它有四个操作:ADD、AND、NOT 和 MUL。此示例显示我们运行设计的 Verilog 版本,但也有 VHDL 版本。
cocotb使用 Makefile 来运行它的模拟。我们看到它examples/TinyALU
:
CWD=$(shell pwd)
COCOTB_REDUCED_LOG_FMT = True
SIM ?= icarus
VERILOG_SOURCES =$(CWD)/hdl/verilog/tinyalu.sv
MODULE := testbench
TOPLEVEL=tinyalu
TOPLEVEL_LANG=verilog
COCOTB_HDL_TIMEUNIT=1us
COCOTB_HDL_TIMEPRECISION=1us
include $(shell cocotb-config --makefiles)/Makefile.sim
您可以在cocotb.org了解有关 Makefile 目标的更多信息。最后一行的cocotb-config
命令指向cocotb Makefile 位置并启动sim
目标。
修改SIM
变量以匹配您的模拟器。所有模拟器类型都在cocotb/share/makefiles/simulators/makefile.$(SIM)
.
您应该能够像这样运行模拟:
% cd <path>/pyuvm/examples/TinyALU
% make sim
cocotb会呈现很多消息,但是在它们中间你会看到这些 UVM 消息。它运行四个示例,每个命令一个随机操作数。
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0x34 ADD 0x23 = 0x0057
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xf9 AND 0x29 = 0x0029
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0x71 XOR 0x01 = 0x0070
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xb8 MUL 0x47 = 0x3308
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xff ADD 0xff = 0x01fe
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xff AND 0xff = 0x00ff
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xff XOR 0xff = 0x0000
250000.00ns INFO testbench.py(209)[uvm_test_top.env.scoreboard]: PASSED: 0xff MUL 0xff = 0xfe01
TinyAluBfm
在_tinyalu_utils.py
这TinyAluBfm
是一个使用cocotb与 TinyALU 通信的单例。BFM 向用户公开了三个协程:send_op()
、get_cmd()
和get_result()
。
单例使用cocotb.top
变量来获取 DUT 的句柄。这是我们通常传递给cocotb.test()
协程的句柄。
在我们的测试平台TinyAluBfm
中定义tinyalu_utils.py
并导入。
pyuvm
测试台
包含整个testbench.py
UVM 测试平台,并TinyAluBfm
通过tinyalu_utils.py
. 我们将检查testbench.py
文件和足够的cocotb测试也运行模拟
输入pyuvm
用 SystemVerilog UVM 编写的测试平台通常会像这样导入包:
import uvm_pkg::*;
这使您无需包路径即可访问类名。要获得与pyuvm
我们类似的行为,请使用from
导入语法。我们导入pyuvm
以区分@pyuvm.test()
装饰器和@cocotb.test()
装饰器:
import pyuvm
from pyuvm import *
AluTest 课程
我们将从顶部(测试)到底部(序列)检查 UVM 类。
pyuvm 按照规范中的名称命名 UVM 类。因此 **pyvu 使用下划线命名,就像在 SystemVerilog 中所做的那样,而不是驼峰式命名。
即使pyuvm不使用它,我们也可以在我们的代码中使用驼峰式大小写扩展uvm_test
来创建,:AluTest
您将在测试中看到以下内容:
-
我们定义了一个扩展类
uvm_test
。 -
我们使用
@pyuvm.test()
装饰器通知cocotb这是一个测试。 -
没有
uvm_component_utils()
宏。pyuvm自动注册uvm_void
与工厂一起扩展的类。 -
相位没有
phase
变量。阶段已被重构为仅支持规范中描述的常见阶段。 -
我们使用
create()
方法和工厂创建环境。请注意,这create()
现在是一个简单的类方法。没有打字驱动的咒语。 -
raise_objection()
现在是一种uvm_component
方法。不再有phase
变量。 -
单
ConfigDB()
例的行为方式与uvm_config_db
SystemVerilog UVM 中的接口相同。pyuvm重构了,uvm_resource_db
因为没有要管理的类的问题。 -
pyuvm利用 Python 日志记录系统,并没有实现 UVM 报告系统。的每个后代
uvm_report_object
都有一个logger
数据成员。 -
序列的工作方式与它们在 SystemVerilog UVM 中的工作方式相同。
@pyuvm.test()
class AluTest(uvm_test):
def build_phase(self):
self.env = AluEnv("env", self)
def end_of_elaboration_phase(self):
self.test_all = TestAllSeq.create("test_all")
async def run_phase(self):
self.raise_objection()
await self.test_all.start()
self.drop_objection()
我们扩展AluTest
该类以创建测试的并行版本和斐波那契程序。您可以在以下位置找到这些序列testbench.py
@pyuvm.test()
class ParallelTest(AluTest):
def build_phase(self):
uvm_factory().set_type_override_by_type(TestAllSeq, TestAllForkSeq)
super().build_phase()
@pyuvm.test()
class FibonacciTest(AluTest):
def build_phase(self):
ConfigDB().set(None, "*", "DISABLE_COVERAGE_ERRORS", True)
uvm_factory().set_type_override_by_type(TestAllSeq, FibonacciSeq)
return super().build_phase()
UVM 测试平台的所有熟悉部分都在pyuvm中。
ALUEnv 类
该类uvm_env
是构成测试平台的组件的容器。实例化了四个组件类:
Monitor
— 实际上实例化了两个监视器,一个用于监视命令 (self.cmd_mod
),另一个用于监视结果 (self.result_mon
)。两者的Monitor
代码相同。我们将获取他们监控的数据的代理函数的名称传递给他们。Scoreboard
— 记分板收集所有命令和结果,并将预测结果与实际结果进行比较。Coverage
——覆盖类检查我们是否覆盖了所有类型的操作,如果我们没有覆盖,则会发出错误。Driver
— 这会uvm_driver
处理序列项目。uvm_sequencer
—uvm_sequencer
队列排序项目并将它们传递给Driver
- 正如我们在上面看到的那样,我们存储
self.seqr
在ConfigDB()
所以测试可以得到它。
在AluEnv
中创建所有这些组件build_phase()
并将导出连接到中的端口connect_phase()
。build_phase()
是自上而下的阶段,而是connect_phase()
自下而上的阶段。
class AluEnv(uvm_env):
def build_phase(self):
self.seqr = uvm_sequencer("seqr", self)
ConfigDB().set(None, "*", "SEQR", self.seqr)
self.driver = Driver.create("driver", self)
self.cmd_mon = Monitor("cmd_mon", self, "get_cmd")
self.coverage = Coverage("coverage", self)
self.scoreboard = Scoreboard("scoreboard", self)
def connect_phase(self):
self.driver.seq_item_port.connect(self.seqr.seq_item_export)
self.cmd_mon.ap.connect(self.scoreboard.cmd_export)
self.cmd_mon.ap.connect(self.coverage.analysis_export)
self.driver.ap.connect(self.scoreboard.result_export)
监视器
Monitor
延伸uvm_component
。_ 将CocotProxy
方法名称的名称作为参数。它使用名称在代理中查找方法,然后调用该方法。您不能在 SystemVerilog 中执行此操作,因为没有自省。
创建一个分析端口并将其Monitor
获取的数据写入分析端口。
注意在run_phase()
我们使用await
关键字等待get_cmd
协程。与 SystemVerilog 不同,Python 可以清楚地说明您何时调用耗时的任务与函数。还要注意,run_phase()
有async
关键字来指定它是一个协程。(SystemVerilog 中的一个任务。)
class Monitor(uvm_component):
def __init__(self, name, parent, method_name):
super().__init__(name, parent)
self.method_name = method_name
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
self.bfm = TinyAluBfm()
self.get_method = getattr(self.bfm, self.method_name)
async def run_phase(self):
while True:
datum = await self.get_method()
self.logger.debug(f"MONITORED {datum}")
self.ap.write(datum)
记分牌
记分板以相同的顺序接收来自命令监视器的命令和来自结果监视器的结果。它使用命令来预测结果并进行比较。
build_phase
用于uvm_tlm_analysis_fifos
从监视器接收数据并存储它的用途。- 记分板通过将 FIFO 导出复制到类数据成员中来公开它们。正如我们在上面的环境中看到的,这使我们能够在不进入
Scoreboard's
内部工作的情况下连接导出。 - 我们连接出口
connect_phase()
- run_phase
check_phase() runs after the
()`。此时计分板已包含所有操作和结果。它循环操作并预测结果,然后将预测结果与实际结果进行比较。 - 请注意,我们不使用 UVM 报告。相反,我们使用 Python
logging
模块。每个uvm_report_object
和它的孩子都有自己的记录器存储在self.logger.
class Scoreboard(uvm_component):
def build_phase(self):
self.cmd_fifo = uvm_tlm_analysis_fifo("cmd_fifo", self)
self.result_fifo = uvm_tlm_analysis_fifo("result_fifo", self)
self.cmd_get_port = uvm_get_port("cmd_get_port", self)
self.result_get_port = uvm_get_port("result_get_port", self)
self.cmd_export = self.cmd_fifo.analysis_export
self.result_export = self.result_fifo.analysis_export
def connect_phase(self):
self.cmd_get_port.connect(self.cmd_fifo.get_export)
self.result_get_port.connect(self.result_fifo.get_export)
def check_phase(self):
while self.result_get_port.can_get():
_, actual_result = self.result_get_port.try_get()
cmd_success, cmd = self.cmd_get_port.try_get()
if not cmd_success:
self.logger.critical(f"result {actual_result} had no command")
else:
(A, B, op_numb) = cmd
op = Ops(op_numb)
predicted_result = alu_prediction(A, B, op)
if predicted_result == actual_result:
self.logger.info(f"PASSED: 0x{A:02x} {op.name} 0x{B:02x} ="
f" 0x{actual_result:04x}")
else:
self.logger.error(f"FAILED: 0x{A:02x} {op.name} 0x{B:02x} "
f"= 0x{actual_result:04x} "
f"expected 0x{predicted_result:04x}")
覆盖范围
类Coverage
extends uvm_subscriber
which extends uvm_analysis_export
。正如我们在AluEnv
上面看到的,这允许我们将对象直接传递给connect()
方法以将其连接到分析端口。
该类Coverage
覆盖了write()
a 的预期方法uvm_subscriber
。如果没有,您将收到运行时错误。该类Coverage
使用一个集合来存储所有看到的操作。然后它从所有操作的集合中减去它。如果结果的长度超过0
它会发出错误。
由于此 tesbench 循环遍历所有操作,因此您不会看到此错误。
class Coverage(uvm_subscriber):
def end_of_elaboration_phase(self):
self.cvg = set()
def write(self, cmd):
(_, _, op) = cmd
self.cvg.add(op)
def report_phase(self):
try:
disable_errors = ConfigDB().get(
self, "", "DISABLE_COVERAGE_ERRORS")
except UVMConfigItemNotFound:
disable_errors = False
if not disable_errors:
if len(set(Ops) - self.cvg) > 0:
self.logger.error(
f"Functional coverage error. Missed: {set(Ops)-self.cvg}")
assert False
else:
self.logger.info("Covered all operations")
assert True
司机
Driver
扩展,因此uvm_driver
它适用于序列和序列项。
从connect_phase()
获取代理ConfigDB()
并run_phase()
使用它来获取项目并通过调用来处理它们send_op
。我们使用while True
是因为我们永远这样做。cocotbrun_phase
将在模拟结束时关闭协程。
class Driver(uvm_driver):
def build_phase(self):
self.ap = uvm_analysis_port("ap", self)
def start_of_simulation_phase(self):
self.bfm = TinyAluBfm()
async def launch_tb(self):
await self.bfm.reset()
self.bfm.start_tasks()
async def run_phase(self):
await self.launch_tb()
while True:
cmd = await self.seq_item_port.get_next_item()
await self.bfm.send_op(cmd.A, cmd.B, cmd.op)
result = await self.bfm.get_result()
self.ap.write(result)
cmd.result = result
self.seq_item_port.item_done()
ALU 序列
ALU 序列创建序列项,将它们随机化并将它们发送到Driver
. 它继承start_item
并finish_item
从uvm_sequence
.
很明显,start_item
andfinish_item
阻塞是因为我们使用await
关键字调用它们。 start_item
等待轮到使用序列器,finish_item
并将 sequence_item 发送给驱动程序并在驱动程序调用时返回item_done()
class TestAllSeq(uvm_sequence):
async def body(self):
seqr = ConfigDB().get(None, "", "SEQR")
random = RandomSeq("random")
max = MaxSeq("max")
await random.start(seqr)
await max.start(seqr)
此虚拟序列启动另外两个序列:RandomSeq
和MaxSeq
。RandomSeq
随机化操作数。
class RandomSeq(uvm_sequence):
async def body(self):
for op in list(Ops):
cmd_tr = AluSeqItem("cmd_tr", None, None, op)
await self.start_item(cmd_tr)
cmd_tr.randomize_operands()
await self.finish_item(cmd_tr)
MaxSeq
将操作数设置为0xff
:
class MaxSeq(uvm_sequence):
async def body(self):
for op in list(Ops):
cmd_tr = AluSeqItem("cmd_tr", 0xff, 0xff, op)
await self.start_item(cmd_tr)
await self.finish_item(cmd_tr)
ALU 序列项
AluSeqItem
包含 TinyALU 命令。它有两个操作数和一个操作。
SystemVeriloguvm_sequence_item
类用于convert2string()
将项目转换为字符串并将do_compare()
项目与另一个项目进行比较。我们不在pyuvm中使用这些,因为 Python 有执行这些功能的魔术方法。
__eq__()
do_compare()
——这和它返回的事情是一样的,True
如果项目是相等的。此方法适用于==
操作员。
__str__()
——这与convert2string()
. 它返回项目的字符串版本。该print
函数自动调用此方法。
class AluSeqItem(uvm_sequence_item):
def __init__(self, name, aa, bb, op):
super().__init__(name)
self.A = aa
self.B = bb
self.op = Ops(op)
def randomize_operands(self):
self.A = random.randint(0, 255)
self.B = random.randint(0, 255)
def randomize(self):
self.randomize_operands()
self.op = random.choice(list(Ops))
def __eq__(self, other):
same = self.A == other.A and self.B == other.B and self.op == other.op
return same
def __str__(self):
return f"{self.get_name()} : A: 0x{self.A:02x} \
OP: {self.op.name} ({self.op.value}) B: 0x{self.B:02x}"
现在我们已经有了 UVM 测试平台,我们可以从cocotb测试中调用它。
Cocotb 测试
cocotb找到uvm_test
用@pyuvm.test()
装饰器标识的类并将它们作为协程启动。我们的测试执行以下操作:
贡献
您可以pyuvm
通过分叉此存储库并提交拉取请求来做出贡献。
存储库使用tox
. flake8
如果 linter 发现任何问题,测试就会运行
并失败。Visual Studio Code 可以设置为自动检查flake8
问题。存储库忽略来自flake8
.
有三组pytest
测试可以测试不使用协程的功能。其余的测试都在tests/cocotb_tests
并且需要一个模拟器来运行。
学分:
- Ray Salemi——原作者,作为西门子员工创建。
- IEEE 1800.2 规范
- 西门子在这方面支持我。
执照
版权所有 2020 西门子 EDA
根据 Apache 许可证 2.0 版(“许可证”)获得许可;除非遵守许可,否则您不得使用此文件。您可以在以下网址获取许可证的副本
http://www.apache.org/licenses/LICENSE-2.0
除非适用法律要求或书面同意,否则根据许可分发的软件将按“原样”分发,没有任何明示或暗示的保证或条件。有关许可下的特定语言管理权限和限制,请参阅许可。