Skip to main content

使用 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.pyUVM 测试平台,并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_dbSystemVerilog 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_sequenceruvm_sequencer队列排序项目并将它们传递给Driver
  • 正如我们在上面看到的那样,我们存储self.seqrConfigDB()所以测试可以得到它。

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 报告。相反,我们使用 Pythonlogging模块。每个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}")

覆盖范围

Coverageextends uvm_subscriberwhich 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_itemfinish_itemuvm_sequence.

很明显,start_itemandfinish_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)

此虚拟序列启动另外两个序列:RandomSeqMaxSeqRandomSeq随机化操作数。

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

除非适用法律要求或书面同意,否则根据许可分发的软件将按“原样”分发,没有任何明示或暗示的保证或条件。有关许可下的特定语言管理权限和限制,请参阅许可。

项目详情