1 - 添加编译脚本
脚本目标
在scripts
目录下使用python编写对应rtl的编译文件(例如build_ut_frontend_ifu_rvc_expander.py
)。
该脚本的目标是提供 RTL 到 Python DUT 的编译、目标覆盖文件,以及自定义功能等内容。
创建过程
确定文件名称
在香山昆明湖 DUT 验证进展中选择需要验证的 UT,如果没有或者进一步细化,可通过编辑configs/dutree/xiangshan-kmh.yaml
自行添加。
比如,我们要验证的是前端部分的ifu
模块下的rvc_expander
模块,那么需要在configs/dutree/xiangshan-kmh.yaml
中添加对应的部分(目前yaml中已经有该模块了,此处为举例):
name: "kmh_dut"
desc: "所有昆明湖DUT"
children:
- name: "frontend"
desc: "前端模块"
children:
- name: "ifu"
desc: "指令单元 (Instruction Fetch Unit)"
children:
- name: "rvc_expander"
desc: "RVC指令扩充器"
脚本文件的命名格式如下:
scripts/build_<顶层模块>_<下层模块名>_..._<目标模块名>.py
目前本项目内置了 4 个顶层模块:
- ut_frontend 前端
- ut_backend 后端
- ut_mem_block 访存
- ut_misc 其他
其中的子模块没有ut_
前缀(顶层目录有该前缀是为了和其他目录区分开)。
例如验证目标 DUT 为rvc_expander
模块:
该模块是属于前端的,所以顶级模块为ut_frontend
,它的下层模块为ifu
,目标模块为rvc_expander
。
通过刚才我们打开的yaml
文件也可以知道,frontend
的children 为ifu
,ifu
的children 为rvc_expander
。
所以,需要创建的脚本名称为build_ut_frontend_ifu_rvc_expander.py
。
编写 build(cfg) -> bool 函数
build 函数定义如下:
def build(cfg) -> bool:
"""编译DUT
Args:
cfg: 运行时配置,可通过它访问配置项,例如 cfg.rtl.version
Return:
返回 True 或者 False,表明该函数是否完成预期目标
"""
build 在 make dut 时会被调用,其主要是将目标 RTL 转换为 Python 模块。在该过程中也可以加入其他必要过程,例如编译依赖项等。以build_ut_frontend_ifu_rvc_expander.py
为例,主要完成了 RTL 检查、DUT 检查、RTL 编译、disasm 依赖编译等工作:
import os
from comm import warning, info
def build(cfg):
# import 相关依赖
from toffee_test.markers import match_version
from comm import is_all_file_exist, get_rtl_dir, exe_cmd, get_root_dir
# 检查RTL版本(version参数为空,表示所有版本都支持)
if not match_version(cfg.rtl.version, "openxiangshan-kmh-*"):
warning("ifu frontend rvc expander: %s" % f"Unsupported RTL version {cfg.rtl.version}")
return False
# 检查在当前RTL中,目标文件是否存在
f = is_all_file_exist(["rtl/RVCExpander.sv"], get_rtl_dir(cfg=cfg))
assert f is True, f"File {f} not found"
# 如果dut中不存在RVCExpander,则调用picker进行Python打包
if not os.path.exists(get_root_dir("dut/RVCExpander")):
info("Exporting RVCExpander.sv")
s, out, err = exe_cmd(f'picker export --cp_lib false {get_rtl_dir("rtl/RVCExpander.sv", cfg=cfg)} --lang python --tdir {get_root_dir("dut")}/ -w rvc.fst -c')
assert s, "Failed to export RVCExpander.sv: %s\n%s" % (out, err)
# 如果tools中不存在disasm/build,则需要编译disasm
if not os.path.exists(get_root_dir("tools/disasm/build")):
info("Building disasm")
s, _, _ = exe_cmd("make -C %s" % get_root_dir("tools/disasm"))
assert s, "Failed to build disasm"
# 编译成功
return True
def line_coverage_files(cfg):
return ["RVCExpander.v"]
picker 的使用方式请参考其文档和使用
在scripts
目录中可以创建子目录保存 UT 验证需要的文件,例如 rvc_expander 模块创建了scripts/frontend_ifu_rvc_expander
目录,其中的rtl_file.f
用来指定输入的 RTL 文件,line_coverage.ignore
用来保存需要忽略的代码行统计。自定义目录的命名需要合理,且能通过名字判断其所属模块和文件。
编写 line_coverage_files(cfg) -> list[str] 函数
line_coverage_files 函数的定义如下:
def line_coverage_files(cfg)-> list[str]:
"""指定需要覆盖的文件
Args:
cfg: 运行时配置,可通过它访问配置项,例如 cfg.rtl.version
Return:
返回统计代码行覆盖率的目标RTL文件名
"""
在build_ut_frontend_ifu_rvc_expander.py
文件中,line_coverage_files
函数的定义如下:
def line_coverage_files(cfg):
return ["RVCExpander.v"]
标识该模块关注的是对RVCExpander.v
文件的覆盖。如果要开启测试结果处理,还需要在configs/_default.yaml
中的doc-result
下disable=False
(默认参数是False
,也就是开启状态);如果不开启测试结果处理则(disable = True
)。注意,如果不开启测试结果处理,那么上述函数就不会被调用。
2 - 构建测试环境
确定目录结构
UT(Unit Test, 单元测试)所在的目录位置的层级结构应该与名称一致,例如frontend.ifu.rvc_expander
应当位于ut_frontend/ifu/rvc_expander
目录,且每层目录都需要有__init__.py
,便于通过 python 进行import
。
本章节的文件为your_module_wrapper.py
(如果你的模块是rvc_expander
,那么文件就是rvc_expander_wrapper.py
)。
wrapper 是包装的意思,也就是我们测试中需要用到的方法封装成和dut解耦合的API提供给测试用例使用。
*注:解耦合是为了测试用例和 DUT 解耦,使得测试用例可以独立于 DUT 进行编写和调试,也就是在测试用例中,不需要知道 DUT 的具体实现细节,只需要知道如何使用 API 即可。可以参照将验证代码与DUT进行解耦
该文件应该放于ut_frontend_or_backend/top_module/your_module/env
(这里依然以rvc_expander
举例:rvc_expander
属于前端,其顶层目录则应该是ut_frontend
;rvc_expander
的顶层模块是ifu
,那么次级目录就是ifu
;之后的就是rvc_expander
自己了;最后,由于我们是在构建测试环境,再建一级env
目录。将它们连起来就是:ut_frontend_or_backend/top_module/your_module/env
)目录下。
ut_frontend/ifu/rvc_expander
├── classical_version
│ ├── env
│ │ ├── __init__.py
│ │ └── rvc_expander_wrapper.py
│ ├── __init__.py
│ └── test_rvc_expander.py
├── __init__.py
├── README.md
└── toffee_version
├── agent
│ └── __init__.py
├── bundle
│ └── __init__.py
├── env
│ ├── __init__.py
│ └── ref_rvc_expand.py
├── __init__.py
└── test
├── __init__.py
├── rvc_expander_fixture.py
└── test_rvc.py
这里rvc_expander
目录下有classical_version
传统版本和toffee_version
使用toffee的版本。
传统版本就是使用pytest
框架来进行测试,toffee
只使用了其Bundle
;而在toffee版本中,我们会使用更多toffee
的特性。
一般来说,使用传统版本就已经可以覆盖绝大多数情况了,只有在传统版本不能满足需求时,才需要使用toffee
版本。
编写测试环境的时候,两个版本选择一个就行。
模块(例如rvc_expander
)中的代码目录结构由贡献者自行决定(我们写的时候并不需要再建一级classical_version
或toffee_version
目录),但需要满足 python 规范,且逻辑和命名合理。
Env 编写要求
- 需要进行 RTL 版本检查
- Env 提供的 API 需要和引脚、时序无关
- Env 提供的 API 需要稳定,不能随意进行接口/返回值修改
- 需要定义必要的 fixture
- 需要初始化功能检查点(功能检查点可以独立成一个模块)
- 需要进行覆盖率统计
- 需要有说明文档
编写测试环境:传统版本
在 UT 验证模块的测试环境中,目标是完成以下工作:
- 对 DUT 进行功能封装,为测试提供稳定 API
- 定义功能覆盖率
- 定义必要 fixture 提供给测试用例
- 在合理时刻统计覆盖率
以 IFU 环境中的 RVCExpander 为例(ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py
):
1. DUT 封装
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py
。
class RVCExpander(toffee.Bundle):
def __init__(self, cover_group, **kwargs):
super().__init__()
self.cover_group = cover_group
self.dut = DUTRVCExpander(**kwargs) # 创建DUT
self.io = toffee.Bundle.from_prefix("io_", self.dut) # 通过 Bundle 使用前缀关联引脚
self.bind(self.dut) # 把 Bundle 与 DUT 进行绑定
def expand(self, instr, fsIsOff):
self.io["in"].value = instr # 给DUT引脚赋值
self.io["fsIsOff"].value = fsIsOff # 给DUT引脚赋值
self.dut.RefreshComb() # 推动组合电路
self.cover_group.sample() # 调用sample对功能覆盖率进行统计
return self.io["out_bits"].value, self.io["ill"].value # 返回结果 和 是否是非法指令
def stat(self): # 获取当前状态
return {
"instr": self.io["in"].value, # 输入指令
"decode": self.io["out_bits"].value, # 返回展开结果
"ilegal": self.io["ill"].value != 0, # 输入是否非法
}
在上述例子中,class RVCExpander
对DUTRVCExpander
进行了封装,对外提供了两个 API:
- expand(instr: int, fsIsOff: bool) -> (int, int) :该函数用于接受输入指令 instr 进行解码,返回(结果,非法指令标记)。如果非法指令标记不为 0,者说明输入指令非法。
- stat() -> dict(instr, decode, ilegal):该函数用于返回当前的状态,其中包含当前的输入指令,解码结果以及非法指令标记。
上述 API 屏蔽了 DUT 的引脚,对外程序通用功能。
2. 定义功能覆盖率
尽可能的在 Env 中定义好功能覆盖率,如果有必要也可以在测试用例中定义覆盖率。toffee 功能覆盖率的定义请参考什么是功能覆盖率。为了完善功能检查点和测试用例之间的对应关系,功能覆盖率定义完成后,需要在适合的位置进行检查点和测试用例的对应(测试点反标)。
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py
。
import toffee.funcov as fc
# 创建功能覆盖率组
g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))
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"])
...
在上述代码中添加了名为RVC_EXPAND_RET
的功能检查点来检查RVCExpander
模块是否具有返回非法指令的能力。需要满足ERROR
和SUCCE
两个条件,即stat()
中的ileage
需要有True
也需要有False
值。在定义完检查点后,通过mark_function
方法,对会覆盖到该检查的测试用例进行了标记。
3. 定义必要fixture
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py
。
version_check = get_version_checker("openxiangshan-kmh-*") # 指定满足要的RTL版本
@pytest.fixture()
def rvc_expander(request):
version_check() # 进行版本检查
fname = request.node.name # 获取调用该fixture的测试用例
wave_file = get_out_dir("decoder/rvc_expander_%s.fst" % fname) # 设置波形文件路径
coverage_file = get_out_dir("decoder/rvc_expander_%s.dat" % fname) # 设置代码覆盖率文件路径
coverage_dir = os.path.dirname(coverage_file)
os.makedirs(coverage_dir, exist_ok=True) # 目标目录不存在则创建目录
expander = RVCExpander(g, coverage_filename=coverage_file, waveform_filename=wave_file)
# 创建RVCExpander
expander.dut.io_in.AsImmWrite() # 设置io_in引脚的写入时机为立即写入
expander.dut.io_fsIsOff.AsImmWrite() # 设置io_fsIsOff引脚的写入时机为立即写入
init_rvc_expander_funcov(expander, g) # 初始化功能检查点
yield expander # 返回创建好的 RVCExpander 给 Test Case
expander.dut.Finish() # Tests Case运行完成后,结束DUT
set_line_coverage(request, coverage_file) # 把生成的代码覆盖率文件告诉 toffee-report
set_func_coverage(request, g) # 把生成的功能覆盖率数据告诉 toffee-report
g.clear() # 清空功能覆盖统计
上述 fixture 完成了以下功能:
- 进行 RTL 版本检查,如果不满足
"openxiangshan-kmh-*"
要求,则跳过调用改 fixture 的测试用例
- 创建 DUT,并指定了波形,代码行覆盖率文件路径(路径中含有调用该 fixure 的用例名称:fname)
- 调用
init_rvc_expander_funcov
添加功能覆盖点
- 结束 DUT,处理代码行覆盖率和功能覆盖率(发往 toffee-report 进行处理)
- 清空功能覆盖率
*注:在 PyTest 中,执行测试用例test_A(rvc_expander, ....)
前(rvc_expander是我们在使用fixure装饰器时定义的方法名),会自动调用并执行rvc_expander(request)
中yield
关键字前的部分(相当于初始化),然后通过yield
返回rvc_expander
调用test_A
用例(yield返回的对象,在测试用例里就是我们fixture下定义的方法名),用例执行完成后,再继续执行fixture
中yield
关键字之后的部分。比如:参照下面统计覆盖率的代码,倒数第四行的
rvc_expand(rvc_expander, generate_rvc_instructions(start, end))
,其中的rvc_expander
就是我们在fixture
中定义的方法名,也就是yield
返回的对象。
4. 统计覆盖率
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py
N = 10
T = 1<<16
@pytest.mark.toffee_tags(TAG_LONG_TIME_RUN)
@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_16bit_full(rvc_expander, start, end):
"""Test the RVC expand function with a full compressed instruction set
Description:
Perform an expand check on 16-bit compressed instructions within the range from 'start' to 'end'.
"""
# Add check point: RVC_EXPAND_RANGE to check expander input range.
# When run to here, the range[start, end] is covered
covered = -1
g.add_watch_point(rvc_expander, {
"RANGE[%d-%d]"%(start, end): lambda _: covered == end
}, name = "RVC_EXPAND_ALL_16B", dynamic_bin=True)
# Reverse mark function to the check point
g.mark_function("RVC_EXPAND_ALL_16B", test_rvc_expand_16bit_full, bin_name="RANGE[%d-%d]"%(start, end))
# Drive the expander and check the result
rvc_expand(rvc_expander, generate_rvc_instructions(start, end))
# When go to here, the range[start, end] is covered
covered = end
g.sample() # 覆盖率采样
在定义了覆盖率之后,还需要在测试用例中进行覆盖率统计。上述代码中,在测试用例中使用add_watch_point
添加了一个功能检查点rvc_expander
,并在后面进行了标记和采样,而且在最后一样对覆盖率进行了采样。
覆盖率采样,实际上是通过回调函数触发了一次add_watch_point
中bins的判断,当其中bins的判断结果为True
时,就会统计一次Pass。
编写测试环境:toffee版本
使用python语言进行的测试可以通过引入我们的开源测试框架toffee来得到更好的支持。
toffee的官方教程可以参考这里。
bundle:快捷DUT封装
toffee通过Bundle实现了对DUT的绑定。toffee提供了多种建立Bundle与DUT绑定的方法。相关代码
手动绑定
toffee框架下,用于支持绑定引脚的最底层类是Signal,其通过命名匹配的方式和DUT中的各个引脚进行绑定。相关代码参照ut_frontend/ifu/rvc_expander/toffee_version
。
以最简单的RVCExpander为例,其io引脚形如:
module RVCExpander(
input [31:0] io_in,
input io_fsIsOff,
output [31:0] io_out_bits,
output io_ill
);
一共四个信号,io_in, io_fsIsOff, io_out_bits, io_ill。我们可以抽取共同的前缀,比如"io_"(不过由于in在python中有其他含义,其不能直接作为变量名,虽然可以使用setattr 和getattr方法来规避这个问题,但是出于代码简洁的考虑,我们只选取"io"作为前缀),将后续部分作为引脚名定义在对应的Bundle类中:
class RVCExpanderIOBundle(Bundle):
_in, _fsIsOff ,_out_bits,_ill = Signals(4)
然后在更高一级的Env或者Bundle中,采取from_prefix的方式完成前缀的绑定:
self.agent = RVCExpanderAgent(RVCExpanderIOBundle.from_prefix("io").bind(dut))
自动定义Bundle
实际上,Bundle类的定义也不一定需要写明,可以仅仅通过前缀绑定:
self.io = toffee.Bundle.from_prefix("io_", self.dut) # 通过 Bundle 使用前缀关联引脚
self.bind(self.dut)
如果Bundle的from_prefix方法传入dut,其将根据前缀和DUT的引脚名自动生成引脚的定义,而在访问的时候,使用dict访问的思路即可:
self.io["in"].value = instr
self.io["fsIsOff"].value = False
Bundle代码生成
toffee框架的scripts提供了两个脚本。
bundle_code_gen.py脚本主要提供了三个方法:
def gen_bundle_code_from_dict(bundle_name: str, dut, dict: dict, max_width: int = 120)
def gen_bundle_code_from_prefix(bundle_name: str, dut, prefix: str = "", max_width: int = 120):
def gen_bundle_code_from_regex(bundle_name: str, dut, regex: str, max_width: int = 120):
通过传入dut和生成规则(包括dict、prefix、regex三种),自动生成对应的bundle代码。
而bundle_code_intel_gen.py则解析picker生成的signals.json文件,自动生成层次化的bundle代码。可以直接在命令行调用:
python bundle_code_intel_gen.py [signal] [target]
如发现自动生成脚本存在bug,欢迎提issue以便我们修正。
Agent:驱动方法
如果说Bundle是将DUT的数据职责进行抽象的话,那么Agent则是将DUT的行为职责封装为一个个接口。简单地说,Agent通过封装多个对外开放的方法,将多组IO操作抽象为一个具体的行为:
class RVCExpanderAgent(Agent):
def __init__(self, bundle:RVCExpanderIOBundle):
super().__init__(bundle)
self.bundle = bundle
@driver_method()
async def expand(self, instr, fsIsOff): # 传入参数:RVC指令和fs.status使能情况
self.bundle._in.value = instr # 引脚赋值
self.bundle._fsIsOff.value = fsIsOff # 引脚赋值
await self.bundle.step() # 推动时钟
return self.bundle._out_bits.value, # 返回值:扩展后指令
self.bundle._ill.value # 返回值:指令合法校验
譬如,RVCExpander的指令扩展功能接收输入的指令(可能为RVI指令,也可能为RVC指令)和CSR对fs.status的使能情况。我们将这个功能抽象为expand方法,提供除self以外的两个参数。同时,指令扩展最终将会返回传入指令对应的RVI指令和该指令是否合法的判断,对应地,该方法也返回这两个值。
Env:测试环境
class RVCExpanderEnv(Env):
def __init__(self, dut:DUTRVCExpander):
super().__init__()
dut.io_in.xdata.AsImmWrite()
dut.io_fsIsOff.xdata.AsImmWrite() # 设置引脚写入时机
self.agent = RVCExpanderAgent(RVCExpanderIOBundle.from_prefix("io").bind(dut)) # 补全前缀,绑定DUT
覆盖率定义
定义覆盖率组的方式和前述方式类似,这里就不再赘述了。
测试套件定义
测试套件的定义略有不同:
@toffee_test.fixture
async def rvc_expander(toffee_request: toffee_test.ToffeeRequest):
import asyncio
version_check()
dut = toffee_request.create_dut(DUTRVCExpander)
start_clock(dut)
init_rvc_expander_funcov(dut, gr)
toffee_request.add_cov_groups([gr])
expander = RVCExpanderEnv(dut)
yield expander
cur_loop = asyncio.get_event_loop()
for task in asyncio.all_tasks(cur_loop):
if task.get_name() == "__clock_loop":
task.cancel()
try:
await task
except asyncio.CancelledError:
break
由于toffee提供了更强大的测试覆盖率管理功能,因此不需要手动设置行覆盖率。同时,由于toffee的时钟机制,建议在套件代码最后额外检查任务是否全部结束。
3 - 添加测试用例
命名要求
所有测试用例文件请以test_*.py
的方式进行命名,*
用测试目标替换(例如test_rvc_expander.py
)。所有测试用例也需要以test_
前缀开头。用例名称需要具有明确意义。
命名举例如下:
def test_a(): # 不合理,无法通过a判断测试目标
pass
def test_rvc_expand_16bit_full(): # 合理,可以通过用例名称大体知道测试内容
pass
使用 Assert
在每个测试用例中,都需要通过assert
来判断本测试是否通过。
pytest
统计的是assert
语句的结果,因此assert
语句需要保证能够通过。
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py
中:
def rvc_expand(rvc_expander, ref_insts, is_32bit=False, fsIsOff=False):
"""compare the RVC expand result with the reference
Args:
rvc_expander (warpper): the fixture of the RVC expander
ref_insts (list[int]]): the reference instruction list
"""
find_error = 0
for insn in ref_insts:
insn_disasm = disasmbly(insn)
value, instr_ex = rvc_expander.expand(insn, fsIsOff)
if is_32bit:
assert value == insn, "RVC expand error, 32bit instruction need to be the same"
if (insn_disasm == "unknown") and (instr_ex == 0):
debug(f"find bad inst:{insn}, ref: 1, dut: 0")
find_error +=1
elif (insn_disasm != "unknown") and (instr_ex == 1):
if (instr_filter(insn_disasm) != 1):
debug(f"find bad inst:{insn},disasm:{insn_disasm}, ref: 0, dut: 1")
find_error +=1
assert 0 == find_error, "RVC expand error (%d errros)" % find_error
编写注释
每个测试用例都需要添加必要的说明和注释,需要满足Python 注释规范。
测试用例说明参考格式:
def test_<name>(a: type_a, b: type_b):
"""Test abstract
Args:
a (type_a): description of arg a.
b (type_b): description of arg b.
Detailed test description here (if need).
"""
...
用例管理
为了方便测试用例管理,可通过 toffee-test 提供的@pytest.mark.toffee_tags
标签功能,请参考
本网站的其他部分和toffee-test。
参考用例
如果很多测试用例(Test)具有相同的操作,该公共操作部分可以提炼成一个通用函数。以 RVCExpander 验证为例,可以把压缩指令的展开与参考模型(disasm)的对比封装成以下函数:
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py
中:
def rvc_expand(rvc_expander, ref_insts, is_32bit=False, fsIsOff=False):
"""compare the RVC expand result with the reference
Args:
rvc_expander (warpper): the fixture of the RVC expander
ref_insts (list[int]]): the reference instruction list
"""
find_error = 0
for insn in ref_insts:
insn_disasm = disasmbly(insn)
value, instr_ex = rvc_expander.expand(insn, fsIsOff)
if is_32bit:
assert value == insn, "RVC expand error, 32bit instruction need to be the same"
if (insn_disasm == "unknown") and (instr_ex == 0):
debug(f"find bad inst:{insn}, ref: 1, dut: 0")
find_error +=1
elif (insn_disasm != "unknown") and (instr_ex == 1):
if (instr_filter(insn_disasm) != 1):
debug(f"find bad inst:{insn},disasm:{insn_disasm}, ref: 0, dut: 1")
find_error +=1
assert 0 == find_error, "RVC expand error (%d errros)" % find_error
在上述公共部分中有 assert,因此调用该函数的 Test 也能提过该 assert 判断运行结果是否提过。
在测试用例的开发过程中,通常存在大量的调试工作,为了让验证环境快速就位,需要编写一些“冒烟测试”进行调试。RVCExpander 展开 16 位压缩指令的冒烟测试如下:
@pytest.mark.toffee_tags(TAG_SMOKE)
def test_rvc_expand_16bit_smoke(rvc_expander):
"""Test the RVC expand function with 1 compressed instruction"""
rvc_expand(rvc_expander, generate_rvc_instructions(start=100, end=101))
为了方便进行管理,上述测试用例通过toffee_tags
标记上了 SMOKE 标签。它的输入参数为rvc_expander
,则在在运行时,会自动调用对应同名的fixture
进行该参数的填充。
RVCExpander 展开 16 位压缩指令的测试目标是对 2^16 所有压缩指令进行遍历,检测所有情况是否都与参考模型 disasm 一致。在实现上,如果仅仅用一个 Test 进行遍历,则需要耗费大量时间,为此我们可以利用 PyTest 提供的parametrize
对 test 进行参数化配置,然后通过pytest-xdist
插件并行执行:
以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py
中:
N = 10
T = 1<<16
@pytest.mark.toffee_tags(TAG_LONG_TIME_RUN)
@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_16bit_full(rvc_expander, start, end):
"""Test the RVC expand function with a full compressed instruction set
Description:
Perform an expand check on 16-bit compressed instructions within the range from 'start' to 'end'.
"""
# Add check point: RVC_EXPAND_RANGE to check expander input range.
# When run to here, the range[start, end] is covered
g.add_watch_point(rvc_expander, {
"RANGE[%d-%d]"%(start, end): lambda _: True
}, name = "RVC_EXPAND_ALL_16B").sample()
# Reverse mark function to the check point
g.mark_function("RVC_EXPAND_ALL_16B", test_rvc_expand_16bit_full, bin_name="RANGE[%d-%d]"%(start, end))
# Drive the expander and check the result
rvc_expand(rvc_expander, generate_rvc_instructions(start, end))
在上述用例中定义了参数化参数start
, end
,用来指定压缩指令的开始值和结束值,然后通过装饰器@pytest.mark.parametrize
对他们进行分组赋值。变量 N 可以指定将目标数据进行分组的组数,默认设置为 10 组。在运行时用例test_rvc_expand_16bit_full
会展开为test_rvc_expand_16bit_full[0-6553]
至test_rvc_expand_16bit_full[58977-65536]
10 个测试用例运行。
4 - 代码覆盖率
代码覆盖率是一项评价指标,它衡量了被测代码中哪些部分被执行了,哪些部分没有被执行。通过统计代码覆盖率,可以评估测试的有效性和覆盖程度。
代码覆盖率包括:
- 行覆盖率(line coverage): 被测代码中被执行的行数,最简单的指标,一般期望达到 100%。
- 条件覆盖率(branch coverage): 每一个控制结构的每个分支是否均被执行。例如,给定一个 if 语句,其 true 和 false 分支是否均被执行?
- 有限状态机覆盖率(fsm coverage): 状态机所有状态是否都达到过。
- 翻转覆盖率(toggle coverage): 统计被测代码中被执行的翻转语句,检查电路的每个节点是否都有 0 -> 1 和 1 -> 0 的跳变。
- 路径覆盖率(path coverage): 检查路径的覆盖情况。在 always 语句块和 initial 语句块中,有时会使用 if … else 和 case 语句,在电路结构上便会产生一系列的数据路径。。
*我们主要使用的模拟器是 Verilator,优先考虑行覆盖率。Verilator 支持覆盖率统计,因此我们在构建 DUT 时,如果要开启覆盖率统计,需要在编译选项中添加-c
参数。
本项目中相关涉及位置
开启覆盖率需要在编译时(使用 picker 命令时)加上“-c”参数(参考 picker 的参数解释),同时在文件中设置启用行覆盖率,这样在使用 toffee 测试时,才能够生成覆盖率统计文件。
结合上面的描述,在本项目中也就是编译,编写和启用行覆盖率函数和测试的时候会涉及到代码覆盖率:
添加编译脚本部分
编写编译脚本
# 省略前面
if not os.path.exists(get_root_dir("dut/RVCExpander")):
info("Exporting RVCExpander.sv")
s, out, err = exe_cmd(f'picker export --cp_lib false {get_rtl_dir("rtl/RVCExpander.sv", cfg=cfg)
} --lang python --tdir {get_root_dir("dut")}/ -w rvc.fst -c')
assert s, "Failed to export RVCExpander.sv: %s\n%s" % (out, err)
# 省略后面
在s, out, err=...
这一行,我们使用 picker 命令,并且开启代码了覆盖率(命令最后的"-c"参数)。
设置目标覆盖文件(line_coverage_files 函数)
按照需求编写line_coverage_files(cfg) -> list[str]
函数,并且开启测试结果处理(doc_result.disable = False
)让其被调用。
构建测试环境部分
定义必要 fixture
set_line_coverage(request, coverage_file) # 把生成的代码覆盖率文件告诉 toffee-report
通过函数toffee-test.set_line_coverage
把覆盖率文件传递给 toffe-test,这样其才能够收集数据,以便于后面生成的报告带有行覆盖率。
忽略指定统计
有时候,我们可能需要手动指定某些内容不参与覆盖率统计。例如有些是不需要被统计的,有些统计不到是正常的。这时候我们就可以忽略这些内容,这对优化覆盖率报告或调试非常有帮助。
目前我们的框架可以使用两种方式来实现忽略统计的功能:
1.通过 verilator 指定忽略统计的内容
使用 verilator_coverage_off/on 指令
Verilator 支持通过注释指令来忽略特定代码段的覆盖率统计。例如,使用如下的指令:
// *verilator coverage_off*
// 忽略统计的代码段
...
// *verilator coverage_on*
举个例子
module example;
always @(posedge clk) begin
// *verilator coverage_off*
if (debug_signal) begin
$display("This is for debugging only");
end
// *verilator coverage_on*
if (enable) begin
do_something();
end
end
endmodule
在上述示例中,debug_signal 部分的代码将不会计入覆盖率统计,而 enable 部分仍然会被统计。
更多 verilator 的忽略统计方式请参照verilator 官方文档
2.通过 toffee 指定需要过滤掉的内存
def set_line_coverage(request, datfile, ignore=[]):
"""Pass
Args:
request (pytest.Request): Pytest的默认fixture,
datfile (string): DUT生成的
ignore (list[str]): 覆盖率过滤文件/或者文件夹
"""
ignore 参数可以指定在覆盖率文件中需要过滤掉的内容,例如:
...
set_line_coverage(request, coverage_file,
get_root_dir("scripts/frontend_ifu_rvc_expander"))
在统计覆盖率时,会在"scripts/frontend_ifu_rvc_expander"目录中搜索到line_coverage.ignore
文件,然后按其中每行的通配符进行过滤。
# Line covarge ignore file
# ignore Top file
*/RVCExpander_top*%
上述文件表示,在统计覆盖率时,会忽略掉包含"RVCExpander_top"关键字的文件(实际上是收集了对应的数据,但是最后统计的时候忽略了)。
查看统计结果
在经过前面所有步骤之后,包括准备测试环境中的下载 RTL 代码、编译 DUT、编辑配置
;添加测试中的添加编译脚本,构建测试环境、添加测试用例。
现在运行测试,之后就默认在out/report
目录会生成 html 版本的测试报告。
也可以在进度概述图形下方的“当前版本”选择对应的测试报告(按照测试时间命名),然后点击右侧链接即可查看统计结果。
5 - 功能覆盖率
功能覆盖率(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 模块)
创建覆盖率组
使用toffee
的funcov
可以创建覆盖率组。
import toffee.funcov as fc
# 使用上面指定的GROUP名字
g = fc.CovGroup(name)
这两步也可以合成一句g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))
。
创建的g对象就表示了一个功能覆盖率组,可以使用其来提供观察点和反标。
添加观察点和反标
在每个测试用例内部,可以使用add_watch_point
(add_cover_point
是其别名,二者完全一致)来添加观察点和mark_function
来添加反标。
观察点是,当对应的信号触发了我们在观察点内部定义的要求后,这个观察点的名字(也就是功能点)就会被统计到功能覆盖率中。
反标是,将功能点和测试用例进行关联,这样在统计时,就能看到每个功能点对应了哪些测试用例。
对于观察点的位置,需要根据实际情况来定,一般来说,在测试用例外直接添加观察点是没有问题的。
不过有时候我们可以更加的灵活。
- 在测试用例之外(
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
框架就能够收集到对应的功能点。
- 在测试用例之中(
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()
这个例子的观察点在测试用例里面,因为这里的start
和end
是由pytest.mark.parametrize
来决定的,数值不是固定的,所以我们需要在测试用例里面添加观察点。
采样
在上一个例子的最后,我们调用了g.sample()
,这个函数的作用是告诉toffee-test
,add_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使用介绍