如何编写参考模型
参考模型 用于模拟待验证设计的行为,以便在验证过程中对设计进行验证。在 toffee 验证环境中,参考模型需要遵循 Env 的接口规范,以便能够附加到 Env 上,由 Env 来完成参考模型的自动同步。
参考模型的两种实现方式
toffee 提供了两种参考模型的实现方式,这两种方式都可以被附加到 Env 上,并由 Env 来完成参考模型的自动同步。在不同的场景下,可以选择更适合的方式来实现参考模型。
这两种方式分别是 函数调用模式 与 独立执行流模式,下面将分别介绍这两种方式的具体概念。
函数调用模式
函数调用模式即是将参考模型的对外接口定义为一系列的函数,通过调用这些函数来驱动参考模型的行为。此时,我们通过输入参数向参考模型发送数据,并通过返回值获取参考模型的输出数据,参考模型通过函数体的逻辑来更新内部状态。
下面是一个简单的函数调用模式的参考模型的定义示例:
例如,这是一个简单的加法器参考模型:
class AdderRefModel:
def add(self, a, b):
return a + b
在这个参考模型中,不需要任何内部状态,通过一个对外函数接口即可实现参考模型所有功能。
独立执行流模式
独立执行流模式即是将参考模型的行为定义为一个独立的执行流,它不再受外部主动调用函数控制,而拥有了主动获取输入数据的能力。当外部给参考模型发送数据时,参考模型不会立即响应,而是将这一数据保存起来,等待其执行逻辑主动获取该数据。
我们用一段代码来说明这种模式,该示例中用到了 toffee 中提供的相关概念来实现,但目前无需关心这些概念的使用细节。
class AdderRefModel(Model):
def __init__(self):
super().__init__()
self.add_port = DriverPort(agent_name="add_agent", driver_name="add")
self.sum_port = MonitorPort(agent_name="add_agent", monitor_name="sum")
async def main():
while True:
operands = await self.add_port()
std_sum = operands["a"] + operands["b"]
dut_sum = await self.sum_port()
assert std_sum == dut_sum, f"Expected {std_sum}, but got {dut_sum}"
在这里,我们在参考模型构造函数中定义了两类接口,一类为驱动接口(DriverPort),即代码中的add_port,用于接收测试代码通过驱动函数向DUT输入的数据;另一类为监测接口(MonitorPort),即代码中的sum_port,用于接收监测函数监测到的DUT输出数据。 定义了这两个接口后,上层代码在给参考模型发送数据时,并不会触发参考模型中的某个函数,而是会将数据发送到 add_port 这个驱动接口中。同时,DUT的输出数据也会被发送到 sum_port 这个监测接口中。
那么参考模型如何去使用这两个接口呢?在参考模型中,有一个 main 函数,这是参考模型执行的入口,当参考模型创建时, main 函数会被自动调用,并在后台持续运行。在上面代码中 main 函数里,参考模型通过不断重复这一过程:等待 add_port 中的数据、计算结果、获取 sum_port 中的数据、比较结果,来完成参考模型的验证工作。
参考模型会主动向 add_port 请求数据,如果 add_port 中没有数据,参考模型会等待数据的到来。当数据到来后,参考模型将会进行计算,之后参考模型再次主动等待 sum_port 中的数据到来。它的执行过程是一个独立的执行流,不受外部的主动调用控制。当参考模型变得复杂时,其将会含有众多的驱动接口和监测接口,通过独立执行流的方式,可以更好的去处理结构之间的相互关系,尤其是接口之间存在调用顺序的情况。
如何编写函数调用模式的参考模型
驱动函数匹配
假如 Env 中定义的接口如下:
StackEnv
- port_agent
- @driver_method push
- @driver_method pop
- @monitor_method monitor_pop_data
那么如果我们想要编写与之对应的参考模型,自然地,我们需要定义这四个驱动函数被调用时参考模型的行为。也就是说为每一个驱动函数编写一个对应的函数,这些函数将会在驱动函数被调用时被框架自动调用。
如何让参考模型中定义的函数能够与某个驱动函数匹配呢?首先应该使用 @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
此时,驱动函数与参考模型的对应关系已经建立,当 Env 中的某个驱动函数被调用时,参考模型中对应的函数将会被自动调用,并自动对比两者的返回值是否一致。
toffee 还提供了以下几种匹配方式,以便更方便地匹配驱动函数:
指定驱动函数路径
可以通过 "." 来指定驱动函数的路径,例如:
class StackRefModel(Model):
@driver_hook("port_agent.push")
def push(self, data):
pass
@driver_hook("port_agent.pop")
def pop(self):
pass
使用函数名称匹配驱动函数名称
如果参考模型中的函数名称与驱动函数名称相同,可以省略 driver_name 参数,例如:
class StackRefModel(Model):
@driver_hook(agent_name="port_agent")
def push(self, data):
pass
@driver_hook(agent_name="port_agent")
def pop(self):
pass
使用函数名称同时匹配 Agent 名称与驱动函数名称
可以在函数名中通过双下划线 "__" 来同时匹配 Agent 名称与驱动函数名称,例如:
class StackRefModel(Model):
@driver_hook()
def port_agent__push(self, data):
pass
@driver_hook()
def port_agent__pop(self):
pass
监测函数匹配
Toffee 目前支持检测函数的匹配,通过 @monitor_hook 装饰器来表示这个函数是一个监测函数的匹配函数。与 @driver_hook 类似,为了建立对应关系,需要在装饰器中指定其对应的 Agent 和监测函数的名称,例如:
class StackRefModel(Model):
@monitor_hook(agent_name="port_agent", monitor_name="monitor_pop_data")
def monitor_pop_data(self, item):
pass
monitor_hook 含有一个固定的额外参数,例如上面代码中的 item,用于接收监测函数的返回值。当 Env 中的监测函数被调用时,参考模型中对应的 monitor_hook 函数将会被自动调用,在函数体的实现中可以判断监测函数的返回值是否符合预期。
monitor_hook 支持 driver_method 所支持的所有匹配方式。
Agent 匹配
除了 driver_hook 与 monitor_hook 两种匹配单一函数的 Hook 之外,Toffee 还提供了 agent_hook,用于一次性匹配多个驱动函数或监测函数,方式如下:
class StackRefModel(Model):
@agent_hook("port_agent")
def port_agent(self, name, item):
pass
在这个例子中,port_agent 函数将会匹配 port_agent Agent 中的所有驱动函数与监测函数。当 Agent 中的任意一个驱动函数被调用时,port_agent 都会被自动调用,并将驱动函数的名称与参数传入。当 Agent 中的任意一个监测函数被调用时,port_agent 也会被自动调用,并将监测函数的名称与返回值传入。此外,如果 agent_hook 有返回值,框架会使用此函数的返回值与驱动函数的返回值进行对比。
与驱动函数类似,@agent_hook 装饰器也支持当函数名与 Agent 名称相同时省略 agent_name 参数。
class StackRefModel(Model):
@agent_hook()
def port_agent(self, driver_name, args):
pass
如果需要同时匹配多个 Agent,可以使用 agent_hook 中的 agents 参数,例如:
class StackRefModel(Model):
@agent_hook(agents=["port_agent", "port_agent2"])
def port_agent(self, driver_name, args):
pass
如果需要同时匹配多个驱动函数或监测函数,可以使用 agent_hook 中的 methods 参数,并指定需要匹配的驱动函数或监测函数的路径,例如:
class StackRefModel(Model):
@agent_hook(methods=["port_agent.push", "port_agent.pop", "port_agent2.monitor_pop_data"])
def port_agent(self, driver_name, args):
pass
与早期版本有差异
agent_hook目前可以独立使用,不需要与driver_hook相关。
Hook 的调用顺序
对于以上三类 Hook,它们在同一周期的调用顺序均可以自行配置。默认情况下,monitor_hook 会在 driver_hook 和 agent_hook 之前被调用。如果需要调整这些 Hook 的调用顺序,可以通过 priority 参数来指定,数值越小,优先级越高,例如:
class StackRefModel(Model):
@driver_hook(agent_name="port_agent", driver_name="push", priority=1)
def push(self, data):
pass
@monitor_hook(agent_name="port_agent", monitor_name="monitor_pop_data", priority=2)
def monitor_pop_data(self, item):
pass
对于 driver_hook 以及 agent_hook 来说,还可以指定其与 driver_method 之间的调用顺序。默认情况下,当 driver_method 被调用时,driver_hook 与 agent_hook 会在 driver_method 之前被调用。
如果需要调整其调用顺序,可以通过 sche_order 参数来指定:
当
sche_order为model_first时driver_method会在driver_hook与agent_hook之前被调用当
sche_order为dut_first时,driver_hook与agent_hook会在driver_method之后被调用
例如:
class StackRefModel(Model):
@driver_hook(agent_name="port_agent", driver_name="push", sche_order="dut_first")
def push(self, data):
pass
如何编写独立执行流模式的参考模型
独立执行流模式的参考模型是通过 port 接口的形式来得到外界的数据,可以主动从 port 中获取数据。在 toffee 中,我们提供了两种接口来实现这一功能,分别是 DriverPort 和 MonitorPort。
类似地,我们需要定义一系列的 DriverPort 使其与 Env 中的驱动函数匹配,同时定义一系列的 MonitorPort 使其与 Env 中的监测函数匹配。
当 Env 中的驱动函数被调用时,调用数据将会被发送到 DriverPort 中,参考模型将会主动获取这些数据,并进行计算。同时,当 Env 中的监测函数被调用时,返回数据将会被发送到 MonitorPort 中,参考模型将会主动获取这些数据,并进行比较。为了在接口中获取数据,需要调用 DriverPort 或 MonitorPort 实例,并通过 await 关键字来等待数据,例如:
与早期版本有差异
MonitorPort的调用更改为了从MonitorPort实例中获取数据。
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")
self.monitor_pop_data_port = MonitorPort(agent_name="port_agent", monitor_name="monitor_pop_data")
async def main(self):
while True:
push_data = await self.push_port()
pop_data = await self.pop_port()
monitor_pop_data = await self.monitor_pop_data_port()
# do something
驱动方法接口匹配
为了接收到 Env 中所有的驱动函数的调用,参考模型可以选择为需要获取驱动函数编写对应的 DriverPort。可以通过 DriverPort 的参数 agent_name 与 driver_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")
与 driver_hook 类似,也可以使用下面的方式来匹配 Env 中的驱动函数:
# 使用 "." 来指定驱动函数的路径
self.push_port = DriverPort("port_agent.push")
# 如果参考模型中的变量名称与驱动函数名称相同,可以省略 driver_name 参数
self.push = DriverPort(agent_name="port_agent")
# 使用变量名称同时匹配 Agent 名称与驱动函数名称,并使用 `__` 分隔
self.port_agent__push = DriverPort()
监测方法接口匹配
为了与 Env 中的监测函数匹配,参考模型需要为每一个监测函数编写对应的 MonitorPort,定义方法与 DriverPort 一致。
class StackRefModel(Model):
def __init__(self):
super().__init__()
self.monitor_pop_data_port = MonitorPort(agent_name="port_agent", monitor_name="monitor_pop_data")
类似的,MonitorPort 支持 DriverPort 中的所有匹配方式。
Agent 接口匹配
也可以选择定义 AgentPort 同时匹配任意的驱动函数和监测函数,例如
class StackRefModel(Model):
def __init__(self):
super().__init__()
self.port_agent = AgentPort(agent_name="port_agent")
类似的,当变量名称与 Agent 名称相同时,可以省略 agent_name 参数:
self.port_agent = AgentPort()
同时,也可以使用 agents 参数来匹配多个 Agent,或使用 methods 参数来匹配多个驱动函数或监测函数。
使用了 AgentPort 之后,参考模型可以通过 await 关键字来等待 Env 中的驱动函数或监测函数的调用,其返回数据将会是包含了函数路径以及相关参数的元组。