功能覆盖率

功能覆盖率(Functional Coverage)是一种用户定义的度量标准,用于度量验证中已执行的设计规范的比例。功能覆盖率关注的是设计的功能和特性是否被测试用例覆盖到了。

反标是指将功能点与测试用例对应起来。这样,在统计时,就能看到每个功能点对应了哪些测试用例,从而方便查看哪些功能点用的测试用例多,哪些功能点用的测试用例少,有利于后期的测试用例优化。

本项目中相关涉及位置

功能覆盖率需要我们先定义了才能统计,主要是在构建测试环境的时候涉及。

构建测试环境中:

其他:

  • 在 Test case 中使用,可以在每个测试用例里也编写一个功能点。

功能覆盖率使用流程

指定 Group 名称

测试报告通过 Group 名字和 DUT 名字进行匹配,利用 comm.UT_FCOV 获取 DUT 前缀,例如在 Python 模块ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py中进行如下调用:

from comm import UT_FCOV
# 本模块名为:ut_frontend.ifu.rvc_expander.classical_version.env.rvc_expander_wrapper
# 通过../../../去掉了classical_version和上级模块env,rvc_expander_wrapper
# UT_FCOV会默认去掉前缀 ut_
g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))
# name = UT_FCOV("../../../CLASSIC")

name 的值为frontend.ifu.rvc_expander.CLASSIC,在最后统计结果时,会按照最长前缀匹配到目标 UT(即匹配到:frontend.ifu.rvc_expander 模块)

创建覆盖率组

使用toffeefuncov可以创建覆盖率组。

import toffee.funcov as fc
# 使用上面指定的GROUP名字
g = fc.CovGroup(name)

这两步也可以合成一句g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))。 创建的g对象就表示了一个功能覆盖率组,可以使用其来提供观察点和反标。

添加观察点和反标

在每个测试用例内部,可以使用add_watch_pointadd_cover_point是其别名,二者完全一致)来添加观察点和mark_function来添加反标。 观察点是,当对应的信号触发了我们在观察点内部定义的要求后,这个观察点的名字(也就是功能点)就会被统计到功能覆盖率中。 反标是,将功能点和测试用例进行关联,这样在统计时,就能看到每个功能点对应了哪些测试用例。

对于观察点的位置,需要根据实际情况来定,一般来说,在测试用例外直接添加观察点是没有问题的。 不过有时候我们可以更加的灵活。

  1. 在测试用例之外(decode_wrapper.py中)
def init_rvc_expander_funcov(expander, g: fc.CovGroup):
    """Add watch points to the RVCExpander module to collect function coverage information"""
    # 1. Add point RVC_EXPAND_RET to check expander return value:
    #    - bin ERROR. The instruction is not illegal
    #    - bin SUCCE. The instruction is not expanded
    g.add_watch_point(expander, {
                                "ERROR": lambda x: x.stat()["ilegal"] == False,
                                "SUCCE": lambda x: x.stat()["ilegal"] != False,
                          }, name = "RVC_EXPAND_RET")
    # 5. Reverse mark function coverage to the check point
    def _M(name):
        # get the module name
        return module_name_with(name, "../../test_rv_decode")

    #  - mark RVC_EXPAND_RET
    g.mark_function("RVC_EXPAND_RET",_M(["test_rvc_expand_16bit_full",
                                              "test_rvc_expand_32bit_full",
                                              "test_rvc_expand_32bit_randomN"]), bin_name=["ERROR", "SUCCE"])

    # The End                                                                              
    return None 

这个例子的第一个g.add_watch_point是放在测试用例之外的,因为它和现有的测试用例没有直接关系,放在测试用例之外反而更加方便。添加观察点之后,只要add_watch_point方法中的bins条件触发了,我们的toffee-test框架就能够收集到对应的功能点。

  1. 在测试用例之中(test_rvc_expander.py中)
N=10
T=1<<32
@pytest.mark.toffee_tags([TAG_LONG_TIME_RUN, TAG_RARELY_USED])
@pytest.mark.parametrize("start,end",
                         [(r*(T//N), (r+1)*(T//N) if r < N-1 else T) for r in range(N)])
def test_rvc_expand_32bit_full(rvc_expander, start, end):
    """Test the RVC expand function with a full 32 bit instruction set

    Description:
        Randomly generate N 32-bit instructions for each check, and repeat the process K times.
    """
    # Add check point: RVC_EXPAND_ALL_32B to check instr bits.
    covered = -1
    g.add_watch_point(rvc_expander, {"RANGE[%d-%d]"%(start, end): lambda _: covered == end},
                      name = "RVC_EXPAND_ALL_32B", dynamic_bin=True)
    # Reverse mark function to the check point
    g.mark_function("RVC_EXPAND_ALL_32B", test_rvc_expand_32bit_full)
    # Drive the expander and check the result
    rvc_expand(rvc_expander, list([_ for _ in range(start, end)]))
    # When go to here, the range[start, end] is covered
    covered = end
    g.sample()

这个例子的观察点在测试用例里面,因为这里的startend是由pytest.mark.parametrize来决定的,数值不是固定的,所以我们需要在测试用例里面添加观察点。

采样

在上一个例子的最后,我们调用了g.sample(),这个函数的作用是告诉toffee-testadd_watch_point里的bins已经执行过了,判断一下是不是True,是的话就为这个观察点记录一次Pass。

有手动就有自动。我们可以在构建测试环境时,在定义fixture中加入StepRis(lambda x: g.sample()),这样就会在每个时钟周期的上升沿自动采样。

以下内容来自ut_backend/ctrl_block/decode/env/decode_wrapper.py

@pytest.fixture()
def decoder(request):
    # before test
    init_rv_decoder_funcov(g)
    func_name = request.node.name
    # If the output directory does not exist, create it
    output_dir_path = get_out_dir("decoder/log")
    os.makedirs(output_dir_path, exist_ok=True)
    decoder = Decode(DUTDecodeStage(
        waveform_filename=get_out_dir("decoder/decode_%s.fst"%func_name),
        coverage_filename=get_out_dir("decoder/decode_%s.dat"%func_name),
    ))
    decoder.dut.InitClock("clock")
    decoder.dut.StepRis(lambda x: g.sample())
    yield decoder
    # after test
    decoder.dut.Finish()
    coverage_file = get_out_dir("decoder/decode_%s.dat"%func_name)
    if not os.path.exists(coverage_file):
        raise FileNotFoundError(f"File not found: {coverage_file}")
    set_line_coverage(request, coverage_file, get_root_dir("scripts/backend_ctrlblock_decode"))
    set_func_coverage(request, g)
    g.clear()

如上面所示,我们在yield之前调用了g.sample(),这样就会在每个时钟周期的上升沿自动采样。

StepRis函数的作用是在每个时钟周期的上升沿执行传入的函数,详情可参照picker使用介绍

最后修改 February 22, 2025: ifu top rtl build scripts modify (#76) (5520758)