这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

快速开始

如何使用开放验证平台的环境参与到硬件验证中来。

在开始前本页会 简单的介绍什么是验证,以及示例里面用到的概念,如 DUT (Design Under Test) 和 RM (Reference Model) 。

芯片验证

芯片验证是确保芯片设计正确性和可靠性的重要环节,主要包括功能验证、形式验证和物理验证等形式,本学习材料仅仅包含对功能验证的介绍,且侧重于基于仿真器的芯片功能验证。芯片功能验证的流程和方法与软件测试有比较大的共同点,例如都有单元测试、系统测试、黑盒测试、白盒测试等。在验证指标上也有共同特点,例如功能覆盖率、代码覆盖率等等。从某种形式上说,除了使用的工具和编程语言不一样外,他们的目标和流程几乎相同。因此,在不考虑工具和编程语言的情况下,会软件测试的工程师应当就会芯片验证。 但在实际工作中,软件测试和芯片验证属于两个完全不相交的行业,其主要原因是验证工具和验证语言的不同,导致软件测试工程师很难实现跨界。在芯片验证领域,通常使用硬件描述语言进行验证(例如 Verilog 或者 System Verilog),使用专业商业工具进行电路仿真。硬件描述语言不同于C++/Python等高级软件编程语言,具有独特的“时钟”特性,对于软件领域的工程师不友好,学习成本高。

为了打通芯片验证与传统软件测试之间的壁垒,让更多的人参与到芯片验证,本项目提供如下内容:

多语言验证工具(Picker),让用户可以使用自己擅长的编程语言进行芯片验证

验证框架(toffee),如何在不关心时钟的情况下进行功能验证

介绍基本电路、验证知识,方便软件背景爱好者更能容易的理解电路特征

提供基本学习材料,学习基本验证知识

提供真实高性能芯片验证案例,让爱好者可以远程参与验证工作

基本术语

DUT: DUT(Design Under Test)指待测试设计,通常指设计好的RTL代码。

RM: Reference Model (RM)指代待测试单元对应的参考模型,参考模型通常被认为是标准的,没有错误的。

RTL: 指寄存器传输级(Register Transfer Level),通常指代芯片设计对应的 verilog 或者 vhdl 代码。

覆盖率: 测试覆盖率是指测试范围与整个需求范围的百分比。在芯片验证领域,通常有代码行覆盖率、函数覆盖率、功能覆盖率等。

DV: DV中的D通常指设计(Desgin),V指验证(Verification)。合在一起指设计与验证协同工作。

差分测试(difftest): 选取两个(或以上)功能相同的被测对象,选取符合被测对象要求的同一测试用例分别提交被测对象进行执行,以观测执行结果是否存在差异的过程。

工具介绍

本学习材料用到的核心工具为 picker (https://github.com/XS-MLVP/picker),它的作用是将RTL编写的设计模块自动提供高级编程语言接口(Python/C++等)。基于该工具,软件开发(测试)背景的验证人员可以不用去学习 Verilog/VHDL 等硬件描述语言进行芯片验证。

系统需求

建议操作系统:Ubuntu 22.04 LTS

在系统结构开发、科研的过程中,Linux 是最为常用的平台,这主要是因为 Linux 拥有丰富的软件、工具资源:由于 Linux 的开源性,各大重要工具软件(如 Verilator)可以很容易地面向 Linux 进行开发。 在本课程的实验中,多语言验证工具Picker、Swig等工具都可以在 Linux 上稳定运行。

1 - 搭建验证环境

安装相关依赖,下载、构建并安装对应的工具。

源码安装Picker工具

依赖安装

  1. cmake ( >=3.11 )
  2. gcc ( 支持c++20,至少为gcc版本10, 建议11及以上 )
  3. python3 ( >=3.8 )
  4. verilator ( >=4.218 )
  5. verible-verilog-format ( >=0.0-3428-gcfcbb82b )
  6. swig ( >=4.2.0, 用于多语言支持 )

请注意,请确保verible-verilog-format等工具的路径已经添加到环境变量$PATH中,可以直接命令行调用。

下载源码

git clone https://github.com/XS-MLVP/picker.git --depth=1
cd picker
make init

构建并安装

cd picker
make
# 可通过 make BUILD_XSPCOMM_SWIG=python,java,scala,golang 开启其他语言支持。
# 各语言需要自己的开发环境,需要自行配置,例如javac等
sudo -E make install

默认的安装的目标路径是 /usr/local, 二进制文件被置于 /usr/local/bin,模板文件被置于 /usr/local/share/picker。 如果需要修改安装目录,可以通过指定ARGS给cmake传递参数,例如make ARGS="-DCMAKE_INSTALL_PREFIX=your_instal_dir" 安装时会自动安装 xspcomm基础库(https://github.com/XS-MLVP/xcomm),该基础库是用于封装 RTL 模块的基础类型,位于 /usr/local/lib/libxspcomm.so可能需要手动设置编译时的链接目录参数(-L) 如果开启了Java等语言支持,还会安装 xspcomm 对应的多语言软件包。

picker也可以编译为wheel文件,通过pip安装

通过以下命令把picker打包成wheel安装包:

make wheel # or BUILD_XSPCOMM_SWIG=python,java,scala,golang make wheel

编译完成后,wheel文件位于dist目录,然后通过pip安装,例如:

pip install dist/xspcomm-0.0.1-cp311-cp311-linux_x86_64.whl
pip install dist/picker-0.0.1-cp311-cp311-linux_x86_64.whl

安装完成后,执行picker命令可以得到以下输出:

XDut Generate.
Convert DUT(*.v/*.sv) to C++ DUT libs.

Usage: ./build/bin/picker [OPTIONS] [SUBCOMMAND]

Options:
  -h,--help                   Print this help message and exit
  -v,--version                Print version
  --show_default_template_path
                              Print default template path
  --show_xcom_lib_location_cpp
                              Print xspcomm lib and include location
  --show_xcom_lib_location_java
                              Print xspcomm-java.jar location
  --show_xcom_lib_location_scala
                              Print xspcomm-scala.jar location
  --show_xcom_lib_location_python
                              Print python module xspcomm location
  --show_xcom_lib_location_golang
                              Print golang module xspcomm location
  --check                     check install location and supproted languages

Subcommands:
  export                      Export RTL Projects Sources as Software libraries such as C++/Python
  pack                        Pack UVM transaction as a UVM agent and Python class

安装测试

当前picker有export和pack两个子命令。

export 子命令用于将RTL设计转换成其他高级编程语言对应的“库”,可以通过软件的方式进行驱动。

picker export --help
Export RTL Projects Sources as Software libraries such as C++/Python
Usage: picker export [OPTIONS] file...

Positionals:
  file TEXT ... REQUIRED      DUT .v/.sv source file, contain the top module

Options:
  -h,--help                   Print this help message and exit
  --fs,--filelist TEXT ...    DUT .v/.sv source files, contain the top module, split by comma.
                              Or use '*.txt' file  with one RTL file path per line to specify the file list
  --sim TEXT [verilator]      vcs or verilator as simulator, default is verilator
  --lang,--language TEXT:{python,cpp,java,scala,golang} [python]
                              Build example project, default is python, choose cpp, java or python
  --sdir,--source_dir TEXT    Template Files Dir, default is ${picker_install_path}/../picker/template
  --sname,--source_module_name TEXT ...
                              Pick the module in DUT .v file, default is the last module in the -f marked file
  --tname,--target_module_name TEXT
                              Set the module name and file name of target DUT, default is the same as source.
                              For example, -T top, will generate UTtop.cpp and UTtop.hpp with UTtop class
  --tdir,--target_dir TEXT    Target directory to store all the results. If it ends with '/' or is empty,
                              the directory name will be the same as the target module name
  --internal TEXT             Exported internal signal config file, default is empty, means no internal pin
  -F,--frequency TEXT [100MHz]
                              Set the frequency of the **only VCS** DUT, default is 100MHz, use Hz, KHz, MHz, GHz as unit
  -w,--wave_file_name TEXT    Wave file name, emtpy mean don't dump wave
  -c,--coverage               Enable coverage, default is not selected as OFF
  --cp_lib,--copy_xspcomm_lib BOOLEAN [1]
                              Copy xspcomm lib to generated DUT dir, default is true
  -V,--vflag TEXT             User defined simulator compile args, passthrough.
                              Eg: '-v -x-assign=fast -Wall --trace' || '-C vcs -cc -f filelist.f'
  -C,--cflag TEXT             User defined gcc/clang compile command, passthrough. Eg:'-O3 -std=c++17 -I./include'
  --verbose                   Verbose mode
  -e,--example                Build example project, default is OFF
  --autobuild BOOLEAN [1]     Auto build the generated project, default is true

pack子命令用于将UVM中的 sequence_item 转换为其他语言,然后通过TLM进行通信(目前支持Python,其他语言在开发中)

picker pack --help
Pack uvm transaction as a uvm agent and python class
Usage: picker pack [OPTIONS] file...

Positionals:
  file TEXT ... REQUIRED      Sv source file, contain the transaction define

Options:
  -h,--help                   Print this help message and exit
  -e,--example                Generate example project based on transaction, default is OFF
  -c,--force                  Force delete folder when the code has already generated by picker
  -r,--rename TEXT ...        Rename transaction name in picker generate code

参数解释

export:
  • file TEXT ... REQUIRED:必须。位置参数,DUT.v/.sv 源文件,包含顶层模块
  • -h,--help: 可选。打印此帮助信息并退出
  • --fs,--filelist TEXT ...: 可选。DUT .v/.sv 源文件,包含顶层模块,逗号分隔。或使用 ‘*.txt’ 文件,每行指定一个 RTL 文件路径来指定文件列表
  • --sim TEXT [verilator]: 可选。使用 vcs 或 verilator 作为模拟器,默认是 verilator
  • --lang,--language TEXT:{python,cpp,java,scala,golang} [python]: 可选。构建示例项目,默认是 python,可选择 cpp、java 或 python
  • --sdir,--source_dir TEXT: 可选。模板文件目录,默认是 ${picker_install_path}/../picker/template
  • --sname,--source_module_name TEXT ...: 可选。在 DUT .v 文件中选择模块,默认是 -f 标记的文件中的最后一个模块
  • --tname,--target_module_name TEXT: 可选。设置目标 DUT 的模块名和文件名,默认与源相同。例如,-T top 将生成 UTtop.cpp 和 UTtop.hpp,并包含 UTtop 类
  • --tdir,--target_dir TEXT: 可选。代码生成渲染文件的目标目录,默认为DUT的模块名。如果该参数以’/‘结尾,则在该参数指定的目录中创建以DUT模块名的子目录。
  • --internal TEXT: 可选。导出的内部信号配置文件,默认为空,表示没有内部引脚
  • -F,--frequency TEXT [100MHz]: 可选。设置 仅 VCS DUT 的频率,默认是 100MHz,可以使用 Hz、KHz、MHz、GHz 作为单位
  • -w,--wave_file_name TEXT: 可选。波形文件名,空表示不导出波形
  • -c,--coverage: 可选。启用覆盖率,默认不选择为 OFF
  • --cp_lib,--copy_xspcomm_lib BOOLEAN [1]: 可选。将 xspcomm 库复制到生成的 DUT 目录,默认是 true
  • -V,--vflag TEXT: 可选。用户定义的模拟器编译参数,透传。例如:’-v -x-assign=fast -Wall –trace’ 或 ‘-C vcs -cc -f filelist.f’
  • -C,--cflag TEXT: 可选。用户定义的 gcc/clang 编译命令,透传。例如:’-O3 -std=c++17 -I./include’
  • --verbose: 可选。详细模式
  • -e,--example: 可选。构建示例项目,默认是 OFF
  • --autobuild BOOLEAN [1]: 可选。自动构建生成的项目,默认是 true

静态多模块支持:

picker在生成dut_top.sv/v的封装时,可以通过--sname参数指定多个模块名称和对应的数量。例如在a.v和b.v设计文件中分别有模块A和B,需要DUT中有2个A,3个B,生成的模块名称为C(若不指定,默认名称为A_B),则可执行如下命令:

picker path/a.v,path/b.v --sname A,2,B,3 --tname C

环境变量:

  • DUMPVARS_OPTION: 设置$dumpvars的option参数。例如DUMPVARS_OPTION="+mda" picker .... 开启vcs中数组波形的支持。
  • SIMULATOR_FLAGS: 传递给后端仿真器的参数。具体可参考所使用的后端仿真器文档。
  • CFLAGS: 设置后端仿真器的-cflags参数。
pack:
  • file: 必需。待解析的UVM transaction文件
  • --example, -e: 可选。根据UVM的transaction生成示例项目。
  • --force, -c: 可选。若已存在picker根据当前transaction解析出的文件,通过该命令可强制删除该文件,并重新生成
  • --rename, -r: 可选。配置生成文件以及生成的agent的名称,默认为transaction名。

测试Examples

编译完成后,在picker目录执行以下命令,进行测试:

bash example/Adder/release-verilator.sh --lang cpp
bash example/Adder/release-verilator.sh --lang python

# 默认仅开启 cpp 和 Python 支持
#   支持其他语言编译命令为:make BUILD_XSPCOMM_SWIG=python,java,scala,golang
bash example/Adder/release-verilator.sh --lang java
bash example/Adder/release-verilator.sh --lang scala
bash example/Adder/release-verilator.sh --lang golang

bash example/RandomGenerator/release-verilator.sh --lang cpp
bash example/RandomGenerator/release-verilator.sh --lang python
bash example/RandomGenerator/release-verilator.sh --lang java

参考材料

如何基于picker进行芯片验证,可参考:https://open-verify.cc/mlvp/docs/

2 - 案例一:简单加法器

基于一个简单的加法器验证展示工具的原理和使用方法,这个加法器内部是简单的组合逻辑。

RTL源码

在本案例中,我们驱动一个 64 位的加法器(组合电路),其源码如下:

// A verilog 64-bit full adder with carry in and carry out

module Adder #(
    parameter WIDTH = 64
) (
    input [WIDTH-1:0] a,
    input [WIDTH-1:0] b,
    input cin,
    output [WIDTH-1:0] sum,
    output cout
);

assign {cout, sum}  = a + b + cin;

endmodule

该加法器包含一个 64 位的加法器,其输入为两个 64 位的数和一个进位信号,输出为一个 64 位的和和一个进位信号。

测试过程

在测试过程中,我们将创建一个名为 Adder 的文件夹,其中包含一个 Adder.v 文件。该文件内容即为上述的 RTL 源码。

将RTL导出为 Python Module

生成中间文件

进入 Adder 文件夹,执行如下命令:

picker export --autobuild=false Adder.v -w Adder.fst --sname Adder --tdir picker_out_adder/ --lang python -e --sim verilator

*注:–tdir 指定的是目标构建目录,如果该参数值为空或者以“/”结尾,picker则会自动以DUT的目标模块名创建构建目录。例如 --tdir picker_out_adder指定了当前目录下的picker_out_adder为构建目录,而参数--tdir picker_out_adder/则指定picker在当前目录picker_out_adder中创建Adder目录作为目标构建目录。

该命令的含义是:

  1. 将 Adder.v 作为 Top 文件,并将 Adder 作为 Top Module,基于 verilator 仿真器生成动态库,生成目标语言为 Python。
  2. 启用波形输出,目标波形文件为Adder.fst。
  3. 包含用于驱动示例项目的文件(-e),同时codegen完成后不自动编译(-autobuild=false)。
  4. 最终的文件输出路径是 picker_out_adder

在使用该命令时,还有部分命令行参数没有使用,这些命令将在后续的章节中介绍。

输出的目录结构如下,请注意这部分均为中间文件,不能直接使用:

picker_out_adder/
└── Adder
    |-- Adder.v # 原始的RTL源码
    |-- Adder_top.sv # 生成的Adder_top顶层封装,使用DPI驱动Adder模块的inputs和outputs
    |-- Adder_top.v # 生成的Adder_top顶层封装,因为Verdi不支持导入SV源码使用,因此需要生成一个Verilog版本
    |-- CMakeLists.txt # 用于调用仿真器编译基本的cpp class并将其打包成有裸DPI函数二进制动态库(libDPIAdder.so)
    |-- Makefile # 生成的Makefile,用于调用CMakeLists.txt,并让用户可以通过make命令编译出libAdder.so,并手动调整Makefile的配置参数。或者编译示例项目
    |-- cmake # 生成的cmake文件夹,用于调用不同仿真器编译RTL代码
    |   |-- vcs.cmake
    |   `-- verilator.cmake
    |-- cpp # CPP example目录,包含示例代码
    |   |-- CMakeLists.txt # 用于将libDPIAdder.so使用基础数据类型封装为一个可直接操作的类(libUTAdder.so),而非裸DPI函数。
    |   |-- Makefile
    |   |-- cmake
    |   |   |-- vcs.cmake
    |   |   `-- verilator.cmake
    |   |-- dut.cpp # 生成的cpp UT封装,包含了对libDPIAdder.so的调用,及UTAdder类的声明及实现
    |   |-- dut.hpp # 头文件
    |   `-- example.cpp # 调用UTAdder类的示例代码
    |-- dut_base.cpp # 用于调用与驱动不同仿真器编译结果的基类,通过继承封装为统一的类,用于隐藏所有仿真器相关的代码细节。
    |-- dut_base.hpp
    |-- filelist.f # 多文件项目使用的其他文件列表,请查看 -f 参数的介绍。本案例中为空
    |-- mk
    |   |-- cpp.mk # 用于控制以cpp为目标语言时的Makefile,包含控制编译示例项目(-e,example)的逻辑
    |   `-- python.mk # 同上,目标语言是python
    `-- python
        |-- CMakeLists.txt
        |-- Makefile
        |-- cmake
        |   |-- vcs.cmake
        |   `-- verilator.cmake
        |-- dut.i # SWIG配置文件,用于将libDPIAdder.so的基类与函数声明,依据规则用swig导出到python,提供python调用的能力
        `-- dut.py # 生成的python UT封装,包含了对libDPIAdder.so的调用,及UTAdder类的声明及实现,等价于 libUTAdder.so

构建中间文件

进入 picker_out_adder/Adder 目录并执行 make 命令,即可生成最终的文件。

Makefile 定义的自动编译过程流如下:

  1. 通过 cmake/*.cmake 定义的仿真器调用脚本,编译 Adder_top.sv 及相关文件为 libDPIAdder.so 动态库。
  2. 通过 CMakelists.txt 定义的编译脚本,将 libDPIAdder.so 通过 dut_base.cpp 封装为 libUTAdder.so 动态库。并将1、2步产物拷贝到 UT_Adder 目录下。
  3. 通过 dut_base.hppdut.hpp 等头文件,利用 SWIG 工具生成封装层,并最终在 UT_Adder 这一目录中构建一个 Python Module。
  4. 如果有 -e 参数,则将预先定义好的 example.py 置于 UT_Adder 目录的上级目录,作为如何调用该 Python Module 的示例代码。

最终目录结果为:

picker_out_adder/
└── Adder
    |-- _UT_Adder.so # Swig生成的wrapper动态库
    |-- __init__.py # Python Module的初始化文件,也是库的定义文件
    |-- libDPIAdder.a # 仿真器生成的库文件
    |-- libUTAdder.so # 基于dut_base生成的libDPI动态库封装
    |-- libUT_Adder.py # Swig生成的Python Module
    `-- xspcomm # xspcomm基础库,固定文件夹,不需要关注

配置测试代码

在picker_out_adder中添加 example.py


from Adder import *
import random

# 生成无符号随机数
def random_int(): 
    return random.randint(-(2**63), 2**63 - 1) & ((1 << 63) - 1)

# 通过python实现的加法器参考模型
def reference_adder(a, b, cin):
    sum = (a + b) & ((1 << 64) - 1)
    carry = sum < a
    sum += cin
    carry = carry or sum < cin
    return sum, 1 if carry else 0

def random_test():
    # 创建DUT
    dut = DUTAdder()
    # 默认情况下,引脚赋值不会立马写入,而是在下一次时钟上升沿写入,这对于时序电路适用,但是Adder为组合电路,所以需要立即写入
    #   因此需要调用AsImmWrite()方法更改引脚赋值行为
    dut.a.AsImmWrite()
    dut.b.AsImmWrite()
    dut.cin.AsImmWrite()
    # 循环测试
    for i in range(114514):
        a, b, cin = random_int(), random_int(), random_int() & 1
        # DUT:对Adder电路引脚赋值,然后驱动组合电路 (对于时序电路,或者需要查看波形,可通过dut.Step()进行驱动)
        dut.a.value, dut.b.value, dut.cin.value = a, b, cin
        dut.RefreshComb()
        # 参考模型:计算结果
        ref_sum, ref_cout = reference_adder(a, b, cin)
        # 检查结果
        assert dut.sum.value == ref_sum, "sum mismatch: 0x{dut.sum.value:x} != 0x{ref_sum:x}"
        assert dut.cout.value == ref_cout, "cout mismatch: 0x{dut.cout.value:x} != 0x{ref_cout:x}"
        print(f"[test {i}] a=0x{a:x}, b=0x{b:x}, cin=0x{cin:x} => sum: 0x{ref_sum}, cout: 0x{ref_cout}")
    # 完成测试
    dut.Finish()
    print("Test Passed")

if __name__ == "__main__":
    random_test()

运行测试

picker_out_adder 目录下执行 python3 example.py 命令,即可运行测试。在测试完成后我们即可看到 example 示例项目的输出。

[...]
[test 114507] a=0x7adc43f36682cffe, b=0x30a718d8cf3cc3b1, cin=0x0 => sum: 0x12358823834579604399, cout: 0x0
[test 114508] a=0x3eb778d6097e3a72, b=0x1ce6af17b4e9128, cin=0x0 => sum: 0x4649372636395916186, cout: 0x0
[test 114509] a=0x42d6f3290b18d4e9, b=0x23e4926ef419b4aa, cin=0x1 => sum: 0x7402657300381600148, cout: 0x0
[test 114510] a=0x505046adecabcc, b=0x6d1d4998ed457b06, cin=0x0 => sum: 0x7885127708256118482, cout: 0x0
[test 114511] a=0x16bb10f22bd0af50, b=0x5813373e1759387, cin=0x1 => sum: 0x2034576336764682968, cout: 0x0
[test 114512] a=0xc46c9f4aa798106, b=0x4d8f52637f0417c4, cin=0x0 => sum: 0x6473392679370463434, cout: 0x0
[test 114513] a=0x3b5387ba95a7ac39, b=0x1a378f2d11b38412, cin=0x0 => sum: 0x6164045699187683403, cout: 0x0
Test Passed

3 - 案例二:随机数生成器

基于一个16bit的LFSR随机数生成器展示工具的用法,该随机数生成器内部存在时钟信号、时序逻辑与寄存器。

RTL源码

在本案例中,我们驱动一个随机数生成器,其源码如下:

module RandomGenerator (
    input wire clk,
    input wire reset,
    input [15:0] seed,
    output [15:0] random_number
);
    reg [15:0] lfsr;

    always @(posedge clk or posedge reset) begin
        if (reset) begin
            lfsr <= seed;
        end else begin
            lfsr <= {lfsr[14:0], lfsr[15] ^ lfsr[14]};
        end
    end
 
    assign random_number = lfsr;
endmodule

该随机数生成器包含一个 16 位的 LFSR,其输入为一个 16 位的种子数,输出为一个 16 位的随机数。LFSR 的更新规则为:

  1. 将当前的 LFSR 的最高位与次高位异或,称为new_bit。
  2. 将原来的 LFSR 向左平移一位,将 new_bit 放在最低位。
  3. 丢弃最高位。

测试过程

在测试过程中,我们将创建一个名为 RandomGenerator 的文件夹,其中包含一个 RandomGenerator.v 文件。该文件内容即为上述的 RTL 源码。

将RTL构建为 Python Module

生成中间文件

进入 RandomGenerator 文件夹,执行如下命令:

picker export --autobuild=false RandomGenerator.v -w RandomGenerator.fst --sname RandomGenerator --tdir picker_out_rmg/ --lang python -e --sim verilator

该命令的含义是:

  1. 将RandomGenerator.v作为 Top 文件,并将RandomGenerator作为 Top Module,基于 verilator 仿真器生成动态库,生成目标语言为 Python。
  2. 启用波形输出,目标波形文件为RandomGenerator.fst
  3. 包含用于驱动示例项目的文件(-e),同时codegen完成后不自动编译(-autobuild=false)。
  4. 最终的文件输出路径是 picker_out_rmg

输出的目录类似加法器验证-生成中间文件,这里不再赘述。

构建中间文件

进入 picker_out_rmg 目录并执行 make 命令,即可生成最终的文件。

备注:其编译过程类似于 加法器验证-编译流程,这里不再赘述。

最终目录结果为:

picker_out_rmg
└── RandomGenerator
    |-- RandomGenerator.fst # 测试的波形文件
    |-- UT_RandomGenerator
    |   |-- RandomGenerator.fst.hier
    |   |-- _UT_RandomGenerator.so # Swig生成的wrapper动态库
    |   |-- __init__.py  # Python Module的初始化文件,也是库的定义文件
    |   |-- libDPIRandomGenerator.a # 仿真器生成的库文件
    |   |-- libUTRandomGenerator.so # 基于dut_base生成的libDPI动态库封装
    |   `-- libUT_RandomGenerator.py # Swig生成的Python Module
    |   `-- xspcomm  # xspcomm基础库,固定文件夹,不需要关注
    `-- example.py # 示例代码

配置测试代码

在picker_out_rmg中创建 example.py

from RandomGenerator import *
import random

# 定义参考模型
class LFSR_16:
    def __init__(self, seed):
        self.state = seed & ((1 << 16) - 1)

    def Step(self):
        new_bit = (self.state >> 15) ^ (self.state >> 14) & 1
        self.state = ((self.state << 1) | new_bit ) & ((1 << 16) - 1)

if __name__ == "__main__":
    dut = DUTRandomGenerator()            # 创建DUT 
    dut.InitClock("clk")                  # 指定时钟引脚,初始化时钟
    seed = random.randint(0, 2**16 - 1)   # 生成随机种子
    dut.seed.value = seed                 # 设置DUT种子
    ref = LFSR_16(seed)                   # 创建参考模型用于对比

    # reset DUT
    dut.reset.value = 1                   # reset 信号置1
    dut.Step()                            # 推进一个时钟周期(DUTRandomGenerator是时序电路,需要通过Step推进)
    dut.reset.value = 0                   # reset 信号置0
    dut.Step()                            # 推进一个时钟周期

    for i in range(65536):                # 循环65536次
        dut.Step()                        # dut 推进一个时钟周期,生成随机数
        ref.Step()                        # ref 推进一个时钟周期,生成随机数
        assert dut.random_number.value == ref.state, "Mismatch"  # 对比DUT和参考模型生成的随机数
        print(f"Cycle {i}, DUT: {dut.random_number.value:x}, REF: {ref.state:x}") # 打印结果
    # 完成测试
    print("Test Passed")
    dut.Finish()    # Finish函数会完成波形、覆盖率等文件的写入

运行测试程序

picker_out_rmg 目录下执行 python example.py 即可运行测试程序。在运行完成后,若输出 Test Passed,则表示测试通过。完成运行后,会生成波形文件:RandomGenerator.fst,可在bash中通过以下命令进行查看。

gtkwave RandomGenerator.fst

输出示例:

···
Cycle 65529, DUT: d9ea, REF: d9ea
Cycle 65530, DUT: b3d4, REF: b3d4
Cycle 65531, DUT: 67a9, REF: 67a9
Cycle 65532, DUT: cf53, REF: cf53
Cycle 65533, DUT: 9ea6, REF: 9ea6
Cycle 65534, DUT: 3d4d, REF: 3d4d
Cycle 65535, DUT: 7a9a, REF: 7a9a
Test Passed, destroy UT_RandomGenerator

4 - 案例三:双端口栈(回调)

双端口栈是一个拥有两个端口的栈,每个端口都支持push和pop操作。本案例以双端口栈为例,展示如何使用回调函数驱动DUT

双端口栈简介

双端口栈是一种数据结构,支持两个端口同时进行操作。与传统单端口栈相比,双端口栈允许同时进行数据的读写操作,在例如多线程并发读写等场景下,双端口栈能够提供更好的性能。本例中,我们提供了一个简易的双端口栈实现,其源码如下:

module dual_port_stack (
    input clk,
    input rst,

    // Interface 0
    input in0_valid,
    output in0_ready,
    input [7:0] in0_data,
    input [1:0] in0_cmd,
    output out0_valid,
    input out0_ready,
    output [7:0] out0_data,
    output [1:0] out0_cmd,

    // Interface 1
    input in1_valid,
    output in1_ready,
    input [7:0] in1_data,
    input [1:0] in1_cmd,
    output out1_valid,
    input out1_ready,
    output [7:0] out1_data,
    output [1:0] out1_cmd
);
    // Command definitions
    localparam CMD_PUSH = 2'b00;
    localparam CMD_POP = 2'b01;
    localparam CMD_PUSH_OKAY = 2'b10;
    localparam CMD_POP_OKAY = 2'b11;

    // Stack memory and pointer
    reg [7:0] stack_mem[0:255];
    reg [7:0] sp;
    reg busy;

    reg [7:0] out0_data_reg, out1_data_reg;
    reg [1:0] out0_cmd_reg, out1_cmd_reg;
    reg out0_valid_reg, out1_valid_reg;

    assign out0_data = out0_data_reg;
    assign out0_cmd = out0_cmd_reg;
    assign out0_valid = out0_valid_reg;
    assign out1_data = out1_data_reg;
    assign out1_cmd = out1_cmd_reg;
    assign out1_valid = out1_valid_reg;

    always @(posedge clk or posedge rst) begin
        if (rst) begin
            sp <= 0;
            busy <= 0;
        end else begin
            // Interface 0 Request Handling
            if (!busy && in0_valid && in0_ready) begin
                case (in0_cmd)
                    CMD_PUSH: begin
                        busy <= 1;
                        sp <= sp + 1;
                        out0_valid_reg <= 1;
                        stack_mem[sp] <= in0_data;
                        out0_cmd_reg <= CMD_PUSH_OKAY;
                    end
                    CMD_POP: begin
                        busy <= 1;
                        sp <= sp - 1;
                        out0_valid_reg <= 1;
                        out0_data_reg <= stack_mem[sp - 1];
                        out0_cmd_reg <= CMD_POP_OKAY;
                    end
                    default: begin
                        out0_valid_reg <= 0;
                    end
                endcase
            end

            // Interface 1 Request Handling
            if (!busy && in1_valid && in1_ready) begin
                case (in1_cmd)
                    CMD_PUSH: begin
                        busy <= 1;
                        sp <= sp + 1;
                        out1_valid_reg <= 1;
                        stack_mem[sp] <= in1_data;
                        out1_cmd_reg <= CMD_PUSH_OKAY;
                    end
                    CMD_POP: begin
                        busy <= 1;
                        sp <= sp - 1;
                        out1_valid_reg <= 1;
                        out1_data_reg <= stack_mem[sp - 1];
                        out1_cmd_reg <= CMD_POP_OKAY;
                    end
                    default: begin
                        out1_valid_reg <= 0;
                    end
                endcase
            end

            // Interface 0 Response Handling
            if (busy && out0_ready) begin
                out0_valid_reg <= 0;
                busy <= 0;
            end

            // Interface 1 Response Handling
            if (busy && out1_ready) begin
                out1_valid_reg <= 0;
                busy <= 0;
            end
        end
    end

    assign in0_ready = (in0_cmd == CMD_PUSH && sp < 255|| in0_cmd == CMD_POP && sp > 0) && !busy;
    assign in1_ready = (in1_cmd == CMD_PUSH && sp < 255|| in1_cmd == CMD_POP && sp > 0) && !busy && !(in0_ready && in0_valid);

endmodule

在该实现中,除了时钟信号(clk)和复位信号(rst)之外,还包含了两个端口的输入输出信号,它们拥有相同的接口定义。每个端口的信号含义如下:

  • 请求端口(in)
    • in_valid 输入数据有效信号
    • in_ready 输入数据准备好信号
    • in_data 输入数据
    • in_cmd 输入命令 (0:PUSH, 1:POP)
  • 响应端口(out)
    • out_valid 输出数据有效信号
    • out_ready 输出数据准备好信号
    • out_data 输出数据
    • out_cmd 输出命令 (2:PUSH_OKAY, 3:POP_OKAY)

当我们想通过一个端口对栈进行一次操作时,首先需要将需要的数据和命令写入到输入端口,然后等待输出端口返回结果。

具体地,如果我们想对栈进行一次 PUSH 操作。首先我们应该将需要 PUSH 的数据写入到 in_data 中,然后将 in_cmd 设置为 0,表示 PUSH 操作,并将 in_valid 置为 1,表示输入数据有效。接着,我们需要等待 in_ready 为 1,保证数据已经正确的被接收,此时 PUSH 请求已经被正确发送。

命令发送成功后,我们需要在响应端口等待栈的响应信息。当 out_valid 为 1 时,表示栈已经完成了对应的操作,此时我们可以从 out_data 中读取栈的返回数据(POP 操作的返回数据将会放置于此),从 out_cmd 中读取栈的返回命令。当读取到数据后,需要将 out_ready 置为 1,以通知栈正确接收到了返回信息。

如果两个端口的请求同时有效时,栈将会优先处理端口 0 的请求。

构建驱动环境

与案例一和案例二类似,在对双端口栈进行测试之前,我们首先需要利用 Picker 工具将 RTL 代码构建为 Python Module。在构建完成后,我们将通过 Python 脚本驱动 RTL 代码进行测试。

首先,创建名为 dual_port_stack.v 的文件,并将上述的 RTL 代码复制到该文件中,接着在相同文件夹下执行以下命令:

picker export --autobuild=true dual_port_stack.v -w dual_port_stack.fst --sname dual_port_stack --tdir picker_out_dual_port_stack/ --lang python -e --sim verilator

生成好的驱动环境位于 picker_out_dual_port_stack 文件夹中, 其中 dual_port_stack 为生成的 Python Module。

若自动编译运行过程中无错误发生,则代表环境被正确构建。

利用回调函数驱动 DUT

在本案例中,为了测试双端口栈的功能,我们需要对其进行驱动。但你可能很快就会发现,仅仅使用案例一和案例二中的方法很难对双端口栈进行驱动。因为在此前的测试中,DUT只有一条执行逻辑,给DUT输入数据后等待DUT输出即可。

但双端口栈却不同,它的两个端口是两个独立的执行逻辑,在驱动中,这两个端口可能处于完全不同的状态,例如端口0在等待DUT返回数据时,端口1有可能正在发送新的请求。这种情况下,使用简单的串行执行逻辑将很难对DUT进行驱动。

因此我们在本案例中我们将以双端口栈为例,介绍一种基于回调函数的驱动方法,来完成此类DUT的驱动。

回调函数简介

回调函数是一种常见的编程技术,它允许我们将一个函数传入,并等待某个条件满足后被调用。构建产生的 Python Module 中,我们提供了向内部执行环境注册回调函数的接口 StepRis,使用方法如下:

from dual_port_stack import DUTdual_port_stack

def callback(cycles):
    print(f"The current clock cycle is {cycles}")

dut = DUTdual_port_stack()
dut.StepRis(callback)
dut.Step(10)

你可以直接运行该代码来查看回调函数的效果。

在上述代码中,我们定义了一个回调函数 callback ,它接受一个参数 cycles ,并在每次调用时打印当前的时钟周期。接着通过 StepRis 将该回调函数注册到 DUT 中。

注册回调函数后,每运行一次 Step 函数,即每个时钟周期,都会在时钟信号上升沿去调用该回调函数,并传入当前的时钟周期。

通过这种方式,我们可以将不同的执行逻辑都写成回调函数的方式,并将多个回调函数注册到 DUT 中,从而实现对 DUT 的并行驱动。

基于回调函数驱动的双端口栈

通过回调函数的形式来完成一条完整的执行逻辑,通常我们会使用状态机的模式进行编写。每调用一次回调函数,就会引起状态机内部的状态变化,多次调用回调函数,就会完成一次完整的执行逻辑。

下面是一个基于回调函数驱动的双端口栈的示例代码:

import random
from dual_port_stack import *
from enum import Enum

class StackModel:
    def __init__(self):
        self.stack = []

    def commit_push(self, data):
        self.stack.append(data)
        print("push", data)

    def commit_pop(self, dut_data):
        print("Pop", dut_data)
        model_data = self.stack.pop()
        assert model_data == dut_data, f"The model data {model_data} is not equal to the dut data {dut_data}"
        print(f"Pass: {model_data} == {dut_data}")

class SinglePortDriver:
    class Status(Enum):
        IDLE = 0
        WAIT_REQ_READY = 1
        WAIT_RESP_VALID = 2
    class BusCMD(Enum):
        PUSH = 0
        POP = 1
        PUSH_OKAY = 2
        POP_OKAY = 3

    def __init__(self, dut, model: StackModel, port_dict):
        self.dut = dut
        self.model = model
        self.port_dict = port_dict

        self.status = self.Status.IDLE
        self.operation_num = 0
        self.remaining_delay = 0

    def push(self):
        self.port_dict["in_valid"].value = 1
        self.port_dict["in_cmd"].value = self.BusCMD.PUSH.value
        self.port_dict["in_data"].value = random.randint(0, 2**32-1)

    def pop(self):
        self.port_dict["in_valid"].value = 1
        self.port_dict["in_cmd"].value = self.BusCMD.POP.value

    def step_callback(self, cycle):
        if self.status == self.Status.WAIT_REQ_READY:
            if self.port_dict["in_ready"].value == 1:
                self.port_dict["in_valid"].value = 0
                self.port_dict["out_ready"].value = 1
                self.status = self.Status.WAIT_RESP_VALID

                if self.port_dict["in_cmd"].value == self.BusCMD.PUSH.value:
                    self.model.commit_push(self.port_dict["in_data"].value)

        elif self.status == self.Status.WAIT_RESP_VALID:
            if self.port_dict["out_valid"].value == 1:
                self.port_dict["out_ready"].value = 0
                self.status = self.Status.IDLE
                self.remaining_delay = random.randint(0, 5)

                if self.port_dict["out_cmd"].value == self.BusCMD.POP_OKAY.value:
                    self.model.commit_pop(self.port_dict["out_data"].value)

        if self.status == self.Status.IDLE:
            if self.remaining_delay == 0:
                if self.operation_num < 10:
                    self.push()
                elif self.operation_num < 20:
                    self.pop()
                else:
                    return

                self.operation_num += 1
                self.status = self.Status.WAIT_REQ_READY
            else:
                self.remaining_delay -= 1

def test_stack(stack):
    model = StackModel()

    port0 = SinglePortDriver(stack, model, {
        "in_valid": stack.in0_valid,
        "in_ready": stack.in0_ready,
        "in_data": stack.in0_data,
        "in_cmd": stack.in0_cmd,
        "out_valid": stack.out0_valid,
        "out_ready": stack.out0_ready,
        "out_data": stack.out0_data,
        "out_cmd": stack.out0_cmd,
    })

    port1 = SinglePortDriver(stack, model, {
        "in_valid": stack.in1_valid,
        "in_ready": stack.in1_ready,
        "in_data": stack.in1_data,
        "in_cmd": stack.in1_cmd,
        "out_valid": stack.out1_valid,
        "out_ready": stack.out1_ready,
        "out_data": stack.out1_data,
        "out_cmd": stack.out1_cmd,
    })

    dut.StepRis(port0.step_callback)
    dut.StepRis(port1.step_callback)

    dut.Step(200)


if __name__ == "__main__":
    dut = DUTdual_port_stack()
    dut.InitClock("clk")
    test_stack(dut)
    dut.Finish()

在上述代码中,实现了这样的驱动过程:每个端口独立对DUT进行驱动,并在一个请求完成后添加随机延迟,每个端口分别完成了 10 次 PUSH 操作与 10 次 POP 操作。

PUSHPOP 请求生效时,会调用同一个 StackModel 中的 commit_pushcommit_pop 函数,以模拟栈的行为,并在每次 POP 操作完成后对比 DUT 的返回数据与模型的数据是否一致。

为了实现对单个端口的驱动行为,我们实现了 SinglePortDriver 类,其中实现了一个接口进行收发的完整过程,通过 step_callback 函数来实现内部的更新逻辑。

在测试函数 test_stack 中,我们为双端口栈的每一个端口都创建了一个 SinglePortDriver 实例,传入了对应的接口,并通过 StepRis 函数将其对应的回到函数其注册到 DUT 中。之后调用 dut.Step(200) 时,每个时钟周期中都会自动调用一次回调函数,来完成整个驱动逻辑。

SinglePortDriver 驱动逻辑

上面提到,一般使用回调函数的形式需要将执行逻辑实现为状态机,因此在 SinglePortDriver 类中,需要记录包含端口所处的状态,它们分别是:

  • IDLE:空闲状态,等待下一次操作
    • 在空闲状态下,需要查看另一个状态 remaining_delay 来判断当前是否已经延时结束,如果延时结束可立即进行下一次操作,否则继续等待。
    • 当需要执行下一次操作时,需要查看状态 operation_num (当前已经执行的操作数)来决定下一次操作时 PUSH 还是 POP。之后调用相关函数对端口进行一次赋值,并将状态切换至 WAIT_REQ_READY
  • WAIT_REQ_READY:等待请求端口准备好
    • 当请求发出后(in_valid 有效),此时需要等待 in_ready 信号有效,以确保请求已经被正确接受。
    • 当请求被正确接受后,需要将 in_valid 置为 0,同时将 out_ready 置为 1,表示请求发送完毕,准备好接收回复。
  • WAIT_RESP_VALID:等待响应端口返回数据
    • 当请求被正确接受后,需要等待 DUT 的回复,即等待 out_valid 信号有效。当 out_valid 信号有效时,表示回复已经产生,一次请求完成,于是将 out_ready 置为 0,同时将状态切换至 IDLE

运行测试

在picker_out_dual_port_stack中创建exmaple.py文件,将上述代码复制到其中,然后执行以下命令:

cd picker_out_dual_port_stack
python3 example.py

可直接运行本案例的测试代码,你将会看到类似如下的输出:

...
push 77
push 140
push 249
push 68
push 104
push 222
...
Pop 43
Pass: 43 == 43
Pop 211
Pass: 211 == 211
Pop 16
Pass: 16 == 16
Pop 255
Pass: 255 == 255
Pop 222
Pass: 222 == 222
Pop 104
...

在输出中,你可以看到每次 PUSHPOP 操作的数据,以及每次 POP 操作的结果。如果输出中没有错误信息,则表示测试通过。

回调函数驱动的优劣

通过使用回调函数,我们能够完成对 DUT 的并行驱动,正如本例所示,我们通过两个回调函数实现了对拥有两个独立执行逻辑的端口的驱动。回调函数在简单的场景下,为我们提供了一种简单的并行驱动方法。

但是通过本例也可以看出,仅仅实现一套简单的“请求-回复”流程,就需要维护大量的内部状态,回调函数将本应完整的执行逻辑拆分为了多次函数调用,为代码的编写和调试增加了诸多复杂性。

5 - 案例四:双端口栈(协程)

双端口栈是一个拥有两个端口的栈,每个端口都支持push和pop操作。本案例以双端口栈为例,展示如何使用协程驱动DUT

双端口栈简介与环境构建

本案例中使用的双端口栈与案例三中的实现完全相同,请查看案例三中的双端口栈简介构建驱动环境

利用协程驱动 DUT

在案例三中,我们使用了回调函数的方式来驱动DUT,回调函数虽然给我们提供了一种能够完成并行操作的方式,然而其却把完成的执行流程割裂为多次函数调用,并需要维护大量中间状态,导致代码的编写及调试变得较为复杂。

在本案例中,我们将会介绍一种通过协程驱动的方法,这种方法不仅能够做到并行操作,同时能够很好地避免回调函数所带来的问题。

协程简介

协程是一种“轻量级”的线程,通过协程,你可以实现与线程相似的并发执行的行为,但其开销却远小于线程。其实现原理是,协程库实现了一个运行于单线程之上的事件循环(EventLoop),程序员可以定义若干协程并且加入到事件循环,由事件循环负责这些协程的调度。

一般来说,我们定义的协程在执行过程中会持续执行,直到遇到一个需要等待的“事件”,此时事件循环就会暂停执行该协程,并调度其他协程运行。当事件发生后,事件循环会再次唤醒该协程,继续执行。

对于硬件验证中的并行执行来说,这种特性正是我们所需要的,我们可以创建多个协程,来完成验证中的多个驱动任务。我们可以将时钟的执行当做事件,在每个协程中等待这个事件,当时钟信号到来时,事件循环会唤醒所有等待的协程,使其继续执行,直到他们等待下一个时钟信号。

我们用 Python 中的 asyncio 来实现对协程的支持:

import asyncio
from dual_port_stack import *

async def my_coro(dut, name):
    for i in range(10):
        print(f"{name}: {i}")
        await dut.AStep(1)

async def test_dut(dut):
    asyncio.create_task(my_coro(dut, "coroutine 1"))
    asyncio.create_task(my_coro(dut, "coroutine 2"))
    await asyncio.create_task(dut.RunStep(10))

dut = DUTdual_port_stack()
dut.InitClock("clk")
asyncio.run(test_dut(dut))
dut.Finish()

你可以直接运行上述代码来观察协程的执行过程。在上述代码中我们用 create_task 创建了两个协程任务并加入到事件循环中,每个协程任务中,会不断打印一个数字并等待下一个时钟信号到来。

我们使用 dut.RunStep(10) 来创建一个后台时钟,它会不断产生时钟同步信号,使得其他协程能够在时钟信号到来时继续执行。

基于协程驱动的双端口栈

利用协程,我们就可以将驱动双端口栈中单个端口逻辑写成一个独立的执行流,不需要再去维护大量的中间状态。

下面是我们提供的一个简单的使用协程驱动的验证代码:

import asyncio
import random
from dual_port_stack import *
from enum import Enum

class StackModel:
    def __init__(self):
        self.stack = []

    def commit_push(self, data):
        self.stack.append(data)
        print("Push", data)

    def commit_pop(self, dut_data):
        print("Pop", dut_data)
        model_data = self.stack.pop()
        assert model_data == dut_data, f"The model data {model_data} is not equal to the dut data {dut_data}"
        print(f"Pass: {model_data} == {dut_data}")

class SinglePortDriver:
    class BusCMD(Enum):
        PUSH = 0
        POP = 1
        PUSH_OKAY = 2
        POP_OKAY = 3

    def __init__(self, dut, model: StackModel, port_dict):
        self.dut = dut
        self.model = model
        self.port_dict = port_dict

    async def send_req(self, is_push):
        self.port_dict["in_valid"].value = 1
        self.port_dict["in_cmd"].value = self.BusCMD.PUSH.value if is_push else self.BusCMD.POP.value
        self.port_dict["in_data"].value = random.randint(0, 2**8-1)
        await self.dut.AStep(1)

        await self.dut.Acondition(lambda: self.port_dict["in_ready"].value == 1)
        self.port_dict["in_valid"].value = 0

        if is_push:
            self.model.commit_push(self.port_dict["in_data"].value)

    async def receive_resp(self):
        self.port_dict["out_ready"].value = 1
        await self.dut.AStep(1)

        await self.dut.Acondition(lambda: self.port_dict["out_valid"].value == 1)
        self.port_dict["out_ready"].value = 0

        if self.port_dict["out_cmd"].value == self.BusCMD.POP_OKAY.value:
            self.model.commit_pop(self.port_dict["out_data"].value)

    async def exec_once(self, is_push):
        await self.send_req(is_push)
        await self.receive_resp()
        for _ in range(random.randint(0, 5)):
            await self.dut.AStep(1)

    async def main(self):
        for _ in range(10):
            await self.exec_once(is_push=True)
        for _ in range(10):
            await self.exec_once(is_push=False)

async def test_stack(stack):
    model = StackModel()

    port0 = SinglePortDriver(stack, model, {
        "in_valid": stack.in0_valid,
        "in_ready": stack.in0_ready,
        "in_data": stack.in0_data,
        "in_cmd": stack.in0_cmd,
        "out_valid": stack.out0_valid,
        "out_ready": stack.out0_ready,
        "out_data": stack.out0_data,
        "out_cmd": stack.out0_cmd,
    })

    port1 = SinglePortDriver(stack, model, {
        "in_valid": stack.in1_valid,
        "in_ready": stack.in1_ready,
        "in_data": stack.in1_data,
        "in_cmd": stack.in1_cmd,
        "out_valid": stack.out1_valid,
        "out_ready": stack.out1_ready,
        "out_data": stack.out1_data,
        "out_cmd": stack.out1_cmd,
    })

    asyncio.create_task(port0.main())
    asyncio.create_task(port1.main())
    await asyncio.create_task(dut.RunStep(200))

if __name__ == "__main__":
    dut = DUTdual_port_stack()
    dut.InitClock("clk")
    asyncio.run(test_stack(dut))
    dut.Finish()

与案例三类似,我们定义了一个 SinglePortDriver 类,用于驱动单个端口的逻辑。在 main 函数中,我们创建了两个 SinglePortDriver 实例,分别用于驱动两个端口。我们将两个端口的驱动过程放在了入口函数 main 中,并通过 asyncio.create_task 将其加入到事件循环中,在最后我们通过 dut.RunStep(200) 来创建了后台时钟,以推动测试的进行。

该代码实现了与案例三一致的测试逻辑,即在每个端口中对栈进行 10 次 PUSH 和 10 次 POP 操作,并在操作完成后添加随机延迟。但你可以清晰的看到,利用协程进行编写,不需要维护任何的中间状态。

SinglePortDriver 逻辑

SinglePortDriver 类中,我们将一次操作封装为 exec_once 这一个函数,在 main 函数中只需要首先调用 10 次 exec_once(is_push=True) 来完成 PUSH 操作,再调用 10 次 exec_once(is_push=False) 来完成 POP 操作即可。

exec_once 函数中,我们首先调用 send_req 函数来发送请求,然后调用 receive_resp 函数来接收响应,最后通过等待随机次数的时钟信号来模拟延迟。

send_reqreceive_resp 函数的实现逻辑类似,只需要将对应的输入输出信号设置为对应的值,然后等待对应的信号变为有效即可,可以完全根据端口的执行顺序来编写。

类似地,我们使用 StackModel 类来模拟栈的行为,在 commit_pushcommit_pop 函数中分别模拟了 PUSH 和 POP 操作,并在 POP 操作中进行了数据的比较。

运行测试

在picker_out_dual_port_stack文件夹中创建example.py将上述代码复制到其中,然后执行以下命令:

cd picker_out_dual_port_stack
python3 example.py

可直接运行本案例的测试代码,你将会看到类似如下的输出:

...
Push 141
Push 102
Push 63
Push 172
Push 208
Push 130
Push 151
...
Pop 102
Pass: 102 == 102
Pop 138
Pass: 138 == 138
Pop 56
Pass: 56 == 56
Pop 153
Pass: 153 == 153
Pop 129
Pass: 129 == 129
Pop 235
Pass: 235 == 235
Pop 151
...

在输出中,你可以看到每次 PUSHPOP 操作的数据,以及每次 POP 操作的结果。如果输出中没有错误信息,则表示测试通过。

协程驱动的优劣

通过协程函数,我们可以很好地实现并行操作,同时避免了回调函数所带来的问题。每个独立的执行流都能被完整保留,实现为一个协程,大大方便了代码的编写。

然而,在更为复杂的场景下你会发现,实现了众多协程,会使得协程之间的同步和时序管理变得复杂。尤其是你需要在两个不与DUT直接交互的协程之间进行同步时,这种现象会尤为明显。

在这时候,你就需要一套协程编写的规范以及验证代码的设计模式,来帮助你更好地编写基于协程的验证代码。因此,我们提供了 toffee 库,它提供了一套基于协程的验证代码设计模式,你可以通过使用 toffee 来更好地编写验证代码,你可以在这里去进一步了解 toffee。