快速开始
本节将介绍如何使用 Toffee 框架完成一个简单的验证任务。我们将通过一个加法器设计的验证过程,演示 Toffee 的基本使用方法。该过程包括将加法器设计转换为 Python 包、编写驱动方法、创建验证模型、编写测试用例以及添加功能检查点等步骤。
在开始之前,您需要安装 Toffee 框架的基础工具 Picker。您可以在 这里 找到 Picker 的安装方法。接着,您可以通过以下命令安装 Toffee 及其测试工具 toffee-test:
pip install pytoffee toffee-test
生成加法器设计
我们将使用一个简单的加法器示例来演示 Toffee 的使用方法。设计代码如下:
// example/adder/adder.v
module Adder #(
parameter WIDTH = 64
) (
input [WIDTH-1:0] io_a,
input [WIDTH-1:0] io_b,
input io_cin,
output [WIDTH-1:0] io_sum,
output io_cout
);
assign {io_cout, io_sum} = io_a + io_b + io_cin;
endmodule
执行以下命令将 adder.v 转换为可供 Toffee 使用的 Python 包,或者直接在 example/adder 目录下执行 make dut:
picker export --autobuild=true adder.v -w Adder.fst --sname Adder --tdir picker_out_adder --lang python -e -c --sim verilator
该命令将生成 picker_out_adder 目录,您可以通过 from picker_out_adder import DUTAdder 导入可直接运行的加法器设计。接下来,我们将通过 Toffee 框架对该加法器的功能进行验证。
创建 Bundle 及 Agent
Toffee 使用 Bundle 来描述需要驱动的接口,并使用 Agent 来编写对该接口的驱动方法。在以下代码中,我们创建了一个 AdderBundle 用于描述加法器的接口,以及一个 AdderAgent 用于编写对该接口的驱动方法:
# test_adder.py
from toffee import *
class AdderBundle(Bundle):
a, b, cin, sum, cout = Signals(5)
class AdderAgent(Agent):
@driver_method()
async def exec_add(self, a, b, cin):
self.bundle.a.value = a
self.bundle.b.value = b
self.bundle.cin.value = cin
await self.bundle.step()
return self.bundle.sum.value, self.bundle.cout.value
使用 Bundle 的意义在于,我们可以在验证代码中预先定义好需要驱动的接口信号结构,而不需要关心 DUT 具体的接口信号名称。通过 Bundle 提供的映射方法,可以将信号映射至任意具有相同结构的 DUT 接口信号上,从而实现验证代码与 DUT 的解耦。
接下来,我们使用 driver_method 装饰器来标记 Agent 中的驱动方法 exec_add。该方法完成了对加法器的一次驱动操作。每当该方法被调用时,它会将输入信号 a、b、cin 的值分别赋给加法器的输入端口,并在下一个时钟周期后读取加法器的输出信号 sum 和 cout 的值并返回。在 Agent 中还可以定义监测方法(monitor_method),该方法会在后台持续监测 DUT 的输出信号,具体请参考 如何编写 Agent。
您可以将上述代码作为一个小型的验证封装,并在测试用例中调用该驱动方法来驱动加法器。
# test_adder.py (continued)
import toffee_test
from picker_out_adder import DUTAdder
@toffee_test.testcase
async def test_adder():
adder = DUTAdder() # 实例化加法器
start_clock(adder) # 启动 toffee 内置时钟
adder_bundle = AdderBundle.from_prefix("io_").bind(adder) # 利用前缀映射方法将 Bundle 与 DUT 进行绑定
adder_agent = AdderAgent(adder_bundle) # 实例化 Agent
sum, cout = await adder_agent.exec_add(1, 2, 0) # 调用驱动方法
assert sum == 3 and cout == 0 # 验证输出结果
在当前目录运行 pytest 命令,您将会看到这个简易的测试用例运行成功。然而,加法器的结果仍需人工比对验证。为了使测试更加自动化,接下来我们将利用 Toffee 提供的 Model 类来自动验证加法器的输出结果。
使用 Model 进行自动化检验
在此之前,我们需要使用 Env 来打包整个验证环境。Env 是 Toffee 中的顶层环境类,用于管理所有的 Agent 和 Model。由于加法器的验证环境中只有一个 Agent,因此我们只需在 Env 中实例化这个 Agent 即可。
# test_adder.py (continued)
class AdderEnv(Env):
def __init__(self, adder_bundle):
super().__init__()
self.add_agent = AdderAgent(adder_bundle)
AdderEnv 创建完成后,整个验证环境的结构也随之确定。此后,测试用例中将无需考虑 DUT 中的硬件接口,而是通过调用该层级结构中的软件接口来驱动 DUT:
AdderEnv
- add_agent
- exec_add
为了编写加法器的参考模型,我们在 Model 类中创建一个 driver_hook 方法,如下所示:
# test_adder.py (continued)
class AdderModel(Model):
@driver_hook(agent_name="add_agent")
def exec_add(self, a, b, cin):
result = a + b + cin
sum = result & ((1 << 64) - 1)
cout = result >> 64
return sum, cout
该方法用于截取 add_agent 中 exec_add 方法的调用。在定义该方法时,我们使其与 Agent 中的 exec_add 方法具有相同的输入参数,并返回加法器的标准返回值。此后,每当 add_agent 中的 exec_add 方法被调用时,Model 中的 exec_add 方法将会被自动触发,并将二者的返回值进行自动比对,从而实现对加法器的自动验证。
使用 toffee-test 管理测试用例
在此之后,我们使用 toffee-test 提供的方法来管理测试用例。我们将创建一个 adder_env 的 Fixture,用于在每个测试用例之前初始化验证环境。在该 Fixture 中,我们使用了 toffee_request.create_dut 方法来创建加法器实例,从而 DUT 的波形、覆盖率文件及测试报告会由 toffee-test 生成并收集到指定文件夹。注意,当初始化含有时钟 clock 信号接口的 DUT 时,您需要将 DUT 的时钟接口名称传入 toffee_request.create_dut 方法中,以便 toffee-test 正确识别时钟信号。
# test_adder.py (continued)
@toffee_test.fixture
async def adder_env(toffee_request: toffee_test.ToffeeRequest):
dut = toffee_request.create_dut(DUTAdder)
start_clock(dut)
return AdderEnv(AdderBundle.from_prefix("io_").bind(dut))
此后,在测试用例参数中使用 adder_env Fixture 即可在测试用例中使用验证环境。以下是为加法器编写的两个测试用例:
# test_adder.py (continued)
import random
@toffee_test.testcase
async def test_random(adder_env):
for _ in range(1000):
a = random.randint(0, 2**64 - 1)
b = random.randint(0, 2**64 - 1)
cin = random.randint(0, 1)
await adder_env.add_agent.exec_add(a, b, cin)
@toffee_test.testcase
async def test_boundary(adder_env):
for cin in [0, 1]:
for a in [0, 2**64 - 1]:
for b in [0, 2**64 - 1]:
await adder_env.add_agent.exec_add(a, b, cin)
在当前目录运行 pytest --toffee-report 命令,您将会看到测试用例运行成功,并且 toffee-test 会自动收集测试用例的执行结果,自动统计覆盖率信息,并在 reports 目录下生成验证报告。您也可以直接在 example/adder 目录下运行 make run 来查看测试结果。
添加功能检查点
功能检查点(Cover Point) 在验证中用于检验待测设计的某种情况是否被验证到。同一类功能检查点可以被组织成一个测试组(Cover Group),用于统计某一类功能检查点的覆盖率。在 Toffee 中,我们使用 CovGroup 和 add_watch_point 来定义测试组并添加功能检查点。例如,以下代码为加法器创建了一个测试组:
# test_adder.py (continued)
import toffee.funcov as fc
from toffee.funcov import CovGroup
def adder_cover_point(adder):
g = CovGroup("Adder addition function")
g.add_cover_point(adder.io_cout, {"io_cout is 0": fc.Eq(0)}, name="Cout is 0")
g.add_cover_point(adder.io_cout, {"io_cout is 1": fc.Eq(1)}, name="Cout is 1")
g.add_cover_point(adder.io_cin, {"io_cin is 0": fc.Eq(0)}, name="Cin is 0")
g.add_cover_point(adder.io_cin, {"io_cin is 1": fc.Eq(1)}, name="Cin is 1")
g.add_cover_point(adder.io_a, {"a > 0": fc.Gt(0)}, name="signal a set")
g.add_cover_point(adder.io_b, {"b > 0": fc.Gt(0)}, name="signal b set")
g.add_cover_point(adder.io_sum, {"sum > 0": fc.Gt(0)}, name="signal sum set")
return g
该测试组检验了加法器的 io_cout 和 io_cin 的取值是否全部覆盖到,并且检验了 io_a、io_b 和 io_sum 的取值是否大于 0。我们可以在此前创建的 Fixture 中将该测试组添加到验证环境中:
# test_adder.py (modified)
@toffee_test.fixture
async def adder_env(toffee_request: toffee_test.ToffeeRequest):
dut = toffee_request.create_dut(DUTAdder)
toffee_request.add_cov_groups(adder_cover_point(dut)) # 添加测试组
start_clock(dut)
return AdderEnv(AdderBundle.from_prefix("io_").bind(dut))
此时,再次运行 pytest --toffee-report 或在 example/adder 目录下运行 make run,您将会看到测试报告中包含了功能覆盖率信息。关于功能检查点的更多信息,请参考 功能检查点。
本节介绍了使用 Toffee 框架完成一个简单的验证任务的流程。继续阅读后续章节以了解更多 Toffee 的功能和用法。