00384 PyUVM 完整入门教程 —— 基于 TinyALU 的 UVM 验证平台


PyUVM 完整入门教程 —— 基于 TinyALU 的 UVM 验证平台

本教程从零开始,面向初学者,带你理解 UVM(Universal Verification Methodology) 的核心概念,并一步步搭建基于 PyUVMVerilator 的仿真环境,实现一个简易的 TinyALU,编写完整的 Python Testbench 进行功能验证。


目录

  1. 什么是 UVM?
  2. 什么是 PyUVM?
  3. PyUVM 核心架构详解
  4. 项目概述
  5. 环境 Setup
  6. RTL 源码详解
  7. Makefile
  8. PyUVM Testbench 详解
  9. 运行仿真
  10. PyUVM 常见踩坑与教训
  11. 扩展建议
  12. 总结

1. 什么是 UVM?

UVM(Universal Verification Methodology,通用验证方法学) 是 IC 行业中最主流的 RTL 验证方法论。它由 IEEE 定义在 IEEE Standard for Universal Verification Methodology Language Reference Manual(即 IEEE 1800.2 标准)中。

1.1 为什么需要 UVM?

在传统的数字验证中,验证工程师需要为每个新设计从头编写 Testbench。这导致:

  • 代码难以复用:每个项目的 Testbench 都是”一次性”的。
  • 结构不统一:不同工程师写的 Testbench 风格迥异,难以维护。
  • 协作困难:新人加入项目后,需要很长时间才能理解现有的 Testbench 架构。

UVM 通过提供一套标准化的验证组件和流程,解决了以上问题:

  • 组件复用:Sequence、Driver、Monitor、Scoreboard 等组件可以在不同项目间复用。
  • 统一结构:所有 UVM Testbench 都遵循相同的层次结构(Test → Env → Agent → Driver/Monitor)。
  • 工厂机制:通过 uvm_factory 可以在不修改源码的情况下替换组件类型。

1.2 UVM 的核心组件

组件 英文 职责
事务 Sequence Item 描述一次总线传输或操作的参数
序列 Sequence 生成一系列事务,交给 Sequencer
序列器 Sequencer 仲裁器,连接 Sequence 和 Driver
驱动器 Driver 从 Sequencer 获取事务,驱动到 DUT 接口
监视器 Monitor 被动监控 DUT 接口,收集数据
记分板 Scoreboard 对比预期结果和实际结果
环境 Env 容器,包含 Sequencer、Driver、Monitor 等
测试 Test 顶层容器,负责配置环境、启动 Sequence

2. 什么是 PyUVM?

PyUVM 是 UVM 的 Python 实现,基于 IEEE 1800.2 标准。它构建在 cocotb 之上,使用 Python 的面向对象特性来实现 UVM 的核心功能。

虽然 IEEE 1800.2 标准使用 SystemVerilog 定义,但标准本身并不强制要求用 SystemVerilog 实现 UVM 库。任何具有足够面向对象支持的语言都可以实现它——例如 Python。PyUVM 正是利用 Python 的易用性和强大的 OOP 能力,实现了 IEEE 1800.2 中最常用的部分。

2.1 PyUVM vs SystemVerilog UVM

特性 SystemVerilog UVM PyUVM
语言 SystemVerilog Python
宏依赖 大量宏(uvm_component_utils 等) 无宏,自动工厂注册
类型系统 严格静态类型,需参数化类 Python 动态类型,更灵活
多继承 不支持 支持
相位参数 phase 变量贯穿所有 phase 已简化,仅保留常用 phase
报告系统 UVM 专用报告宏 集成 Python logging 模块
仿真交互 直接调用仿真器 通过 cocotb 调用仿真器

2.2 PyUVM 实现的 IEEE 1800.2 章节

章节 名称 说明
5 Base Classes uvm_voiduvm_object 等基础类
6 Reporting Classes 使用 Python logging 模块实现
8 Factory Classes 完整工厂功能,无需宏
9 Phasing 仅实现常用 phase(build/connect/run/check/report)
12 UVM TLM Interfaces 完整的 TLM 系统
13 预定义组件 uvm_componentuvm_rootConfigDB
14 & 15 Sequences Sequence、Sequencer、Sequence Item(重构了 Sequencer 功能)

3. PyUVM 核心架构详解

在深入代码之前,我们需要先理解 PyUVM 的各个核心概念。本节的内容完全基于 pyuvm 对 IEEE 1800.2 的实现。

3.1 导入 PyUVM

SystemVerilog UVM 中通常这样导入:

import uvm_pkg::*;

在 PyUVM 中,使用 from 导入语法可以达到类似效果——无需包路径前缀即可使用类名:

import pyuvm
from pyuvm import *

为什么要同时导入两种方式?

  • from pyuvm import * 让你可以直接使用 uvm_testuvm_env 等类名,无需写 pyuvm.uvm_test
  • import pyuvm 让你可以使用 @pyuvm.test() 装饰器,明确区分 PyUVM 测试和 @cocotb.test() 装饰器。

3.2 基础类(Base Classes)

PyUVM 的类层次从 uvm_void 开始,所有 UVM 类都继承自它:

uvm_void
├── uvm_object
│   ├── uvm_sequence_item
│   └── ...
└── uvm_component
    ├── uvm_test
    ├── uvm_env
    ├── uvm_driver
    ├── uvm_monitor
    └── ...
  • **uvm_void**:最顶层的基类,没有任何数据成员。
  • **uvm_object**:所有非组件类(如 Sequence Item)的基类,提供 get_name()get_full_name() 等方法。
  • **uvm_component**:所有组件类(如 Test、Env、Driver)的基类,提供层次化构建功能和 phase 支持。

3.3 工厂机制(Factory Classes)

SystemVerilog UVM 中,你必须使用 uvm_component_utils 宏来注册类到工厂:

class my_driver extends uvm_driver;
    `uvm_component_utils(my_driver)  // 必须写这个宏

PyUVM 完全不需要宏。所有继承自 uvm_void 的类都会自动注册到工厂。你可以直接:

# 自动注册,无需任何宏
class Driver(uvm_driver):
    pass

# 使用工厂创建实例
driver = Driver.create("driver", self)

工厂还支持类型覆盖(Type Override),允许你在不修改源代码的情况下替换组件类型:

# 将 TestAllSeq 替换为 TestAllForkSeq
uvm_factory().set_type_override_by_type(TestAllSeq, TestAllForkSeq)

create() 是类方法:PyUVM 的 create() 是一个简单的类方法,不像 SystemVerilog 那样需要复杂的类型驱动调用。

3.4 相位系统(Phasing)

IEEE 1800.2 定义了两套 phase 系统:

  • 常用 Phase(Common Phases):这是所有人都使用的,包括 build、connect、run、check 等。
  • 自定义 Phase(Custom Phases):几乎没有人使用的复杂系统。

PyUVM 只实现了常用 Phase,但这已经覆盖了 99% 的使用场景。你可以通过 Python OOP 技术来扩展 phase。

常用 Phase 的执行顺序和特性如下:

Phase 遍历方向 说明
build_phase 自顶向下(Top-down) 父组件先创建子组件。用于实例化 Env、Driver、Monitor 等。
connect_phase 自底向上(Bottom-up) 子组件先连接。用于连接 TLM Port/Export(如 Driver 连接 Sequencer)。
end_of_elaboration_phase 自顶向下 连接完成后,做一些最终配置(如创建 Sequence)。
start_of_simulation_phase 自顶向下 仿真开始前,做一些初始化(如获取 BFM 句柄)。
run_phase 自顶向下 核心运行阶段,启动 Sequence、驱动 DUT。是唯一的 async phase。
extract_phase 自底向上 从各组件中提取数据。
check_phase 自底向上 检查数据(如 Scoreboard 对比结果)。
report_phase 自底向上 生成报告(如 Coverage 报告)。
final_phase 自顶向下 仿真结束前的清理工作。

关键特性

  • build_phaseconnect_phase最常用的两个 phase。
  • run_phase唯一需要 async 关键字的 phase,因为它内部会等待仿真事件。
  • PyUVM 的 phase 没有 phase 参数变量。SystemVerilog UVM 中每个 phase 方法都带一个 phase 变量,但 PyUVM 已经将其简化掉了。

3.5 报告系统(Reporting Classes)

PyUVM 没有实现自己的报告系统,而是直接集成 Python 的 logging 模块。

每个 uvm_report_object 及其子类(包括所有 uvm_component)都有一个 self.logger 数据成员:

class Scoreboard(uvm_component):
    def check_phase(self):
        self.logger.info("这是 INFO 级别的日志")
        self.logger.error("这是 ERROR 级别的日志")
        self.logger.critical("这是 CRITICAL 级别的日志")

PyUVM 的日志输出格式与 cocotb 保持一致,包含时间戳、日志级别、文件名行号和组件层次路径:

49000.00ns INFO  testbench.py(244) [uvm_test_top.env.scoreboard]: PASSED: ...

3.6 TLM 接口(Transaction Level Modeling)

PyUVM 完整实现了 UVM TLM 系统。TLM 是组件之间传递事务(Transaction)的标准机制。

在本教程中使用的 TLM 组件:

TLM 类型 用途 代码示例
uvm_analysis_port 发送端,向多个订阅者广播事务 self.ap = uvm_analysis_port("ap", self)
uvm_analysis_export 接收端,连接 analysis_port self.cmd_export = self.cmd_fifo.analysis_export
uvm_tlm_analysis_fifo 带 FIFO 的分析通道,缓存事务 self.cmd_fifo = uvm_tlm_analysis_fifo("cmd_fifo", self)
uvm_get_port 从 FIFO 中读取事务 self.cmd_get_port = uvm_get_port("cmd_get_port", self)
seq_item_port / seq_item_export Sequence 与 Driver 之间的专用通道 self.driver.seq_item_port.connect(self.seqr.seq_item_export)

连接(connect())在 connect_phase 中完成

def connect_phase(self):
    self.driver.seq_item_port.connect(self.seqr.seq_item_export)
    self.cmd_mon.ap.connect(self.scoreboard.cmd_export)

3.7 ConfigDB(配置数据库)

SystemVerilog UVM 中有两套配置数据库:uvm_config_dbuvm_resource_db。PyUVM 简化为单一的 ConfigDB() 单例,因为 Python 中没有 SystemVerilog 的类管理问题。

写入 ConfigDB

# 第一个参数 context 通常为 None,表示全局设置
ConfigDB().set(None, "*", "SEQR", self.seqr)

读取 ConfigDB

# 在 uvm_component 子类中,第一个参数传 self
self.dut = ConfigDB().get(self, "", "DUT")

# 在 uvm_sequence 中,第一个参数必须传 None(因为 Sequence 不是 uvm_component)
seqr = ConfigDB().get(None, "", "SEQR")

3.8 Objection 机制

Objection 机制控制 run_phase 的生命周期:

  • **raise_objection()**:告诉 UVM “我还在运行,不要结束仿真”。
  • **drop_objection()**:告诉 UVM “我完成了,可以结束 run_phase 了”。

当所有组件的 objection 都被 drop 后,run_phase 结束,UVM 进入 extract/check/report 等后续 phase。

async def run_phase(self):
    self.raise_objection()       # 开始运行,保持仿真存活
    await self.test_all.start()  # 执行测试主体
    self.drop_objection()        # 测试完成,允许仿真结束

3.9 Sequencer 与 Sequence 机制

PyUVM 重构了 Sequencer 功能,相比 SystemVerilog UVM 的实现更加简洁。

Sequence 的核心 API

方法 是否阻塞 说明
start_item(item) 请求使用 Sequencer,等待获得仲裁权
finish_item(item) 发送事务给 Driver,等待 Driver 调用 item_done()
get_response() 获取 Driver 返回的响应(可选)

Driver 的核心 API

方法 是否阻塞 说明
get_next_item() 从 Sequencer 队列取出一个事务
item_done(rsp) 通知 Sequencer 事务已完成,可附带响应

Sequence / Driver / Sequencer 数据流

  1. Sequence 调用 start_item() 将事务放入 Sequencer 队列,并等待 Sequencer 就绪。
  2. Driver 调用 get_next_item() 从队列取出事务。
  3. Sequencer 通知 Sequence “item 已就绪”,Sequence 调用 finish_item()
  4. Driver 收到 finish_item 信号后开始执行(驱动 DUT)。
  5. Driver 执行完成后调用 item_done(rsp) 将结果返回给 Sequencer。
  6. Sequence 调用 get_response() 获取结果(如果需要)。

3.10 Sequence Item 的 Python 化

SystemVerilog UVM 使用 convert2string()do_compare() 来处理字符串转换和对象比较。PyUVM 利用 Python 的魔术方法(Magic Methods)替代了这些功能:

SystemVerilog Python 魔术方法 触发方式
convert2string() __str__() print(obj)str(obj)
do_compare() __eq__() obj1 == obj2
class AluSeqItem(uvm_sequence_item):
    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} B: 0x{self.B:02x}"

# 使用方式
item1 = AluSeqItem("item1", 1, 2, Ops.ADD)
item2 = AluSeqItem("item2", 1, 2, Ops.ADD)
print(item1 == item2)  # True,因为 __eq__ 被重载
print(item1)           # 自动调用 __str__

4. 项目概述

我们要验证的是一个 TinyALU(微型算术逻辑单元),它支持四种操作:

  • ADD:加法
  • AND:按位与
  • XOR:按位异或
  • MUL:乘法(三周期)

验证方面,我们使用:

  • PyUVM 提供标准 UVM 验证架构。
  • cocotb 作为仿真框架,与 Verilator 交互。
  • Verilator 作为仿真器,编译并执行 Verilog 代码。

4.1 项目目录结构

pyuvm_tutorial/
├── tinyalu.sv          # RTL 源码(TinyALU)
├── tinyalu_utils.py    # BFM(Bus Functional Model)和工具函数
├── testbench.py        # PyUVM Testbench
├── Makefile            # Cocotb 编译规则
└── pyuvm_tutorial.md   # 本教程

4.2 验证架构

Testbench (Python + PyUVM)
    ├── uvm_test        AluTest / ParallelTest / FibonacciTest / AluTestErrors
    │   └── uvm_env     AluEnv
    │       ├── uvm_sequencer
    │       ├── Driver
    │       ├── Monitor (cmd_mon)
    │       ├── Coverage
    │       └── Scoreboard
    └── TinyAluBfm (cocotb 与 DUT 的桥梁)

Simulator (Verilator)
    └── DUT (tinyalu)

5. 环境 Setup

5.1 系统要求

  • Linux (Ubuntu / WSL 均可)
  • Python 3.10+
  • GCC / G++ (用于编译 Verilator 生成的 C++ 代码)

5.2 安装 Verilator

# 安装依赖
sudo apt update
sudo apt install -y git help2man perl python3 make autoconf g++ flex bison libfl2 libfl-dev zlib1g-dev

# 从源码编译安装 Verilator
git clone https://github.com/verilator/verilator
cd verilator
autoconf
./configure --prefix=/usr/local
make -j$(nproc)
sudo make install

# 验证安装
verilator --version

注意:确保安装的是 **Verilator 5.0+**,因为 Cocotb 2.x 需要较新的 VPI 支持。

5.3 安装 Python 依赖

推荐使用 Conda 或 venv 创建虚拟环境:

# 创建虚拟环境
python3 -m venv .venv
source .venv/bin/activate

# 安装核心包
pip install cocotb pyuvm

# 验证安装
python -c "import cocotb; print(cocotb.__version__)"
python -c "import pyuvm; print(pyuvm.__version__)"

6. RTL 源码详解

下面是完整的 TinyALU Verilog 代码。它包含三个模块:

  • tinyalu:顶层模块,根据 op[2] 选择单周期或三周期执行路径。
  • single_cycle:单周期模块,实现 ADD、AND、XOR。
  • three_cycle:三周期流水线模块,实现 MUL。

创建文件 tinyalu.sv

module tinyalu (input [7:0] A,
        input [7:0] B,
        input [2:0] op,
        input clk,
        input reset_n,
        input start,
        output done,
        output [15:0] result);

   wire [15:0] 		      result_aax, result_mult;
   wire 		          start_single, start_mult;
   wire                   done_aax;
   wire                   done_mult;

   assign start_single = start & ~op[2];
   assign start_mult   = start & op[2];

   single_cycle and_add_xor (.A, .B, .op, .clk, .reset_n, .start(start_single),
                 .done(done_aax), .result(result_aax));

   three_cycle mult (.A, .B, .op, .clk, .reset_n, .start(start_mult),
            .done(done_mult), .result(result_mult));


   assign done = (op[2]) ? done_mult : done_aax;

   assign result = (op[2]) ? result_mult : result_aax;

endmodule // tinyalu


module single_cycle(input [7:0] A,
           input [7:0] B,
           input [2:0] op,
           input clk,
           input reset_n,
           input start,
           output logic done,
           output logic [15:0] result);

  always @(posedge clk)
    if (!reset_n)
      result <= 0;
    else
      case(op)
            3'b001 : result <= {8'd0,A} + {8'd0,B};
            3'b010 : result <= {8'd0,A} & {8'd0,B};
            3'b011 : result <= {8'd0,A} ^ {8'd0,B};
            default : result <= {A,B};
      endcase // case (op)

   always @(posedge clk)
     if (!reset_n)
       done <= 0;
     else
       done <= ((start == 1'b1) && (op != 3'b000));

endmodule : single_cycle


module three_cycle(input [7:0] A,
           input [7:0] B,
           input [2:0] op,
           input clk,
           input reset_n,
           input start,
           output logic done,
           output logic [15:0] result);

   logic [7:0] 		       a_int, b_int;
   logic [15:0] 	       mult1, mult2;
   logic 		       done1, done2, done3;

   always @(posedge clk)
     if (!reset_n) begin
    done  <= 0;
    done3 <= 0;
    done2 <= 0;
    done1 <= 0;
    a_int <= 0;
    b_int <= 0;
    mult1 <= 0;
    mult2 <= 0;
    result<= 0;
     end else begin
    a_int  <= A;
    b_int  <= B;
    mult1  <= a_int * b_int;
    mult2  <= mult1;
    result <= mult2;
    done3  <= start & !done;
    done2  <= done3 & !done;
    done1  <= done2 & !done;
    done   <= done1 & !done;
     end
endmodule : three_cycle

6.1 模块接口解析

信号 方向 位宽 说明
A Input 8 bit 操作数 A
B Input 8 bit 操作数 B
op Input 3 bit 操作码(001=ADD, 010=AND, 011=XOR, 100=MUL)
clk Input 1 bit 时钟,上升沿触发
reset_n Input 1 bit 低电平有效的异步复位
start Input 1 bit 启动信号,高电平有效
done Output 1 bit 操作完成标志
result Output 16 bit 运算结果

6.2 操作码编码

op[2:0] 操作 周期数
3'b001 ADD 1
3'b010 AND 1
3'b011 XOR 1
3'b100 MUL 3

op[2] 为 1 时选择 three_cycle 乘法器,否则选择 single_cycle


7. Makefile

Cocotb 使用 Makefile 驱动仿真流程。下面是针对 Verilator 的完整 Makefile。

创建文件 Makefile

CWD=$(shell pwd)
export COCOTB_REDUCED_LOG_FMT = 1
SIM ?= verilator
TOPLEVEL_LANG ?= verilog
VERILOG_SOURCES =$(CWD)/tinyalu.sv
ifeq ($(SIM), verilator)
    COMPILE_ARGS += --timing
endif
MODULE := testbench
TOPLEVEL = tinyalu
COCOTB_HDL_TIMEUNIT = 1us
COCOTB_HDL_TIMEPRECISION = 1us
include $(shell cocotb-config --makefiles)/Makefile.sim

7.1 关键配置说明

变量 说明
SIM ?= verilator 指定仿真器为 Verilator
TOPLEVEL DUT 顶层模块名,必须和 Verilog 的 module 名一致
MODULE Python Testbench 文件名(不含 .py
COMPILE_ARGS += --timing Verilator 专用:启用时序处理(支持 #delay
COCOTB_HDL_TIMEUNIT HDL 时间单位
COCOTB_HDL_TIMEPRECISION HDL 时间精度

8. PyUVM Testbench 详解

Testbench 分为两个 Python 文件:

  • tinyalu_utils.py:BFM(Bus Functional Model),负责通过 cocotb 直接驱动 DUT 信号。
  • testbench.py:PyUVM 验证平台,包含 Sequence、Driver、Monitor、Coverage、Scoreboard、Env 和 Test。

8.1 BFM 文件:tinyalu_utils.py

BFM 是 Bus Functional Model 的缩写,它是 cocotb 与 DUT 之间的”桥梁”。在 UVM 架构中,只有 Driver 直接与 BFM 交互,其他组件(如 Monitor)通过 TLM 接口与 Driver 通信。

创建文件 tinyalu_utils.py

import enum
import logging

import cocotb
from cocotb.queue import Queue, QueueEmpty
from cocotb.triggers import FallingEdge

from pyuvm import Singleton

logging.basicConfig(level=logging.NOTSET)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)


@enum.unique
class Ops(enum.IntEnum):
    """Legal ops for the TinyALU"""

    ADD = 1
    AND = 2
    XOR = 3
    MUL = 4


def alu_prediction(A, B, op, error=False):
    """Python model of the TinyALU"""
    assert isinstance(op, Ops), "The tinyalu op must be of type Ops"
    if op == Ops.ADD:
        result = A + B
    elif op == Ops.AND:
        result = A & B
    elif op == Ops.XOR:
        result = A ^ B
    elif op == Ops.MUL:
        result = A * B
    if error:
        result = result + 1
    return result


def get_int(signal):
    try:
        sig = int(signal.value)
    except ValueError:
        sig = 0
    return sig


class TinyAluBfm(metaclass=Singleton):
    def __init__(self):
        self.dut = cocotb.top
        self.driver_queue = Queue(maxsize=1)
        self.cmd_mon_queue = Queue(maxsize=0)
        self.result_mon_queue = Queue(maxsize=0)

    async def send_op(self, aa, bb, op):
        command_tuple = (aa, bb, op)
        await self.driver_queue.put(command_tuple)

    async def get_cmd(self):
        cmd = await self.cmd_mon_queue.get()
        return cmd

    async def get_result(self):
        result = await self.result_mon_queue.get()
        return result

    async def reset(self):
        await FallingEdge(self.dut.clk)
        self.dut.reset_n.value = 0
        self.dut.A.value = 0
        self.dut.B.value = 0
        self.dut.op.value = 0
        await FallingEdge(self.dut.clk)
        self.dut.reset_n.value = 1
        await FallingEdge(self.dut.clk)

    async def driver_bfm(self):
        self.dut.start.value = 0
        self.dut.A.value = 0
        self.dut.B.value = 0
        self.dut.op.value = 0
        while True:
            await FallingEdge(self.dut.clk)
            start = get_int(self.dut.start)
            done = get_int(self.dut.done)
            if start == 0 and done == 0:
                try:
                    (aa, bb, op) = self.driver_queue.get_nowait()
                    self.dut.A.value = aa
                    self.dut.B.value = bb
                    self.dut.op.value = op
                    self.dut.start.value = 1
                except QueueEmpty:
                    pass
            elif start == 1:
                if done == 1:
                    self.dut.start.value = 0

    async def cmd_mon_bfm(self):
        prev_start = 0
        while True:
            await FallingEdge(self.dut.clk)
            start = get_int(self.dut.start)
            if start == 1 and prev_start == 0:
                cmd_tuple = (
                    get_int(self.dut.A),
                    get_int(self.dut.B),
                    get_int(self.dut.op),
                )
                self.cmd_mon_queue.put_nowait(cmd_tuple)
            prev_start = start

    async def result_mon_bfm(self):
        prev_done = 0
        while True:
            await FallingEdge(self.dut.clk)
            done = get_int(self.dut.done)
            if prev_done == 0 and done == 1:
                result = get_int(self.dut.result)
                self.result_mon_queue.put_nowait(result)
            prev_done = done

    def start_bfm(self):
        cocotb.start_soon(self.driver_bfm())
        cocotb.start_soon(self.cmd_mon_bfm())
        cocotb.start_soon(self.result_mon_bfm())

TinyAluBfm 关键点

  • Singleton 模式:使用 pyuvm.Singleton 元类,确保整个仿真中只有一个 BFM 实例。
  • 三个队列
    • driver_queue:Driver 写入命令,BFM 读取并驱动到 DUT。
    • cmd_mon_queue:BFM 监控 start 上升沿,将命令放入队列供 Monitor 读取。
    • result_mon_queue:BFM 监控 done 上升沿,将结果放入队列供 Monitor 读取。
  • **reset()**:在时钟下降沿复位 DUT,释放复位后再等待一个下降沿。
  • **start_bfm()**:启动三个后台协程(driver、cmd_mon、result_mon),它们会永远运行。

8.2 Testbench 文件:testbench.py

创建文件 testbench.py

import logging
import random

import cocotb
from cocotb.clock import Clock
from cocotb.triggers import Combine

import pyuvm
from pyuvm import *

from tinyalu_utils import Ops, TinyAluBfm, alu_prediction

# ============================================================
# 1. Sequence Items(事务)
# ============================================================

class AluSeqItem(uvm_sequence_item):
    """
    ALU 事务:描述一次 ALU 操作的参数
    """
    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

    __hash__: None  # type: ignore

    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}"


# ============================================================
# 2. Sequences(序列)
# ============================================================

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)


class MaxSeq(uvm_sequence):
    """最大值序列:对每种操作使用 0xFF 作为操作数"""
    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)


class TestAllSeq(uvm_sequence):
    """虚拟序列:顺序执行 RandomSeq 和 MaxSeq"""
    async def body(self):
        seqr = ConfigDB().get(None, "", "SEQR")
        random = RandomSeq("random")
        max = MaxSeq("max")
        await random.start(seqr)
        await max.start(seqr)


class TestAllForkSeq(uvm_sequence):
    """并行序列:同时启动 RandomSeq 和 MaxSeq"""
    async def body(self):
        seqr = ConfigDB().get(None, "", "SEQR")
        random = RandomSeq("random")
        max = MaxSeq("max")
        random_task = cocotb.start_soon(random.start(seqr))
        max_task = cocotb.start_soon(max.start(seqr))
        await Combine(random_task, max_task)


class OpSeq(uvm_sequence):
    """单次操作序列:执行指定的一次 ALU 操作"""
    def __init__(self, name, aa, bb, op):
        super().__init__(name)
        self.aa = aa
        self.bb = bb
        self.op = Ops(op)

    async def body(self):
        seq_item = AluSeqItem("seq_item", self.aa, self.bb, self.op)
        await self.start_item(seq_item)
        await self.finish_item(seq_item)
        self.result = seq_item.result


async def do_add(seqr, aa, bb):
    seq = OpSeq("seq", aa, bb, Ops.ADD)
    await seq.start(seqr)
    return seq.result


async def do_and(seqr, aa, bb):
    seq = OpSeq("seq", aa, bb, Ops.AND)
    await seq.start(seqr)
    return seq.result


async def do_xor(seqr, aa, bb):
    seq = OpSeq("seq", aa, bb, Ops.XOR)
    await seq.start(seqr)
    return seq.result


async def do_mul(seqr, aa, bb):
    seq = OpSeq("seq", aa, bb, Ops.MUL)
    await seq.start(seqr)
    return seq.result


class FibonacciSeq(uvm_sequence):
    """斐波那契序列:使用 ALU 的 ADD 操作计算斐波那契数列"""
    def __init__(self, name):
        super().__init__(name)
        self.seqr = ConfigDB().get(None, "", "SEQR")

    async def body(self):
        prev_num = 0
        cur_num = 1
        fib_list = [prev_num, cur_num]
        for _ in range(7):
            sum = await do_add(self.seqr, prev_num, cur_num)
            fib_list.append(sum)
            prev_num = cur_num
            cur_num = sum
        uvm_root().logger.info("Fibonacci Sequence: " + str(fib_list))
        uvm_root().set_logging_level_hier(logging.CRITICAL)


# ============================================================
# 3. Driver(驱动器)
# ============================================================

class Driver(uvm_driver):
    """
    ALU Driver:从 Sequencer 获取事务,通过 BFM 驱动到 DUT
    """
    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_bfm()

    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()


# ============================================================
# 4. Coverage(覆盖率)
# ============================================================

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


# ============================================================
# 5. Scoreboard(记分板)
# ============================================================

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):
        passed = True
        try:
            self.errors = ConfigDB().get(self, "", "CREATE_ERRORS")
        except UVMConfigItemNotFound:
            self.errors = False
        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, self.errors)
                if predicted_result == actual_result:
                    self.logger.info(
                        f"PASSED: 0x{A:02x} {op.name} 0x{B:02x} = 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}"
                    )
                    passed = False
        assert passed


# ============================================================
# 6. Monitor(监视器)
# ============================================================

class Monitor(uvm_component):
    """
    监视器:通过 BFM 监控 DUT 的命令或结果
    """
    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)


# ============================================================
# 7. Environment(环境)
# ============================================================

class AluEnv(uvm_env):
    """
    ALU 验证环境:包含 Sequencer、Driver、Monitor、Coverage、Scoreboard
    """
    def build_phase(self):
        self.clk_drv = Clock(cocotb.top.clk, 2, "us")
        cocotb.start_soon(self.clk_drv.start())
        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)


# ============================================================
# 8. Tests(测试)
# ============================================================

@pyuvm.test()
class AluTest(uvm_test):
    """基础测试:顺序执行 RandomSeq 和 MaxSeq"""

    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()


@pyuvm.test()
class ParallelTest(AluTest):
    """并行测试:同时启动 RandomSeq 和 MaxSeq"""

    def build_phase(self):
        uvm_factory().set_type_override_by_type(TestAllSeq, TestAllForkSeq)
        super().build_phase()


@pyuvm.test()
class FibonacciTest(AluTest):
    """斐波那契测试:使用 ALU 计算斐波那契数列"""

    def build_phase(self):
        ConfigDB().set(None, "*", "DISABLE_COVERAGE_ERRORS", True)
        uvm_factory().set_type_override_by_type(TestAllSeq, FibonacciSeq)
        return super().build_phase()


@pyuvm.test(expect_fail=True)
class AluTestErrors(AluTest):
    """错误注入测试:在所有操作中注入错误,预期失败"""

    def start_of_simulation_phase(self):
        ConfigDB().set(None, "*", "CREATE_ERRORS", True)

8.3 Testbench 整体架构

Testbench (Python + PyUVM)
    ├── AluSeqItem          Sequence Item(事务)
    ├── RandomSeq           随机操作数序列
    ├── MaxSeq              最大值序列
    ├── TestAllSeq          虚拟序列(顺序执行)
    ├── TestAllForkSeq      虚拟序列(并行执行)
    ├── FibonacciSeq        斐波那契序列
    ├── Driver              驱动器
    ├── Coverage            覆盖率检查
    ├── Scoreboard          记分板
    ├── Monitor             监视器
    ├── AluEnv              环境容器
    └── AluTest             基础测试类
        ├── ParallelTest    并行测试
        ├── FibonacciTest   斐波那契测试
        └── AluTestErrors   错误注入测试(预期失败)

8.4 PyUVM 核心概念速查

概念 代码示例 说明
导入 pyuvm from pyuvm import * 导入所有 UVM 类名,类似 SystemVerilog 的 import uvm_pkg::*
定义测试 @pyuvm.test() 装饰器标记这是一个 pyuvm 测试用例
事务基类 class AluSeqItem(uvm_sequence_item) 定义 Sequence 与 Driver 之间传递的数据结构
Sequence class RandomSeq(uvm_sequence) 生成事务序列,通过 start_item/finish_item 发送
Driver class Driver(uvm_driver) 从 Sequencer 拉取事务,驱动到 DUT
连接 Port self.driver.seq_item_port.connect(...) connect_phase 中完成 TLM 连接
ConfigDB 写 ConfigDB().set(None, "*", "SEQR", self.seqr) 全局设置共享资源
ConfigDB 读 ConfigDB().get(self, "", "SEQR") 在组件中读取共享资源
Objection self.raise_objection() / self.drop_objection() 控制 run_phase 生命周期
工厂覆盖 uvm_factory().set_type_override_by_type(A, B) 运行时替换组件类型
启动时钟 cocotb.start_soon(Clock(...).start()) 自动生成时钟

8.5 各组件详解

AluSeqItem — Sequence Item

AluSeqItem 继承 uvm_sequence_item,是 Sequence 与 Driver 之间传递的数据结构。

  • randomize_operands():随机化操作数 A 和 B。
  • __eq__():重载 == 运算符,用于比较两个事务是否相等。
  • __str__():重载字符串转换,用于打印调试信息。

PyUVM 没有 convert2string()do_compare(),而是直接使用 Python 的 __str__()__eq__() 魔术方法。

Driver — 驱动器

Driver 继承 uvm_driver,是 UVM 架构中直接操作 DUT 的组件。

  • build_phase:创建 uvm_analysis_port,用于将结果广播给 Scoreboard。
  • start_of_simulation_phase:获取 BFM 实例。
  • run_phaseasync):主循环,不断从 Sequencer 获取事务并驱动到 DUT。

run_phaseasync 方法,因为它内部需要 await BFM 的协程。

Monitor — 监视器

Monitor 继承 uvm_component,被动监控 DUT 的行为。

  • 通过构造函数传入 method_name(如 "get_cmd""get_result"),使用 Python 的 getattr() 动态获取 BFM 方法。
  • run_phase 中无限循环,等待 BFM 返回数据后写入 analysis_port

这种通过字符串名称动态获取方法的能力是 Python 内省(Introspection) 的特性,SystemVerilog 不支持。

Scoreboard — 记分板

Scoreboard 继承 uvm_component,在 check_phase 中对比预期结果和实际结果。

  • build_phase:创建两个 uvm_tlm_analysis_fifo 分别缓存命令和结果。
  • connect_phase:将 FIFO 的 get_export 连接到 get_port。
  • check_phase:遍历所有结果,调用 alu_prediction() 计算预期值,与实际值对比。

check_phaserun_phase 结束后执行,此时所有数据都已收集完毕。

Coverage — 覆盖率

Coverage 继承 uvm_subscriber,被动接收 Monitor 发送的事务。

  • uvm_subscriber 本身就是一个 uvm_analysis_export,可以直接连接到 analysis_port
  • write() 方法必须被重写,否则会报运行时错误。
  • report_phase:检查是否覆盖了所有操作类型(ADD、AND、XOR、MUL)。

AluEnv — 环境

AluEnv 继承 uvm_env,是所有验证组件的容器。

  • build_phase自顶向下):先创建时钟,然后依次创建 Sequencer、Driver、Monitor、Coverage、Scoreboard。
  • connect_phase自底向上):连接 Driver ↔ Sequencer、Monitor ↔ Scoreboard、Monitor ↔ Coverage、Driver ↔ Scoreboard。

AluTest — 测试

AluTest 继承 uvm_test,是 UVM 验证树的根节点。

  • build_phase:创建 AluEnv
  • end_of_elaboration_phase:创建要运行的 Sequence。
  • run_phase:通过 raise_objection() / drop_objection() 控制仿真生命周期。

@pyuvm.test() 装饰器会自动调用 uvm_root().run_test() 来启动 UVM 测试树。

8.6 测试用例详解

AluTest — 基础功能测试

顺序执行 RandomSeq(随机操作数)和 MaxSeq(0xFF 操作数),共 8 个事务,覆盖所有四种操作。

ParallelTest — 并行测试

通过工厂机制将 TestAllSeq 替换为 TestAllForkSeq,同时启动 RandomSeqMaxSeq。验证 Sequencer 的仲裁能力。

FibonacciTest — 斐波那契测试

使用 ALU 的 ADD 操作计算斐波那契数列 [0, 1, 1, 2, 3, 5, 8, 13, 21]。展示了如何在 Sequence 中嵌套调用其他 Sequence。

AluTestErrors — 错误注入测试(预期失败)

通过 ConfigDB 设置 CREATE_ERRORS = True,使 alu_prediction() 返回错误结果(结果 +1)。Scoreboard 会检测到不匹配并报告 FAILED。此测试使用 @pyuvm.test(expect_fail=True) 标记为”预期失败”。


9. 运行仿真

确保你的终端位于项目目录下,且已激活安装了 cocotbpyuvm 的 Python 环境。

9.1 编译与仿真

make clean
make SIM=verilator

如果一切正常,你会看到如下输出:

** TESTS=4 PASS=4 FAIL=0 SKIP=0             183000.00           0.03    6149956.59  **

中间的日志会显示每个操作的具体结果:

49000.00ns INFO  [uvm_test_top.env.scoreboard]: PASSED: 0x35 ADD 0xa7 = 0x00dc
49000.00ns INFO  [uvm_test_top.env.scoreboard]: PASSED: 0x21 AND 0x1f = 0x0001
49000.00ns INFO  [uvm_test_top.env.scoreboard]: PASSED: 0x4b XOR 0x18 = 0x0053
49000.00ns INFO  [uvm_test_top.env.scoreboard]: PASSED: 0x99 MUL 0x55 = 0x32cd

9.2 使用 Icarus Verilog 仿真

如果你更习惯使用 Icarus Verilog(iverilog),只需修改 SIM 变量:

make clean
make SIM=icarus

Icarus 不需要 --timing 参数,因此无需额外配置。


10. PyUVM 常见踩坑与教训

以下是在编写和运行本教程过程中遇到的真实踩坑记录,强烈建议仔细阅读

10.1 raise_objection() 不能传参数

现象

TypeError: uvm_component.raise_objection() takes 1 positional argument but 2 were given

原因:PyUVM 4.0.1 中的 raise_objection()drop_objection() 方法不接受描述字符串参数,而原始文档和 SystemVerilog UVM 中是可以传的。

解决:调用时不传任何参数:

# 正确
self.raise_objection()
self.drop_objection()

# 错误(pyuvm 4.0.1 不支持)
self.raise_objection("Keep simulation alive")
self.drop_objection("Simulation may end now")

教训:PyUVM 的 API 虽然与 SystemVerilog UVM 类似,但在细节上可能有差异。遇到 TypeError 时,优先查看 PyUVM 源码确认方法签名。

10.2 Sequence 中不能用 self 作为 ConfigDB 的 context

现象

AssertionError: config_db context must be None or a uvm_component

原因ConfigDB().get(self, "", "DUT") 的第一个参数 selfuvm_sequence 中是 uvm_object,而不是 uvm_component。ConfigDB 的实现要求 context 必须是 Noneuvm_component 实例。

解决:在 Sequence 中访问 ConfigDB 时,第一个参数传 None

# 正确
seqr = ConfigDB().get(None, "", "SEQR")

# 错误(只在 uvm_component 子类中可用)
seqr = ConfigDB().get(self, "", "SEQR")

教训ConfigDB().get(None, "*", "KEY") 是最安全的跨组件访问方式;带 self 的写法只适用于 uvm_component 子类(如 Driver、Env、Test)。

10.3 cocotb.start_soon() 与 UVM phasing 的交互

现象uvm_run_phase 完成后,后台的 Clock 协程和 Driver 的 while True 循环仍在运行,但不会影响测试结束。

说明:PyUVM 的 uvm_run_phase 通过 ObjectionHandler 管理生命周期。当 Test 中所有 objection 都被 drop 后,run_phase_complete() 返回,UVM 继续执行后续 phase(extract/check/report/final),然后 run_test() 结束。后台残留的 cocotb.start_soon() 协程(如 Clock、Driver while True)虽然还在事件循环中,但 cocotb 的 regression manager 会正常结束当前测试并进入下一个。

教训:PyUVM 测试的正常结束不依赖于”杀死所有后台协程”,而依赖于 Objection 机制。只要确保 raise_objection()drop_objection() 成对出现即可。


11. 扩展建议

  1. 增加随机测试:使用 Python random 模块生成随机操作数和操作类型,配合更多的 Sequence 变体。
  2. 接入 CI/CD:将 make clean && make SIM=verilator 写入 GitHub Actions 或 GitLab CI,实现自动化回归。
  3. 使用 UVM Agent:将 Sequencer + Driver + Monitor 封装成 uvm_agent,更符合工业标准架构。
  4. 加入功能覆盖率:使用 cocotb-coverage 库收集更详细的覆盖率数据(如操作数边界值)。
  5. 添加波形调试:在 Makefile 中添加 --trace--trace-vcd 参数,生成 VCD 波形文件用 GTKWave 查看。

12. 总结

本项目完整展示了:

  • UVM 的核心概念:为什么需要 UVM、UVM 的标准组件(Sequence/Driver/Monitor/Scoreboard/Env/Test)。
  • PyUVM 的优势:基于 Python 的简洁语法、无宏工厂注册、与 cocotb 的无缝集成。
  • PyUVM 对 IEEE 1800.2 的实现:Base Classes、Factory、Phasing、TLM、ConfigDB、Reporting、Sequences。
  • 如何用 Verilog 实现一个简单的 ALU:组合逻辑 + 流水线乘法器的设计。
  • 如何用 PyUVM + cocotb 编写标准 UVM 验证平台:Sequence、Driver、Sequencer、Env、Test 的经典架构,ConfigDB 与 Objection 的使用。
  • 如何用 Verilator 进行编译仿真

所有代码均已在本教程中完整给出,复制到对应文件即可直接运行。

结语

第三百八十三篇博文写完,开心!!!!

今天,也是充满希望的一天。


文章作者: LuYF-Lemon-love
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LuYF-Lemon-love !
  目录