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

返回本页常规视图.

学习资源

本学习资源介绍验证相关的基本概念、技术,以及如何使用本项目提供的开源工具进行芯片验证

学习本材料前,假定您已经拥有linux、python等相关基础知识。

相关学习材料:

  1. 《Linux 101》在线讲义
  2. Python官方教程
  3. Javatpoint上的Git基础

若计划参与本平台上发布的“开源开放验证项目”,建议提前完本材料的学习。

1 - 快速开始

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

在开始前本页会 简单的介绍什么是验证,以及示例里面用到的概念,如 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.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/

1.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

1.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

1.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 的并行驱动,正如本例所示,我们通过两个回调函数实现了对拥有两个独立执行逻辑的端口的驱动。回调函数在简单的场景下,为我们提供了一种简单的并行驱动方法。

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

1.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。

2 - 基础工具

开放验证平台的基础工具的详细使用方法。

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

2.1 - 工具介绍

验证工具的基本使用。

为满足开放验证的环境要求,我们开发了 Picker 工具,用于将 RTL 设计转换为多语言接口,并在此基础上进行验证,我们将会使用 Picker 工具生成的环境作为基础的验证环境。接下来我们将介绍 Picker 工具,及其基础的使用方法。

Picker 简介

picker 是一个芯片验证辅助工具,具有两个主要功能:

  1. 打包RTL设计验证模块: picker 可以将 RTL 设计验证模块(.v/.scala/.sv)打包成动态库,并提供多种高级语言(目前支持 C++、Python、Java、Scala、Golang)的编程接口来驱动电路。
  2. UVM-TLM代码自动生成: picker 能够基于用户提供的 UVM sequence_item 进行自动化的 TLM 代码封装,提供 UVM 与其他高级语言(如 Python)的通信接口。 该工具允许用户基于现有的软件测试框架,例如 pytest、junit、TestNG、go test 等,进行芯片单元测试。

基于 Picker 进行验证的优点:

  1. 不泄露 RTL 设计:经过 Picker 转换后,原始的设计文件(.v)被转化成了二进制文件(.so),脱离原始设计文件后,依旧可进行验证,且验证者无法获取 RTL 源代码。
  2. 减少编译时间:当 DUT(设计待测)稳定时,只需要编译一次(打包成 .so 文件)。
  3. 用户范围广:提供的编程接口多,覆盖不同语言的开发者。
  4. 使用丰富的软件生态:支持 Python3、Java、Golang 等生态系统。
  5. 自动化的 UVM 事务封装:通过自动化封装 UVM 事务,实现 UVM 和 Python 的通信。

Picker 目前支持的 RTL 仿真器:

  1. Verilator
  2. Synopsys VCS

Picker的工作原理
Picker的主要功能就是将Verilog代码转换为C++或者Python代码,以Chisel开发的处理器为例:先通过Chisel自带的工具将其转换为Verilog代码,再通Picker提供高级编程语言接口。

Picker的工作原理

Python 模块生成

生成模块的过程

Picker 导出 Python Module 的方式是基于 C++ 的。

  • Picker 是 代码生成(codegen)工具,它会先生成项目文件,再利用 make 编译出二进制文件。
  • Picker 首先会利用仿真器将 RTL 代码编译为 C++ Class,并编译为动态库。(见C++步骤详情)
  • 再基于 Swig 工具,利用上一步生成的 C++ 的头文件定义,将动态库导出为 Python Module。
  • 最终将生成的模块导出到目录,并按照需求清理或保留其他中间文件。

Swig 是一个用于将 C/C++ 导出为其他高级语言的工具。该工具会解析 C++ 头文件,并生成对应的中间代码。 如果希望详细了解生成过程,请参阅 Swig 官方文档。 如果希望知道 Picker 如何生成 C++ Class,请参阅 C++

  • 该这个模块和标准的 Python 模块一样,可以被其他 Python 程序导入并调用,文件结构也与普通 Python 模块无异。

Python 模块使用

  • 参数 --language python--lang python 用于指定生成Python基础库。
  • 参数 --example, -e 用于生成包含示例项目的可执行文件。
  • 参数 --verbose, -v 用于保留生成项目时的中间文件。

使用工具生成Python的DUT类

以案例一中的简单加法器为例:

  • Picker会自动生成Python的一个基础类,我们称之为DUT类,以前加法器为例,用户需要编写测试用例,即导入上一章节生成的 Python Module,并调用其中的方法,以实现对硬件模块的操作。 目录结构为:
picker_out_adder
├── Adder                   # Picker 工具生成的项目
│   ├── _UT_Adder.so
│   ├── __init__.py
│   ├── libUTAdder.so
│   ├── libUT_Adder.py
│   └── signals.json
└── example.py              # 用户需要编写的代码
  • 在DUT对应的DUTAdder类中共有8个方法(位于Adder/init.py文件),具体如下:
class DUTAdder:
    def InitClock(name: str)    # 初始化时钟,参数时钟引脚对应的名称,例如clk
    def Step(i:int = 1)         # 推进电路i个周期
    def StepRis(callback: Callable, args=None,  args=(), kwargs={})  # 设置上升沿回调函数
    def StepFal(callback: Callable, args=None,  args=(), kwargs={})  # 设置下降沿回调函数
    def SetWaveform(filename)   # 设置波形文件
    def SetCoverage(filename)   # 设置代码覆盖率文件
    def RefreshComb()           # 推进组合电路
    def Finish()                # 销毁电路
  • DUT对应的引脚,例如reset,clock等在DUTAdder类中以成员变量的形式呈现。如下所示,可以通过value进行引脚的读取和写入。
from Adder import *
dut = DUTAdder()
dut.a.value = 1  # 通过给引脚的.value属性赋值完成对引脚的赋值
dut.a[12] = 1    # 对引脚输入a的第12bit进行赋值
x = dut.a.value  # 读取引脚a的值
y = dut.a[12]    # 读取引脚a的第12bit的值

驱动DUT的一般流程

  1. 创建DUT,设置引脚模式。默认情况下,引脚是在一下个周期的上升沿进行赋值,如果是组合逻辑,需要设置赋值模式为立即赋值。
  2. 初始化时钟。其目的是将时钟引脚与DUT内置的xclock进行绑定。组合逻辑没有时钟可以忽略。
  3. reset电路。大部分时序电路都需要reset。
  4. 给DUT输入引脚写入数据。通过“pin.Set(x)”接口,或者pin.vaulue=x进行赋值。
  5. 驱动电路。时序电路用Step,组合电路用RefreshComb。
  6. 获取DUT各个引脚的输出进行检测。例如和参考模型进行的结果进行assert对比。
  7. 完成验证,销毁DUT。调用Finish()时,会把波形,覆盖率等写入到文件。

对应伪代码如下:


# Python DUT 的名字可通过 --tdir 指定
from DUT import *

# 1 创建
dut = DUT()

# 2 初始化
dut.SetWaveform("test.fst")
dut.InitClock("clock")

# 3 reset
dut.reset = 1
dut.Step(1)
dut.reset = 0
dut.Step(1)

# 4 输入数据
dut.input_pin1.value = 0x123123
dut.input_pin3.value = "0b1011"

# 5 驱动电路 
dut.Step(1)

# 6 得到结果
x = dut.output_pin.value
print("result:", x)

# 7 销毁
dut.Finish()

其他数据类型

一般情况下,通过上述DUT类自带的接口就能完成绝大部分DUT的验证,但一些特殊情况需要其他对应的接口,例如自定义时钟、异步操作、推进组合电路并写入波形、修改引脚属性等。

在picker生成的DUT类中,除了XData类型的引脚成员变量外,还有XClock类型的xclockXPort类型的xport

class DUTAdder(object):
    xport: XPort         # 成员变量 xport,用于管理DUT中的所有引脚
    xclock: XClock       # 成员变量 xclock,用于管理时钟
    # DUT 引脚
    a: XData
    b: XData
    cin: XData
    cout: XData

XData 类

  • DUT引脚中的数据通常位宽不确定,且有四种状态:0、1、Z和X。为此picker提供了XData进行电路引脚数据表示。

主要方法

class XData:
    #拆分XData,例如把一个32位XData中的第7-10位创建成为一个独立XData
    #  name:名称,start:开始位,width:位宽,例如auto sub = a.SubDataRef("sub_pin", 0, 4)
    def SubDataRef(name, start, width): XData
    def GetWriteMode():WriteMode     #获取XData的写模式,写模式有三种:Imme立即写,Rise上升沿写,Fall下降沿写
    def SetWriteMode(mode:WriteMode) #设置XData的写模式 eg: a.SetWriteMode(WriteMode::Imme)
    def DataValid():bool             #检测数据是否有效(Value中含有X或者Z态返回false否者true)
    def W():int             #获取XData的位宽(如果为0,表示XData为verilog中的logic类型,否则为Vec类型的位宽)
    def U():int             #获取XData的值(无符号,同 x = a.value)
    def S():int             #获取XData的值(有符号类型)
    def String():str        #将XData转位16进制的字符串类型,eg: "0x123ff",如果出现?,表现对应的4bit中有x或z态
    def Equal(xdata):bool   #判断2个XData是否相等
    def Set(value)          #对XData进行赋值,value类型可以为:XData, string, int, bytes等
    def GetBytes(): bytes   #以bytes格式获取XData中的数
    def Connect(xdata):bool #连接2个XData,只有In和Out类型的可以连接,当Out数据发生变化时,In类型的XData会自动写入
    def IsInIO():bool       #判断XData是否为In类型,改类型可读可写
    def IsOutIO():bool      #判断XData是否为Out类型,改类型只可读
    def IsBiIO():bool       #判断XData是否为Bi类型,改类型可读可写
    def IsImmWrite(): bool  #判断XData是否为Imm写入模式
    def IsRiseWrite(): bool #判断XData是否为Rise写入模式
    def IsFallWrite(): bool #判断XData是否为Fall写入模式
    def AsImmWrite()        #更改XData的写模式为Imm
    def AsRiseWrite()       #更改XData的写模式为Rise
    def AsFallWrite()       #更改XData的写模式为Fall
    def AsBiIO()            #更改XData为Bi类型
    def AsInIO()            #更改XData为In类型
    def AsOutIO()           #更改XData为Out类型
    def FlipIOType()        #将XData的IO类型进行取反,例如In变为Out或者Out变为In
    def Invert()            #将XData中的数据进行取反
    def At(index): PinBind  #获取第index, eg: x = a.At(12).Get() or a.At(12).Set(1)
    def AsBinaryString()    #将XData的数据变为二进制字符串,eg: "1001011"

为了简化赋值操作,XData 对 Set(value) 和 U() 方法进行了属性赋值重载,可以通过pin.value=xx=pin.value进行赋值和取值。

# 使用.value可以进行访问
# a 为XData类型
a.value = 12345        # 十进制赋值
a.value = 0b11011      # 二进制赋值
a.value = 0o12345      # 八进制赋值
a.value = 0x12345      # 十六进制赋值
a.value = -1           # 所有bit赋值1, a.value = x 与 a.Set(x) 等价
a[31] = 0              # 对第31位进行赋值
a.value = "x"          # 赋值高阻态
a.value = "z"          # 赋值不定态
x = a.value            # 获取值,与 x = a.U() 等价

XPort 类

  • 在处理少数几个XData引脚时,直接操作XData是比较清晰和直观的。但是,当涉及到多个XData时,进行批量管理就不太方便了。XPort是对XData的一种封装,它允许我们对多个XData进行集中操作。我们还提供了一些方法来方便地进行批量管理。

初始化与添加引脚

port = XPort("p") #创建前缀为p的XPort实例

主要方法

class XPort:
    def XPort(prefix = "")      #创建前缀为prefix的port, eg:p = XPort("tile_link_")
    def PortCount(): int        #获取端口中的Pin数量(即绑定的XData个数)
    def Add(pin_name, XData)    #添加Pin, eg:p.Add("reset", dut.reset)
    def Del(pin_name)           #删除Pin
    def Connect(xport2)         #链接2个Port
    def NewSubPort(std::string subprefix): XPort # 创建子Port,以subprefix开头的所有Pin构成子Port
    def Get(key, raw_key = False): XData         # 获取XData
    def SetZero()                                #设置Port中的所有XData为0

XClock 类

  • XClock是电路时钟的封装,用于驱动电路。在传统仿真工具(例如Verilator)中,需要手动为clk赋值,并通过step_eval函数更新状态。但在我们的工具中,我们提供了相应的方法,可以将时钟直接绑定到XClock上。只需使用我们的Step()方法,就可以同时更新clk和电路状态。

初始化与添加引脚

# 初始化
clk = XClock(stepfunc)  #参数stepfunc为DUT后端提供的电路推进方法,例如verilaor的step_eval等

主要方法

class XClock:
    def Add(xdata)       #将Clock和时钟进行绑定, eg:clock.Add(dut.clk)
    def Add(xport)       #将Clock和XData进行绑定
    def RefreshComb()                     #推进电路状态,不推进时间,不dump波形
    def RefreshCombT()                    #推进电路状态(推进时间,dump波形)
    def Step(int s = 1)                   #推进电路s个时钟周期,  DUT.Step = DUT.xclock.Step
    def StepRis(func, args=(), kwargs={}) #设置上升沿回调函数,DUT.StepRis = DUT.xclock.StepRis
    def StepFal(func, args=(), kwargs={}) #设置下降沿回调函数,DUT.StepFal = DUT.xclock.StepFal
    # 异步方法
    async AStep(cycle: int)      #异步推进cycle个时钟, eg:await dut.AStep(5)
    async ACondition(condition)  #异步等待conditon()为true
    async ANext()                #异步推进一个时钟周期,等同AStep(1)
    async RunStep(cycle: int)    #持续推进时钟cycle个时钟,用于最外层

2.2 - 波形生成

生成电路波形

使用方法

在使用 Picker 工具封装 DUT 时,使用选项-w [wave_file]指定需要保存的波形文件。 针对不同的后端仿真器,支持不同的波形文件类型,具体如下:

  1. Verilator
    • .vcd格式的波形文件。
    • .fst格式的波形文件,更高效的压缩文件。
  2. VCS
    • .fsdb格式的波形文件,更高效的压缩文件。

需要注意的是,如果你选择自行生成 libDPI_____.so 文件,那么波形文件格式不受上述约束的限制。因为波形文件是在仿真器构建 libDPI.so 时决定的,如果你自行生成,那么波形文件格式也需要自行用对应仿真器的配置指定。

Python 示例

正常情况下,dut需要被显式地声明完成任务,以通知进行模拟器的后处理工作(写入波形、覆盖率等文件)。 在Python中,需要在完成所有测试后,调用dut的.Finish()方法以通知模拟器任务已完成,进而将文件flush到磁盘。

加法器为例,以下为测试程序:

from Adder import *

if __name__ == "__main__":
    dut = DUTAdder()

    for i in range(10):
        dut.a.value = i * 2
        dut.b.value = int(i / 4)
        dut.Step(1)
        print(dut.sum.value, dut.cout.value)

    dut.Finish() # flush the wave file to disk

运行结束后即可生成指定文件名的波形文件。

查看结果

GTKWave

使用 GTKWave 打开 fstvcd 波形文件,即可查看波形图。

GTKWave

Verdi

使用 Verdi 打开 fsdbvcd 波形文件,即可查看波形图。

Verdi

2.3 - 多文件输入

处理多个Verilog源文件

多文件输入输出

在许多情况中,某文件下的某个模块会例化其他文件下的模块,在这种情况下您可以使用Picker工具的-f选项处理多个verilog源文件。例如,假设您有Cache.sv, CacheStage.sv以及CacheMeta.sv三个源文件:

文件列表

Cache.sv

// In 
module Cache(
    ...
);
    CacheStage s1(
        ...
    );

    CacheStage s2(
        ...
    );

    CacheStage s3(
        ...
    );

    CacheMeta cachemeta(
        ...
    );
endmodule

CacheStage.sv

// In CacheStage.sv
module CacheStage(
    ...
);
    ...
endmodule

CacheMeta.sv

// In CacheMeta.sv
module CacheMeta(
    ...
);
    ...
endmodule

应用方式

其中,待测模块为Cache,位于Cache.sv中,则您可以通过以下命令生成DUT:

命令行指定

picker export Cache.sv --fs CacheStage.sv,CacheMeta.sv --sname Cache

通过文件列表文件指定

您也可以通过传入.txt文件的方式来实现多文件输入:

picker export Cache.sv --fs src.txt --sname Cache

其中src.txt的内容为:

CacheStage.sv
CacheMeta.sv

注意事项

  1. 需要注意的是,使用多文件输入时仍需要指定待测顶层模块所在的文件,例如上文中所示的Cache.sv
  2. 在使用多文件输入时,Picker会将所有文件都交给仿真器,仿真器同时进行编译,因此需要保证所有文件中的模块名不重复。

2.4 - 覆盖率统计

覆盖率工具

Picker 工具支持生成代码行覆盖率报告,toffee-test(https://github.com/XS-MLVP/toffee-test) 项目支持生成功能覆盖率报告。

代码行覆盖率

目前 Picker 工具支持基于 Verilator 仿真器生成的代码行覆盖率报告。

Verilator

Verilator 仿真器提供了覆盖率支持功能。 该功能的实现方式是:

  1. 利用 verilator_coverage 工具处理或合并覆盖率数据库,最终针对多个 DUT 生成一个 coverage.info 文件。
  2. 利用 lcov 工具的 genhtml 命令基于coverage.info和 RTL 代码源文件,生成完整的代码覆盖率报告。

使用时的流程如下:

  1. 使用 Picker 生成 dut 时,使能 COVERAGE 功能 (添加-c选项)。
  2. 仿真器运行完成后,dut.Finish() 之后会生成覆盖率数据库文件 V{DUT_NAME}.dat
  3. 基于 verilator_coverage 的 write-info 功能将其转换成 .info文件。
  4. 基于 lcovgenhtml 功能,使用.info文件和文件中指定的rtl 源文件,生成 html 报告。

注意: 文件中指定的rtl 源文件是指在生成的DUT时使用的源文件路径,需要保证这些路径在当前环境下是有效的。简单来说,需要编译时用到的所有.sv/.v文件都需要在当前环境下存在,并且目录不变。

verilator_coverage

verilator_coverage 工具用于处理 DUT 运行后生成的 .dat 的覆盖率数据。该工具可以处理并合并多个 .dat 文件,同时具有两类功能:

  1. 基于 .dat 文件生成 .info 文件,用于后续生成网页报告。

    • -annotate <output_dir>:以标注的形式在源文件中呈现覆盖率情况,结果保存到output_dir中。形式如下:

      100000  input logic a;   // Begins with whitespace, because
                              // number of hits (100000) is above the limit.
      %000000  input logic b;   // Begins with %, because
                              // number of hits (0) is below the limit.
      
    • -annotate-min <count>:指定上述的 limit 为 count

  2. 可以将 .dat 文件,结合源代码文件,将覆盖率数据以标注的形式与源代码结合在一起,并写入到指定目录。

    • -write <merged-datafile> -read <datafiles>:将若干个.dat(datafiles)文件合并为一个.dat 文件
    • -write-info <merged-info> -read <datafiles>:将若干个.dat(datafiles)文件合并为一个.info 文件

genhtml

locv 包提供的 genhtml 可以由上述的.info 文件导出可读性更好的 html 报告。命令格式为:genhtml [OPTIONS] <infofiles>。 建议使用-o <outputdir>选项将结果输出到指定目录。

加法器为例。

adder.jpg

使用示例

如果您使用 Picker 时打开了-c选项,那么在仿真结束后,会生成一个V{DUT_NAME}.dat文件。并且顶层目录会有一个 Makefile 文件,其中包含了生成覆盖率报告的命令。

命令内容如下:

coverage:
    ...
    verilator_coverage -write-info coverage.info ./${TARGET}/V${PROJECT}_coverage.dat
    genhtml coverage.info --output-directory coverage
    ...

在 shell 中输入make coverage,其会根据生成的.dat 文件生成 coverage.info,再使用genhtml再 coverage 目录下生成 html 报告。

VCS

VCS 对应的文档正在完善当中。

2.5 - 多时钟

多时钟示例

部分电路有多个时钟,XClock 类提供了分频功能,可以通过它实现对多时钟电路的驱动。

XClock 中的 FreqDivWith 接口

XClock 函数提供如下分频接口

void XClock::FreqDivWith(int div,   // 分频数,,即绑定的XClock的频率为原时钟频率的div分之1
                     XClock &clk,   // 绑定的XClock
                     int shift=0)   // 对波形进行 shift 个半周期的移位

XClock 的一般驱动流程

  1. 创建 XClock,绑定 DUT 的驱动函数
# 假设已经创建了DUT,并将其命名为dut
# 创建XClock
xclock = XClock(dut.dut.simStep)
  1. 绑定关联 clk 引脚
# clk是dut的时钟引脚
xclock.Add(dut.clk)
# Add方法具有别名:AddPin
  1. 通过 XPort 绑定与 clk 关联的引脚

因为在我们的工具中,对于端口的读写是通过 xclock 来驱动的,所以如果不将与 clk 关联的引脚绑定到 XClock 上,那么在驱动时,相关的引脚数值不会发生变化。
比如,我们要进行复位操作,那么可以将 reset 绑定到 xclock 上。

方法:

class XClock:
    def Add(xport)       #将Clock和XData进行绑定

举例:

# xclock.Add(dut.xport.Add(pin_name, XData))
xclock.Add(dut.xport.Add("reset", dut.reset))

在经过了前面的绑定之后,接下来可以使用了。
我们根据需要来设置回调、设置分频。当然,时序电路肯定也要驱动时钟。
这些方法都可以参照工具介绍

下面是举例:

# func为回调函数,args为自定义参数
#设置上升沿回调函数
dut.StepRis(func, args=(), kwargs={})
#设置下降沿回调函数
dut.StepFal(func, args=(), kwargs={})
# 假设xclock是XClock的实例
xclock.FreqDivWith(2, half_clock)           # 将xclock的频率分频为原来的一半
xclock.FreqDivWith(1, left_clock -2)      # 将xclock的频率不变,对波形进行一个周期的左移
dut.Step(10) #推进10个时钟周期

多时钟案例

例如多时钟电路有 6 个 clock,每个 clock 都有一个对应的计数器,设计代码如下:

module multi_clock (
    input wire clk1,
    input wire clk2,
    input wire clk3,
    input wire clk4,
    input wire clk5,
    input wire clk6,
    output reg [31:0] reg1,
    output reg [31:0] reg2,
    output reg [31:0] reg3,
    output reg [31:0] reg4,
    output reg [31:0] reg5,
    output reg [31:0] reg6
);
    initial begin
        reg1 = 32'b0;
        reg2 = 32'b0;
        reg3 = 32'b0;
        reg4 = 32'b0;
        reg5 = 32'b0;
        reg6 = 32'b0;
    end
    always @(posedge clk1) begin
        reg1 <= reg1 + 1;
    end
    always @(posedge clk2) begin
        reg2 <= reg2 + 1;
    end
    always @(posedge clk3) begin
        reg3 <= reg3 + 1;
    end
    always @(posedge clk4) begin
        reg4 <= reg4 + 1;
    end
    always @(posedge clk5) begin
        reg5 <= reg5 + 1;
    end
    always @(posedge clk6) begin
        reg6 <= reg6 + 1;
    end
endmodule

通过picker导出:

picker export multi_clock.v -w mc.fst --tdir picker_out/MultiClock --lang python

可以通过如下 Python 进行多时钟驱动:


from MultiClock import *
from xspcomm import XClock

def test_multi_clock():
    # 创建DUT
    dut = DUTmulti_clock()
    # 创建主时钟
    main_clock = XClock(dut.dut.simStep)
    # 创建子时钟
    clk1, clk2, clk3 = XClock(lambda x: 0), XClock(lambda x: 0), XClock(lambda x: 0)
    clk4, clk5, clk6 = XClock(lambda x: 0), XClock(lambda x: 0), XClock(lambda x: 0)
    # 给子时钟添加相关的clock引脚及关联端口
    clk1.Add(dut.xport.SelectPins(["reg1"])).AddPin(dut.clk1.xdata)
    clk2.Add(dut.xport.SelectPins(["reg2"])).AddPin(dut.clk2.xdata)
    clk3.Add(dut.xport.SelectPins(["reg3"])).AddPin(dut.clk3.xdata)
    clk4.Add(dut.xport.SelectPins(["reg4"])).AddPin(dut.clk4.xdata)
    clk5.Add(dut.xport.SelectPins(["reg5"])).AddPin(dut.clk5.xdata)
    clk6.Add(dut.xport.SelectPins(["reg6"])).AddPin(dut.clk6.xdata)
    # 将主时钟频率分频到子时钟
    main_clock.FreqDivWith(1, clk1)
    main_clock.FreqDivWith(2, clk2)
    main_clock.FreqDivWith(3, clk3)
    main_clock.FreqDivWith(1, clk4, -1)
    main_clock.FreqDivWith(2, clk5, 1)
    main_clock.FreqDivWith(3, clk6, 2)
    # 驱动时钟
    main_clock.Step(100)
    dut.Finish()

if __name__ == "__main__":
    test_multi_clock()

上述代码输出的波形如下:

multi_clock

可以看到:

  • clk2 的周期是 clk1 的 2 倍
  • clk3 的周期是 clk1 的 3 倍,
  • clk4 的周期和 clk1 相同,但是进行了半个周期的右移
  • clk5 的周期和 clk2 相同,但是进行了半个周期的左移
  • clk6 的周期和 clk3 相同,但是进行了一个周期的左移

2.6 - 多实例

多实例示例

在Verilog中,一个module只有一个实例,但很多测试场景下需要实现多个module,为此picker提供了动态多实例和静态多实例的支持。

动态多实例

动态多实例相当于类的实例化,在创建dut的同时实例化对应的module,所以用户无感知。支持最大16个实例同时运行。

例子:

以Adder为例,我们可以在测试时根据需要在合适的位置创建多个dut,来动态创建多个Adder实例。 当需要销毁一个dut时,也不会影响后续创建新的dut。

创建一个名为 picker_out_adder 的文件夹,其中包含一个 Adder.v 文件。该文件的源码参考案例一:简单加法器

运行下述命令将RTL导出为 Python Module:

picker export Adder.v --autobuild true -w Adder.fst --sname Adder

在picker_out_adder中添加 example.py,动态创建多个Adder实例:

from Adder import *

import random

def random_int():
    return random.randint(-(2**127), 2**127 - 1) & ((1 << 127) - 1)

def main():
    dut=[]
    # 可以通过创建多个dut,实例化多个Adder,理论上支持最大16个实例同时运行
    for i in range(7):
        # 这里通过循环创建了7个dut
        dut.append(DUTAdder(waveform_filename=f"{i}.fst"))
    for d in dut:
        d.a.value = random_int()
        d.b.value = random_int()
        d.cin.value = random_int() & 1
        d.Step(1)
        print(f"DUT: sum={d.sum.value}, cout={d.cout.value}")
        # 通过Finish()函数在合适的时机撤销某个dut,也即销毁某个实例
        d.Finish()
    # 可以根据需要在合适的时机创建新的Adder实例
    # 下面创建了一个新的dut,旨在说明可以在程序结束前的任何时机创建新的dut
    dut_new = DUTAdder(waveform_filename=f"new.fst")
    dut_new.a.value = random_int()
    dut_new.b.value = random_int()
    dut_new.cin.value = random_int() & 1
    dut_new.Step(1)
    print(f"DUT: sum={dut_new.sum.value}, cout={dut_new.cout.value}")
    dut_new.Finish()

if __name__ == "__main__":
    main()

注:目前仅支持 verilator模拟器

静态多实例

静态多实例的使用不如动态多实例灵活,相当于在进行dut封装时就创建了n个目标模块。 需要在使用 picker 生成 dut_top.sv/v 的封装时,通过–sname参数指定多个模块名称和对应的数量。

单个模块需要多实例

同样以Adder为例,在使用picker对dut进行封装时执行如下命令:

picker export Adder.v --autobuild true -w Adder.fst --sname Adder,3

通过–sname参数指定在dut中创建3个Adder,封装后dut的引脚定义为:

# init.py 
# 这里仅放置了部分代码
class DUTAdder(object):
        ...
        # all Pins
        # 静态多实例
        self.Adder_0_a = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_0_b = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_0_cin = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.Adder_0_sum = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.Adder_0_cout = xsp.XPin(xsp.XData(0, xsp.XData.Out), self.event)
        self.Adder_1_a = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_1_b = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_1_cin = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.Adder_1_sum = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.Adder_1_cout = xsp.XPin(xsp.XData(0, xsp.XData.Out), self.event)
        self.Adder_2_a = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_2_b = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_2_cin = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.Adder_2_sum = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.Adder_2_cout = xsp.XPin(xsp.XData(0, xsp.XData.Out), self.event)
        ...

可以看到在 picker 生成 dut 时,就在 DUTAdder 内创建了多个Adder实例。

下面是简单的多实例代码举例:

from Adder import *

import random

def random_int():
    return random.randint(-(2**127), 2**127 - 1) & ((1 << 127) - 1)

def main():
    # 在dut内部实例化了多个Adder
    dut = DUTAdder(waveform_filename = "1.fst")
    dut.Adder_0_a.value = random_int()
    dut.Adder_0_b.value = random_int()
    dut.Adder_0_cin.value = random_int() & 1
    dut.Adder_1_a.value = random_int()
    dut.Adder_1_b.value = random_int()
    dut.Adder_1_cin.value = random_int() & 1
    dut.Adder_2_a.value = random_int()
    dut.Adder_2_b.value = random_int()
    dut.Adder_2_cin.value = random_int() & 1
    dut.Step(1)
    print(f"Adder_0: sum={dut.Adder_0_sum.value}, cout={dut.Adder_0_cout.value}")
    print(f"Adder_1: sum={dut.Adder_1_sum.value}, cout={dut.Adder_1_cout.value}")
    print(f"Adder_2: sum={dut.Adder_2_sum.value}, cout={dut.Adder_2_cout.value}")
    # 静态多实例不可以根据需要动态的创建新的Adder实例,三个Adder实例的周期与dut的生存周期相同
    dut.Finish()

if __name__ == "__main__":
    main()

多个模块需要多实例

例如在 Adder.v 和 RandomGenerator.v 设计文件中分别有模块 Adder 和 RandomGenerator,RandomGenerator.v文件的源码为:

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

    always @(posedge clk or posedge reset) begin
        if (reset) begin
            lfsr <= seed;
        end else begin
            lfsr <= {lfsr[126:0], lfsr[127] ^ lfsr[126]};
        end
    end

    assign random_number = lfsr;
endmodule

需要 DUT 中有 2 个 Adder,3 个 RandomGenerator,生成的模块名称为 RandomAdder(若不指定,默认名称为 Adder_Random),则可执行如下命令:

picker export Adder.v,RandomGenerator.v --sname Adder,2,RandomGenerator,3 --tname RandomAdder -w randomadder.fst

得到封装后的dut为DUTRandomAdder,包含2个Adder实例和3个RandomGenerator实例。

封装后dut的引脚定义为:

# init.py 
# 这里仅放置了部分代码
class DUTRandomAdder(object):
        ...
        # all Pins
        # 静态多实例
        self.Adder_0_a = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_0_b = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_0_cin = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.Adder_0_sum = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.Adder_0_cout = xsp.XPin(xsp.XData(0, xsp.XData.Out), self.event)
        self.Adder_1_a = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_1_b = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.Adder_1_cin = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.Adder_1_sum = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.Adder_1_cout = xsp.XPin(xsp.XData(0, xsp.XData.Out), self.event)
        self.RandomGenerator_0_clk = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_0_reset = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_0_seed = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.RandomGenerator_0_random_number = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.RandomGenerator_1_clk = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_1_reset = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_1_seed = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.RandomGenerator_1_random_number = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        self.RandomGenerator_2_clk = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_2_reset = xsp.XPin(xsp.XData(0, xsp.XData.In), self.event)
        self.RandomGenerator_2_seed = xsp.XPin(xsp.XData(128, xsp.XData.In), self.event)
        self.RandomGenerator_2_random_number = xsp.XPin(xsp.XData(128, xsp.XData.Out), self.event)
        ...

可以看到在 picker 生成 dut 时,就在 DUTAdder 内创建了多个Adder实例。

对应的测试代码举例为:

from RandomAdder import *

import random

def random_int():
    return random.randint(-(2**127), 2**127 - 1) & ((1 << 127) - 1)

def main():
    # 在dut内部实例化了多个Adder
    dut = DUTRandomAdder()
    dut.InitClock("RandomGenerator_0_clk")
    dut.InitClock("RandomGenerator_1_clk")
    dut.InitClock("RandomGenerator_2_clk")
    dut.Adder_0_a.value = random_int()
    dut.Adder_0_b.value = random_int()
    dut.Adder_0_cin.value = random_int() & 1
    dut.Adder_1_a.value = random_int()
    dut.Adder_1_b.value = random_int()
    dut.Adder_1_cin.value = random_int() & 1
    
    # 在dut内部实例化了多个RandomGenerator
    seed = random.randint(0, 2**128 - 1)
    dut.RandomGenerator_0_seed.value = seed
    dut.RandomGenerator_0_reset.value = 1
    dut.Step(1)
    for i in range(10):
        print(f"Cycle {i}, DUT: {dut.RandomGenerator_0_random_number.value:x}")
        dut.Step(1)
    dut.RandomGenerator_1_seed.value = seed
    dut.RandomGenerator_1_reset.value = 1
    dut.Step(1)
    for i in range(10):
        print(f"Cycle {i}, DUT: {dut.RandomGenerator_1_random_number.value:x}")
        dut.Step(1)
    dut.RandomGenerator_2_seed.value = seed
    dut.RandomGenerator_2_reset.value = 1
    dut.Step(1)
    for i in range(10):
        print(f"Cycle {i}, DUT: {dut.RandomGenerator_2_random_number.value:x}")
        dut.Step(1)
    print(f"Adder_0: sum={dut.Adder_0_sum.value}, cout={dut.Adder_0_cout.value}")
    print(f"Adder_1: sum={dut.Adder_1_sum.value}, cout={dut.Adder_1_cout.value}")
    # 静态多实例各个模块多个实例的生命周期与dut的生命周期相同
    dut.Finish()

if __name__ == "__main__":
    main()

2.7 - 内部信号

内部信号示例

内部信号是指未在模块IO端口中暴露,但在模块内部承担控制、数据传输或状态跟踪等功能的信号。通常,picker在将RTL转换为DUT时只会自动暴露IO端口,内部信号不会被主动导出。

但在需要对模块内部逻辑进行更细致验证,或根据已知bug进一步定位问题时,验证人员往往需要访问这些内部信号。除了传统的verilator和VCS等工具,picker还提供了内部信号提取机制,可作为辅助手段。

动机

以上限计数器为例:


module UpperCounter (
    input wire clk,           
    input wire reset,         
    output reg [3:0] count   
);
    wire upper;

    assign upper = (count == 4'b1111);

    always @(posedge clk) begin
        if (reset) begin
            count = 4'b0000;
        end else if (!upper) begin
            count = count + 1;
        end
    end
endmodule

模块的IO信号指的是直接写在模块定义中的信号,也就是:

module UpperCounter (
    input wire clk,           
    input wire reset,         
    output reg [3:0] count   
);

该部分中的 clk、reset 和 count 是 IO 信号,可以直接暴露访问。而紧接着的 wire upper; 则属于内部信号,其取值由模块输入和模块内部逻辑共同决定。本案例中的计数器逻辑较为简单,但对于更大规模的硬件模块,常常会遇到以下难题:

  • 当模块输出与预期不符时,问题范围较大,难以及时定位,需要有效手段快速缩小排查范围;
  • 模块内部逻辑复杂,理解和分析存在困难,此时也需要借助内部信号作为关键标记,理清模块运行机制。

针对上述问题,访问和分析内部信号是非常有效的手段。传统上,通常借助如 Verilator、VCS 等仿真工具来查看内部信号。为进一步降低验证门槛,picker 还提供了三种内部信号访问方式:DPI 直接导出、VPI 动态访问和直接内存读写。

DPI 直接导出

DPI即Direct Programming Interface,是verilog与其他语言交互的接口,在picker的默认实现中,支持了为待测硬件模块的IO端口提供DPI。在执行picker时,如果添加了--internal 选项,则可同样为待测模块的内部信号提供DPI。此时,picker将会基于预定义的内部信号文件,在将verilog转化为DUT时,同步抽取rtl中的内部信号和IO端口一并暴露出来。

编写信号文件

信号文件是我们向picker指定需要提取的内部信号的媒介,它规定了需提取内部信号的模块和该模块需要提取的内部信号。

示例internal.yaml,内容如下:

UpperCounter:
  - "wire upper"

第一行是模块名称,如UpperCounter,第二行开始是需要提取的模块内部信号,以“类型 信号名”的格式写出。比如,upper的类型为wire,我们就写成“wire upper” (理论上只要信号名符合verilog代码中的变量名就可以匹配到对应的信号,类型随便写都没问题,但还是建议写verilog语法支持的类型,比如wire、log、logic等)

内部信号提取的能力取决于模拟器,譬如,verilator就无法提取下划线_开头的信号。

注:多位宽的内部信号需要显式写出位宽,所以实际的格式是“类型 [宽度] 信号名”

UpperCounter:
  - "wire upper"
  - "reg [3:0] another_multiples" # 本案例中这个信号不存在,只是用于说明yaml的格式

选项支持

写好信号文件之后,需要在运行picker时显式指定内部文件,这通过internal选项完成:

--internal=[internal_signal_file]

完整命令如下:

picker export --autobuild=true upper_counter.sv -w upper_counter.fst --sname UpperCounter \
--tdir picker_out_upper_counter/ --lang python -e --sim verilator --internal=internal.yaml

我们可以找到picker为DUT配套生成的signals.json文件:

{
    "UpperCounter_upper": {
        "High": -1,
        "Low": 0,
        "Pin": "wire",
        "_": true
    },
    "clk": {
        "High": -1,
        "Low": 0,
        "Pin": "input",
        "_": true
    },
    "count": {
        "High": 3,
        "Low": 0,
        "Pin": "output",
        "_": true
    },
    "reset": {
        "High": -1,
        "Low": 0,
        "Pin": "input",
        "_": true
    }
}

这个文件展示了picker生成的信号接口,可以看到,第一个信号UpperCounter_upper就是我们需要提取的内部信号, 其中第一个下划线之前的部分是我们在internal.yaml中的第一行定义的模块名UpperCounter,后面的部分则是内部信号名。

信号访问

picker完成提取之后,内部信号的访问和io信号的访问就没有什么区别了,本质上他们都是dut上的一个XData,使用“dut.信号名”的方式访问即可。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    print(dut.UpperCounter_upper.value)

VPI动态访问

VPI(Verilog Procedural Interface)是Verilog语言的一种标准接口,用于在仿真时让C语言等外部程序与Verilog仿真器进行交互。通过VPI,用户可以在C程序中访问、读取、修改Verilog仿真中的信号、变量、模块实例等信息,还可以注册回调函数,实现对仿真过程的控制和扩展。VPI常用于开发自定义系统任务、实现高级验证功能、动态信号访问和波形处理等。VPI 是 IEEE 1364 标准的一部分。

选项支持

picker export --help
...
--vpi Enable VPI, for flexible internal signal access default is OFF

可通过参数--vpi开启VPI支持,例如:

picker export upper_counter.sv --sname UpperCounter --tdir picker_out_upper_counter/ --lang python --vpi

信号访问

开启--vpi后,可通过DUT的接口dut.GetInternalSignalList(use_vpi=True)列出所有内部可访问信号,通过dut.GetInternalSignal(name, use_vpi=True)动态构建XData进行数据访问。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    # 列出所有内部信号
    # 或者通过 dut.VPIInternalSignalList()
    dut.GetInternalSignalList(use_vpi=True)
    # 动态构建 XData
    internal_upper = dut.GetInternalSignal("UpperCounter.upper", use_vpi=True)
    # 读访问
    print(internal_upper.value)
    # 写访问 (虽然能写入,但是dut step后值会被覆盖,不建议对非reg类型进行写操作)
    internal_upper.value = 0x1

直接内存读写

无论是基于DPI还是VPI进行内部信号访问都有一定的性能开销,为了实现极致性能体验,picker针对verilator/GSIM仿真器实现了内部信号直接访问。

选项支持

picker export --help
...
--rw,--access-mode ENUM:value in {dpi->0,mem_direct->1} OR {0,1}

可通过参数--rw 1开启针对verilator仿真器内部信号直接读写功能,例如:

picker export upper_counter.sv --sname UpperCounter --tdir picker_out_upper_counter/ --lang python --rw 1

信号访问

开启直接内存读写后,可通过dut.GetInternalSignalList(use_vpi=False)列出所有内部信号,通过dut.GetInternalSignal(name, use_vpi=False)动态构建XData实现信号读写。

from UpperCounter import *

def test():
    dut = DUTUpperCounter()
    # 列出所有内部信号
    dut.GetInternalSignalList(use_vpi=False)
    # 动态构建 XData
    internal_upper = dut.GetInternalSignal("UpperCounter_top.UpperCounter.upper", use_vpi=False)
    # 读访问
    print(internal_upper.value)
    # 写访问 (虽然能写入,但是dut step后值会被覆盖,不建议对非reg类型进行写操作)
    internal_upper.value = 0x1

内部信号访问方法对比

picker提供的每种内部信号访问方法都有各自的优缺点,需要按需要进行选择。

方法名称 开启参数 优点 缺点 访问接口 支持仿真器 适用场景
DPI 直接导出 –internal=cfg.yaml 速度快 需要提前指定信号
信号只读
修改后需要重新编译
无(同普通引脚) verilator、VCS 信号少,不需要写操作
VPI动态访问 –vpi 灵活,信号全
不需要提前指定信号
速度慢 GetInternalSignalList
GetInternalSignal
verilator、VCS 小规模电路或不在意仿真速度
直接内存读写 –rw 1 速度快
灵活
不需要提前指定信号
部分信号可能被优化掉 GetInternalSignalList
GetInternalSignal
verilator、GSIM 大规模电路,例如整个香山核

*注: 上述方法彼此独立,可以混用

2.8 - 集成测试框架

可用软件测试框架

在芯片验证的传统实践中,UVM等框架被广泛采用。尽管它们提供了一整套验证方法,但通常只适用于特定的硬件描述语言和仿真环境。本工具突破了这些限制,能够将仿真代码转换成C++或Python,使得我们可以利用软件验证工具来进行更全面的测试。
因为Python具有强大的生态系统,所以本项目主要以Python作为示例,简单介绍Pytest和Hypothesis两个经典软件测试框架。Pytest以其简洁的语法和丰富的功能,轻松应对各种测试需求。而Hypothesis则通过生成测试用例,揭示出意料之外的边缘情况,提高了测试的全面性和深度
我们的项目从一开始就设计为与多种现代软件测试框架兼容。我们鼓励您探索这些工具的潜力,并将其应用于您的测试流程中。通过亲身实践,您将更深刻地理解这些工具如何提升代码的质量和可靠性。让我们一起努力,提高芯片开发的质量。

2.8.1 - PyTest

可用来管理测试,生成测试报告

软件测试

在正式开始pytest 之间我们先了解一下软件的测试,软件测试一般分为如下四个方面

  • 单元测试:称模块测试,针对软件设计中的最小单位——程序模块,进行正确性检查的测试工作
  • 集成测试:称组装测试,通常在单元测试的基础上,将所有程序模块进行有序的、递增测试,重点测试不同模块的接口部分
  • 系统测试:将整个软件系统看成一个整体进行测试,包括对功能、性能以及软件所运行的软硬件环境进行测试
  • 验收测试:指按照项目任务书或合同、供需双方约定的验收依据文档进行的对整个系统的测试与评审,决定是否接收或拒收系统

pytest最初是作为一个单元测试框架而设计的,但它也提供了许多功能,使其能够进行更广泛的测试,包括集成测试,系统测试,他是一个非常成熟的全功能的python 测试框架。 它通过收集测试函数和模块,并提供丰富的断言库来简化测试的编写和运行,是一个非常成熟且功能强大的 Python 测试框架,具有以下几个特点:

  • 简单灵活:Pytest 容易上手,且具有灵活性。
  • 支持参数化:您可以轻松地为测试用例提供不同的参数。
  • 全功能:Pytest 不仅支持简单的单元测试,还可以处理复杂的功能测试。您甚至可以使用它来进行自动化测试,如 Selenium 或 Appium 测试,以及接口自动化测试(结合 Pytest 和 Requests 库)。
  • 丰富的插件生态:Pytest 有许多第三方插件,您还可以自定义扩展。一些常用的插件包括:
    • pytest-selenium:集成 Selenium。
    • pytest-html:生成HTML测试报告。
    • pytest-rerunfailures:在失败的情况下重复执行测试用例。
    • pytest-xdist:支持多 CPU 分发。
  • 与 Jenkins 集成良好
  • 支持 Allure 报告框架

本文将基于测试需求简单介绍pytest的用法,其完整手册在这里,供同学们进行深入学习。

Pytest安装

# 安装pytest:
pip install pytest
# 升级pytest
pip install -U pytest
# 查看pytest版本
pytest --version
# 查看已安装包列表
pip list
# 查看pytest帮助文档
pytest -h
# 安装第三方插件
pip install pytest-sugar
pip install pytest-rerunfailures
pip install pytest-xdist
pip install pytest-assume
pip install pytest-html

Pytest使用

命名规则

# 首先在使用pytest 时我们的模块名通常是以test开头或者test结尾,也可以修改配置文件,自定义命名规则
# test_*.py 或 *_test.py
test_demo1
demo2_test

# 模块中的类名要以Test 开始且不能有init 方法
class TestDemo1:
class TestLogin:

# 类中定义的测试方法名要以test_开头
test_demo1(self)
test_demo2(self)

# 测试用例
class test_one:
    def test_demo1(self):
        print("测试用例1")

    def test_demo2(self):
        print("测试用例2")

Pytest 参数

pytest支持很多参数,可以通过help命令查看

pytest -help

我们在这里列出来常用的几个:

-m: 用表达式指定多个标记名。 pytest 提供了一个装饰器 @pytest.mark.xxx,用于标记测试并分组(xxx是你定义的分组名),以便你快速选中并运行,各个分组直接用 and、or 来分割。

-v: 运行时输出更详细的用例执行信息 不使用-v参数,运行时不会显示运行的具体测试用例名称;使用-v参数,会在 console 里打印出具体哪条测试用例被运行。

-q: 类似 unittest 里的 verbosity,用来简化运行输出信息。 使用 -q 运行测试用例,仅仅显示很简单的运行信息, 例如:

.s..  [100%]
3 passed, 1 skipped in 9.60s

-k: 可以通过表达式运行指定的测试用例。 它是一种模糊匹配,用 and 或 or 区分各个关键字,匹配范围有文件名、类名、函数名。

-x: 出现一条测试用例失败就退出测试。 在调试时,这个功能非常有用。当出现测试失败时,停止运行后续的测试。

-s: 显示print内容 在运行测试脚本时,为了调试或打印一些内容,我们会在代码中加一些print内容,但是在运行pytest时,这些内容不会显示出来。如果带上-s,就可以显示了。

pytest test_se.py -s

Pytest 选择测试用例执行

在 Pytest 中,您可以按照测试文件夹、测试文件、测试类和测试方法的不同维度来选择执行测试用例。

  • 按照测试文件夹执行
# 执行所有当前文件夹及子文件夹下的所有测试用例
pytest .
# 执行跟当前文件夹同级的tests文件夹及子文件夹下的所有测试用例
pytest ../tests

# 按照测试文件执行
# 运行test_se.py下的所有的测试用例
pytest test_se.py

# 按照测试类执行,必须以如下格式:
pytest 文件名 .py:: 测试类其中::是分隔符用于分割测试module和测试类
# 运行test_se.py文件下的,类名是TestSE下的所有测试用例
pytest test_se.py::TestSE

# 测试方法执行,必须以如下格式:
pytest 文件名 .py:: 测试类 :: 测试方法其中 :: 是分隔符用于分割测试module测试类以及测试方法
# 运行test_se.py文件下的,类名是TestSE下的,名字为test_get_new_message的测试用例
pytest test_se.py::TestSE::test_get_new_message

# 以上选择测试用例的方法均是在**命令行**,如果您想直接在测试程序里执行可以直接在main函数中**调用pytest.main()**,其格式为:
pytest.main([模块.py::::方法])

此外,Pytest 还支持控制测试用例执行的多种方式,例如过滤执行、多进程运行、重试运行等。

使用Pytest编写验证

  • 在测试过程中,我们使用之前验证过的加法器,进入Adder文件夹,在picker_out_adder目录下新建一个test_adder.py文件,内容如下:
# 导入测试模块和所需的库
from Adder import *
import pytest
import ctypes
import random

# 使用 pytest fixture 来初始化和清理资源
@pytest.fixture
def adder():
    # 创建 DUTAdder 实例,加载动态链接库
    dut = DUTAdder()
    # 执行一次时钟步进,准备 DUT
    dut.Step(1)
    # yield 语句之后的代码会在测试结束后执行,用于清理资源
    yield dut
    # 清理DUT资源,并生成测试覆盖率报告和波形
    dut.Finish()

class TestFullAdder:
    # 将 full_adder 定义为静态方法,因为它不依赖于类实例
    @staticmethod
    def full_adder(a, b, cin):
        cin = cin & 0b1
        Sum = ctypes.c_uint64(a).value
        Sum += ctypes.c_uint64(b).value + cin
        Cout = (Sum >> 64) & 0b1
        Sum &= 0xffffffffffffffff
        return Sum, Cout

    # 使用 pytest.mark.usefixtures 装饰器指定使用的 fixture
    @pytest.mark.usefixtures("adder")
    # 定义测试方法,adder 参数由 pytest 通过 fixture 注入
    def test_adder(self, adder):
        # 进行多次随机测试
        for _ in range(114514):
            # 随机生成 64 位的 a 和 b,以及 1 位的进位 cin
            a = random.getrandbits(64)
            b = random.getrandbits(64)
            cin = random.getrandbits(1)
            # 设置 DUT 的输入
            adder.a.value = a
            adder.b.value = b
            adder.cin.value = cin
            # 执行一次时钟步进
            adder.Step(1)
            # 使用静态方法计算预期结果
            sum, cout = self.full_adder(a, b, cin)
            # 断言 DUT 的输出与预期结果相同
            assert sum == adder.sum.value
            assert cout == adder.cout.value

if __name__ == "__main__":
    pytest.main(['-v', 'test_adder.py::TestFullAdder'])
  • 运行测试之后输出如下:
collected 1 item

 test_adder.py ✓                                                 100% ██████████

Results (4.33s):

测试成功表明,在经过114514次循环之后,我们的设备暂时没有发现bug。然而,使用多次循环的随机数生成测试用例会消耗大量资源,并且这些随机生成的测试用例可能无法有效覆盖所有边界条件。在下一部分,我们将介绍一种更有效的测试用例生成方法。

2.8.2 - Hypothesis

可用来生成激励

Hypothesis

在上一节中,我们通过手动编写测试用例,并为每个用例指定输入和预期输出。这种方式存在一些问题,例如测试用例覆盖不全面、边界条件 容易被忽略等。它是一个用于属性基于断言的软件测试的 Python 库。Hypothesis 的主要目标是使测试更简单、更快速、更可靠。它使用了一种称为属性基于断言的测试方法,即你可以为你的代码编写一些假(hypotheses),然后 Hypothesis 将会自动生成测试用例并验证这些假设。这使得编写全面且高效的测试变得更加容易。Hypothesis 可以自动生成各种类型的输入数据,包括基本类型(例如整数、浮点数、字符串等)、容器类型(例如列表、集合、字典等)、自定义类型等。然后,它会根据你提供的属性(即断言)进行测试,如果发现测试失败,它将尝试缩小输入数据的范围以找出最小的失败案例。通过 Hypothesis,你可以更好地覆盖代码的边界条件,并发现那些你可能没有考虑到的错误情况。这有助于提高代码的质量和可靠性。

基本概念

  • 测试函数:即待测试的函数或方法,我们需要对其进行测试。
  • 属性:定义了测试函数应该满足的条件。属性是以装饰器的形式应用于测试函数上的。
  • 策略:用于生成测试数据的生成器。Hypothesis 提供了一系列内置的策略,如整数、字符串、列表等。我们也可以自定义策略
  • 测试生成器:基于策略生成测试数据的函数。Hypothesis 会自动为我们生成测试数据,并将其作为参数传递给测试函数。

本文将基于测试需求简单介绍Hypothesis的用法,其完整手册在这里,供同学们进行深入学习。

安装

使用pip安装,在python中导入即可使用

pip install hypothesis

import hypothesis

基本用法

属性和策略

Hypothesis 使用属性装饰器来定义测试函数的属性。最常用的装饰器是 @given,它指定了测试函数应该满足的属性。
我们可以通过@given 装饰器定义了一个测试函数 test_addition。并给x 添加对应的属性,测试生成器会自动为测试函数生成测试数据,并将其作为参数传递给函数,例如

def addition(number: int) -> int:
    return number + 1

@given(x=integers(), y=integers())  
def test_addition(x, y):     
	assert x + 1 == addition1

其中integers () 是一个内置的策略,用于生成整数类型的测试数据。Hypothesis 提供了丰富的内置策略,用于生成各种类型的测试数据。除了integers ()之外,还有字符串、布尔值、列表、字典等策略。例如使用 text () 策略生成字符串类型的测试数据,使用 lists (text ()) 策略生成字符串列表类型的测试数据

@given(s=text(), l=lists(text()))
def test_string_concatenation(s, l):     
	result = s + "".join(l)     
	assert len(result) == len(s) + sum(len(x) for x in l)

除了可以使用内置的策略以外,还可以使用自定义策略来生成特定类型的测试数据,例如我们可以生产一个非负整形的策略

def non_negative_integers():
  return integers(min_value=0)
@given(x=non_negative_integers())
  def test_positive_addition(x):
  assert x + 1 > x

期望

我们可以通过expect 来指明需要的函数期待得到的结果

@given(x=integers())
def test_addition(x):
    expected = x + 1
    actual = addition(x)

假设和断言

在使用 Hypothesis 进行测试时,我们可以使用标准的 Python 断言来验证测试函数的属性。Hypothesis 会自动为我们生成测试数据,并根据属性装饰器中定义的属性来运行测试函数。如果断言失败,Hypothesis 会尝试缩小测试数据的范围,以找出导致失败的最小样例。

假如我们有一个字符串反转函数,我们可以通过assert 来判断翻转两次后他是不是等于自身

def test_reverse_string(s):
    expected = x + 1
    actual = addition(x)
	assert actual == expected

编写测试

  • Hypothesis 中的测试由两部分组成:一个看起来像您选择的测试框架中的常规测试但带有一些附加参数的函数,以及一个@given指定如何提供这些参数的装饰器。以下是如何使用它来验证我们之前验证过的全加器的示例:

  • 在上一节的代码基础上,我们进行一些修改,将生成测试用例的方法从随机数修改为integers ()方法,修改后的代码如下:

from Adder import *
import pytest
from hypothesis import given, strategies as st

# 使用 pytest fixture 来初始化和清理资源
@pytest.fixture(scope="class")
def adder():
    # 创建 DUTAdder 实例,加载动态链接库
    dut = DUTAdder()
    # yield 语句之后的代码会在测试结束后执行,用于清理资源
    yield dut
    # 清理DUT资源,并生成测试覆盖率报告和波形
    dut.Finish()

class TestFullAdder:
    # 将 full_adder 定义为静态方法,因为它不依赖于类实例
    @staticmethod
    def full_adder(a, b, cin):
        cin = cin & 0b1
        Sum = a
        Sum += b + cin
        Cout = (Sum >> 128) & 0b1
        Sum &= 0xffffffffffffffffffffffffffffffff
        return Sum, Cout
    # 使用 hypothesis 自动生成测试用例
    @given(
        a=st.integers(min_value=0, max_value=0xffffffffffffffff),
        b=st.integers(min_value=0, max_value=0xffffffffffffffff),
        cin=st.integers(min_value=0, max_value=1)
    )
    # 定义测试方法,adder 参数由 pytest 通过 fixture 注入
    def test_full_adder_with_hypothesis(self, adder, a, b, cin):
        # 计算预期的和与进位
        sum_expected, cout_expected = self.full_adder(a, b, cin)
        # 设置 DUT 的输入
        adder.a.value = a
        adder.b.value = b
        adder.cin.value = cin
        # 执行一次时钟步进
        adder.Step(1)
        # 断言 DUT 的输出与预期结果相同
        assert sum_expected == adder.sum.value
        assert cout_expected == adder.cout.value

if __name__ == "__main__":
    # 以详细模式运行指定的测试
    pytest.main(['-v', 'test_adder.py::TestFullAdder'])

这个例子中,@given 装饰器和 strategies 用于生成符合条件的随机数据。st.integers() 是生成指定范围整数的策略,用于为 a 和 b 生成 0 到 0xffffffffffffffff 之间的数,以及为 cin 生成 0 或 1。Hypothesis会自动重复运行这个测试,每次都使用不同的随机输入,这有助于揭示潜在的边界条件或异常情况。

  • 运行测试,输出结果如下:
collected 1 item                                                               

 test_adder.py ✓                                                 100% ██████████

Results (0.42s):
       1 passed

可以看到在很短的时间里我们已经完成了测试

3 - 验证基础

介绍开放验证平台工作所需要的基础知识。

介绍芯片验证,以果壳 Cache 为例,介绍基本的验证流程、报告撰写。

3.1 - 芯片验证

关于芯片验证的基本概念

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

芯片验证过程需要和企业、团队的实际情况契合,没有符合所有要求,必须参考的绝对标准。

什么是芯片验证


芯片从设计到成品的过程主要包括芯片设计、芯片制造、芯片封测试三大阶段。在芯片设计中,又分前端设计和后端设计,前端设计也称之为逻辑设计,目标是让电路逻辑达到预期功能要求。后端设计也称为物理设计,主要工作是优化布局布线,减小芯片面积,降低功耗,提高频率等。芯片验证(Chip Verification)是芯片设计流程中的一个重要环节。它的目标是确保设计的芯片在功能、性能和功耗等方面都满足预定的规格。验证过程通常包括功能验证、时序验证和功耗验证等多个步骤,使用的方法和工具包括仿真、形式验证、硬件加速和原型制作等。针对本文,芯片验证仅包含对芯片前端设计的验证,验证设计的电路逻辑是否满足既定需求(“Does this proposed design do what is intended?"),通常也称为功能验证(Functional verification),不包含功耗、频率等后端设计

对于芯片产品,一旦设计错误被制造出来修改成本将会非常高昂,因为可能需要召回产品,并重新制造芯片,无论是经济成本还是时间成本都十分昂贵。经典由于芯片验证不足导致失败的典型案例如下:

Intel Pentium FDIV Bug:在1994年,Intel的Pentium处理器被发现存在一个严重的除法错误,这个错误被称为FDIV bug。这个错误是由于在芯片的浮点单元中,一个查找表中的几个条目错误导致的。这个错误在大多数应用中不会出现,但在一些特定的计算中会导致结果错误。由于这个错误,Intel不得不召回了大量的处理器,造成了巨大的经济损失。

Ariane 5 Rocket Failure:虽然这不是一个芯片的例子,但它展示了硬件验证的重要性。在1996年,欧洲空间局的Ariane 5火箭在发射后不久就爆炸了。原因是火箭的导航系统中的一个64位浮点数被转换为16位整数时溢出,导致系统崩溃。这个错误在设计阶段没有被发现,导致了火箭的失败。

AMD Barcelona Bug:在2007年,AMD的Barcelona处理器被发现存在一个严重的转译查找缓冲(TLB)错误。这个错误会导致系统崩溃或者重启。AMD不得不通过降低处理器的频率和发布BIOS更新来解决这个问题,这对AMD的声誉和财务状况造成了重大影响。

这些案例都强调了芯片验证的重要性。如果在设计阶段就能发现并修复这些错误,那么就可以避免这些昂贵的失败。验证不足的案例不仅发生在过去,也发生在现在,例如某新入局 ASIC 芯片市场的互联网企业打造一款 55 纳米芯片,极力追求面积缩减并跳过验证环节,最终导致算法失败,三次流片皆未通过测试,平均每次流片失败导致企业损失约 50 万美元。

芯片验证流程


验证在芯片设计中的位置

芯片设计和验证的耦合关系如上图所示,设计和验证有同样的输入,即规范文档(specification)。参考规范,设计与验证人员双方按照各自的理解,以及各自的需求进行独立编码实现。设计方需要满足的前提是编码的RTL代码“可综合”,需要考虑电路特性,而验证方一般只要考虑功能是否满足要求,编码限制少。双方完成模块开发后,需要进行健全性对比测试(Sanity Test),判定功能是否表现一致,若不一致需要进行协同排查,确定问题所在并进行修复,再进行对比测试,直到所有功能点都满足预期。由于芯片设计和芯片验证耦合度很高,因此有些企业在研发队伍上也进行了直接耦合,为每个子模块的设计团队都配置了对应的验证团队(DV)。上图中的设计与验证的耦合流程为粗粒度的关系,具体到具体芯片(例如Soc、DDR)、具体企业等都有其适合自身的合作模式。

在上述对比测试中,设计方的产出的模块通常称为DUT(Design Under Test),验证方开发的模型通常称为RM(Reference Model)。针对图中的验证工作,按照流程可以有:编写验证计划、创建验证平台、整理功能点、构建测试用例、运行调试、收集Bug/覆盖率、回归测试、编写测试报告等多个阶段。

验证计划: 验证计划描述了如何进行验证,以及如何保证验证质量,达到功能验证要求。在文档结构上通常包含验证目标,验证策略、验证环境、验证项、验证过程、风险防范、资源及时间表、结果和报告等部分。验证目标明确需要验证的功能或性能指标,这些目标应该直接从芯片的规范文档中提取。验证策略描述如何进行验证,包括可能使用的验证方法,例如仿真、形式化、FPGA加速等,以及如何组织验证任务。验证环境用于描述具体的测试环境,例如验证工具类型,版本号等。验证项包含了需要验证的具体项以及预期结果。验证计划可以有总计划,也可以针对具体验证的子任务进行编写。

平台搭建: 验证平台是具体验证任务的执行环境,同一类验证任务可以使用相同的验证平台。验证平台的搭建是验证流程中的关键步骤、具体包含验证工具选择(例如是采用软件仿真,还是采用形式化验证,或者硬件加速)、环境配置(例如配置服务器环境,FPGA环境)、创建测试环境、基本测试案例等。创建好基本测试平台,跑通基本测试案例,也通常称为“冒烟测试”。后继具体的测试代码,都将基于该测试平台进行,因此测试平台需要具有可重用性。验证平台通常包含测试框架和被测试代码,以及对应的基本信号激励。

功能点整理: 根据规范手册(spec)列出DUT的基本功能,并对其进行明确的描述,以及如何对该功能点进行测试。功能点整理过程中,需要根据重要性、风险、复杂性等因数对其进行优先级排序。功能点整理还需要对各个功能点进行追踪和状态,如果发现原始功能点有更新需要及时进行对应计划的同步。

测试用例: 测试用例是指一组条件或变量,用于确定DUT是否满足特定需求并能正确运行。每个测试用例通常包含测试条件,输入数据,预期结果,实际结果和测试结果。通过运行测试用例并比较预期结果和实际结果,可以确定系统或应用是否正确实现了特定的功能或需求。在芯片验证中,测试用例是用来验证芯片设计是否满足规格要求的重要工具。

编码实现: 编码实现即测试用例的具体执行过程,包括测试数据生成、测试框架选择、编程语言选择、参考模型编写等。编码实现是对功能点和测试用例充分理解后工作,如果理解不到位,可能导致DUT无法驱动,不能发现潜在bug等问题。

收集bug/覆盖率: 验证的目标就是提前发现设计中存在的bug,因此需要对发现的bug进行收集和管理。每发现一个新缺陷,需要给定唯一标号,并同设计工程师进行bug定级,然后进行状态追踪。能发现bug最好,但在实际验证中不是每次测试都能发现bug,因此需要另外一个指标评价验证是否到位。该指标通常采用覆盖率,当覆盖率超过一点阈值(例如代码覆盖率大于90%)后方可任务进行了充分验证。

回归测试: 验证和设计是一个相互迭代的过程,因此当验证出bug后,需要设计进行修正,且需要保证修正后的DUT仍然能正常工作。这种测试的目的是捕获可能由于修改而引入的新错误,或者重新激活旧错误。回归测试可以是全面的,也就是说,它涵盖了所有的功能,或者可以是选择性的,只针对某些特定的功能或系统部分。

测试报告: 测试报告是对整个验证过程的总结,它提供了关于测试活动的全面视图,包括测试的目标、执行的测试用例、发现的问题和缺陷、测试覆盖率和测试效率等。

芯片验证层次


按照验证对象的大小,芯片验证通常包含UT、BT、IT、ST四个层次。

单元测试(Unit Testing, UT): 这是最低的验证层次,主要针对单个模块或组件进行。目标是验证每个模块或组件的功能是否正确。

块测试(Block Testing,BT): 很多时候,单个模块和其他模块存在紧耦合,如果进行单独UT测试,可能存在信号处理复杂,功能验证不准确等问题,这时候可以把多个有耦合关系的模块合并成一个DUT块进行测试。

集成测试(Integration Testing): 在单元测试的基础上,将多个模块或组件组合在一起,验证它们能否正确地协同工作,通常用于测试子系统功能是否正常。

系统测试(System Testing): ST通常也称为Top验证,在集成测试的基础上,将所有的模块或组件组合在一起,形成一个完整的系统,验证系统的功能是否正确,以及系统的性能是否满足要求。

理论上,这些层次的验证通常按照从低到高的顺序进行,每个层次的验证都建立在前一个层次的验证的基础上。但实际验证活动中,需要根据企业验证人员的规模、熟练度,功能需求等进行选择,不一定所有层次的验证都需要涉及。在每个层次,都需要编写相应的测试用例,运行测试,收集和分析结果,以确保芯片设计的正确性和质量。

芯片验证指标


芯片验证的指标,通常包含功能正确性、测试覆盖率、缺陷密度、验证效率、验证成本等多个方面。功能正确性是最基本的验证指标,即芯片是否能够正确地执行其设计的功能。这通常通过运行一系列的功能测试用例来验证,包括正常情况下的功能测试,以及异常情况下的鲁棒性测试。测试覆盖率是指测试用例覆盖了多少设计的功能点,以及覆盖的程度如何。高的测试覆盖率通常意味着更高的验证质量。测试覆盖率可以进一步细分为代码覆盖率、功能覆盖率、条件覆盖率等。缺陷密度是指在一定的设计规模或代码量中,发现的缺陷的数量。低的缺陷密度通常意味着更高的设计质量。验证效率是指在一定的时间和资源下,能够完成的验证工作量。高的验证效率通常意味着更高的验证生产力。验证成本是指进行验证所需要的总体资源,包括人力、设备、时间等。低的验证成本通常意味着更高的验证经济性。

功能正确性是验证的绝对指标,但在实践中,很多时候无法确定测试方案是否完备,所有测试空间是否全部测试到位,因此需要一个可量化的指标来指导验证是否足够充分,是否可以结束验证。该指标通常采用“测试覆盖率”。测试覆盖率通常有代码覆盖率(行,函数,分支)、功能覆盖率。

代码行覆盖率: 即在测试过程中,DUT的设计代码中有多少行被执行;

函数覆盖率: 即在测试过程中,DUT的设计代码中有多少函数被执行;

分支覆盖率: 即在测试过程中,DUT的设计代码中有多少分支被执行(if else);

功能覆盖率: 即在测试过程中,有多少预定义功能被触发。

高的代码覆盖率可以提高验证的质量和可靠性,但并不能保证验证的完全正确性,因为它不能覆盖所有的输入和状态组合。因此,除了追求高的代码覆盖率,还需要结合其他测试方法和指标,如功能测试、性能测试、缺陷密度等。

芯片验证管理


芯片验证管理是一个涵盖了芯片验证过程中所有活动的管理过程,包括之前提到的验证策略的制定、验证环境的搭建、测试用例的编写和执行、结果的收集和分析、以及问题和缺陷的跟踪和修复等。芯片验证管理的目标是确保芯片设计满足所有的功能和性能要求,以及规格和标准。

在芯片验证管理中,首先需要制定一个详细的验证策略,包括验证的目标、范围、方法、时间表等。然后,需要搭建一个适合的验证环境,包括硬件设备、软件工具、测试数据等。接下来,需要编写一系列的测试用例,覆盖所有的功能和性能点,然后执行这些测试用例,收集和分析结果,找出问题和缺陷。最后,需要跟踪和修复这些问题和缺陷,直到所有的测试用例都能通过。

芯片验证管理是一个复杂的过程,需要多种技能和知识,包括芯片设计、测试方法、项目管理等。它需要与芯片设计、生产、销售等其他活动紧密协作,以确保芯片的质量和性能。芯片验证管理的效果直接影响到芯片的成功和公司的竞争力。因此,芯片验证管理是芯片开发过程中的一个重要环节。

芯片验证管理过程可以基于“项目管理平台”和“bug管理平台”进行,基于平台的管理效率通常情况下明显高于基于人工的管理模式。

芯片验证现状


当前,芯片验证通常是在芯片设计公司内部完成的,这一过程不仅技术上复杂,而且具有巨大的成本。从验收与设计的紧密关系来看,芯片验证不可避免地涉及芯片设计的源代码。然而,芯片设计公司通常将芯片设计源代码视为商业机密,这使得必须由公司内部人员来执行芯片验证,难以将验证工作外包。

验证工作量占比

芯片验证的重要性在于确保设计的芯片在各种条件下能够可靠运行。验证工作不仅仅是为了满足技术规格,还需要应对不断增长的复杂性和新兴技术的要求。随着半导体行业的发展,芯片验证的工作量不断增加,尤其是对于复杂的芯片而言,验证工作已经超过了设计工作,占比超过70%。这使得在工程师人员配比上,验证工程师人数通常是设计工程师人数的2倍或以上(例如zeku的三千人规模团队中,大约有一千人的设计工程师,两千人的验证工程师。其他大型芯片设计公司的验证人员占比类似或更高)。

由于验证工作的特殊性,需要对芯片设计源代码进行访问,这在很大程度上限制了芯片验证的外包可能性。芯片设计源代码被视为公司的核心商业机密,涉及到技术细节和创新,因此在安全和法律层面上不太可能与外部方共享。这也导致了公司内部人员必须承担验证工作的重任,增加了公司内部的工作负担和成本。

在当前情况下,芯片验证工程师的需求持续增加。他们需要具备深厚的技术背景,熟悉各种验证工具和方法,并且对新兴技术有敏锐的洞察力。由于验证工作的复杂性,验证团队通常需要庞大的规模,这与设计团队规模形成鲜明对比。

为了应对这一挑战,行业可能需要不断探索创新的验证方法和工具,以提高验证效率,降低成本。

小结:复杂芯片验证成本昂贵,表现在如下几个方面

验证工作量大: 对于复杂芯片,验证工作在整个芯片设计工作中,占比超过 70%。

人力成本高: 验证工程师人数是设计工程师人数的2倍,对于复杂业务,工程师数量在千人以上。

内部验证: 芯片设计公司为了保证商业秘密(芯片设计代码)不被泄露,只能选择招聘大量验证工程师,在公司内部进行验证工作。

芯片验证众包


相比与硬件,软件领域为了减少软件测试成本,测试外包(分包)已经成为常态,该领域的分包业务非常成熟,市场规模已经是千亿人民币级别,并朝万亿级别规模进发。从工作内容上看,软件测试和硬件验证,有非常大的共同特征(系统的目的不同的对象),如果以软件的方式对硬件验证进行分包是否可行?

软件外包市场

把芯片验证工作进行外包(分包)面临诸多挑战,例如:

从业人员基数少: 相比软件领域,硬件开发者数量少了几个数量级。例如在github的统计上(https://madnight.github.io/githut/#/pull_requests/2023/2),传统软件编程语言占(Python、Java、C++,Go)比接近 50%, 而硬件描述语言,verilog占比仅 0.076%,这能从侧面反应出各自领域的开发者数量。

验证工具商业化: 企业中使用的验证工具(仿真器、形式化、数据分析)几乎都是商业工具,这类工具对于普通人来说几乎不可见,自学难度高。

开放学习资料少: 芯片验证涉及到访问芯片设计的源代码,而这些源代码通常被视为公司的商业机密和专有技术。芯片设计公司可能不愿意公开详细的验证过程和技术,限制了学习材料的可用性。

可行性分析

虽然芯片验证领域一直以来相对封闭,但从技术角度而言,采用分包的方式进行验证是一种可行的选择。这主要得益于以下几个因素:

首先,随着开源芯片项目的逐渐增多,验证过程中所涉及的源代码已经变得更加开放和透明。这些开源项目在设计和验证过程中没有商业机密的顾虑,为学习和研究提供了更多的可能性。即使某些项目涉及商业机密,也可以通过采用加密等方式来隐藏设计代码,从而在一定程度上解决了商业机密的问题,使验证更容易实现。

其次,芯片验证领域已经涌现出大量的基础验证工具,如verilator和systemc等。这些工具为验证工程师提供了强大的支持,帮助他们更高效地进行验证工作。通过这些工具,验证过程的复杂性和难度得到了一定程度的缓解,为采用分包的验证方法提供了更为可行的技术基础。

在开源软件领域,已经有一些成功的案例可供参考。例如,Linux内核的验证过程采用了分包的方式,不同的开发者和团队分别负责不同的模块验证,最终形成一个整体完备的系统。类似地,机器学习领域的ImageNet项目也采用了分包标注的策略,通过众包的方式完成大规模的图像标注任务。这些案例为芯片验证领域提供了成功的经验,证明了分包验证在提高效率、降低成本方面的潜力。

因此,尽管芯片验证领域相对于其他技术领域而言仍显得封闭,但技术的进步和开源项目的增多为采用分包验证提供了新的可能性。通过借鉴其他领域的成功经验和利用现有的验证工具,我们有望在芯片验证中推动更加开放、高效的验证方法的应用,进一步促进行业的发展。这种技术的开放性和灵活性将为验证工程师提供更多的选择,推动芯片验证领域迎来更为创新和多样化的发展。

技术路线

为了克服挑战,让更多的人参与到芯片验证,本项目从如下几个技术方向进行持续尝试

提供多语言验证工具: 传统芯片验证是基于System Verilog编程语言进行,但是该语言用户基数少,为了让其他软件开发/测试的技术人员参与到芯片验证,本项目提供多语言验证转换工具Picker,它可以让验证者使用自己熟悉的编程语言(例如C++/Python/Java/Go)基于开源验证工具参与验证工作。

提供验证学习材料: 芯片验证学习材料少,主要原因由于商业公司几乎不可能公开其内部资料,为此本项目会持续更新学习材料,让验证人员可在线,免费学习所需要的技能。

提供真实芯片验证案例: 为了让学习材料更具使用性,本项目以“香山昆明湖(工业级高性能risc-v处理器)IP核”作为基础,从中摘取模块持续更新验证案例。

组织芯片设计分包验证: 学以致用是每个人学习的期望目标,为此本项目定期组织芯片设计的验证分包,让所有人(无论你是大学生、验证专家、软件开发测试者、还是中学生)都可以参与到真实芯片的设计工作中去。

本项目的目标是达到如下愿景,“打开传统验证模式的黑盒,让所有感兴趣的人可以随时随地的,用自己擅长的编程语言参与芯片验证”。

愿景

3.2 - 数字电路

关于数字电路的基本概念

本页将介绍数字电路的基础知识。数字电路是利用数字信号的电子电路。近年来,绝大多数的计算机都是基于数字电路实现的。

什么是数字电路


数字电路是一种利用两种不连续的电位来表示信息的电子电路。在数字电路中,通常使用两个电源电压,分别表示高电平(H)和低电平(L),分别代表数字1和0。这样的表示方式通过离散的电信号,以二进制形式传递和处理信息。

大多数数字电路的实现基于场效应管,其中最常用的是 MOSFET(Metal-Oxide-Semiconductor Field-Effect Transistor,金属氧化物半导体场效应管)。MOSFET 是一种半导体器件,可以在电场的控制下调控电流流动,从而实现数字信号的处理。

在数字电路中,MOSFET 被组合成各种逻辑电路,如与门、或门、非门等。这些逻辑门通过不同的组合方式,构建了数字电路中的各种功能和操作。以下是一些数字电路的基本特征:

(1) 电位表示信息: 数字电路使用两种电位,即高电平和低电平,来表示数字信息。通常,高电平代表数字1,低电平代表数字0。

(2) MOSFET 实现: MOSFET 是数字电路中最常用的元件之一。通过控制 MOSFET 的导通和截止状态,可以实现数字信号的处理和逻辑运算。

(3) 逻辑门的组合: 逻辑门是数字电路的基本构建块,由 MOSFET 组成。通过组合不同的逻辑门,可以构建复杂的数字电路,实现各种逻辑功能。

(4) 二进制表达: 数字电路中的信息通常使用二进制系统进行表示。每个数字都可以由一串二进制位组成,这些位可以在数字电路中被处理和操作。

(5) 电平转换和信号处理: 数字电路通过电平的变化和逻辑操作,实现信号的转换和处理。这种离散的处理方式使得数字电路非常适用于计算和信息处理任务。

为什么要学习数字电路


学习数字电路是芯片验证过程中的基础和必要前提,主要体现在以下多个方面:

(1) 理解设计原理: 数字电路是芯片设计的基础,了解数字电路的基本原理和设计方法是理解芯片结构和功能的关键。芯片验证的目的是确保设计的数字电路在实际硬件中按照规格正常工作,而理解数字电路原理是理解设计的关键。

(2) 设计规范: 芯片验证通常涉及验证设计是否符合特定的规范和功能要求。学习数字电路可以帮助理解这些规范,从而更好地构建测试用例和验证流程,确保验证的全面性和准确性。

(3) 时序和时钟: 时序问题是数字电路设计和验证中的常见挑战。学习数字电路可以帮助理解时序和时钟的概念,以确保验证过程中能够正确处理时序问题,避免电路中的时序迟滞和冲突。

(4) 逻辑分析: 芯片验证通常涉及对逻辑的分析,确保电路的逻辑正确性。学习数字电路可以培养对逻辑的深刻理解,从而更好地进行逻辑分析和故障排查。

(5) 测试用例编写: 在芯片验证中,需要编写各种测试用例来确保设计的正确性。对数字电路的理解可以帮助设计更全面、有针对性的测试用例,涵盖电路的各个方面。

(6) 信号完整性: 学习数字电路有助于理解信号在电路中的传播和完整性问题。在芯片验证中,确保信号在不同条件下的正常传递是至关重要的,特别是在高速设计中。

整体而言,学习数字电路为芯片验证提供了基础知识和工具,使验证工程师能够更好地理解设计,编写有效的测试用例,分析验证结果,并解决可能出现的问题。数字电路的理论和实践经验对于芯片验证工程师来说都是不可或缺的。

数字电路基础知识

可以通过以下在线资源进行数字电路学习:

硬件描述语言Chisel


传统描述语言

硬件描述语言(Hardware Description Language,简称 HDL)是一种用于描述数字电路、系统和硬件的语言。它允许工程师通过编写文本文件来描述硬件的结构、功能和行为,从而实现对硬件设计的抽象和建模。

HDL 通常被用于设计和仿真数字电路,如处理器、存储器、控制器等。它提供了一种形式化的方法来描述硬件电路的行为和结构,使得设计工程师可以更方便地进行硬件设计、验证和仿真。

常见的硬件描述语言包括:

  • Verilog:Verilog 是最常用的 HDL 之一,它是一种基于事件驱动的硬件描述语言,广泛应用于数字电路设计、验证和仿真。
  • VHDL:VHDL 是另一种常用的 HDL,它是一种面向对象的硬件描述语言,提供了更丰富的抽象和模块化的设计方法。
  • SystemVerilog:SystemVerilog 是 Verilog 的扩展,它引入了一些高级特性,如对象导向编程、随机化测试等,使得 Verilog 更适用于复杂系统的设计和验证。

Chisel

Chisel 是一种现代化高级的硬件描述语言,与传统的 Verilog 和 VHDL 不同,它是基于 Scala 编程语言的硬件构建语言。Chisel 提供了一种更加现代化和灵活的方法来描述硬件,通过利用 Scala 的特性,可以轻松地实现参数化、抽象化和复用,同时保持硬件级别的效率和性能。

Chisel 的特点包括:

  • 现代化的语法:Chisel 的语法更加接近软件编程语言,如 Scala,使得硬件描述更加直观和简洁。
  • 参数化和抽象化:Chisel 支持参数化和抽象化,可以轻松地创建可配置和可重用的硬件模块。
  • 类型安全:Chisel 是基于 Scala 的,因此具有类型安全的特性,可以在编译时检测到许多错误。
  • 生成性能优化的硬件:Chisel 代码可以被转换成 Verilog,然后由标准的 EDA 工具链进行综合、布局布线和仿真,生成性能优化的硬件。
  • 强大的仿真支持:Chisel 提供了与 ScalaTest 和 Firrtl 集成的仿真支持,使得对硬件进行仿真和验证更加方便和灵活。

Chisel版的全加法器实例

电路设计如下图所示:

全加器电路

完整的Chisel代码如下:

package examples

import chisel3._

class FullAdder extends Module {
  // Define IO ports
  val io = IO(new Bundle {
    val a = Input(UInt(1.W))    // Input port 'a' of width 1 bit
    val b = Input(UInt(1.W))    // Input port 'b' of width 1 bit
    val cin = Input(UInt(1.W))  // Input port 'cin' (carry-in) of width 1 bit
    val sum = Output(UInt(1.W)) // Output port 'sum' of width 1 bit
    val cout = Output(UInt(1.W))// Output port 'cout' (carry-out) of width 1 bit
  })

  // Calculate sum bit (sum of a, b, and cin)
  val s1 = io.a ^ io.b               // XOR operation between 'a' and 'b'
  io.sum := s1 ^ io.cin              // XOR operation between 's1' and 'cin', result assigned to 'sum'

  // Calculate carry-out bit
  val s3 = io.a & io.b               // AND operation between 'a' and 'b', result assigned to 's3'
  val s2 = s1 & io.cin               // AND operation between 's1' and 'cin', result assigned to 's2'
  io.cout := s2 | s3                 // OR operation between 's2' and 's3', result assigned to 'cout'
}

Chisel 学习材料可以参考官方文档:https://www.chisel-lang.org/docs

3.3 - 创建DUT

以果壳cache为例,介绍如何创建基于chisel的DUT

以果壳cache为例,介绍如何创建基于Chisel的DUT

在本文档中,DUT(Design Under Test)是指在芯片验证过程中,被验证的对象电路或系统。DUT是验证的主体,在基于picker工具创建DUT时,需要考虑被测对象的功能、性能要求和验证目标,例如是需要更快的执行速度,还是需要更详细的测试信息。通常情况下DUT,即RTL编写的源码,与外围环境一起构成验证环境(test_env),然后基于该验证环境编写测试用例。在本项目中,DUT是需要测试的Python模块,需要通过RTL进行转换。传统的RTL语言包括Verilog、System Verilog、VHDL等,然而作为新兴的RTL设计语言,Chisel(https://www.chisel-lang.org/)也以其面向对象的特征和便捷性,逐渐在RTL设计中扮演越来越重要的角色。本章以果壳处理器-NutShell中的cache源代码到Python模块的转换为例进行介绍如何创建DUT。

Chisel与果壳

准确来说,Chisel是基于Scala语言的高级硬件构造(HCL)语言。传统HDL是描述电路,而HCL则是生成电路,更加抽象和高级。同时Chisel中提供的Stage包则可以将HCL设计转化成Verilog、System Verilog等传统的HDL语言设计。配合上Mill、Sbt等Scala工具则可以实现自动化的开发。

果壳是使用 Chisel 语言模块化设计的、基于 RISC-V RV64 开放指令集的顺序单发射处理器实现。果壳更详细的介绍请参考链接:https://oscpu.github.io/NutShell-doc/

果壳 cache

果壳cache(Nutshell Cache)是果壳处理器中使用的缓存模块。其采用三级流水设计,当第三级流水检出当前请求为MMIO或者发生重填(refill)时,会阻塞流水线。同时,果壳cache采用可定制的模块化设计,通过改变参数可以生成存储空间大小不同的一级cache(L1 Cache)或者二级cache(L2 Cache)。此外,果壳cache留有一致性(coherence)接口,可以处理一致性相关的请求。

nt_cache

Chisel 转 Verilog

Chisel中的stage库可以帮助由Chisel代码生成Verilog、System Verilog等传统的HDL代码。以下将简单介绍如何由基于Chisel的cache实现转换成对应的Verilog电路描述。

初始化果壳环境

首先从源仓库下载整个果壳源代码,并进行初始化:

mkdir cache-ut
cd cache-ut
git clone https://github.com/OSCPU/NutShell.git
cd NutShell && git checkout 97a025d
make init

创建scala编译配置

在cache-ut目录下创建build.sc,其中内容如下:

import $file.NutShell.build
import mill._, scalalib._
import coursier.maven.MavenRepository
import mill.scalalib.TestModule._

// 指定Nutshell的依赖
object difftest extends NutShell.build.CommonNS {
  override def millSourcePath = os.pwd / "NutShell" / "difftest"
}

// Nutshell 配置
object NtShell extends NutShell.build.CommonNS with NutShell.build.HasChiselTests {
  override def millSourcePath = os.pwd / "NutShell"
  override def moduleDeps = super.moduleDeps ++ Seq(
        difftest,
  )
}

// UT环境配置
object ut extends NutShell.build.CommonNS with ScalaTest{
    override def millSourcePath = os.pwd
    override def moduleDeps = super.moduleDeps ++ Seq(
        NtShell
    )
}

实例化 cache

创建好配置信息后,按照scala规范,创建src/main/scala源代码目录。之后,就可以在源码目录中创建nut_cache.scala,利用如下代码实例化Cache并转换成Verilog代码:

package ut_nutshell

import chisel3._
import chisel3.util._
import nutcore._
import top._
import chisel3.stage._

object CacheMain extends App {
  (new ChiselStage).execute(args, Seq(
      ChiselGeneratorAnnotation(() => new Cache()(CacheConfig(ro = false, name = "tcache", userBits = 16)))
    ))
}

生成RTL

完成上述所有文件的创建后(build.sc,src/main/scala/nut_cache.scala),在cache-ut目录下执行如下命令:

mkdir build
mill --no-server -d ut.runMain ut_nutshell.CacheMain --target-dir build --output-file Cache

注:mill环境的配置请参考 https://mill-build.com/mill/Intro_to_Mill.html

上述命令成功执行完成后,会在build目录下生成verilog文件:Cache.v。之后就可以通过picker工具进行Cache.v到 Python模块的转换。除去chisel外,其他HCL语言几乎都能生成对应的 RTL代码,因此上述基本流程也适用于其他HCL。

DUT编译

一般情况下,如果需要DUT生成波形、覆盖率等会导致DUT的执行速度变慢,因此在通过picker工具生成python模块时会根据多种配置进行生成:(1)关闭所有debug信息;(2)开启波形;(3)开启代码行覆盖率。其中第一种配置的目标是快速构建环境,进行回归测试等;第二种配置用于分析具体错误,时序等;第三种用于提升覆盖率。

3.4 - DUT验证

介绍验证的一般流程

本节介绍基于Picker验证DUT的一般流程

开放验证平台的目标是功能性验证,其一般有以下步骤:

1. 确定验证对象和目标

通常来说,同时交付给验证工程师的还有DUT的设计文档。此时您需要阅读文档或者源代码,了解验证对象的基本功能、主体结构以及预期功能。

2. 构建基本验证环境

充分了解设计之后,您需要构建验证的基本环境。例如,除了由Picker生成的DUT外,您可能还需要搭建用于比对的参考模型,也可能需要为后续功能点的评测搭建信号的监听平台。

3. 功能点与测试点分解

在正式开始验证之前,您还需要提取功能点,并将其进一步分解成测试点。提取和分解方法可以参考:CSDN:芯片验证系列——Testpoints分解

4. 构造测试用例

有了测试点之后,您需要构造测试用例来覆盖相应的测试点。一个用例可能覆盖多个测试点。

5. 收集测试结果

运行完所有的测试用例之后,您需要汇总所有的测试结果。一般来说包括代码行覆盖率以及功能覆盖率。前者可以通过Picker工具提供的覆盖率功能获得,后者则需要您通过监听DUT的行为判断某功能是否被用例覆盖到。

6. 评估测试结果

最后您需要评估得到的结果,如是否存在错误的设计、某功能是否无法被触发、设计文档表述是否与DUT行为一致、设计文档是否表述清晰等。


接下来我们以果壳Cache的MMIO读写为例,介绍一般验证流程:

1 确定验证对象和目标: 果壳Cache的MMIO读写功能。MMIO是一类特殊的IO映射,其支持通过访问内存地址的方式访问IO设备寄存器。由于IO设备的寄存器状态是随时可能改变的,因此不适合将其缓存在cache中。当收到MMIO请求时,果壳cache不会在普通的cache行中查询命中/缺失情况,而是会直接访问MMIO的内存区域来读取或者写入数据。

2 构建基本验证环境: 我们可以将验证环境大致分为五个部分: env

1. Testcase Driver:负责由用例产生相应的信号驱动 2. Monitor:监听信号,判断功能是否被覆盖以及功能是否正确 3. Ref Cache:一个简单的参考模型 4. Memory/MMIO Ram:外围设备的模拟,用于模拟相应cache的请求 5. Nutshell Cache Dut:由Picker生成的DUT

此外,您可能还需要对DUT的接口做进一步封装以实现更方便的读写请求操作,具体可以参考Nutshll cachewrapper

3 功能点与测试点分解: 果壳cache可以响应MMIO请求,进一步分解可以得到一下测试点:

测试点1:MMIO请求会被转发到MMIO端口上 测试点2:cache响应MMIO请求时,不会发出突发传输(Burst Transfer)的请求 测试点3:cache响应MMIO请求时,会阻塞流水线

4 构造测试用例: 测试用例的构造是简单的,已知通过创建DUT得到的Nutshell cache的MMIO地址范围是0x30000000~0x7fffffff,则我们只需访问这段内存区间,应当就能获得MMIO的预期结果。需要注意的是,为了触发阻塞流水线的测试点,您可能需要连续地发起请求。 以下是一个简单的测试用例:

# import CacheWrapper here

def mmio_test(cache: CacheWrapper):
	mmio_lb	= 0x30000000
	mmio_rb	= 0x30001000

	print("\n[MMIO Test]: Start MMIO Serial Test")
	for addr in range(mmio_lb, mmio_rb, 16):
		addr &= ~(0xf)
		addr1 = addr
		addr2 = addr + 4
		addr3 = addr + 8

		cache.trigger_read_req(addr1)
		cache.trigger_read_req(addr2)
		cache.trigger_read_req(addr3)

		cache.recv()
		cache.recv()
		cache.recv()

	print("[MMIO Test]: Finish MMIO Serial Test")

5 收集测试结果

'''
    In tb_cache.py
'''

# import packages here

class TestCache():
    def setup_class(self):
        color.print_blue("\nCache Test Start")

        self.dut = DUTCache("libDPICache.so")
        self.dut.init_clock("clock")

        # Init here
        # ...

        self.testlist = ["mmio_serial"]

    def teardown_class(self):
        self.dut.Finish()
        color.print_blue("\nCache Test End")

    def __reset(self):
        # Reset cache and devices

    # MMIO Test
    def test_mmio(self):
        if ("mmio_serial" in self.testlist):
            # Run test
            from ..test.test_mmio import mmio_test
            mmio_test(self.cache, self.ref_cache)
        else:
            print("\nmmio test is not included")

    def run(self):
        self.setup_class()

        # test
        self.test_mmio()

        self.teardown_class()
    pass

if __name__ == "__main__":
	tb = TestCache()
	tb.run()

运行:

    python3 tb_cache.py

以上仅为大致的运行流程,具体可以参考:Nutshell Cache Verify

6 评估运行结果 运行结束之后可以得到以下数据: 行覆盖率: line_cov

功能覆盖率: func_cov

可以看到预设的MMIO功能均被覆盖且被正确触发。

3.5 - 验证报告

概述验证报告的结构与内容。

在我们完成DUT验证后,编写验证报告是至关重要的一环。本节将从整体角度概述验证报告的结构以及报告所需覆盖的内容。

验证报告是对整个验证过程的回顾,是验证合理与否的重要支持文件。一般情况下,验证报告需要包含以下内容:

  1. 文档基本信息(作者、日志、版本等)
  2. 验证对象(验证目标)
  3. 功能点介绍
  4. 验证方案
  5. 测试点分解
  6. 测试用例
  7. 测试环境
  8. 结果分析
  9. 缺陷分析
  10. 测试结论

以下内容对列表进行进一步解释,具体示例可以参考nutshell_cache_report_demo.pdf


1. 基本信息

应当包括作者、日志、版本、日期等。

2. 验证对象(验证目标)

需要对您的验证对象做必要的介绍,可以包括其结构、基本功能、接口信息等。

3. 功能点介绍

通过阅读设计文档或者源码,您需要总结DUT的目标功能,并将其细化为各功能点。

4. 验证方案

应当包括您计划的验证流程以及验证框架。同时,您也应当接受您的框架各部分是如何协同工作的。

5. 测试点分解

针对功能点提出的测试方法。具体可以包括在怎样的信号输入下应当观测到怎样的信号输出。

6. 测试用例

测试点的具体实现。一个测试用例可以包括多个测试点。

7. 测试环境

包括硬件信息、软件版本信息等。

8. 结果分析

结果分析一般指覆盖率分析。通常来说应当考虑两类覆盖率:
1. 行覆盖率: 在测试用例中有多少RTL行代码被执行。一般来说我们要求行覆盖率在98%以上。
2. 功能覆盖率:根据相应的信号判断您提取的功能点是否被覆盖且被正确触发。一般我们要求测试用例覆盖每个功能点。

9. 缺陷分析

对DUT存在的缺陷进行分析。可以包括设计文档的规范性和详细性、DUT功能的正确性(是否存在bug)、DUT功能是否能被触发等方面。

10. 验证结论

验证结论是在完成芯片验证过程后得出的最终结论,是对以上内容的总结。

4 - 高级案例

基于开放验证平台完成验证的复杂案例。

4.1 - 完整果壳 Cache 验证

利用Python语言对果壳Cache进行验证,

验证报告

https://github.com/XS-MLVP/Example-NutShellCache/blob/master/nutshell_cache_report_demo.pdf

验证环境&用例代码

https://github.com/XS-MLVP/Example-NutShellCache

4.2 - TileLink 协议

基于C++驱动使用 TillLink 协议的 L2 Cache

5 - 验证框架

搭建硬件验证环境所需的框架——Toffee

Toffee 是使用 Python 语言编写的一套硬件验证框架,它依赖于多语言转换工具 Picker,该工具能够将硬件设计的 Verilog 代码转换为 Python Package,使得用户可以使用 Python 来驱动并验证硬件设计。

其吸收了部分 UVM 验证方法学,以保证验证环境的规范性和可复用性,并重新设计了整套验证环境的搭建方式,使其更符合软件领域开发者的使用习惯,从而使软件开发者可以轻易地上手硬件验证工作。

Toffee Documentation 中查看 Toffee 的详细使用说明。

6 - 验证智能体

基于大模型进行自动化UT验证智能体

UCAgent 是一个基于大语言模型的自动化硬件验证智能体,专注于芯片设计的单元测试(Unit Test)验证工作。该项目通过 AI 技术自动分析硬件设计,生成测试用例,并执行验证任务生成测试报告,从而提高验证效率。

6.1 - 工具介绍

工具介绍与安装。

随着芯片设计的愈发复杂,其验证难度和耗时也成倍增长,而近年来大语言模型的能力突飞猛进。于是我们推出了 UCAgent——一个基于大语言模型的自动化硬件验证 AI 代理,专注于芯片设计的单元测试(Unit Test)验证工作。 接下来我将从介绍、安装、使用、工作流、高级这五个方面来说明 UCAgent。

介绍

背景

  • 芯片验证时间已经占据了芯片开发时间的 50-60%,并且设计工程师也将 49%的时间投入了硬件验证工作,但是 2024 年首次流片成功率仅有 14%。
  • 随着 LLM 与编程类 Agent 兴起,将“硬件验证”抽象为“软件测试问题”可实现高比例自动化。

UCAgent 是什么

  • 面向芯片设计单元测试(Unit Test)的 AI Agent,基于 LLM 驱动,围绕“阶段化工作流 + 工具编排”自动/半自动完成需求理解、测试生成、执行与报告产出。
  • 以用户为主导,LLM 为助理的协作式交互 Agent
  • 以 Picker & Toffee 为基础,DUT 以 Python 包形式被测试;可与 OpenHands/Copilot/Claude Code/Gemini-CLI/Qwen Code/ 等通过 MCP 协议深度协作。

能力与目标

  • 自动/半自动:生成/完善测试代码与文档、运行用例、汇总报告
  • 完整:功能覆盖率、代码行覆盖率与文档一致性
  • 可集成:支持标准 CLI、TUI;提供 MCP server 接口便于外部 Code Agent 接入
  • 目标:有效减少用户在验证过程中的重复工作

安装

系统要求

  • Python 版本: 3.11+
  • 操作系统:Linux / macOS
  • API 需求:可访问 OpenAI 兼容 API
  • 内存:建议 4GB+
  • 依赖:picker(将 Verilog DUT 导出为 Python 包)

安装方式

  • 方式一:克隆仓库并安装依赖

    git clone https://github.com/XS-MLVP/UCAgent.git
    cd UCAgent
    pip install -r requirements.txt
    
  • 方式二(pip 安装)

    pip install git+ssh://git@github.com/XS-MLVP/UCAgent@main
    ucagent --help # 确认安装成功
    

使用

快速开始

  1. pip 安装 UCAgent

    pip install git+ssh://git@github.com/XS-MLVP/UCAgent@main
    
  2. 准备 DUT(待测模块)

  • 创建目录:在{工作区}目录下创建Adder目录。

    • mkdir -p Adder
  • RTL:使用快速开始-简单加法器的加法器,将其代码放入Adder/Adder.v

  • 注入 bug:将输出和位宽修改为 63 位(用于演示位宽错误导致的缺陷)。

    • Adder.v第九行由output [WIDTH-1:0] sum,改为output [WIDTH-2:0] sum,vim Adder/Adder.v。目前的 verilog 代码为:

      // 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-2:0] sum,
          output cout
      );
      
      assign {cout, sum}  = a + b + cin;
      
      endmodule
      
  1. 将 RTL 导出为 Python Module

    picker 可以将 RTL 设计验证模块打包成动态库,并提供 Python 的编程接口来驱动电路。参照基础工具-工具介绍picker 文档

  • 直接在{工作区}目录下执行命令picker export Adder/Adder.v --rw 1 --sname Adder --tdir output/ -c -w output/Adder/Adder.fst
  1. 编写 README
  • 将加法器的说明、验证目标、bug 分析和其他都写在Adder文件夹的README.md文件中,同时将这个文件向output/Adder文件夹复制一份。

    • 将内容写入 readme 中,vim Adder/README.md,将下面内容复制到README.md中。

    • 复制文件,cp Adder/README.md output/Adder/README.md

    • Adder/README.md内容可以是如下:

      ### Adder 64 位加法器
      
      输入 a, b, cin 输出 sum,cout
      实现 sum = a + b + cin
      cin 是进位输入
      cout 是进位输出
      
      ### 验证目标
      
      只要验证加法相关的功能,其他验证,例如波形、接口等,不需要出现
      
      ### bug 分析
      
      在 bug 分析时,请参考源码:examples/MyAdder/Adder.v
      
      ### 其他
      
      所有的文档和注释都用中文编写
      
  1. 安装 Qwen Code CLI
  1. 配置 Qwen Code CLI
  • 修改~/.qwen/settings.json 配置文件,vim ~/.qwen/settings.json,示例 Qwen 配置文件如下:
{
	"mcpServers": {
		"unitytest": {
			"httpUrl": "http://localhost:5000/mcp",
			"timeout": 10000
		}
	}
}
  1. 启动 MCP Server
  • 在仓库内:
    ucagent output/ Adder -s -hm --tui --mcp-server-no-file-tools --no-embed-tools
    
    见到如下图则表示启动成功: tui.png
  1. 启动 Qwen Code
  • UCAgent/output目录输入qwen启动 Qwen Code,看见 >QWEN 图就表示启动成功。

qwen.png

  1. 开始验证
  • 在框内输入提示词并且同意 Qwen Code 的使用工具、命令和读写文件请求。(通过j/k控制上/下)提示词如下:

    请通过工具 RoleInfo 获取你的角色信息和基本指导,然后完成任务。请使用工具 ReadTextFile 读取文件。你需要在当前工作目录进行文件操作,不要超出该目录。

qwen-allow.png

有时候 Qwen Code 停止了,但是我们不确定是否完成了任务,此时可以通过查看 server 的 tui 界面来确认。

tui-pause.png 此时 Mission 部分显示阶段还在 13,所以我们还要让 Qwen Code 继续执行任务。

qwen-pause.png 中途停止了,但是任务没有完成,可以通过在输入框里输入“继续”来继续。

  1. 结果分析

最终的结果都在output文件夹中,其中的内容如下:

.
├── Adder  # 打包好的python DUT
├── Guide_Doc # 各种模板文件
├── uc_test_report # 跑完的测试报告,包含可以直接网页运行的index.html
└── unity_test # 各种生成的文档和测试用例文件
    └── tests # 测试用例及其依赖
  • Guide_Doc:这些文件是“规范/示例/模板型”的参考文档,启动时会从vagent/lang/zh/doc/Guide_Doc复制到工作区的 Guide_Doc/(当前以 output 作为 workspace 时即 output/Guide_Doc/)。它们不会被直接执行,供人和 AI 作为编写 unity_test 文档与测试的范式与规范,并被语义检索工具读取,在 UCAgent 初始化时复制过来。

    • dut_functions_and_checks.md
      用途:定义功能分组 FG-、功能点 FC-、检测点 CK-* 的组织方式与写法规范,要求覆盖所有功能点,每个功能点至少一个检测点。
      最终要产出的对应物:unity_test/{DUT}_functions_and_checks.md(如 Adder_functions_and_checks.md)。

    • dut_fixture.md
      用途:说明如何编写 DUT Fixture/Env(包含接口、时序、复位、激励、监视、检查、钩子等),给出标准写法和必备项。
      对应物:unity_test/DutFixture 与 EnvFixture 相关实现/文档。

    • dut_api_instruction.md
      用途:DUT API 设计与文档规范(接口命名、参数、返回、约束、边界条件、错误处理、示例)。
      对应物:unity_test/{DUT}_api.md 或 API 实现+测试(如 Adder_api.py)。

    • dut_function_coverage_def.md
      用途:功能覆盖(Functional Coverage)定义方法,如何从 FG/FC/CK 推导覆盖项、covergroup/coverpoint/bin 的组织与命名。
      对应物:coverage 定义文件与生成的覆盖数据、以及相关说明文档,如Adder_function_coverage_def.py

    • dut_line_coverage.md
      用途:行覆盖采集与分析方法,如何启用、统计、解读未命中行、定位冗余或缺失测试。
      对应物:行覆盖数据文件与分析笔记(unity_test/{DUT}_line_coverage_analysis.md,如 Adder_line_coverage_analysis.md)。

    • dut_test_template.md
      用途:测试用例的骨架/模板,给出最小可行的结构与编写范式(Arrange-Act-Assert、前后置、标记/选择器等)。
      对应物:tests/ 下各具体测试文件的基本结构参考。

    • dut_test_case.md
      用途:单个测试用例的撰写规范(命名、输入空间、边界/异常、可重复性、断言质量、日志、标记)。
      对应物:tests/ 中具体 test_xxx.py::test_yyy 的质量基准与填写要求。

    • dut_test_program.md
      用途:测试计划/测试编排(回归集合、分层/分阶段执行、标记与选择、超时控制、先后顺序、依赖关系)。
      对应物:回归集配置、命令/脚本、阶段化执行策略文档。

    • dut_test_summary.md
      用途:测试阶段性/最终总结的结构(通过率、覆盖率、主要问题、修复状态、风险/残留问题、下一步计划)。
      对应物:unity_test/{DUT}_test_summary.md(如Adder_test_summary.md) 或报告页面(output/uc_test_report)。

    • dut_bug_analysis.md
      用途:Bug 记录与分析规范(复现步骤、根因分析、影响范围、修复建议、验证状态、标签与追踪)。
      对应物:unity_test/{DUT}_bug_analysis.md(如Adder_bug_analysis.md)。

  • uc_test_report:由 toffee-test 生成的 index.html 报告,可直接使用浏览器打开。

    • 这个报告包含了 Line Coverage 行覆盖率,Functional Coverage 功能覆盖率,测试用例的通过情况,功能点标记具体情况等内容。
  • unity_test/tests:验证代码文件夹

    • Adder.ignore
      作用:行覆盖率忽略清单。支持忽略整个文件,或以“起止行段”形式忽略代码段。
      被谁使用:Adder_api.pyset_line_coverage(request, get_coverage_data_path(request, new_path=False), ignore=current_path_file("Adder.ignore"))

      与 Guide_Doc 的关系: 对应参考:dut_line_coverage.md(说明如何启用/统计/分析行覆盖,以及忽略规则的意义和使用场景)。

    • Adder_api.py
      作用:测试公共基座,集中放 DUT 构造、覆盖率接线与采样、pytest 基础夹具(fixtures)和示例 API。

      • create_dut(request): 实例化 DUT、设置覆盖率文件、可选波形、绑定 StepRis 采样。
      • AdderEnv: 封装引脚与常用操作(Step)。
      • api_Adder_add: 对外暴露的测试 API,完成参数校验、信号赋值、推进、读取结果。
      • pytest fixtures:dut(模块级,负责覆盖率采样/收集交给 toffee_test)、env(函数级,给每个 test 一个全新环境)。

      与 Guide_Doc 的关系:

      • dut_fixture.md:夹具/环境(Fixture/Env)的组织、Step/StepRis 的用法与职责边界。
      • dut_api_instruction.md:API 设计(命名、参数约束、返回、示例、异常)和文档规范。
      • dut_function_coverage_def.md:如何将功能覆盖组接线到 DUT 并在 StepRis 内采样。
      • dut_line_coverage.md:如何设置行覆盖文件、忽略清单,并将数据上报给 toffee_test。
    • Adder_function_coverage_def.py
      作用:功能覆盖定义(Functional Coverage),声明 FG/FC/CK 并给出 watch_point 条件。

      • 定义覆盖组:FG-API、FG-ARITHMETIC、FG-BIT-WIDTH。
    • 每组下定义 FC-_ 和 CK-_ 条件(如 CK-BASIC/CK-CARRY-IN/CK-OVERFLOW 等)。

      • get_coverage_groups(dut): 初始化并返回覆盖组列表,供 Adder_api.py 绑定与采样。

      与 Guide_Doc 的关系:

      • dut_function_coverage_def.md:覆盖组/覆盖点的组织方式与命名规范、watch_point 的表达方式。
      • dut_functions_and_checks.md:FG/FC/CK 的命名体系与映射关系来源,测试中用 mark_function 标记覆盖时需与此保持一致。
    • test_Adder_api_basic.py
      作用:API 层面的基础功能测试,覆盖典型输入、进位、零值、溢出、边界等。

      • 使用 from Adder_api import * 来获取 fixtures(dut/env)与 API。
      • 在每个测试中通过 env.dut.fc_cover[“FG-…”].mark_function(“FC-…”, <test_fn>, [“CK-…”]) 标注功能覆盖命中关系。 与 Guide_Doc 的关系:
      • dut_test_case.md:单测结构(目标/流程/预期)、命名与断言规范、可重复性、标记与日志。
      • dut_functions_and_checks.md:FG/FC/CK 的正确引用与标注。
      • dut_test_template.md:docstring 和结构写法的范式来源。
    • test_Adder_functional.py
      作用:功能行为测试(接近“场景/功能项”的角度),比 API 基测覆盖更全面的功能点验证。

      • 同样通过 mark_function 与 FG/FC/CK 标签体系对齐。 与 Guide_Doc 的关系:
        • dut_test_case.md:功能类测试的编写规范与断言要求。
        • dut_functions_and_checks.md:功能覆盖标注的规范与完整性。
        • dut_test_template.md:测试函数组织的范式。
    • test_example.py
      作用:空白样例(脚手架),用于新增测试文件的最小模板参考。 与 Guide_Doc 的关系:

      • dut_test_template.md:新建测试文件/函数时的结构、引入方式与标注方法的模板。
  • unity_test/*.md:验证相关文档

    • Adder_basic_info.md

      • 用途:DUT 概览与接口说明(功能、端口、类型、粗粒度功能分类)。
      • 参考:Guide_Doc/dut_functions_and_checks.md(接口/功能分类用语)、Guide_Doc/dut_fixture.md(从验证视角描述 I/O 与 Step 时可参考)。
    • Adder_verification_needs_and_plan.md

      • 用途:验证需求与计划(目标、风险点、测试项规划、方法论)。
      • 参考:Guide_Doc/dut_test_program.md(编排与选择策略)、Guide_Doc/dut_test_case.md(单测质量要求)、Guide_Doc/dut_functions_and_checks.md(从需求到 FG/FC/CK 的映射)。
    • Adder_functions_and_checks.md

      • 用途:FG/FC/CK 真源清单,测试标注与功能覆盖定义需与此保持一致。
      • 参考:Guide_Doc/dut_functions_and_checks.md(结构/命名)、Guide_Doc/dut_function_coverage_def.md(如何落地为覆盖实现)。
    • Adder_line_coverage_analysis.md

      • 用途:行覆盖率结论与分析,解释忽略清单、未命中行、补测建议。
      • 参考:Guide_Doc/dut_line_coverage.md;配合同目录 tests 下的 Adder.ignore
    • Adder_bug_analysis.md

      • 用途:缺陷分析报告,按 CK/TC 对应、置信度、根因、修复建议与回归方法撰写。
      • 参考:Guide_Doc/dut_bug_analysis.md(结构/要素)、Guide_Doc/dut_functions_and_checks.md(命名一致)。
    • Adder_test_summary.md

      • 用途:阶段性/最终测试总结(执行统计、覆盖情况、缺陷分布、建议、结论)。
      • 参考:Guide_Doc/dut_test_summary.md,与 Guide_Doc/dut_test_program.md 呼应。
  1. 流程总结

要做什么:

  • 将 DUT(如 Adder)打包为可测的 Python 模块
  • 启动 UCAgent(可带 MCP Server),让 Code Agent 协作按阶段推进验证
  • 依据 Guide_Doc 规范生成/完善 unity_test 文档与 tests,并以功能覆盖+行覆盖驱动测试
  • 发现并分析缺陷,产出报告与结论

做了什么:

  • 用 picker 将 RTL 导出为 Python 包(output/Adder/),准备最小 README 与文件清单
  • 启动 ucagent(含 --mcp-server/--mcp-server-no-file-tools),在 TUI/MCP 下协作
  • 在 Guide_Doc 规范约束下,生成/补全:
    • 功能清单与检测点:unity_test/Adder_functions_and_checks.md(FG/FC/CK)
    • 夹具/环境与 API:tests/Adder_api.pycreate_dutAdderEnvapi_Adder_*
    • 功能覆盖定义:tests/Adder_function_coverage_def.py(绑定 StepRis 采样)
    • 行覆盖配置与忽略:tests/Adder.ignore,分析文档 unity_test/Adder_line_coverage_analysis.md
    • 用例实现:tests/test_*.py(标注 mark_function 与 FG/FC/CK)
    • 缺陷分析与总结:unity_test/Adder_bug_analysis.mdunity_test/Adder_test_summary.md
  • 通过工具编排推进:RunTestCases/Check/StdCheck/KillCheck/Complete/GoToStage
  • 权限控制仅允许写 unity_test/testsadd_un_write_path/del_un_write_path

实现的效果:

  • 自动/半自动地产出合规的文档与可回归的测试集,支持全量与定向回归
  • 功能覆盖与行覆盖数据齐备,未命中点可定位与补测
  • 缺陷根因、修复建议与验证方法有据可依,形成结构化报告(uc_test_report/index.html
  • 支持 MCP 集成与 TUI 协作,过程可暂停/检查/回补,易于迭代与复用

典型操作轨迹(卡住时):

  • CheckStdCheck(lines=-1)KillCheck → 修复 → CheckComplete

6.2 - 使用

两种使用方式、各个选项参数、TUI界面和FAQ说明。

6.2.1 - MCP集成模式(推荐)

如何使用MCP集成模式来使用UCAgent。

MCP 集成(推荐)集成Code Agent

基于 MCP 的外部编程 CLI 协作方式。该模式能与所有支持 MCP-Server 调用的 LLM 客户端进行协同验证,例如:Cherry Studio、Claude Code、 Gemini-CLI、VS Code Copilot、Qwen-Code等。 平常使用是直接使用make命令的,要看详细命令可参考快速开始,也可以直接查看项目根目录的Makefile文件。

  • 准备RTL和对应的SPEC文档放入examples/{dut}文件夹。{dut}是模块的名称,比如Adder,如果是Adder,目录则为examples/Adder

  • 打包RTL,将文档放入工作目录并且启动 MCP server:make mcp_{dut}{dut}为对应的模块。此处如果使用的Adder,则命令为make mcp_Adder

  • 在支持 MCP client 的应用中配置 JSON:

    {
    	"mcpServers": {
    		"unitytest": {
    			"httpUrl": "http://localhost:5000/mcp",
    			"timeout": 10000
    		}
    	}
    }
    
  • 启动应用:此处使用的Qwen Code,在UCAgent/output启动qwen,然后输入提示词。

  • 输入提示词:

请通过工具RoleInfo获取你的角色信息和基本指导,然后完成任务。工具ReadTextFile读取文件。你需要在当前工作目录进行文件操作,不要超出该目录。

6.2.2 - 直接使用模式

两种使用方式、各个选项参数、TUI界面和FAQ说明。

直接使用

基于本地 CLI 和大模型的使用方式。需要准备好 OpenAI 兼容的 API 和嵌入模型 API。

使用环境变量配置(推荐)

配置文件内容:

# OpenAI兼容的API配置
openai:
  model_name: "$(OPENAI_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct)" # 模型名称
  openai_api_key: "$(OPENAI_API_KEY: YOUR_API_KEY)" # API密钥
  openai_api_base: "$(OPENAI_API_BASE: http://10.156.154.242:8000/v1)" # API基础URL
# 向量嵌入模型配置
# 用于文档搜索和记忆功能,不需要可通过 --no-embed-tools 关闭
embed:
  model_name: "$(EMBED_MODEL: Qwen/Qwen3-Embedding-0.6B)" # 嵌入模型名称
  openai_api_key: "$(EMBED_OPENAI_API_KEY: YOUR_API_KEY)" # 嵌入模型API密钥
  openai_api_base: "$(EMBED_OPENAI_API_BASE: http://10.156.154.242:8001/v1)" # 嵌入模型API URL
  dims: 4096 # 嵌入维度

UCAgent 的配置文件支持 Bash 风格的环境变量占位:$(VAR: default)。加载时会用当前环境变量 VAR 的值替换;若未设置,则使用 default

  • 例如在内置配置 vagent/setting.yaml 中:
    • openai.model_name: "$(OPENAI_MODEL: <your_chat_model_name>)"
    • openai.openai_api_key: "$(OPENAI_API_KEY: [your_api_key])"
    • openai.openai_api_base: "$(OPENAI_API_BASE: http://<your_chat_model_url>/v1)"
    • embed.model_name: "$(EMBED_MODEL: <your_embedding_model_name>)"
    • 也支持其他提供商:model_type 可选 openaianthropicgoogle_genai(详见 vagent/setting.yaml)。

你可以仅通过导出环境变量完成模型与端点切换,而无需改动配置文件。

示例:设置聊天模型与端点

# 指定聊天模型(OpenAI 兼容)
export OPENAI_MODEL='Qwen/Qwen3-Coder-30B-A3B-Instruct'

# 指定 API Key 与 Base(按你的服务商填写)
export OPENAI_API_KEY='你的API密钥'
export OPENAI_API_BASE='https://你的-openai-兼容端点/v1'

# 可选:嵌入模型(若使用检索/记忆等功能)
export EMBED_MODEL='text-embedding-3-large'
export EMBED_OPENAI_API_KEY="$OPENAI_API_KEY"
export EMBED_OPENAI_API_BASE="$OPENAI_API_BASE"

然后按前述命令启动 UCAgent 即可。若要长期生效,可将上述 export 追加到你的默认 shell 启动文件(例如 bash: ~/.bashrc,zsh: ~/.zshrc,fish: ~/.config/fish/config.fish),保存后重新打开终端或手动加载。

使用 config.yaml 来配置

  • 在项目根目录创建并编辑 config.yaml 文件,配置 AI 模型和嵌入模型:
# OpenAI兼容的API配置
openai:
  openai_api_base: <your_openai_api_base_url> # API基础URL
  model_name: <your_model_name> # 模型名称,如 gpt-4o-mini
  openai_api_key: <your_openai_api_key> # API密钥

# 向量嵌入模型配置
# 用于文档搜索和记忆功能,不需要可通过 --no-embed-tools 关闭
embed:
  model_name: <your_embed_model_name> # 嵌入模型名称
  openai_api_base: <your_openai_api_base_url> # 嵌入模型API URL
  openai_api_key: <your_api_key> # 嵌入模型API密钥
  dims: <your_embed_model_dims> # 嵌入维度,如 1536

开始使用

  • 第一步和 MCP 模式相同,准备 RTL 和对应的 SPEC 文档放入examples/{dut}文件夹。{dut}是模块的名称,如果是Adder,目录则为examples/Adder
  • 第二步开始就不同了,打包 RTL,将文档放入工作目录并启动 UCAgent TUI:make test_{dut}{dut}为对应的模块。若使用 Adder,命令为 make test_Adder(可在 Makefile 查看全部目标)。该命令会:
    • examples/{dut} 下文件拷贝到 output/{dut}(含 .v/.sv/.md/.py 等)
    • 执行 python3 ucagent.py output/ {dut} --config config.yaml -s -hm --tui -l
    • 启动带 TUI 的 UCAgent,并自动进入任务循环(loop)

提示:验证产物默认写入 output/unity_test/,若需更改可通过 CLI 的 --output 参数指定目录名。

直接用 CLI 启动(不经 Makefile)

  • 未安装命令时(项目内运行):
    • python3 ucagent.py output/ Adder --config config.yaml -s -hm --tui -l
  • 安装为命令后:
    • ucagent output/ Adder --config config.yaml -s -hm --tui -l

参数对齐 vagent/cli.py

  • workspace:工作区目录(此处为 output/
  • dut:DUT 名称(工作区子目录名,如 Adder
  • 常用可选项:
    • --tui 启动终端界面
    • -l/--loop --loop-msg "..." 启动后立即进入循环并注入提示
    • -s/--stream-output 实时输出
    • -hm/--human 进入人工可干预模式(在阶段间可暂停)
    • --no-embed-tools 如不需要检索/记忆工具
    • --skip/--unskip 跳过/取消跳过阶段(可多次传入)

常用 TUI 命令速查(直接使用模式)

  • 列出工具:tool_list
  • 阶段检查:tool_invoke Check timeout=0
  • 查看日志:tool_invoke StdCheck lines=-1(-1 表示所有行)
  • 终止检查:tool_invoke KillCheck
  • 阶段完成:tool_invoke Complete timeout=0
  • 运行用例:
    • 全量:tool_invoke RunTestCases target='' timeout=0
    • 单测函数:tool_invoke RunTestCases target='tests/test_checker.py::test_run' timeout=120 return_line_coverage=True
    • 过滤:tool_invoke RunTestCases target='-k add or mul'
  • 阶段跳转:tool_invoke GoToStage index=2(索引从 0 开始)
  • 继续执行:loop 继续修复 ALU754 的未命中分支并重试用例

建议的最小可写权限(只允许生成验证产物处可写):

  • 仅允许 unity_test/unity_test/tests/ 可写:
    • add_un_write_path *
    • del_un_write_path unity_test
    • del_un_write_path unity_test/tests

常见问题与提示

  • 检查卡住/无输出:
    • tool_invoke StdCheck lines=-1 查看全部日志;必要时 tool_invoke KillCheck;修复后重试 tool_invoke Check
  • 没找到工具名:
    • 先执行 tool_list 确认可用工具;若缺失,检查是否在 TUI 模式、是否禁用了嵌入工具(通常无关)。
  • 产物位置:
    • 默认在 workspace/output_dir,即本页示例为 output/unity_test/

相关文档

6.2.3 - 人机协同验证

如何与AI配合来验证模块。

UCAgent 支持在验证过程中进行人机协同,允许用户暂停 AI 执行,人工干预验证过程,然后继续 AI 执行。这种模式适用于需要精细控制或复杂决策的场景。

协同流程:

  1. 暂停 AI 执行:

    • 在直接接入 LLM 模式下:按 Ctrl+C 暂停。
    • 在 Code Agent 协同模式下:根据 Agent 的暂停方式(如 Gemini-cli 使用 Esc)暂停。
  2. 人工干预:

    • 手动编辑文件、测试用例或配置。
    • 使用交互命令进行调试或调整。
  3. 阶段控制:

    • 使用 tool_invoke Check 检查当前阶段状态。
    • 使用 tool_invoke Complete 标记阶段完成并进入下一阶段。
  4. 继续执行:

    • 使用 loop [prompt] 命令继续 AI 执行,并可提供额外的提示信息。
    • 在 Code Agent 模式下,通过 Agent 的控制台输入提示。
  5. 权限管理:

    • 可使用 add_un_write_pathdel_un_write_path 等命令设置文件写权限,控制 AI 是否可以编辑特定文件。
    • 适用于直接接入 LLM 或强制使用 UCAgent 文件工具。

6.2.4 - 参数说明

各个选项参数说明。

参数与选项

UCAgent 的使用方式为:

ucagent <workspace> <dut_name> {参数与选项}

输入

  • workspace:工作目录:
    • workspace/<DUT_DIR>: 待测设计(DUT),即由 picker 导出的 DUT 对应的 Python 包 <DUT_DIR>,例如:Adder
    • workspace/<DUT_DIR>/README.md: 以自然语言描述的该 DUT 验证需求与目标
    • workspace/<DUT_DIR>/*.md: 其他参考文件
    • workspace/<DUT_DIR>/*.v/sv/scala: 源文件,用于进行 bug 分析
    • 其他与验证相关的文件(例如:提供的测试实例、需求说明等)
  • dut_name: 待测设计的名称,即 <DUT_DIR>,例如:Adder

输出

  • workspace:工作目录:
    • workspace/Guide_Doc:验证过程中所遵循的各项要求与指导文档
    • workspace/uc_test_report: 生成的 Toffee-test 测试报告
    • workspace/unity_test/tests: 自动生成的测试用例
    • workspace/*.md: 生成的各类文档,包括 Bug 分析、检查点记录、验证计划、验证结论等

对输出的详细解释可以参考快速开始的9

位置参数

参数 必填 说明 示例
workspace 运行代理的工作目录 ./output
dut DUT 名称(工作目录下的子目录名) Adder

执行与交互

选项 简写 取值/类型 默认值 说明
--stream-output -s flag 关闭 流式输出到控制台
--human -hm flag 关闭 启动时进入人工输入/断点模式
--interaction-mode -im standard/enhanced/advanced standard 交互模式;enhanced 含规划与记忆管理,advanced 含自适应策略
--tui flag 关闭 启用终端 TUI 界面
--loop -l flag 关闭 启动后立即进入主循环(可配合 --loop-msg),适用于直接使用模式
--loop-msg str 进入循环时注入的首条消息
--seed int 随机 随机种子(未指定则自动随机)
--sys-tips str 覆盖系统提示词

配置与模板

选项 简写 取值/类型 默认值 说明
--config path 配置文件路,如–config config.yaml径
--template-dir path 自定义模板目录
--template-overwrite flag 渲染模板到 workspace 时允许覆盖已存在内容
--output dir unity_test 输出目录名
--override A.B.C=VALUE[,X.Y=VAL2,…] 以“点号路径=值”覆盖配置;字符串需引号,其它按 Python 字面量解析
--gen-instruct-file -gif file 在 workspace 下生成外部 Agent 的引导文件(存在则覆盖)
--guid-doc-path path 使用自定义 Guide_Doc 目录(默认使用内置拷贝)

计划与 ToDo

选项 简写 取值/类型 默认值 说明
--force-todo -fp flag 在 standard 模式下也启用 ToDo 工具,并在每轮提示中附带 ToDo 信息
--use-todo-tools -utt flag 启用 ToDo 相关工具(不限于 standard 模式)

ToDo 工具概览与示例 给模型规划的,小模型关闭,大模型自行打开

说明:ToDo 工具是用于提升模型规划能力的工具,用户可以利用它来自定义模型的ToDo列表。目前该功能对模型能力要求较高,默认处于关闭状态。

启用条件:任意模式下使用 --use-todo-tools;或在 standard 模式用 --force-todo 强制启用并在每轮提示中附带 ToDo 信息。

约定与限制:步骤索引为 1-based;steps 数量需在 2~20;notes 与每个 step 文本长度 ≤ 100;超限会拒绝并返回错误字符串。

工具总览

工具类 调用名 主要功能 参数 返回 关键约束/行为
CreateToDo CreateToDo 新建当前 ToDo(覆盖旧 ToDo) task_description: str; steps: List[str] 成功提示 + 摘要字符串 校验步数与长度;成功后写入并返回摘要
CompleteToDoSteps CompleteToDoSteps 将指定步骤标记为完成,可附加备注 completed_steps: List[int]=[]; notes: str="" 成功提示(完成数)+ 摘要 仅未完成步骤生效;无 ToDo 时提示先创建;索引越界忽略
UndoToDoSteps UndoToDoSteps 撤销步骤完成状态,可附加备注 steps: List[int]=[]; notes: str="" 成功提示(撤销数)+ 摘要 仅已完成步骤生效;无 ToDo 时提示先创建;索引越界忽略
ResetToDo ResetToDo 重置/清空当前 ToDo 重置成功提示 清空步骤与备注,随后可重新创建
GetToDoSummary GetToDoSummary 获取当前 ToDo 摘要 摘要字符串 / 无 ToDo 提示 只读,不修改状态
ToDoState ToDoState 获取状态短语(看板/状态栏) 状态描述字符串 动态显示:无 ToDo/已完成/进度统计等

调用示例(以 MCP/内部工具调用为例,参数为 JSON 格式):

{
	"tool": "CreateToDo",
	"args": {
		"task_description": "为 Adder 核心功能完成验证闭环",
		"steps": [
			"阅读 README 与规格,整理功能点",
			"定义检查点与通过标准",
			"生成首批单元测试",
			"运行并修复失败用例",
			"补齐覆盖率并输出报告"
		]
	}
}
{
	"tool": "CompleteToDoSteps",
	"args": { "completed_steps": [1, 2], "notes": "初始问题排查完成,准备补充用例" }
}
{ "tool": "UndoToDoSteps", "args": { "steps": [2], "notes": "第二步需要微调检查点" } }
{ "tool": "ResetToDo", "args": {} }
{ "tool": "GetToDoSummary", "args": {} }
{ "tool": "ToDoState", "args": {} }

外部与嵌入工具

选项 简写 取值/类型 默认值 说明
--ex-tools name1[,name2…] 逗号分隔的外部工具类名列表(如:SqThink)
--no-embed-tools flag 禁用内置的检索/记忆类嵌入工具

日志

选项 简写 取值/类型 默认值 说明
--log flag 启用日志
--log-file path 自动 日志输出文件(未指定则使用默认)
--msg-file path 自动 消息日志文件(未指定则使用默认)

MCP Server

选项 简写 取值/类型 默认值 说明
--mcp-server flag 启动 MCP Server(含文件工具)
--mcp-server-no-file-tools flag 启动 MCP Server(无文件操作工具)
--mcp-server-host host 127.0.0.1 Server 监听地址
--mcp-server-port int 5000 Server 端口

阶段控制与安全

选项 简写 取值/类型 默认值 说明
--force-stage-index int 0 强制从指定阶段索引开始
--skip int(可多次) [] 跳过指定阶段索引,可重复提供
--unskip int(可多次) [] 取消跳过指定阶段索引,可重复提供
--no-write / –nw path1 path2 … 限制写入目标列表;必须位于 workspace 内且存在

版本与检查

选项 简写 取值/类型 默认值 说明
--check flag 检查默认配置、语言目录、模板与 Guide_Doc 是否存在后退出
--version flag 输出版本并退出

示例

python3 ucagent.py ./output Adder \
  \
  -s \
  -hm \
  -im enhanced \
  --tui \
  -l \
  --loop-msg 'start verification' \
  --seed 12345 \
  --sys-tips '按规范完成Adder的验证' \
  \
  --config config.yaml \
  --template-dir ./templates \
  --template-overwrite \
  --output unity_test \
  --override 'conversation_summary.max_tokens=16384,...' \
  \
  --use-todo-tools \
  \
  --ex-tools 'SqThink,AnotherTool' \
  --no-embed-tools \
  \
  --log \
  --log-file ./output/ucagent.log \
  --msg-file ./output/ucagent.msg \
  \
  --mcp-server-no-file-tools \
  --mcp-server-host 127.0.0.1 \
  --mcp-server-port 5000 \
  \
  --force-stage-index 2 \
  --skip 5 --skip 7 \
  --unskip 6 \
  --nw ./output/Adder ./output/unity_test
  • 位置参数
    • ./output:workspace 工作目录
    • Adder:dut 子目录名
  • 执行与交互
    • -s:流式输出
    • -hm:启动即人工可介入
    • -im enhanced:交互模式为增强(含规划与记忆)
    • --tui:启用 TUI
    • --loop/–loop-msg:启动后立即进入循环并注入首条消息
    • --seed 12345:固定随机种子
    • --sys-tips:自定义系统提示
  • 配置与模板
    • --config config.yaml:从config.yaml加载项目配置
    • --template-dir ./templates:指定模板目录为./templates
    • --template-overwrite:渲染模板时允许覆盖
    • --output unity_test:输出目录名unity_test
    • --override ‘…’: 覆盖配置键值(点号路径=值,多项用逗号分隔;字符串需内层引号,整体用单引号包裹以保留引号),示例里设置了会话摘要上限、启用裁剪、文档语言为“中文”、模型名为 gpt-4o-mini
    • -gif/--gen-instruct-file GEMINI.md:在 <workspace>/GEMINI.md 下生成外部协作引导文件
    • --guid-doc-path ./output/Guide_Doc:自定义 Guide_Doc 目录为./output/Guide_Doc
  • 计划与 ToDo
    • --use-todo-tools:启用 ToDo 工具及强制附带 ToDo 信息
  • 外部与嵌入工具
    • --ex-tools ‘SqThink,AnotherTool’:启用外部工具SqThink,AnotherTool
    • --no-embed-tools:禁用内置嵌入检索/记忆工具
  • 日志
    • --log:开启日志文件
    • --log-file ./output/ucagent.log:指定日志输出文件为./output/ucagent.log
    • --msg-file ./output/ucagent.msg:指定消息日志文件为./output/ucagent.msg
  • MCP Server
    • --mcp-server-no-file-tools:启动 MCP(无文件操作工具)
    • --mcp-server-host:Server 监听地址为127.0.0.1
    • --mcp-server-port:Server 监听端口为5000
  • 阶段控制与安全
    • --force-stage-index 2:从阶段索引 2 开始
    • --skip 5 --skip 7:跳过阶段5和阶段7
    • --unskip 7:取消跳过阶段7
    • --nw ./output/Adder ./output/unity_test:限制仅./output/Adder./output/unity_test路径可写
  • 说明
    • --check 与 --version 会直接退出,未与运行组合使用
    • --mcp-server 与 --mcp-server-no-file-tools 二选一;此处选了后者带路径参数(如 --template-dir/--guid-doc-path/–nw 的路径)需实际存在,否则会报错
    • --override 字符串值务必带引号,并整体用单引号包住以避免 shell 吃掉引号(示例写法已处理)

6.2.5 - TUI

tui界面的组成与操作说明。

TUI(界面与操作)

UCAgent 自带基于 urwid 的终端界面(TUI),用于在本地交互式观察任务进度、消息流与控制台输出,并直接输入命令(如进入/退出循环、切换模式、执行调试命令等)。

界面组成

tui界面组成

  • Mission 面板(左侧)

    • 阶段列表:显示当前任务的阶段(索引、标题、失败数、耗时)。颜色含义:
      • 绿色:已完成阶段
      • 红色:当前进行阶段
      • 黄色:被跳过的阶段(显示“skipped”)
    • Changed Files:近期修改文件(含修改时间与相对时间,如“3m ago”)。较新的文件以绿色显示。
    • Tools Call:工具调用状态与计数。忙碌中的工具会以黄色高亮(如 SqThink(2))。
    • Deamon Commands:后台运行的 demo 命令列表(带开始时间与已运行时长)。
  • Status 面板(右上)

    • 显示 API 与代理状态摘要,以及当前面板尺寸参数(便于调节布局时参考)。
  • Messages 面板(右上中)

    • 实时消息流(模型回复、工具输出、系统提示)。
    • 支持焦点与滚动控制,标题会显示“当前/总计”的消息定位。例如:Messages (123/456)。
  • Console(底部)

    • Output:命令与系统输出区域,支持分页浏览。
    • Input:命令输入行(默认提示符 “(UnityChip) ”)。提供历史、补全、忙碌提示等。

提示:界面每秒自动刷新一次(不影响输入)。当消息或输出过长时,会进入分页或手动滚动模式。

操作与快捷键

  • Enter:执行当前输入命令;若输入为空会重复上一次命令;输入 q/Q/exit/quit 退出 TUI。
  • Esc:
    • 若正在浏览 Messages 的历史,退出滚动并返回末尾;
    • 若 Output 正在分页查看,退出分页;
    • 否则聚焦到底部输入框。
  • Tab:命令补全;再次按 Tab 可分批显示更多可选项。
  • Shift+Right:清空 Console Output。
  • Shift+Up / Shift+Down:在 Messages 中向上/向下移动焦点(浏览历史)。
  • Ctrl+Up / Ctrl+Down:增/减 Console 输出区域高度。
  • Ctrl+Left / Ctrl+Right:减/增 Mission 面板宽度。
  • Shift+Up / Shift+Down(另一路径):调整 Status 面板高度(最小 3,最大 100)。
  • Up / Down:
    • 若 Output 在分页模式,Up/Down 用于翻页;
    • 否则用于命令历史导航(将历史命令放入输入行,可编辑后回车执行)。

分页模式提示:当 Output 进入分页浏览时,底部标题会提示 “Up/Down: scroll, Esc: exit”,Esc 退出分页并返回输入状态。

命令与用法

  • 普通命令:直接输入并回车,例如 loop、tui、help 等(由内部调试器处理)。
  • 历史命令:在输入行为空时按 Enter,将重复执行上一条命令。
  • 清屏:输入 clear 并回车,仅清空 Output(不影响消息记录)。
  • 演示/后台命令:命令末尾添加 & 将在后台运行,完成后会在 Output 区域提示结束;当前后台命令可通过 list_demo_cmds 查看。
  • 直接执行系统/危险命令:以 ! 前缀执行(例如 !loop),该模式执行后优先滚动到最新输出。
  • 列出后台命令:list_demo_cmds 显示正在运行的 demo 命令列表与开始时间。

消息配置(message_config)

  • 作用:在运行中查看/调整消息裁剪策略,控制历史保留与 LLM 输入 token 上限。
  • 命令:
    • message_config 查看当前配置
    • message_config 设置配置项
  • 可配置项:
    • max_keep_msgs:保留的历史消息条数(影响会话记忆窗口)
    • max_token:进入模型前的消息裁剪 token 上限(影响开销/截断)
  • 示例:
    • message_config
    • message_config max_keep_msgs 8
    • message_config max_token 4096

其他说明

  • 自动补全:支持命令名与部分参数的补全;候选项过多时分批显示,可多次按 Tab 查看剩余项。
  • 忙碌提示:命令执行期间,输入框标题会轮转显示 (wait.), (wait..), (wait…),表示正在处理。
  • 消息焦点:当未手动滚动时,消息焦点自动跟随最新消息;进入手动滚动后,会保持当前位置,直至按 Esc 或滚动至末尾。
  • 错误容错:若某些 UI 操作异常(如终端不支持某些控制序列),TUI 会尽量回退到安全状态继续运行。

6.2.6 - FAQ

常见问题与解答。

FAQ

  • 模型切换:在 config.yamlopenai.model_name
  • 验证过程中出现错误怎么办:使用 Ctrl+C 进入交互模式,通过 status 查看当前状态,使用 help 获取调试命令。
  • Check 失败:先 ReadTextFile 阅读 reference_files;再按返回信息修复,循环 RunTestCases → Check
  • 自定义阶段:修改 vagent/lang/zh/config/default.yamlstage;或用 --override 临时覆盖
  • 添加工具:vagent/tools/ 下新建类,继承 UCTool,运行时 --ex-tools YourTool
  • MCP 连接失败:检查端口/防火墙,改 --mcp-server-port;无嵌入可加 --no-embed-tools
  • 只读保护:通过 --no-write/--nw 指定路径限制写入(必须位于 workspace 内)

为什么快速启动找不到config.yaml/定制流程时找不到config.yaml?

  • 使用pip 安装后并没有config.yaml那个文件,所以在快速启动的启动 MCP Server没有加--config config.yaml这个选项。
  • 可以通过在工作目录添加config.yaml文件并且加上--config config.yaml参数来启动;也可以使用克隆仓库来使用UCAgent的方式来解决。

运行中如何调整消息窗口与 token 上限?

  • 在 TUI 输入:message_config 查看当前配置;
  • 设置:message_config max_keep_msgs 8message_config max_token 4096
  • 作用范围:影响会话历史裁剪与送入 LLM 的最大 token 上限(通过 Summarization/Trim 节点生效)。

文档中的 “CK bug” 要改吗?

  • 是。术语统一为 “TC bug”。同时确保 bug 文档里的 <TC-*> 能匹配失败用例(文件/类/用例名)。

为什么找不到 WriteTextFile 工具?

  • 该工具已移除。请改用 EditTextFile(支持 overwrite/append/replace 三种模式)或其他文件工具(Copy/Move/Delete 等)。

6.3 - 工作流

整体的工作流说明。

整体采用“按阶段渐进推进”的方式,每个阶段都有明确目标、产出与通过标准;完成后用工具 Check 验证并用 Complete 进入下一阶段。若阶段包含子阶段,需按顺序 逐一完成子阶段并各自通过 Check。

  • 顶层阶段总数:11(见 vagent/lang/zh/config/default.yaml
  • 推进原则:未通过的阶段不可跳转;可用工具 CurrentTips 获取当前阶段详细指导;需要回补时可用 GotoStage 回到指定阶段;命令行也可用 –skip/--unskip 控制阶段索引。

整体流程概览(11 个阶段)

目前的流程包含:

  1. 需求分析与验证规划 → 2) {DUT} 功能理解 → 3) 功能规格分析与测试点定义 → 4) 测试平台基础架构设计 → 5) 功能覆盖率模型实现 → 6) 基础 API 实现 → 7) 基础 API 功能测试 → 8) 测试框架脚手架 → 9) 全面验证执行与缺陷分析 → 10) 代码行覆盖率分析与提升(默认跳过,可启用)→ 11) 验证审查与总结

以实际的工作流为准,下图仅供参考。 工作流图

说明:以下路径中的 默认为工作目录下的输出目录名(默认 unity_test)。例如文档输出到 <workspace>/unity_test/


阶段 1:需求分析与验证规划

  • 目标:理解任务、明确验证范围与策略。
  • 怎么做:
    • 阅读 {DUT}/README.md,梳理“需要测哪些功能/输入输出/边界与风险”。
    • 形成可执行的验证计划与目标清单。
  • 产出:<OUT>/{DUT}_verification_needs_and_plan.md(中文撰写)。
  • 通过标准:文档存在、结构规范(自动检查 markdown_file_check)。
  • 检查器:
    • UnityChipCheckerMarkdownFileFormat
      • 作用:校验 Markdown 文件存在与格式,禁止把换行写成字面量“\n”。
      • 参数:
        • markdown_file_list (str | List[str]): 待检查的 MD 文件路径或路径列表。示例:{OUT}/{DUT}_verification_needs_and_plan.md
        • no_line_break (bool): 是否禁止把换行写成字面量 “\n”;true 表示禁止。

阶段 2:{DUT} 功能理解

  • 目标:掌握 DUT 的接口与基本信息,明确是组合/时序电路。
  • 怎么做:
    • 阅读 {DUT}/README.md{DUT}/__init__.py
    • 分析 IO 端口、时钟/复位需求与功能范围。
  • 产出:<OUT>/{DUT}_basic_info.md
  • 通过标准:文档存在、格式规范(markdown_file_check)。
  • 检查器:
    • UnityChipCheckerMarkdownFileFormat
      • 作用:校验 Markdown 文件存在与格式,禁止把换行写成字面量“\n”。
      • 参数:
        • markdown_file_list (str | List[str]): 待检查的 MD 文件路径或路径列表。示例:{OUT}/{DUT}_basic_info.md
        • no_line_break (bool): 是否禁止把换行写成字面量 “\n”;true 表示禁止。

阶段 3:功能规格分析与测试点定义(含子阶段 FG/FC/CK)

  • 目标:把功能分组(FG)、功能点(FC)和检测点(CK)结构化,作为后续自动化的依据。
  • 怎么做:
    • 阅读 {DUT}/*.md 与已产出文档,建立 {DUT}_functions_and_checks.md 的 FG/FC/CK 结构。
    • 规范标签:<FG-组名>、<FC-功能名>、<CK-检测名>,每个功能点至少 1 个检测点。
  • 子阶段:
    • 3.1 功能分组与层次(FG):检查器 UnityChipCheckerLabelStructure(FG)
    • 3.2 功能点定义(FC):检查器 UnityChipCheckerLabelStructure(FC)
    • 3.3 检测点设计(CK):检查器 UnityChipCheckerLabelStructure(CK)
  • 产出:<OUT>/{DUT}_functions_and_checks.md
  • 通过标准:三类标签结构均通过对应检查。
  • 对应检查器(默认配置):
    • 3.1 UnityChipCheckerLabelStructure
      • 作用:解析 {DUT}_functions_and_checks.md 中的标签结构并校验层级与数量(FG)。
      • 参数:
        • doc_file (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • leaf_node (“FG” | “FC” | “CK”): 需要校验的叶子类型。示例:"FG"
        • min_count (int, 默认 1): 该叶子类型的最小数量阈值。
        • must_have_prefix (str, 默认 “FG-API”): FG 名称要求的前缀,用于规范化分组命名。
    • 3.2 UnityChipCheckerLabelStructure
      • 作用:解析文档并校验功能点定义(FC)。
      • 参数:
        • doc_file (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • leaf_node (“FG” | “FC” | “CK”): 需要校验的叶子类型。示例:"FC"
        • min_count (int, 默认 1): 该叶子类型的最小数量阈值。
        • must_have_prefix (str, 默认 “FG-API”): 所属 FG 的前缀规范,用于一致性检查。
    • 3.3 UnityChipCheckerLabelStructure
      • 作用:解析文档并校验检测点设计(CK),并缓存 CK 列表用于后续分批实现。
      • 参数:
        • doc_file (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • leaf_node (“FG” | “FC” | “CK”): 需要校验的叶子类型。示例:"CK"
        • data_key (str): 共享数据键名,用于缓存 CK 列表(供后续分批实现使用)。示例:"COVER_GROUP_DOC_CK_LIST"
        • min_count (int, 默认 1): 该叶子类型的最小数量阈值。
        • must_have_prefix (str, 默认 “FG-API”): 所属 FG 的前缀规范,用于一致性检查。

阶段 4:测试平台基础架构设计(fixture/API 框架)

  • 目标:提供统一的 DUT 创建与测试生命周期管理能力。
  • 怎么做:
    • <OUT>/tests/{DUT}_api.py 实现 create_dut();时序电路配置时钟(InitClock),组合电路无需时钟。
    • 实现 pytest fixture dut,负责初始化/清理与可选的波形/行覆盖率开关。
  • 产出:<OUT>/tests/{DUT}_api.py(含注释与文档字符串)。
  • 通过标准:DUT 创建与 fixture 检查通过(UnityChipCheckerDutCreation / UnityChipCheckerDutFixture)。
  • 子阶段检查器:
    • DUT 创建:UnityChipCheckerDutCreation
      • 作用:校验 {DUT}_api.py 中的 create_dut(request) 是否实现规范(签名、时钟/复位、覆盖率路径等约定)。
      • 参数:
        • target_file (str): DUT API 与 fixture 所在文件路径。示例:{OUT}/tests/{DUT}_api.py
    • dut fixture:UnityChipCheckerDutFixture
      • 作用:校验 pytest fixture dut 的生命周期管理、yield/清理,以及覆盖率收集调用是否到位。
      • 参数:
        • target_file (str): 包含 dut fixture 的文件路径。示例:{OUT}/tests/{DUT}_api.py
    • env fixture:UnityChipCheckerEnvFixture
      • 作用:校验 env* 系列 fixture 的存在、数量与 Bundle 封装是否符合要求。
      • 参数:
        • target_file (str): 包含 env* 系列 fixture 的文件路径。示例:{OUT}/tests/{DUT}_api.py
        • min_env (int, 默认 1): 至少需要存在的 env* fixture 数量。示例:1
        • force_bundle (bool, 当前未使用): 是否强制要求 Bundle 封装。

覆盖率路径规范(重要):

  • 在 create_dut(request) 中,必须通过 get_coverage_data_path(request, new_path=True) 获取新的行覆盖率文件路径,并传入 dut.SetCoverage(...)
  • dut fixture 的清理阶段,必须通过 get_coverage_data_path(request, new_path=False) 获取已有路径,并调用 set_line_coverage(request, <path>, ignore=...) 写入统计。
  • 若缺失上述调用,检查器会直接报错,并给出修复提示(含 tips_of_get_coverage_data_path 示例)。

阶段 5:功能覆盖率模型实现

  • 目标:将 FG/FC/CK 转为可统计的覆盖结构,支撑进度度量与回归。
  • 怎么做:
    • <OUT>/tests/{DUT}_function_coverage_def.py 实现 get_coverage_groups(dut)
    • 为每个 FG 建立 CovGroup;为 FC/CK 建 watch_point 与检查函数(优先用 lambda,必要时普通函数)。
  • 子阶段:
    • 5.1 覆盖组创建(FG)
    • 5.2 覆盖点与检查实现(FC/CK),支持“分批实现”提示(COMPLETED_POINTS/TOTAL_POINTS)。
  • 产出:<OUT>/tests/{DUT}_function_coverage_def.py
  • 通过标准:CoverageGroup 检查(FG/FC/CK)与批量实现检查通过。
  • 子阶段检查器:
    • 5.1 UnityChipCheckerCoverageGroup
      • 作用:比对覆盖组定义与文档 FG 一致性。
      • 参数:
        • test_dir (str): 测试目录根路径。示例:{OUT}/tests
        • cov_file (str): 覆盖率模型定义文件路径。示例:{OUT}/tests/{DUT}_function_coverage_def.py
        • doc_file (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • check_types (str | List[str]): 检查的类型集合。示例:"FG"
    • 5.2 UnityChipCheckerCoverageGroup
      • 作用:比对覆盖点/检查点实现与文档 FC/CK 一致性。
      • 参数:
        • test_dir (str): 测试目录根路径。示例同上
        • cov_file (str): 覆盖率模型定义文件路径。示例同上
        • doc_file (str): 功能/检查点文档路径。示例同上
        • check_types (List[str]): 检查类型集合。示例:["FC", "CK"]
    • 5.2(分批)UnityChipCheckerCoverageGroupBatchImplementation
      • 作用:按 CK 分批推进实现与对齐检查,维护进度(TOTAL/COMPLETED)。
      • 参数:
        • test_dir (str): 测试目录根路径。
        • cov_file (str): 覆盖率模型定义文件路径。
        • doc_file (str): 功能/检查点文档路径。
        • batch_size (int, 默认 20): 每批实现与校验的 CK 数量上限。示例:20
        • data_key (str): 共享数据键名,用于读取 CK 列表。示例:"COVER_GROUP_DOC_CK_LIST"

阶段 6:基础 API 实现

  • 目标:用 api_{DUT}_* 前缀提供可复用的操作封装,隐藏底层信号细节。
  • 怎么做:
    • <OUT>/tests/{DUT}_api.py 实现至少 1 个基础 API;建议区分“底层功能 API”与“任务功能 API”。
    • 补充详细 docstring:功能、参数、返回值、异常。
  • 产出:<OUT>/tests/{DUT}_api.py
  • 通过标准:UnityChipCheckerDutApi 通过(前缀必须为 api_{DUT}_)。
  • 检查器:
    • UnityChipCheckerDutApi
      • 作用:扫描/校验 api_{DUT}_* 函数的数量、命名、签名与 docstring 完整度。
      • 参数:
        • api*prefix (str): API 前缀匹配表达式。建议:"api*{DUT}\_"
        • target_file (str): API 定义所在文件。示例:{OUT}/tests/{DUT}_api.py
        • min_apis (int, 默认 1): 至少需要的 API 数量。

阶段 7:基础 API 功能正确性测试

  • 目标:为每个已实现 API 编写至少 1 个基础功能用例,并标注覆盖率。
  • 怎么做:
    • <OUT>/tests/test_{DUT}_api_*.py 新建测试;导入 from {DUT}_api import *
    • 每个测试函数的第一行:dut.fc_cover['FG-API'].mark_function('FC-API-NAME', test_func, ['CK-XXX'])
    • 设计典型/边界/异常数据,断言预期输出。
    • 用工具 RunTestCases 执行与回归。
  • 产出:<OUT>/tests/test_{DUT}_api_*.py 与缺陷记录(若发现 bug)。
  • 通过标准:UnityChipCheckerDutApiTest 通过(覆盖、用例质量、文档记录齐备)。
  • 检查器:
    • UnityChipCheckerDutApiTest
      • 作用:运行 pytest 并检查每个 API 至少 1 个基础功能用例且正确覆盖标记;核对缺陷记录与文档一致。
      • 参数:
        • api*prefix (str): API 前缀匹配表达式。建议:"api*{DUT}\_"
        • target_file_api (str): API 文件路径。示例:{OUT}/tests/{DUT}_api.py
        • target*file_tests (str): 测试文件 Glob。示例:{OUT}/tests/test*{DUT}\_api\*.py
        • doc_func_check (str): 功能/检查点文档。示例:{OUT}/{DUT}_functions_and_checks.md
        • doc_bug_analysis (str): 缺陷分析文档。示例:{OUT}/{DUT}_bug_analysis.md
        • min_tests (int, 默认 1): 单 API 最少测试用例数。
        • timeout (int, 默认 15): 单次测试运行超时(秒)。

阶段 8:测试框架脚手架构建

  • 目标:为尚未实现的功能点批量生成“占位”测试模板,确保覆盖版图完整。
  • 怎么做:
    • 依据 {DUT}_functions_and_checks.md,在 <OUT>/tests/ 创建 test_*.py,文件与用例命名语义化。
    • 每个函数首行标注覆盖率 mark;补充 TODO 注释说明要测什么;末尾添加 assert False, 'Not implemented' 防误通过。
  • 产出:批量测试模板;覆盖率进度指标(COVERED_CKS/TOTAL_CKS)。
  • 通过标准:UnityChipCheckerTestTemplate 通过(结构/标记/说明完整)。
  • 检查器:
    • UnityChipCheckerTestTemplate
      • 作用:检查模板文件/用例结构、覆盖标记、TODO 注释与防误通过断言;统计覆盖进度。
      • 参数:
        • doc_func_check (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • test_dir (str): 测试目录根路径。示例:{OUT}/tests
        • ignore*ck_prefix (str): 统计覆盖时忽略的 CK 前缀(通常为基础 API 的用例)。示例:"test_api*{DUT}\_"
        • data_key (str): 共享数据键名,用于生成/读取模板实现进度。示例:"TEST_TEMPLATE_IMP_REPORT"
        • batch_size (int, 默认 20): 每批模板检查数量。
        • min_tests (int, 默认 1): 最少要求的模板测试数。
        • timeout (int, 默认 15): 测试运行超时(秒)。

阶段 9:全面验证执行与缺陷分析

  • 目标:将模板填充为真实测试,系统发现并分析 DUT bug。
  • 怎么做:
    • test_*.py 填充逻辑,优先通过 API 调用,不直接操纵底层信号。
    • 设计充分数据并断言;用 RunTestCases 运行;对 Fail 进行基于源码的缺陷定位与记录。
  • 子阶段:
    • 9.1 分批测试用例实现与对应缺陷分析(COMPLETED_CASES/TOTAL_CASES)。
  • 产出:成体系的测试集与 /{DUT}_bug_analysis.md
  • 通过标准:UnityChipCheckerTestCase(质量/覆盖/缺陷分析)通过。
  • 检查器:
    • 父阶段:UnityChipCheckerTestCase
      • 作用:运行整体测试并对照功能/缺陷文档检查质量、覆盖与记录一致性。
      • 参数:
        • doc_func_check (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • doc_bug_analysis (str): 缺陷分析文档路径。示例:{OUT}/{DUT}_bug_analysis.md
        • test_dir (str): 测试目录根路径。示例:{OUT}/tests
        • min_tests (int, 默认 1): 最少要求的测试用例数量。
        • timeout (int, 默认 15): 测试运行超时(秒)。
    • 子阶段(分批实现):UnityChipCheckerBatchTestsImplementation
      • 作用:分批将模板落地为真实用例并回归,维护实现进度与报告。
      • 参数:
        • doc_func_check (str): 功能/检查点文档路径。
        • doc_bug_analysis (str): 缺陷分析文档路径。
        • test_dir (str): 测试目录根路径。
        • ignore*ck_prefix (str): 统计覆盖时忽略的 CK 前缀。示例:"test_api*{DUT}\_"
        • batch_size (int, 默认 10): 每批转化并执行的用例数量。
        • data_key (str): 共享数据键名(必填),用于保存分批实现进度。示例:"TEST_TEMPLATE_IMP_REPORT"
        • pre_report_file (str): 历史进度报告路径。示例:{OUT}/{DUT}/.TEST_TEMPLATE_IMP_REPORT.json
        • timeout (int, 默认 15): 测试运行超时(秒)。

TC bug 标注规范与一致性(与文档/报告强关联):

  • 术语:统一使用 “TC bug”(不再使用 “CK bug”)。
  • 标注结构:<FG-*>/<FC-*>/<CK-*>/<BG-NAME-XX>/<TC-test_file.py::[ClassName]::test_case>;其中 BG 的置信度 XX 为 0–100 的整数。
  • 失败用例与文档关系:
    • 文档中出现的 <TC-*> 必须能与测试报告中的失败用例一一对应(文件名/类名/用例名匹配)。
    • 失败的测试用例必须标注其关联检查点(CK),否则会被判定为“未标记”。
    • 若存在失败用例未在 bug 文档中记录,将被提示为“未文档化的失败用例”。

阶段 10:代码行覆盖率分析与提升(默认跳过,可启用)

  • 目标:回顾未覆盖代码行,定向补齐。
  • 怎么做:
    • 运行 Check 获取行覆盖率;若未达标,围绕未覆盖行增补测试并回归;循环直至满足阈值。
  • 产出:行覆盖率报告与补充测试。
  • 通过标准:UnityChipCheckerTestCaseWithLineCoverage 达标(默认阈值 0.9,可在配置中调整)。
  • 说明:该阶段在配置中标记 skip=true,可用 --unskip 指定索引启用。
  • 检查器:
    • UnityChipCheckerTestCaseWithLineCoverage
      • 作用:在 TestCase 基础上统计行覆盖率并对比阈值。
      • 参数:
        • doc_func_check (str): 功能/检查点文档路径。示例:{OUT}/{DUT}_functions_and_checks.md
        • doc_bug_analysis (str): 缺陷分析文档路径。示例:{OUT}/{DUT}_bug_analysis.md
        • test_dir (str): 测试目录根路径。示例:{OUT}/tests
        • cfg (dict | Config): 必填,用于推导默认路径以及环境配置。
        • min_line_coverage (float, 默认按配置,未配置则 0.8): 最低行覆盖率阈值。
        • coverage_json (str, 可选): 行覆盖率 JSON 路径。默认:uc_test_report/line_dat/code_coverage.json
        • coverage_analysis (str, 可选): 行覆盖率分析 MD 输出。默认:unity_test/{DUT}_line_coverage_analysis.md
        • coverage_ignore (str, 可选): 忽略文件清单。默认:unity_test/tests/{DUT}.ignore

阶段 11:验证审查与总结

  • 目标:沉淀成果、复盘流程、给出改进建议。
  • 怎么做:
    • 完善 /{DUT}_bug_analysis.md 的缺陷条目(基于源码分析)。
    • 汇总并撰写 /{DUT}_test_summary.md,回看规划是否达成;必要时用 GotoStage 回补。
  • 产出:<OUT>/{DUT}_test_summary.md 与最终结论。
  • 通过标准:UnityChipCheckerTestCase 复核通过。
  • 检查器:
    • UnityChipCheckerTestCase
      • 作用:复核整体测试结果与文档一致性,形成最终结论。
      • 参数:doc_func_check: “{OUT}/{DUT}_functions_and_checks.md”;doc_bug_analysis: “{OUT}/{DUT}_bug_analysis.md”;test_dir: “{OUT}/tests”。

提示与最佳实践

  • 随时用工具:Detail/Status 查看 Mission 进度与当前阶段;CurrentTips 获取步骤级指导;Check/Complete 推进阶段。
  • TUI 左侧 Mission 会显示阶段序号、跳过状态与失败计数;可结合命令行 --skip/--unskip/--force-stage-index 控制推进。

定制工作流(增删阶段/子阶段)

原理说明

  • 工作流定义在语言配置 vagent/lang/zh/config/default.yaml 的顶层 stage: 列表。
  • 配置加载顺序:setting.yaml → ~/.ucagent/setting.yaml → 语言默认(含 stage)→ 项目根 config.yaml → CLI --override
  • 注意:列表类型(如 stage 列表)在合并时是“整体覆盖”,不是元素级合并;因此要“增删改”阶段,建议把默认的 stage 列表复制到你的项目 config.yaml,在此基础上编辑。
  • 临时不执行某阶段:优先使用 CLI --skip <index> 或在运行中用工具 Skip/Goto;持久跳过可在你的 config.yaml 中把该阶段条目的 skip: true 写上(同样需要提供完整的 stage 列表)。

增加阶段

  • 需求:在“全面验证执行”之后新增一个“静态检查与 Lint 报告”阶段,要求生成 <OUT>/{DUT}_lint_report.md 并做格式检查。
  • 做法:在项目根 config.yaml 中提供完整的 stage: 列表,并在合适位置插入如下条目(片段示例,仅展示新增项,实际需要放入你的完整 stage 列表里)。
stage:
  # ...前面的既有阶段...
  - name: static_lint_and_style_check
    desc: "静态分析与代码风格检查报告"
    task:
      - "目标:完成 DUT 的静态检查/Lint,并输出报告"
      - "第1步:运行 lint 工具(按项目需要)"
      - "第2步:将结论整理为 <OUT>/{DUT}_lint_report.md(中文)"
      - "第3步:用 Check 校验报告是否存在且格式规范"
    checker:
      - name: markdown_file_check
        clss: "UnityChipCheckerMarkdownFileFormat"
        args:
          markdown_file_list: "{OUT}/{DUT}_lint_report.md" # MD 文件路径或列表
          no_line_break: true # 禁止字面量 "\n" 作为换行
    reference_files: []
    output_files:
      - "{OUT}/{DUT}_lint_report.md"
    skip: false
  # ...后续既有阶段...

减少子阶段

  • 场景:在“功能规格分析与测试点定义”中,临时不执行“功能点定义(FC)”子阶段。
  • 推荐做法:运行时使用 CLI --skip 跳过该索引;若需长期配置,复制默认 stage: 列表到你的 config.yaml,在父阶段 functional_specification_analysisstage: 子列表里移除对应子阶段条目,或为该子阶段加 skip: true

子阶段移除(片段示例,仅展示父阶段结构与其子阶段列表):

stage:
  - name: functional_specification_analysis
    desc: "功能规格分析与测试点定义"
    task:
      - "目标:将芯片功能拆解成可测试的小块,为后续测试做准备"
      # ...省略父阶段任务...
    stage:
      - name: functional_grouping # 保留 FG 子阶段
        # ...原有配置...
      # - name: function_point_definition  # 原来的 FC 子阶段(此行及其内容整体删除,或在其中加 skip: true)
      - name: check_point_design # 保留 CK 子阶段
        # ...原有配置...
    # ...其他字段...

小贴士

  • 仅需临时跳过:用 --skip/--unskip 最快,无需改配置文件。
  • 需要永久增删:复制默认 stage: 列表到项目 config.yaml,编辑后提交到仓库;注意列表是整体覆盖,别只贴新增/删减的片段。
  • 新增阶段的检查器可复用现有类(如 Markdown/Fixture/API/Coverage/TestCase 等),也可以扩展自定义检查器(放在 vagent/checkers/ 并以可导入路径填写到 clss)。

定制校验器(checker)

原理说明

  • 每个(子)阶段下的 checker: 是一个列表;执行 Check 时会依次运行该列表里的所有检查器。
  • 配置字段:
    • name: 该检查器在阶段内的标识(便于阅读/日志)
    • clss: 检查器类名;短名默认从 vagent.checkers 命名空间导入,也可写完整模块路径(如 mypkg.mychk.MyChecker
    • args: 传给检查器构造函数的参数,支持模板变量(如 {OUT}{DUT}
    • extra_args: 可选,部分检查器支持自定义提示/策略(如 fail_msgbatch_sizepre_report_file 等)
  • 解析与实例化:vagent/stage/vstage.py 会读取 checker:,按 clss/args 生成实例;运行期由 ToolStdCheck/Check 调用其 do_check()
  • 合并语义:配置合并时列表是“整体替换”,要在项目 config.yaml 修改某个阶段的 checker:,建议复制该阶段条目并完整替换其 checker: 列表。

增加 checker

在“功能规格分析与测试点定义”父阶段,新增“文档格式检查”,确保 {OUT}/{DUT}_functions_and_checks.md 没有把换行写成字面量 \n

# 片段示例:需要放入你的完整 stage 列表对应阶段中
- name: functional_specification_analysis
  desc: "功能规格分析与测试点定义"
  # ...existing fields...
  output_files:
    - "{OUT}/{DUT}_functions_and_checks.md"
  checker:
    - name: functions_and_checks_doc_format
      clss: "UnityChipCheckerMarkdownFileFormat"
      args:
        markdown_file_list: "{OUT}/{DUT}_functions_and_checks.md" # 功能/检查点文档
        no_line_break: true # 禁止字面量 "\n"
  stage:
    # ...子阶段 FG/FC/CK 原有配置...

(可扩展)自定义检查器(最小实现,放在 vagent/checkers/unity_test.py

很多场景下“增加的 checker”并非复用已有检查器,而是需要自己实现一个新的检查器。最小实现步骤:

  1. 新建类并继承基类 vagent.checkers.base.Checker
  2. __init__ 里声明你需要的参数(与 YAML args 对应)
  3. 实现 do_check(self, timeout=0, **kw) -> tuple[bool, object],返回 (是否通过, 结构化消息)
  4. 如需读/写工作区文件,使用 self.get_path(rel) 获取绝对路径;如需跨阶段共享数据,使用 self.smanager_set_value/get_value
  5. 若想用短名 clss 引用,请在 vagent/checkers/__init__.py 导出该类(或在 clss 写完整模块路径)

最小代码骨架(示例):

# 文件:vagent/checkers/unity_test.py
from typing import Tuple
import os
from vagent.checkers.base import Checker

class UnityChipCheckerMyCustomCheck(Checker):
    def __init__(self, target_file: str, threshold: int = 1, **kw):
        self.target_file = target_file
        self.threshold = threshold

    def do_check(self, timeout=0, **kw) -> Tuple[bool, object]:
        """检查 target_file 是否存在并做简单规则校验。"""
        real = self.get_path(self.target_file)
        if not os.path.exists(real):
            return False, {"error": f"file '{self.target_file}' not found"}
        # TODO: 这里写你的具体校验逻辑,例如统计/解析/比对等
        return True, {"message": "MyCustomCheck passed"}

在阶段 YAML 中引用(与“增加一个 checker”一致):

checker:
  - name: my_custom_check
    clss: "UnityChipCheckerMyCustomCheck" # 若未在 __init__.py 导出,写完整路径 mypkg.mychk.UnityChipCheckerMyCustomCheck
    args:
      target_file: "{OUT}/{DUT}_something.py"
      threshold: 2
    extra_args:
      fail_msg: "未满足自定义阈值,请完善实现或调低阈值。" # 可选:通过 extra_args 自定义默认失败提示

进阶提示(按需):

  • 长时任务/外部进程:在运行子进程时调用 self.set_check_process(p, timeout),即可用工具 KillCheck/StdCheck 管理与查看进程输出。
  • 模板渲染:实现 get_template_data() 可将进度/统计渲染到阶段标题与任务文案中。
  • 初始化钩子:实现 on_init() 以在阶段开始时加载缓存/准备批任务(与 Batch 系列 checker 一致)。

删除 checker

如临时不要“第 2 阶段 基本信息文档格式检查”,将该阶段的 checker: 置空或移除该项:

- name: dut_function_understanding
  desc: "{DUT}功能理解"
  # ...existing fields...
  checker: [] # 删除原本的 markdown_file_check

修改 checker

把“行覆盖率检查”的阈值从 0.9 调整到 0.8,并自定义失败提示:

- name: line_coverage_analysis_and_improvement
  desc: "代码行覆盖率分析与提升{COVERAGE_COMPLETE}"
  # ...existing fields...
  checker:
    - name: line_coverage_check
      clss: "UnityChipCheckerTestCaseWithLineCoverage"
      args:
        doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
        doc_bug_analysis: "{OUT}/{DUT}_bug_analysis.md"
        test_dir: "{OUT}/tests"
        min_line_coverage: 0.8 # 调低阈值
      extra_args:
        fail_msg: "未达到 80% 的行覆盖率,请补充针对未覆盖行的测试。"

可选:自定义检查器类

  • vagent/checkers/ 新增类,继承 vagent.checkers.base.Checker 并实现 do_check()
  • vagent/checkers/__init__.py 导出类后,可在 clss 用短名;或直接写完整模块路径;
  • args 中的字符串支持模板变量渲染;extra_args 可用于自定义提示文案(具体视检查器实现而定)。

常用 checker 参数(结构化)

以下参数均来自实际代码实现(vagent/checkers/unity_test.py),名称、默认值与类型与代码保持一致;示例片段可直接放入阶段 YAML 的 checker[].args

UnityChipCheckerMarkdownFileFormat

  • 参数:
    • markdown_file_list (str | List[str]): 要检查的 Markdown 文件路径或路径列表。
    • no_line_break (bool, 默认 false): 是否禁止把换行写成字面量 “\n”;true 表示禁止。
  • 示例:
    args:
      markdown_file_list: "{OUT}/{DUT}_basic_info.md"
      no_line_break: true
    

UnityChipCheckerLabelStructure

  • 参数:
    • doc_file (str)
    • leaf_node (“FG”|“FC”|“CK”)
    • min_count (int, 默认 1)
    • must_have_prefix (str, 默认 “FG-API”)
    • data_key (str, 可选)
  • 示例:
    args:
      doc_file: "{OUT}/{DUT}_functions_and_checks.md"
      leaf_node: "CK"
      data_key: "COVER_GROUP_DOC_CK_LIST"
    

UnityChipCheckerDutCreation

  • 参数:
    • target_file (str)
  • 示例:
    args:
      target_file: "{OUT}/tests/{DUT}_api.py"
    

UnityChipCheckerDutFixture

  • 参数:
    • target_file (str)
  • 示例:
    args:
      target_file: "{OUT}/tests/{DUT}_api.py"
    

UnityChipCheckerEnvFixture

  • 参数:
    • target_file (str)
    • min_env (int, 默认 1)
    • force_bundle (bool, 当前未使用)
  • 示例:
    args:
      target_file: "{OUT}/tests/{DUT}_api.py"
      min_env: 1
    

UnityChipCheckerDutApi

  • 参数:
    • api_prefix (str)
    • target_file (str)
    • min_apis (int, 默认 1)
  • 示例:
    args:
      api_prefix: "api_{DUT}_"
      target_file: "{OUT}/tests/{DUT}_api.py"
      min_apis: 1
    

UnityChipCheckerCoverageGroup

  • 参数:
    • test_dir (str)
    • cov_file (str)
    • doc_file (str)
    • check_types (str|List[str])
  • 示例:
    args:
      test_dir: "{OUT}/tests"
      cov_file: "{OUT}/tests/{DUT}_function_coverage_def.py"
      doc_file: "{OUT}/{DUT}_functions_and_checks.md"
      check_types: ["FG", "FC", "CK"]
    

UnityChipCheckerCoverageGroupBatchImplementation

  • 参数:
    • test_dir (str)
    • cov_file (str)
    • doc_file (str)
    • batch_size (int)
    • data_key (str)
  • 示例:
    args:
      test_dir: "{OUT}/tests"
      cov_file: "{OUT}/tests/{DUT}_function_coverage_def.py"
      doc_file: "{OUT}/{DUT}_functions_and_checks.md"
      batch_size: 20
      data_key: "COVER_GROUP_DOC_CK_LIST"
    

UnityChipCheckerTestTemplate

  • 基类参数:doc_func_check (str), test_dir (str), min_tests (int, 默认 1), timeout (int, 默认 15)
  • 扩展参数(extra_args):batch_size (默认 20), ignore_ck_prefix (str), data_key (str)
  • 示例:
    args:
      doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
      test_dir: "{OUT}/tests"
      ignore_ck_prefix: "test_api_{DUT}_"
      data_key: "TEST_TEMPLATE_IMP_REPORT"
      batch_size: 20
    

UnityChipCheckerDutApiTest

  • 参数:
    • api_prefix (str)
    • target_file_api (str)
    • target_file_tests (str)
    • doc_func_check (str)
    • doc_bug_analysis (str)
    • min_tests (int, 默认 1)
    • timeout (int, 默认 15)
  • 示例:
    args:
      api_prefix: "api_{DUT}_"
      target_file_api: "{OUT}/tests/{DUT}_api.py"
      target_file_tests: "{OUT}/tests/test_{DUT}_api*.py"
      doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
      doc_bug_analysis: "{OUT}/{DUT}_bug_analysis.md"
    

UnityChipCheckerBatchTestsImplementation

  • 基类参数:doc_func_check (str), test_dir (str), doc_bug_analysis (str), ignore_ck_prefix (str), timeout (int, 默认 15)
  • 进度参数:data_key (str, 必填)
  • 扩展参数(extra_args):batch_size (默认 5), pre_report_file (str)
  • 示例:
    args:
      doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
      doc_bug_analysis: "{OUT}/{DUT}_bug_analysis.md"
      test_dir: "{OUT}/tests"
      ignore_ck_prefix: "test_api_{DUT}_"
      batch_size: 10
      data_key: "TEST_TEMPLATE_IMP_REPORT"
      pre_report_file: "{OUT}/{DUT}/.TEST_TEMPLATE_IMP_REPORT.json"
    

UnityChipCheckerTestCase

  • 参数:
    • doc_func_check (str)
    • doc_bug_analysis (str)
    • test_dir (str)
    • min_tests (int, 默认 1)
    • timeout (int, 默认 15)
  • 示例:
    args:
      doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
      doc_bug_analysis: "{OUT}/{DUT}_bug_analysis.md"
      test_dir: "{OUT}/tests"
    

UnityChipCheckerTestCaseWithLineCoverage

  • 基础参数同 UnityChipCheckerTestCase
  • 额外必需:cfg (dict|Config)
  • 额外可选(extra_args):
    • min_line_coverage (float, 默认按配置,未配置则 0.8)
    • coverage_json (str, 默认 uc_test_report/line_dat/code_coverage.json)
    • coverage_analysis (str, 默认 unity_test/{DUT}_line_coverage_analysis.md)
    • coverage_ignore (str, 默认 unity_test/tests/{DUT}.ignore)
  • 示例:
    args:
      doc_func_check: "{OUT}/{DUT}_functions_and_checks.md"
      doc_bug_analysis: "{OUT}/{DUT}_bug_analysis.md"
      test_dir: "{OUT}/tests"
      cfg: "<CONFIG_OBJECT_OR_DICT>"
      min_line_coverage: 0.9
    

提示:上面的 示例 仅展示 args 片段;实际需置于阶段条目下的 checker[].args

6.4 - 定制功能

如何自行定义参数、流程和工具。

添加工具与 MCP Server 工具

面向可修改本仓库代码的高级用户,以下说明如何:

  • 添加一个新工具(供本地/Agent 内调用)
  • 将工具暴露为 MCP Server 工具(供外部 IDE/客户端调用)
  • 控制选择哪些工具被暴露与如何调用

涉及关键位置:

  • vagent/tools/uctool.py:工具基类 UCTool、to_fastmcp(LangChain Tool → FastMCP Tool)
  • vagent/util/functions.pyimport_and_instance_tools(按名称导入实例)、create_verify_mcps(启动 FastMCP)
  • vagent/verify_agent.py:装配工具清单,start_mcps 组合并启动 Server
  • vagent/cli.py / vagent/verify_pdb.py:命令行与 TUI 内的 MCP 启动命令

1) 工具体系与装配

  • 工具基类 UCTool:
    • 继承 LangChain BaseTool,内置:call_count 计数、call_time_out 超时、流式/阻塞提示、MCP Context 注入(ctx.info)、防重入等。
    • 推荐自定义工具继承 UCTool,获得更好的 MCP 行为与调试体验。
  • 运行期装配(VerifyAgent 初始化):
    • 基础工具:RoleInfo、ReadTextFile
    • 嵌入工具:参考检索与记忆(除非 --no-embed-tools
    • 文件工具:读/写/查找/路径等(可在 MCP 无文件工具模式下剔除)
    • 阶段工具:由 StageManager 按工作流动态提供
    • 外部工具:来自配置项 ex_tools 与 CLI --ex-tools(通过 import_and_instance_tools 零参实例化)
  • 名称解析:
    • 短名:类/工厂函数需在 vagent/tools/__init__.py 导出(例如 from .mytool import HelloTool),即可在 ex_toolsHelloTool
    • 全路径:mypkg.mytools.HelloTool / mypkg.mytools.Factory

2) 添加一个新工具(本地/Agent 内)

规范要求:

  • 唯一 name、清晰 description
  • 使用 pydantic BaseModel 定义 args_schema(MCP 转换依赖)
  • 实现 _run(同步)或 _arun(异步);继承 UCTool 可直接获得超时、流式与 ctx 注入

示例 1:同步工具(计数问候)

from pydantic import BaseModel, Field
from vagent.tools.uctool import UCTool

class HelloArgs(BaseModel):
		who: str = Field(..., description="要问候的人")

class HelloTool(UCTool):
		name: str = "Hello"
		description: str = "向指定对象问候,并统计调用次数"
		args_schema = HelloArgs

		def _run(self, who: str, run_manager=None) -> str:
				return f"Hello, {who}! (called {self.call_count+1} times)"

注册与使用:

  • 临时:--ex-tools mypkg.mytools.HelloTool
  • 持久:项目 config.yaml
ex_tools:
	- mypkg.mytools.HelloTool

(可选)短名注册:在 vagent/tools/__init__.py 导出 HelloTool 后,可写 --ex-tools HelloTool

示例 2:异步流式工具(ctx.info + 超时)

from pydantic import BaseModel, Field
from vagent.tools.uctool import UCTool
import asyncio

class ProgressArgs(BaseModel):
		steps: int = Field(5, ge=1, le=20, description="进度步数")

class ProgressTool(UCTool):
		name: str = "Progress"
		description: str = "演示流式输出与超时处理"
		args_schema = ProgressArgs

		async def _arun(self, steps: int, run_manager=None):
				for i in range(steps):
						self.put_alive_data(f"step {i+1}/{steps}")  # 供阻塞提示/日志缓冲
						await asyncio.sleep(0.5)
				return "done"

说明:UCTool.ainvoke 会在 MCP 模式下注入 ctx,并启动阻塞提示线程;当 sync_block_log_to_client=True 时会周期性 ctx.info 推送日志,超时后返回错误与缓冲日志。

3) 暴露为 MCP Server 工具

工具 → MCP 转换(vagent/tools/uctool.py::to_fastmcp):

  • 必须:args_schema 继承 BaseModel;不支持“注入参数”签名。
  • UCTool 子类会得到 context_kwarg=“ctx” 的 FastMCP 工具,具备流式交互能力。

Server 端启动:

  • VerifyAgent.start_mcps 组合工具:tool_list_base + tool_list_task + tool_list_ext + [tool_list_file]
  • vagent/util/functions.py::create_verify_mcps 将工具序列转换为 FastMCP 工具并启动 uvicorn(mcp.streamable_http_app())。

如何选择暴露范围:

  • CLI:
    • 启动(含文件工具):--mcp-server
    • 启动(无文件工具):--mcp-server-no-file-tools
    • 地址:--mcp-server-host,端口:--mcp-server-port
  • TUI 命令:start_mcp_server [host] [port] / start_mcp_server_no_file_ops [host] [port]

4) 客户端调用流程

FastMCP Python 客户端(参考 tests/test_mcps.py):

from fastmcp import Client

client = Client("http://127.0.0.1:5000/mcp", timeout=10)
print(client.list_tools())
print(client.call_tool("Hello", {"who": "UCAgent"}))

IDE/Agent(Claude Code、Copilot、Qwen Code 等):将 httpUrl 指向 http://<host>:<port>/mcp,即可发现并调用工具。

5) 生命周期、并发与超时

  • 计数:UCTool 内置 call_count;非 UCTool 工具由 import_and_instance_tools 包装计数。
  • 并发保护:is_in_streaming/is_alive_loop 防止重入;同一实例不允许并发执行。
  • 超时:call_time_out(默认 20s)+ 客户端 timeout;阻塞时可用 put_alive_data + sync_block_log_to_client=True 推送心跳。

6) 配置策略与最佳实践

  • ex_tools 列表为“整体覆盖”,项目 config.yaml 需写出完整清单。
  • 短名 vs 全路径:短名更便捷,全路径适用于私有包不修改本仓库时。
  • 无参构造/工厂:装配器直接调用 (...)(),复杂配置建议在工厂内部处理(读取环境/配置文件)。
  • 文件写权限:MCP 无文件工具模式下不要暴露写类工具;如需写入,请在本地 Agent 内使用或显式允许写目录。

通过环境变量注入外部工具(EX_TOOLS)

配置文件支持 Bash 风格环境变量占位:$(VAR: default)。你可以让 ex_tools 从环境变量注入工具类列表(支持模块全名或 vagent.tools 下的短名)。

  1. 在项目的 config.yaml 或用户级 ~/.ucagent/setting.yaml 中写入:
ex_tools: $(EX_TOOLS: [])
  1. 用环境变量提供列表(必须是可被 YAML 解析的数组字面量):
export EX_TOOLS='["SqThink","HumanHelp"]'
# 或使用完整类路径:
# export EX_TOOLS='["vagent.tools.extool.SqThink","vagent.tools.human.HumanHelp"]'
  1. 启动后本地对话与 MCP Server 中都会出现这些工具。短名需要在 vagent/tools/__init__.py 导出;否则请使用完整模块路径。

  2. 与 CLI 的 --ex-tools 选项是合并关系(两边都会被装配)。

7) 常见问题排查

  • 工具未出现在 MCP 列表:未被装配(ex_tools 未配置/未导出)、args_schema 非 BaseModel、Server 未按预期启动。
  • 调用报“注入参数不支持”:工具定义包含 LangChain 的 injected args;请改成显式 args_schema 参数。
  • 超时:调大 call_time_out 或客户端 timeout;在长任务中输出进度维持心跳。
  • 短名无效:未在 vagent/tools/__init__.py 导出;改用全路径或补导出。

完成以上步骤后:你的工具既能在本地对话中被 ReAct 自动调用,也能通过 MCP Server 暴露给外部 IDE/客户端统一调用。

6.5 - 工具列表

UCAgent 可用工具清单(按类别归纳)。

以下为当前仓库内内置工具(UCTool 家族)的概览,按功能类别归纳:名称(调用名)、用途与参数说明(字段: 类型 — 含义)。

提示:

  • 带有“文件写”能力的工具仅在本地/允许写模式下可用;MCP 无文件工具模式不会暴露写类工具。
  • 各工具均基于 args_schema 校验参数,MCP 客户端将根据 schema 生成参数表单。

基础/信息类

  • RoleInfo(RoleInfo)

    • 用途:返回当前代理的角色信息(可在启动时自定义 role_info)。
    • 参数:无
  • HumanHelp(HumanHelp)

    • 用途:向人类请求帮助(仅在确实卡住时使用)。
    • 参数:
      • message: str — 求助信息

规划/ToDo 类

  • CreateToDo
    • 用途:创建 ToDo(覆盖旧 ToDo)。
    • 参数:
      • task_description: str — 任务描述
      • steps: List[str] — 步骤(1–20 步)
  • CompleteToDoSteps
    • 用途:将指定步骤标记为完成,可附加备注。
    • 参数:
      • completed_steps: List[int] — 完成的步骤序号(1-based)
      • notes: str — 备注
  • UndoToDoSteps
    • 用途:撤销步骤完成状态,可附加备注。
    • 参数:
      • steps: List[int] — 撤销的步骤序号(1-based)
      • notes: str — 备注
  • ResetToDo
    • 用途:重置/清空当前 ToDo。
    • 参数:无
  • GetToDoSummary / ToDoState
    • 用途:获取 ToDo 摘要 / 看板状态短语。
    • 参数:无

记忆/检索类

  • SemanticSearchInGuidDoc(SemanticSearchInGuidDoc)

    • 用途:在 Guide_Doc/项目文档中做语义检索,返回最相关片段。
    • 参数:
      • query: str — 查询语句
      • limit: int — 返回条数(1–100,默认 3)
  • MemoryPut

    • 用途:按 scope 写入长时记忆。
    • 参数:
      • scope: str — 命名空间/范围(如 general/task-specific)
      • data: str — 内容(可为 JSON 文本)
  • MemoryGet

    • 用途:按 scope 检索记忆。
    • 参数:
      • scope: str — 命名空间/范围
      • query: str — 查询语句
      • limit: int — 返回条数(1–100,默认 3)

测试/执行类

  • RunPyTest(RunPyTest)

    • 用途:在指定目录/文件下运行 pytest,支持返回 stdout/stderr。
    • 参数:
      • test_dir_or_file: str — 测试目录或文件
      • pytest_ex_args: str — 额外 pytest 参数(如 “-v --capture=no”)
      • return_stdout: bool — 是否返回标准输出
      • return_stderr: bool — 是否返回标准错误
      • timeout: int — 超时秒数(默认 15)
  • RunUnityChipTest(RunUnityChipTest)

    • 用途:面向 UnityChip 项目封装的测试执行,产生 toffee_report.json 等结果。
    • 参数:同 RunPyTest;另含内部字段(workspace/result_dir/result_json_path)。

文件/路径/文本类

  • SearchText(SearchText)

    • 用途:在工作区内按文本搜索,支持通配与正则。
    • 参数:
      • pattern: str — 搜索模式(明文/通配/正则)
      • directory: str — 相对目录(为空则全仓;填文件则仅搜该文件)
      • max_match_lines: int — 每个文件返回的最大匹配行数(默认 20)
      • max_match_files: int — 返回的最大文件数(默认 10)
      • use_regex: bool — 是否使用正则
      • case_sensitive: bool — 区分大小写
      • include_line_numbers: bool — 返回是否带行号
  • FindFiles(FindFiles)

    • 用途:按通配符查找文件。
    • 参数:
      • pattern: str — 文件名模式(fnmatch 通配)
      • directory: str — 相对目录(为空则全仓)
      • max_match_files: int — 返回最大文件数(默认 10)
  • PathList(PathList)

    • 用途:列出目录结构(可限制深度)。
    • 参数:
      • path: str — 目录(相对 workspace)
      • depth: int — 深度(-1 全部,0 当前)
  • ReadBinFile(ReadBinFile)

    • 用途:读取二进制文件(返回 [BIN_DATA])。
    • 参数:
      • path: str — 文件路径(相对 workspace)
      • start: int — 起始字节(默认 0)
      • end: int — 结束字节(默认 -1 表示 EOF)
  • ReadTextFile(ReadTextFile)

    • 用途:读取文本文件(带行号,返回 [TXT_DATA])。
    • 参数:
      • path: str — 文件路径(相对 workspace)
      • start: int — 起始行(1-based,默认 1)
      • count: int — 行数(-1 到文件末尾)
  • EditTextFile(EditTextFile)

    • 用途:编辑/创建文本文件,模式:replace/overwrite/append。
    • 参数:
      • path: str — 文件路径(相对 workspace,不存在则创建)
      • data: str — 写入的文本(None 表示清空)
      • mode: str — 编辑模式(replace/overwrite/append,默认 replace)
      • start: int — replace 模式的起始行(1-based)
      • count: int — replace 模式替换行数(-1 到末尾,0 插入)
      • preserve_indent: bool — replace 时是否保留缩进
  • CopyFile(CopyFile)

    • 用途:复制文件;可选覆盖。
    • 参数:
      • source_path: str — 源文件
      • dest_path: str — 目标文件
      • overwrite: bool — 目标存在时是否覆盖
  • MoveFile(MoveFile)

    • 用途:移动/重命名文件;可选覆盖。
    • 参数:
      • source_path: str — 源文件
      • dest_path: str — 目标文件
      • overwrite: bool — 目标存在时是否覆盖
  • DeleteFile(DeleteFile)

    • 用途:删除文件。
    • 参数:
      • path: str — 文件路径
  • CreateDirectory(CreateDirectory)

    • 用途:创建目录(递归)。
    • 参数:
      • path: str — 目录路径
      • parents: bool — 递归创建父目录
      • exist_ok: bool — 已存在是否忽略
  • ReplaceStringInFile(ReplaceStringInFile)

    • 用途:精确字符串替换(强约束匹配;可新建文件)。
    • 参数:
      • path: str — 目标文件
      • old_string: str — 需要被替换的完整原文(含上下文,精确匹配)
      • new_string: str — 新内容
  • GetFileInfo(GetFileInfo)

    • 用途:获取文件信息(大小、修改时间、人类可读尺寸等)。
    • 参数:
      • path: str — 文件路径

扩展示例

  • SimpleReflectionTool(SimpleReflectionTool)
    • 用途:示例型“自我反思”工具(来自 extool.py),可作为扩展参考。
    • 参数:
      • message: str — 自我反思文本

备注:

  • 工具调用超时默认 20s(具体工具可重写);长任务请周期性输出进度避免超时。
  • MCP 无文件工具模式下默认不暴露写类工具;如需写入,建议在本地 Agent 模式或按需限制可写目录。

7 - 多语言支持

开放验证平台支持多种语言

7.1 - 验证接口

DUT文件与编程语言都支持的验证接口

生成库文件

picker可以通过参数--lang指定转换的对应语言(参数已支持cpp、python、java、lua、scala、golang),由于不同编程语言对应的“库”不同,因此生成的库文件有所区别,例如java生成的是jar包,python生成的为文件夹。picker导出对应编程语言的库,需要xcomm的支持,可以通过picker --check查看支持情况:

$picker --check
[OK ] Version: 0.9.0-feat_performance_improve-b7001a6-2025-04-11-dirty
[OK ] Exec path: /usr/local/share/lib/python3.11/site-packages/picker/bin/picker
[OK ] Template path: /usr/local/share/lib/python3.11/site-packages/picker/share/picker/template
[OK ] Support    Cpp (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/lib' success)
[OK ] Support Golang (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/golang' success)
[OK ] Support   Java (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/java/xspcomm-java.jar' success)
[OK ] Support    Lua (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/lua/luaxspcomm.so' success)
[OK ] Support Python (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/python' success)
[OK ] Support  Scala (find: '/usr/local/share/lib/python3.11/site-packages/picker/share/picker/scala/xspcomm-scala.jar' success)

输出显示success表示支持,fail表示不支持。

C++

对于C++语言,picker生成的为so动态链接库,和对应的头文件。例如:

UT_Adder/
├── UT_Adder.cpp       # DUT 文件
├── UT_Adder.hpp       # DUT 头文件
├── UT_Adder_dpi.hpp   # DPI 头文件
├── dut_base.hpp       # DUT base 头文件
├── libDPIAdder.a      # DPI 静态库
└── libUTAdder.so      # DUT 动态库

在使用时,设置好LD路径,然后再测试代码中#include UT_Adder.hpp

Python

Python语言生成的为目录(Python module以目录的方式表示)

UT_Adder/
├── _UT_Adder.so
├── __init__.py
├── libUTAdder.so
└── libUT_Adder.py

设置PYTHONPATH后,可以在test中import UT_Adder

Java/scala

对于Java和scala基于JVM的编程语言,picker生成的为对应的jar包。

UT_Adder/
├── UT_Adder-scala.jar
└── UT_Adder-java.jar

go

go语言生成的为目录(类似python)。

UT_Adder/
└── golang
    └── src
        └── UT_Adder
            ├── UT_Adder.go
            ├── UT_Adder.so
            ├── UT_Adder_Wrapper.go
            ├── go.mod
            └── libUTAdder.so

设置GOPATH后,可直接进行import

验证接口

DUT验证接口可以参考连接:https://github.com/XS-MLVP/picker/blob/master/doc/API.zh.md

xspcomm库接口请参考连接:https://github.com/XS-MLVP/xcomm/blob/master/docs/APIs.cn.md

7.2 - 验证案例

多语言案例介绍

本页将展示使用多种语言验证的各种案例。

7.2.1 - 加法器

使用C++、Java、Python和Golang验证加法器的案例

以Adder为例,各语言的验证代码和注释如下:

#include "UT_Adder.hpp"

int64_t random_int64()
{
    static std::random_device rd;
    static std::mt19937_64 generator(rd());
    static std::uniform_int_distribution distribution(INT64_MIN,
                                                               INT64_MAX);
    return distribution(generator);
}

int main()
{
    UTAdder *dut = new UTAdder();
    dut->Step(1);
    printf("Initialized UTAdder\n");

    struct input_t {
        uint64_t a;
        uint64_t b;
        uint64_t cin;
    };

    struct output_t {
        uint64_t sum;
        uint64_t cout;
    };

    for (int c = 0; c < 114514; c++) {
        input_t i;
        output_t o_dut, o_ref;

        i.a   = random_int64();
        i.b   = random_int64();
        i.cin = random_int64() & 1;

        auto dut_cal = [&]() {
            dut->a   = i.a;
            dut->b   = i.b;
            dut->cin = i.cin;
            dut->Step(1);
            o_dut.sum  = (uint64_t)dut->sum;
            o_dut.cout = (uint64_t)dut->cout;
        };

        auto ref_cal = [&]() {
            uint64_t sum = i.a + i.b;
            bool carry   = sum < i.a;

            sum += i.cin;
            carry = carry || sum < i.cin;

            o_ref.sum  = sum;
            o_ref.cout = carry ;
        };

        dut_cal();
        ref_cal();
        printf("[cycle %lu] a=0x%lx, b=0x%lx, cin=0x%lx\n", dut->xclock.clk, i.a,
               i.b, i.cin);
        printf("DUT: sum=0x%lx, cout=0x%lx\n", o_dut.sum, o_dut.cout);
        printf("REF: sum=0x%lx, cout=0x%lx\n", o_ref.sum, o_ref.cout);
        Assert(o_dut.sum == o_ref.sum, "sum mismatch");
    }

    dut->Finish();
    printf("Test Passed, destory UTAdder\n");
    return 0;
}

7.2.2 - CoupledL2

用C++、Java和Python简单验证香山L2 Cache的案例

CoupledL2是一个非阻塞的L2 Cache

下面的代码会对CoupledL2进行简单的验证,并使用数组作为参考模型,验证过程如下:

  1. 生成随机的地址addr、执行AcquireBlock,请求读取addr的数据。
  2. 执行GrantData,接收DUT响应的数据。
  3. 把接收到的数据和参考模型的内容进行比较,验证行为是否一致。
  4. 执行GrantAck,响应DUT
  5. 执行ReleaseData,向DUT请求在addr写入随机数据data
  6. 同步参考模型,把addr的数据更新为data
  7. 执行ReleaseAck,接收DUT的写入响应。

上述步骤会重复4000次。

验证代码:

#include "UT_CoupledL2.hpp"
using TLDataArray = std::array;

enum class OpcodeA : uint32_t {
  PutFullData = 0x0,
  PutPartialData = 0x1,
  ArithmeticData = 0x2,
  LogicalData = 0x3,
  Get = 0x4,
  Hint = 0x5,
  AcquireBlock = 0x6,
  AcquirePerm = 0x7,
};

enum class OpcodeB : uint32_t { ProbeBlock = 0x6, ProbePerm = 0x7 };

enum class OpcodeC : uint32_t { ProbeAck = 0x4, ProbeAckData = 0x5, Release = 0x6, ReleaseData = 0x7 };

enum class OpcodeD : uint32_t { AccessAck, AccessAckData, HintAck, Grant = 0x4, GrantData = 0x5, ReleaseAck = 0x6 };

enum class OpcodeE : uint32_t { GrantAck = 0x4 };

constexpr std::initializer_list ARGS = {"+verilator+rand+reset+0"};
auto dut = UTCoupledL2(ARGS);
auto &clk = dut.xclock;

void sendA(OpcodeA opcode, uint32_t size, uint32_t address) {
  const auto &valid = dut.master_port_0_0_a_valid;
  const auto &ready = dut.master_port_0_0_a_ready;
  while (ready.value == 0x0) clk.Step();
  valid.value = 1;
  dut.master_port_0_0_a_bits_opcode.value = opcode;
  dut.master_port_0_0_a_bits_size.value = size;
  dut.master_port_0_0_a_bits_address.value = address;
  clk.Step();
  valid.value = 0;
}

void getB() {
  assert(false);
  const auto &valid = dut.master_port_0_0_b_valid;
  const auto &ready = dut.master_port_0_0_b_ready;
  ready.value = 1;
  while (valid.value == 0)
    clk.Step();
  dut.master_port_0_0_b_bits_opcode = 0x0;
  dut.master_port_0_0_b_bits_param = 0x0;
  dut.master_port_0_0_b_bits_size = 0x0;
  dut.master_port_0_0_b_bits_source = 0x0;
  dut.master_port_0_0_b_bits_address = 0x0;
  dut.master_port_0_0_b_bits_mask = 0x0;
  dut.master_port_0_0_b_bits_data = 0x0;
  dut.master_port_0_0_b_bits_corrupt = 0x0;
  clk.Step();
  ready.value = 0;
}

void sendC(OpcodeC opcode, uint32_t size, uint32_t address, uint64_t data) {
  const auto &valid = dut.master_port_0_0_c_valid;
  const auto &ready = dut.master_port_0_0_c_ready;

  while (ready.value == 0) clk.Step();
  valid.value = 1;
  dut.master_port_0_0_c_bits_opcode.value = opcode;
  dut.master_port_0_0_c_bits_size.value = size;
  dut.master_port_0_0_c_bits_address.value = address;
  dut.master_port_0_0_c_bits_data.value = data;
  clk.Step();
  valid.value = 0;
}

void getD() {
  const auto &valid = dut.master_port_0_0_d_valid;
  const auto &ready = dut.master_port_0_0_d_ready;
  ready.value = 1;
  clk.Step();
  while (valid.value == 0) clk.Step();
  ready.value = 0;
}

void sendE(uint32_t sink) {
  const auto &valid = dut.master_port_0_0_e_valid;
  const auto &ready = dut.master_port_0_0_e_ready;
  while (ready.value == 0) clk.Step();
  valid.value = 1;
  dut.master_port_0_0_e_bits_sink.value = sink;
  clk.Step();
  valid.value = 0;
}

void AcquireBlock(uint32_t address) { sendA(OpcodeA::AcquireBlock, 0x6, address); }

void GrantData(TLDataArray &r_data) {
  const auto &opcode = dut.master_port_0_0_d_bits_opcode;
  const auto &data = dut.master_port_0_0_d_bits_data;

  for (int i = 0; i < 2; i++) {
    do { getD(); } while (opcode.value != OpcodeD::GrantData);
    r_data[i] = data.value;
  }
}

void GrantAck(uint32_t sink) { sendE(sink); }

void ReleaseData(uint32_t address, const TLDataArray &data) {
  for (int i = 0; i < 2; i++)
    sendC(OpcodeC::ReleaseData, 0x6, address, data[i]);
}

void ReleaseAck() {
  const auto &opcode = dut.master_port_0_0_d_bits_opcode;
  do { getD(); } while (opcode.value != OpcodeD::ReleaseAck);
}

int main() {
  TLDataArray ref_data[16] = {};
  /* Random generator */
  std::random_device rd;
  std::mt19937_64 gen_rand(rd());
  std::uniform_int_distribution distrib(0, 0xf - 1);

  /* DUT init */
  dut.InitClock("clock");
  dut.reset.SetWriteMode(xspcomm::WriteMode::Imme);
  dut.reset.value = 1;
  clk.Step();
  dut.reset.value = 0;
  for (int i = 0; i < 100; i++) clk.Step();

  /* Test loop */
  for (int test_loop = 0; test_loop < 4000; test_loop++) {
    uint32_t d_sink;
    TLDataArray data{}, r_data{};
    /* Generate random */
    const auto address = distrib(gen_rand) << 6;
    for (auto &i : data)
      i = gen_rand();

    printf("[CoupledL2 Test\t%d]: At address(0x%03x), ", test_loop + 1, address);
    /* Read */
    AcquireBlock(address);
    GrantData(r_data);

    // Print read result
    printf("Read: ");
    for (const auto &x : r_data)
      printf("%08lx", x);

    d_sink = dut.master_port_0_0_d_bits_sink.value;
    assert ((r_data == ref_data[address >> 6]) && "Read Failed");
    GrantAck(d_sink);

    /* Write */
    ReleaseData(address, data);
    ref_data[address >> 6] = data;
    ReleaseAck();

    // Print write data
    printf(", Write: ");
    for (const auto &x : data)
      printf("%08lx", x);
    printf(".\n");
  }

  return 0;
}