toffee基本使用

toffee基本使用

来源:[万众一芯开放验证](https://open-verify.cc)

1. 异步环境

toffee 使用了 Python 的协程来完成对异步程序的管理,其在单线程之上建立了一个事件循环,用于管理多个同时运行的协程,协程之间可以相互等待并通过事件循环来进行切换。

1.1 基本关键字

当函数前加上async 关键字时,这个函数就变成了一个协程函数,例如:

async def my_coro():
    ...

当我们在协程函数内部使用 await 关键字时,我们就可以执行一个协程函数,并等待其执行完成并返回结果,例如:

async def my_coro():
    return "my_coro"

async def my_coro2():
    result = await my_coro()
    print(result)

如果不需要等待函数完成,使用create_task方法将函数加入后台运行:

import toffee

async def my_coro():
    return "my_coro"

async def my_coro2():
    toffee.create_task(my_coro())

在toffee中使用toffee.run启动事件循环,并运行异步程序:

import toffee

toffee.run(my_coro2())

在toffee中需要先启动事件循环,然后才能在事件循环中创建验证环境:

import toffee

async def start_test():
    # 创建验证环境
    env = MyEnv()

    ...

toffee.run(start_test())

1.2 时钟管理

在 toffee 中,通过start_clock来创建后台时钟:

import toffee

async def start_test():
    dut = MyDUT()
    toffee.start_clock(dut)

toffee.run(start_test())

在其他协程中,我们可以通过 ClockCycles 来等待时钟信号到来,ClockCycles 的参数可以是 DUT,也可以是 DUT 的每一个引脚。例如:

import toffee
from toffee.triggers import *

async my_coro(dut):
    await ClockCycles(dut, 10)
    print("10 cycles passed")

async def start_test():
    dut = MyDUT()
    toffee.start_clock(dut)

    await my_coro(dut)

toffee.run(start_test())

每当十个周期,my_coro()就会继续执行,打印文字。更多等待时钟信号的方法见API文档(?还没完善)。

2. Bundle 使用

Bundle用于测试环境与DUT的解耦,一个简单的Bundle定义如下:

from toffee import Bundle, Signals

class AdderBundle(Bundle):
    a, b, sum, cin, cout = Signals(5)

它继承toffee中的Bundle类,定义了几个接口。随后可以创建AdderBundle实例,通过x.value来访问信号:

adder_bundle = AdderBundle()

adder_bundle.a.value = 1
adder_bundle.b.value = 2
adder_bundle.cin.value = 0
print(adder_bundle.sum.value)
print(adder_bundle.cout.value)

2.1 DUT与Bundle的绑定

如果一个DUT与一个Bundle的接口信号完全相同,可以使用bind方法进行绑定:

adder = DUTAdder()

adder_bundle = AdderBundle()
adder_bundle.bind(adder)

同时toffee也提供了一些方法用于两者的绑定

2.1.1 通过字典进行绑定

假设 Bundle 中的接口名称与 DUT 中的接口名称拥有如下对应关系:

a    -> a_in
b    -> b_in
sum  -> sum_out
cin  -> cin_in
cout -> cout_out

在创建Bundle时可以使用from_dict方法传入一个字典,告知Bundle以这种方式绑定DUT引脚:

adder = DUTAdder()
adder_bundle = AdderBundle.from_dict({
    'a': 'a_in',
    'b': 'b_in',
    'sum': 'sum_out',
    'cin': 'cin_in',
    'cout': 'cout_out'
})
adder_bundle.bind(adder)

2.1.2 通过前缀进行绑定

假设 DUT 中的接口名称与 Bundle 中的接口名称如上所示,实际 DUT 的接口名称比Bundle中的名称多了一个io_,

可以使用from_prefix方法创建Bundle,告知Bundle以前缀的方式完成绑定:

adder = DUTAdder()
adder_bundle = AdderBundle.from_prefix('io_')
adder_bundle.bind(adder)

2.1.3 通过正则表达式进行绑定

有时候DUT 中的接口名称与 Bundle 中的接口名称之间有一些复杂的前缀后缀:

a    -> io_a_in
b    -> io_b_in
sum  -> io_sum_out
cin  -> io_cin_in
cout -> io_cout_out

在这种情况下,我们可以通过传入正则表达式,来告知 Bundle 以正则表达式的方式进行绑定:

adder = DUTAdder()
adder_bundle = AdderBundle.from_regex(r'io_(.*)_.*')
adder_bundle.bind(adder)

2.2 创建子Bundle

我们可以将其他已经定义好的Budle作为当前Bundle的子Bundle:

from toffee import Bundle, Signal, Signals

class AdderBundle(Bundle):
    a, b, sum, cin, cout = Signals(5)

class MultiplierBundle(Bundle):
    a, b, product = Signals(3)

class ArithmeticBundle(Bundle):
    selector = Signal()
    adder = AdderBundle.from_prefix('add_')
    multiplier = MultiplierBundle.from_prefix('mul_')

在类ArithmeticBundle中,有一个自己的信号selector,以及两个子Bundle,并指定了信号绑定的方法,可以使用如下方法进行访问:

arithmetic_bundle = ArithmeticBundle()

arithmetic_bundle.selector.value = 1
arithmetic_bundle.adder.a.value = 1
arithmetic_bundle.adder.b.value = 2
arithmetic_bundle.multiplier.a.value = 3
arithmetic_bundle.multiplier.b.value = 4

需要注意的是,子 Bundle 的创建方法去匹配的信号名称,是经过上一次 Bundle 的创建方法进行处理过后的名称。例如在上面的代码中,我们将顶层 Bundle 的匹配方式设置为 from_prefix('io_'),那么在 AdderBundle 中去匹配的信号,是去除了 io_ 前缀后的名称。

2.3 Bundle中一般操作

在将DUT以及Bundle进行bind时,可以通过传入 unconnected_signal_access 参数来控制是否允许访问未连接的信号:

def bind(self, dut, unconnected_signal_access=True)

可以通过set__all或者randomize_all方法将所有方法赋值:

adder_bundle.set_all(0)
adder_bundle.randomize_all()

bundle中可以使用set_write_mode_as_imme, set_write_mode_as_riseset_write_mode_as_fall,分别用于设置 Bundle 的赋值模式为立即赋值、上升沿赋值与下降沿赋值。

bundle可以通过assign方法将一个字典赋值给 Bundle 中的信号,当需要将未指定的信号赋值成某个默认值时,可以通过 * 来指定默认值:

adder_bundle.assign({
    'a': 1,
    'b': 2,
    'cin': 0
})
-----------------------
adder_bundle.assign({
    '*': 0,
    'a': 1,
})
------------------------ #子bundle赋值
arithmetic_bundle.assign({
    'selector': 1,
    'adder': {
        '*': 0,
        'cin': 0
    },
    'multiplier': {
        'a': 3,
        'b': 4
    }
}, multilevel=True)
------------------------
arithmetic_bundle.assign({
    '*': 0,
    'selector': 1,
    'adder.cin': 0,
    'multiplier.a': 3,
    'multiplier.b': 4
}, multilevel=False)

在bundle中使用as_dict 方法将 Bundle 当前的信号值转换为字典。其同样支持两种格式,当 multilevelTrue 时,返回多级字典;当 multilevelFalse 时,返回扁平化的字典:

> arithmetic_bundle.as_dict(multilevel=True)
{
    'selector': 1,
    'adder': {
        'a': 0,
        'b': 0,
        'sum': 0,
        'cin': 0,
        'cout': 0
    },
    'multiplier': {
        'a': 0,
        'b': 0,
        'product': 0
    }
}

> arithmetic_bundle.as_dict(multilevel=False)
{
    'selector': 1,
    'adder.a': 0,
    'adder.b': 0,
    'adder.sum': 0,
    'adder.cin': 0,
    'adder.cout': 0,
    'multiplier.a': 0,
    'multiplier.b': 0,
    'multiplier.product': 0
}

在自定义消息结构中,实现__bundle_assign__ 函数,其接收一个 Bundle 实例,将自定义消息结构赋值给 Bundle。实现后,可以通过 assign 方法赋值给 Bundle,Bundle 将会自动调用 __bundle_assign__ 函数进行赋值:

class MyMessage:
    def __init__(self):
        self.a = 0
        self.b = 0
        self.cin = 0

    def __bundle_assign__(self, bundle):
        bundle.a.value = self.a
        bundle.b.value = self.b
        bundle.cin.value = self.cin

my_message = MyMessage()
adder_bundle.assign(my_message)

在自定义消息实例中我们还可以实现一个from_bundle 的类方法,接收一个 Bundle 实例,返回一个自定义消息结构。可以通过 from_bundle 方法将 Bundle 中的信号值转换为自定义消息结构:

class MyMessage:
    def __init__(self):
        self.a = 0
        self.b = 0
        self.cin = 0

    @classmethod
    def from_bundle(cls, bundle):
        message = cls()
        message.a = bundle.a.value
        message.b = bundle.b.value
        message.cin = bundle.cin.value
        return message

my_message = MyMessage.from_bundle(adder_bundle)

Bundle类中有process_requests(data_list)函数,它接受一个数组输入,第i个时钟周期,会将data_list[i]对应的数据赋值给引脚。data_list中的数据可以是dict类型。以Adder为例,期望第三次加后返回结果:

# Adder虽然为存组合逻辑,但此处当时序逻辑使用
class AdderBundle(Bundle):
    a, b, sum, cin, cout = Signals(5)             # 指定引脚

    def __init__(self, dut):
        super().__init__()
        # init clock
        # dut.InitClock("clock")
        self.bind(dut)                            # 绑定到dut

    def add_list(data_list =[(1,2),(3,4),(5,6),(7,8)]):
        # make input dit
        data = []
        for i, (a, b) in enumerate(data_list):
            x = {"a":a, "b":b, "*":0}             # 构建budle赋值的dict
            if i >= 2:
                x["__return_bundles__"] = self    # 设置需要返回的bundle

                #此处疑似有bug
                
        return self.process_requests(data)        # 推动时钟,赋值,返回结果

可以通过 step 函数来完成时钟周期的等待:

async def adder_process(adder_bundle):
    adder_bundle.a.value = 1
    adder_bundle.b.value = 2
    adder_bundle.cin.value = 0
    await adder_bundle.step()
    print(adder_bundle.sum.value)
    print(adder_bundle.cout.value)

其他函数:

#检查 DUT 中未连接到任何 Bundle 的信号
Bundle.detect_unconnected_signals(adder)

#检查 DUT 中同时连接到多个 Bundle 的信号
Bundle.detect_multiple_connections(adder)

#设置 Bundle 的名称
adder_bundle.set_name('adder')

#all_signals 信号返回一个 generator,其中包含了包括子 Bundle 信号在内的所有信号
for signal in adder_bundle.all_signals():
    print(signal)

2.4 Bundle的自动生成脚本

在toffee仓库下的 scripts 文件夹中有 bundle_code_gen.py 脚本,该脚本可以通过解析 DUT 实例,以及指定的绑定规则自动生成 Bundle 的定义。

有三个函数如下,分别用于通过字典、前缀、正则表达式的方式生成 Bundle 的定义:

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):

使用时,指定Bundle名称,DUT实例,以及对应规则即可生成Bundle定义的代码:

from bundle_code_gen import *

gen_bundle_code_from_dict('AdderBundle', dut, {
    'a': 'io_a',
    'b': 'io_b',
    'sum': 'io_sum',
    'cin': 'io_cin',
    'cout': 'io_cout'
})
gen_bundle_code_from_prefix('AdderBundle', dut, 'io_')
gen_bundle_code_from_regex('AdderBundle', dut, r'io_(.*)')

3. Agent编写与使用

简单来说,Agent驱动方法监测方法组成,驱动方法用于主动驱动 Bundle 中的信号,而监测方法用于被动监测 Bundle 中的信号。

3.1 Agent初始化

初始化Agent需要自己定义一个新类,继承自toffee中的Agent 类:

from toffee.agent import *

class AdderAgent(Agent):
    def __init__(self, bundle):
        super().__init__(bundle.step)
        self.bundle = bundle

3.2 Agent驱动方法创建

在Agent中驱动方法是一个异步函数,是用@driver_method 装饰器进行修饰:

from toffee.agent import *

class AdderAgent(Agent):
    def __init__(self, bundle):
        super().__init__(bundle.step)
        self.bundle = bundle

    @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

3.3 监测方法创建

监测方法使用@monitor_method 装饰器进行修饰,简单的监测方法定义实例如下:

from toffee.agent import *

class AdderAgent(Agent):
    def __init__(self, bundle):
        super().__init__(bundle.step)
        self.bundle = bundle

    @monitor_method()
    async def monitor_sum(self):
        if self.bundle.sum.value > 0:
            return self.bundle.as_dict()

如何获得监测消息

没想好怎么总结

4. Env

Env用于在验证中打包整个环境,直接实例化验证环境中的所有Agent,并将Bundle传给它们。参考模型的编写规范是由Env确定的,按照规范编写的参考模型可以由Env来自动同步。

下面是一个简单的Env定义示例:

from toffee.env import *

class DualPortStackEnv(Env):
    def __init__(self, port1_bundle, port2_bundle):
        super().__init__()

        self.port1_agent = StackAgent(port1_bundle)
        self.port2_agent = StackAgent(port2_bundle)

在这个类中,我们实例化了两个Agent来驱动不同的Bundle。可以选择在Env外部连接Bundle,也可以在Env内部连接Bundle。我们可以直接编写测试用例并使用Env的接口:

port1_bundle = StackPortBundle()
port2_bundle = StackPortBundle()
env = DualPortStackEnv(port1_bundle, port2_bundle)

await env.port1_agent.push(1)
await env.port2_agent.push(1)
print(await env.port1_agent.pop())
print(await env.port2_agent.pop())

4.1 参考模型的添加

假设我有这样一个验证环境结构:

DualPortStackEnv
  - port1_agent
    - @driver_method push
    - @driver_method pop
    - @monitor_method some_monitor
  - port2_agent
    - @driver_method push
    - @driver_method pop
    - @monitor_method some_monitor

按照此方法编写的参考模型都可以直接附加到Env上面,由Env来进行自动同步:

env = DualPortStackEnv(port1_bundle, port2_bundle)
env.attach(StackRefModel())

5. 参考模型

在toffee验证环境中,参考模型可以有两种实现方法:函数调用模式独立执行流模式

函数调用模式即是将对外接口定义为一系列的函数,通过函数来驱动参考模型:

#加法器参考模型
class AdderRefModel():
    def add(self, a, b):
        return a + b

独立执行流模式将参考模型的行为定义为独立的执行流,拥有主动获取输入数据和主动输出数据的能力,当外部给参考模型发送数据时,参考模型不会立即响应,而是将这一数据保存起来,等待其执行逻辑主动获取该数据:

class AdderRefModel(Model):
    def __init__(self):
        super().__init__()

        self.add_port = DriverPort()
        self.sum_port = MonitorPort()

    async def main():
        while True:
            operands = await self.add_port()
            sum = operands["a"] + operands["b"]
            await self.sum_port(sum)

其中add_port用于接收外部输入数据,sum_port用于向外部输出数据。当上层代码给参考模型发送数据时,会将数据发送至add_port 这个驱动接口中,同时参考模型通过 sum_port 这个监测接口主动输出数据。

在参考模型中,main函数是参考模型的执行入口,当参考函数创建时,main函数会自动被调用,并在后台持续运行。

5.1 参考模型编写方法

假设Env中有以下接口定义:

StackEnv
  - port_agent
    - @driver_method push
    - @driver_method pop

我们需要在参考模型中我们需要对每一个驱动函数编写一个对应函数,当驱动函数被调用时,这些函数会被框架自动调用。我们使用@driver_hook 装饰器来表示这个函数是一个驱动函数的匹配函数,然后在装饰器中指定其对应Agent 和驱动函数的名称,只要保证函数参数与驱动函数一致,便能建立两者的对应关系:

class StackRefModel(Model):
    @driver_hook(agent_name="port_agent", driver_name="push")
    def push(self, data):
        pass

    @driver_hook(agent_name="port_agent", driver_name="pop")
    def pop(self):
        pass

5.2 Agent匹配

我们可以通过@agent_hook 装饰器来一次性匹配 Agent 中的所有驱动函数:

class StackRefModel(Model):
    @agent_hook("port_agent")
    def port_agent(self, driver_name, args):
        pass

port_agent 函数将会匹配 port_agent Agent 中的所有驱动函数,并自动调用。port_agent 函数接收两个参数,第一个为驱动函数的名称,第二个是驱动函数的参数

5.3 独立执行流模式的参考模型

在toffee中,使用DriverPort MonitorPort两种接口实现参考模型对的port接口。我们需要定义一系列的DriverPortMonitorPort使之与Env中的函数匹配。

5.3.1 驱动方法接口匹配

参考模型可以通过 DriverPort 的参数 agent_namedriver_name 来匹配 Env 中的驱动函数:

class StackRefModel(Model):
    def __init__(self):
        super().__init__()

        self.push_port = DriverPort(agent_name="port_agent", driver_name="push")
        self.pop_port = DriverPort(agent_name="port_agent", driver_name="pop")

5.3.2 Agent 接口匹配

我们也可以选择使用AgentPort 同时匹配一个 Agent 中的所有驱动函数,所有驱动函数的调用会被发送到AgentPort 中:

class StackRefModel(Model):
    def __init__(self):
        super().__init__()

        self.port_agent = AgentPort(agent_name="port_agent")

5.3.3 监测方法接口匹配

参考模型使用MonitorPort与Env中的监测函数匹配:

self.monitor_port = MonitorPort(agent_name="port_agent", monitor_name="monitor")

# 使用 "." 来指定监测函数的路径
self.monitor_port = MonitorPort("port_agent.monitor")

# 如果参考模型中的变量名称与监测函数名称相同,可以省略 monitor_name 参数
self.monitor = MonitorPort(agent_name="port_agent")

# 使用变量名称同时匹配 Agent 名称与监测函数名称,并使用 `__` 分隔
self.port_agent__monitor = MonitorPort()

6. 测试环境接口驱动

toffee为同时调用多个驱动函数提供了简便的方式。假设当前Env的目录结构如下:

DualPortStackEnv
  - port1_agent
    - @driver_method push
    - @driver_method pop
  - port2_agent
    - @driver_method push
    - @driver_method pop

我们需要在测试用例当中同时调用两个Agent的push函数,我们可以使用toffee中的 Executor 来完成:

from toffee import Executor

def test_push(env):
    async with Executor() as exec:
        exec(env.port1_agent.push(1))
        exec(env.port2_agent.push(2))

    print("result", exec.get_results())

我们使用async with 创建了一个 Executor 对象,直接调用exec就可以添加需要执行的驱动函数。get_results 方法会以字典的形式返回所有驱动函数的返回值,其中键为驱动函数的名称,值为一个列表,列表中存放了对应驱动函数的返回值。

当同一个驱动函数被多次调用时,Executor 会自动将这些调用串行执行:

from toffee import Executor

def test_push(env):
    async with Executor() as exec:
        for i in range(5):
            exec(env.port1_agent.push(1))
        exec(env.port2_agent.push(2))

    print("result", exec.get_results())

我们建立了一个如下所示的调度过程:

------------------  current time --------------------
  +---------------------+   +---------------------+
  | group "agent1.push" |   | group "agent2.push" |
  | +-----------------+ |   | +-----------------+ |
  | |   agent1.push   | |   | |   agent2.push   | |
  | +-----------------+ |   | +-----------------+ |
  | +-----------------+ |   +---------------------+
  | |   agent1.push   | |
  | +-----------------+ |
  | +-----------------+ |
  | |   agent1.push   | |
  | +-----------------+ |
  | +-----------------+ |
  | |   agent1.push   | |
  | +-----------------+ |
  | +-----------------+ |
  | |   agent1.push   | |
  | +-----------------+ |
  +---------------------+
------------------- Executor exit -------------------

通过使用sche_group 参数,我们可以在执行函数时手动指定驱动函数调用时所属的调度组,例如:

from toffee import Executor

def test_push(env):
    async with Executor() as exec:
        for i in range(5):
            exec(env.port1_agent.push(1), sche_group="group1")
        exec(env.port2_agent.push(2), sche_group="group1")

    print("result", exec.get_results())

port1_agent.pushport2_agent.push 将会被按顺序添加到同一个调度组 group1 中,表现出串行执行的特性。

6.1 测试案例一

假设我们的环境接口如下:

Env
- agent1
    - @driver_method send
- agent2
    - @driver_method send

两个 Agent 中的 send 函数各需要被并行调用 5 次,并且调用时需要发送上一次的返回结果,第一次发送时发送 0,两个函数调用相互独立。编写程序如下:

from toffee import Executor

async def send(agent):
    result = 0
    for i in range(5):
        result = await agent.send(result)

async def test_send(env):
    async with Executor() as exec:
        exec(send(env.agent1), sche_group="agent1")
        exec(send(env.agent2), sche_group="agent2")

    print("result", exec.get_results())

6.2 测试案例二

假设我们的环境接口如下:

env
- agent1
    - @driver_method long_task
- agent2
    - @driver_method task1
    - @driver_method task2

task1 和 task2 需要并行执行,并且一次调用结束后需要同步,task1 和 task2 都需要调用 5 次,long_task 需要与 task1 和 task2 并行执行。

from toffee import Executor

async def exec_once(env):
    async with Executor() as exec:
        exec(env.agent2.task1())
        exec(env.agent2.task2())

async def test_case(env):
    async with Executor() as exec:
        for i in range(5):
            exec(exec_once(env))
        exec(env.agent1.long_task())

    print("result", exec.get_results())

6.3 Executor 退出条件

可以在创建Executor 时使用 exit 参数来设置退出条件。当参数分别被设置为all, anynone时,代表所有调度组执行完毕后退出、任意一个调度组执行完毕后退出、不等待直接退出。

from toffee import Executor

async def send_forever(agent):
    result = 0
    while True:
        result = await agent.send(result)

async def test_send(env):
    async with Executor(exit="any") as exec:
        exec(send_forever(env.agent1))
        exec(env.agent2.send(1))

    print("result", exec.get_results())

6.4 参考模型调度

在使用 Executor 执行时,可以使用参数sche_order。具体的,当参数为model_first时,,参考模型会在驱动函数之前执行;当为 dut_first 时,驱动函数会在参考模型之前执行;当为 parallel 时,参考模型会与驱动函数同时执行。默认情况下为并行执行:

def test_push(env):
    async with Executor() as exec:
        exec(env.port1_agent.push(1), sche_order="dut_first")
        exec(env.port2_agent.push(2), sche_order="dut_first")

    print("result", exec.get_results())

toffee为具有调用顺序的函数提供了一个priority 参数,用于指定参考模型函数的调用顺序,数值越小其优先级较高:

from toffee import Executor

def test_push(env):
    async with Executor() as exec:
        exec(env.port1_agent.push(1), priority=1)
        exec(env.port2_agent.push(2), priority=0)

    print("result", exec.get_results())

7. Pytest测试用例管理

7.1 测试用例编写

我们需要创建一个测试用例文件,以test_ 开头,或以 _test.py 结尾,以便 pytest 能够识别:

# test_adder.py

async def my_test():
    env = AdderEnv()
    env.add_agent.exec_add(1, 2, 0)

def test_adder():
    toffee.run(my_test())

在终端中运行pytest:

pytest

pytest会查找当前目录下所有以 test_ 开头或以 _test.py 结尾的文件,并运行其中以 test_ 开头的函数。

为例使pytest能够直接运行协程测试用例,toffee使用toffee_async 标记来标记异步测试用例。

# test_adder.py

@pytest.mark.toffee_async
async def test_adder():
    env = AdderEnv(DUTAdder())
    await env.add_agent.exec_add(1, 2, 0)

只要在测试用例函数上添加@pytest.mark.toffee_async 标记,pytest 就能够直接运行协程测试用例。

7.2 测试报告生成

在调用pytest时添加--toffee-report 参数,可以生成验证报告:

pytest --toffee-report

如果想要在报告中显示覆盖率信息,需要在每个测试用例中传入功能覆盖组及行覆盖率文件的名称:

@pytest.mark.toffee_async
async def test_adder(request):
    adder = DUTAdder(
        waveform_filename="adder.fst",
        coverage_filename="adder.dat"
    )
    g = CovGroup("Adder")

    env = AdderEnv(adder)
    await env.add_agent.exec_add(1, 2, 0)

    adder.Finish()
    set_func_coverage(request, cov_groups)
    set_line_coverage(request, "adder.dat")

7.3 toffee-test 资源管理

toffee-test提供了toffee_request Fixture 来管理资源,简化了测试用例的编写:

# test_adder.py

@pytest.mark.toffee_async
async def test_adder(my_request):
    dut = my_request
    env = AdderEnv(dut)
    await env.add_agent.exec_add(1, 2, 0)

@pytest.fixture()
def my_request(toffee_request: ToffeeRequest):
    toffee_request.add_cov_groups(CovGroup("Adder"))
    return toffee_request.create_dut(DUTAdder)

通过 add_cov_groups 添加覆盖组,toffee-test 会自动将其生成至报告中。 通过 create_dut 创建 DUT 实例,toffee-test 会自动管理 DUT 的波形文件和覆盖率文件的生成,并确保文件名称不产生冲突。

如果想要任意测试用例都能访问到该Fixture,可以将 Fixture 定义在 conftest.py 文件中。

8. 功能覆盖率

在toffee中,功能检查点(Cover Point) 是指对设计的某个功能进行验证的最小单元。测试组(Cover Croup) 是一类检查点的集合。

8.1 检查点编写

首先我们需要创建一个测试组,并指定测试组的名称:

import toffee.funcov as fc

g = fc.CovGroup("Group-A")

需要在这个测试组中添加测试点,一个功能点通常对应一个或者多个检查点来检查是否满足功能。例如我们需要检查Addercout是否有0出现,我们可以通过如下方式添加:

g.add_watch_point(adder.io_cout,
                  {"io_cout is 0": fc.Eq(0)},
                  name="cover_point_1")

其中函数add_watch_point的参数说明如下:

def add_watch_point(target,
                    bins: dict,
                    name: str = "", once=None):
        """
        @param target: 检查目标,可以是一个引脚,也可以是一个DUT对象
        @param bins: 检查条件,dict格式,key为条件名称,value为具体检查方法或者检查方法的数组。
        @param name: 检查点名称
        @param once,如果once=True,表明只检查一次,一旦该检查点满足要求后就不再进行重复条件判断。

funcov模块内部实现了一些检查函数,例如Eq(x), Gt(x), Lt(x), Ge(x), Le(x), Ne(x), In(list), NotIn(list), isInRange([low,high])等,我们也可以自定义检查函数,输入参数是target, 返回值是bool,例如:

g.add_watch_point(adder.io_cout,
                  {
                    "io_cout is 0": lambda x: x.value == 0,
                    "io_cout is 1": lambda x: x.value == 1,
                    "io_cout is x": [fc.Eq(0), fc.In([0,1]), lambda x:x.value < 4],
                  },
                  name="cover_point_1")

当添加完所有的检查点后,在DUTStep回调函数中调用CovGroupsample()方法进行判断。在检查过程中,或者测试运行完后,可以通过CovGroupas_dict()方法查看检查情况:

dut.StepRis(lambda x: g.sample())

...

print(g.as_dict())

8.2 在测试报告中展示

在测试case每次运行结束时,可以通过set_func_coverage(request, cov_groups)告诉框架对所有的功能覆盖情况进行合并收集。相同名字的CoverGroup会被自动合并。下面是一个简单的例子:

import pytest
import toffee.funcov as fc
from toffee_test.reporter import set_func_coverage

g = fc.CovGroup("Group X")

def init_function_coverage(g):
    # add your points here
    pass

@pytest.fixture()
def dut_input(request):
    # before test
    init_function_coverage(g)
    dut = DUT()
    dut.InitClock("clock")
    dut.StepRis(lambda x: g.sample())
    yield dut
    # after test
    dut.Finish()
    set_func_coverage(request, g)
    g.clear()

def test_case1(dut_input):
    assert True

def test_case2(dut_input):
    assert True

# ...
posted @ 2025-01-09 22:10  史茗宇  阅读(136)  评论(0)    收藏  举报