如何使用 Bundle
Bundle 在 toffee 验证环境中,用于构建 Agent 与 DUT 之间交互的中间层,以保证 Agent 与 DUT 之间的解耦。同时 Bundle 也起到了对 DUT 接口层次结构划分的作用,使得对 DUT 接口的访问变得更加清晰、方便。
一个简单的 Bundle 的定义
为了定义一个 Bundle,需要自定义一个新类,并继承 toffee 中的 Bundle 类。下面是一个简单的 Bundle 的定义示例:
from toffee import Bundle, Signals
class AdderBundle(Bundle):
a, b, sum, cin, cout = Signals(5)
该 Bundle 定义了一个简单的加法器接口,在 AdderBundle 类中,我们定义了五个信号 a, b, sum, cin, cout,这五个信号分别代表了加法器的输入端口 a, b,输出端口 sum,以及进位输入端口 cin 和进位输出端口 cout。
定义完成后,我们可以通过 AdderBundle 类的实例来访问这些信号,例如:
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)
将 DUT 绑定到 Bundle
在上述代码中,我们虽然创建了一个 bundle 实例,并对他进行了驱动,但是我们并没有将这个 bundle 与任何 DUT 绑定,也就意味着对这个 bundle 的操作,无法真正影响到 DUT。
使用 bind 方法,可以将一个 DUT 绑定到 bundle 上。例如我们有一个简单的加法器 DUT,其接口名称与 Bundle 中定义的名称相同。
adder = DUTAdder()
adder_bundle = AdderBundle()
adder_bundle.bind(adder)
bind 函数会自动检索 DUT 中所有的接口,并将名称相同的接口进行绑定。绑定完成后,对 bundle 的操作,就会直接影响到 DUT。
但是,如果 DUT 的接口名称与 Bundle 中定义的名称不同,直接使用 bind 则无法正确绑定。在 Bundle 中,我们提供多种绑定方法,以适应不同的绑定需求。
通过字典进行绑定
在 bind 函数中,我们可以通过传入一个字典,来指定 DUT 中的接口名称与 Bundle 中的接口名称之间的映射关系。
假设 Bundle 中的接口名称与 DUT 中的接口名称拥有如下对应关系:
a -> a_in
b -> b_in
sum -> sum_out
cin -> cin_in
cout -> cout_out
在实例化 bundle 时,我们可以通过 from_dict 方法创建,并传入一个字典,告知 Bundle 以字典的方式进行绑定。
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)
此时,adder_bundle 可正确绑定至 adder。
通过前缀进行绑定
假设 Bundle 中的接口名称与 DUT 中的接口名称拥有如下对应关系:
a -> io_a
b -> io_b
sum -> io_sum
cin -> io_cin
cout -> io_cout
可以发现,实际 DUT 的接口名称相比于 Bundle 中的接口名称,都多了一个 io_ 的前缀。在这种情况下,我们可以通过 from_prefix 方法创建 Bundle,并传入前缀名称,告知 Bundle 以前缀的方式进行绑定。
adder = DUTAdder()
adder_bundle = AdderBundle.from_prefix('io_')
adder_bundle.bind(adder)
通过正则表达式进行绑定
在某些情况下,DUT 中的接口名称与 Bundle 中的接口名称之间的对应关系并不是简单的前缀或者字典关系,而是更为复杂的规则。例如,Bundle 中的接口名称与 DUT 中的接口名称之间的对应关系为:
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)
使用正则表达式时,Bundle 会尝试将 DUT 中的接口名称与正则表达式进行匹配,匹配成功的接口,将会读取正则表达式中的所有捕获组,将其连接为一个字符串。再使用这个字符串与 Bundle 中的接口名称进行匹配。
例如对于上面代码中的正则表达式,io_a_in 会与正则表达式成功匹配,唯一的捕获组捕获到的内容为 a。a 这个名称与 Bundle 中的接口名称 a 匹配,因此 io_a_in 会被正确绑定至 a。
创建子 Bundle
很多时候,我们会需要一个 Bundle 包含一个或多个其他 Bundle 的情况,这时我们可以将其他已经定义好的 Bundle 作为当前 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。除此之外它还包含了一个 AdderBundle 和一个 MultiplierBundle,这两个子 Bundle 分别被命名为 adder 和 multiplier。
当我们需要访问 ArithmeticBundle 中的子 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 也绑定到 DUT 上,在定义子 Bundle 时,依然可以使用前文提到的多种绑定方式。
需要注意的是,子 Bundle 的创建方法去匹配的信号名称,是经过上一次 Bundle 的创建方法进行处理过后的名称。例如在上面的代码中,我们将顶层 Bundle 的匹配方式设置为 from_prefix('io_'),那么在 AdderBundle 中去匹配的信号,是去除了 io_ 前缀后的名称。
同时,字典匹配方法会将信号名称转换为字典映射后的名称传递给子 Bundle 进行匹配,正则表达式匹配方法会将正则表达式捕获到的名称传递给子 Bundle 进行匹配。
创建 SignalList
在某些情况下,会存在有一组信号的名称相似,且数量较多,例如以下情况:
io_vec_0
io_vec_1
io_vec_2
...
io_vec_9
如果使用 Signal 一个一个定义,会显得非常繁琐。为了解决这个问题,toffee 提供了 SignalList 类,用于定义一组信号。按照如下方式使用:
from toffee import Bundle, SignalList
class VectorBundle(Bundle):
vec = SignalList("vec_#", 10)
上面的代码定义了一个 VectorBundle,它包含了一个 vec 信号列表,该信号列表包含了 10 个信号,信号名称分别为 vec_0, vec_1, vec_2, ..., vec_9。这取决于 SignalList 定义时传入的名称字符串vec_# ,其中的 # 根据第二个参数 10 被替换为 0 到 9。
在实例化 VectorBundle 后,我们可以通过 vec 来访问这一组信号:
vector_bundle = VectorBundle()
vector_bundle.vec[0].value = 1
vector_bundle.vec[1].value = 2
...
vector_bundle.vec[9].value = 10
如果想要自定义从数字到信号名称的映射,可以通过传入一个函数来实现,例如:
def custom_name_func(i):
return f"vec_{i + 1}"
class VectorBundle(Bundle):
vec = SignalList("vec_#", 10, custom_name_func)
创建 BundleList
类似的,还有可能会存在有一组 Bundle 的情况,例如以下情况:
io_vec0_a
io_vec0_b
io_vec0_c
io_vec1_a
io_vec1_b
io_vec1_c
...
io_vec9_a
io_vec9_b
io_vec9_c
如果使用多个 SubBundle 一个一个定义,也会非常繁琐。为此,Toffee 还提供了 BundleList 类,用于定义一组 Bundle。按照如下方式使用:
from toffee import Bundle, BundleList
class SubBundle(Bundle):
a, b, c = Signals(3)
class VectorBundle(Bundle):
vec = BundleList(SubBundle, "vec#_", 10)
上面的代码定义了一个 VectorBundle,它包含了一个 vec Bundle 列表,该 Bundle 列表包含了 10 个 SubBundle,每个 SubBundle 的匹配方法都被指定为前缀匹配,待匹配前缀分别为 vec0_, vec1_, vec2_, ..., vec9_。如果要访问这一组 Bundle,可以通过 vec 来访问:
vector_bundle = VectorBundle()
vector_bundle.vec[0].a.value = 1
vector_bundle.vec[0].b.value = 2
vector_bundle.vec[0].c.value = 3
...
vector_bundle.vec[9].a.value = 28
vector_bundle.vec[9].b.value = 29
vector_bundle.vec[9].c.value = 30
与 SignalList 类似,如果想要自定义从数字到 Bundle 名称的映射,可以通过传入一个函数来实现,例如:
def custom_name_func(i):
return f"vec{i + 1}_"
class VectorBundle(Bundle):
vec = BundleList(SubBundle, "vec#_", 10, custom_name_func)
Bundle 中的实用操作
信号访问与赋值
访问信号值
在 Bundle 中,我们不仅可以通过 . 运算符来访问 Bundle 中的信号,也可以通过 [] 运算符来访问 Bundle 中的信号。
adder_bundle = AdderBundle()
adder_bundle['a'].value = 1
访问未连接信号
def bind(self, dut, unconnected_signal_access=True)
在 bind 时,我们可以通过传入 unconnected_signal_access 参数来控制是否允许访问未连接的信号。默认为 True,即允许访问未连接的信号,此时当写入该信号时,该信号不会发生变化,当读取该信号时,会返回 None。 当 unconnected_signal_access 为 False 时,访问未连接的信号会抛出异常。
同时赋值所有信号
可以通过 set_all 方法同时将所有输入信号更改为某个值。
adder_bundle.set_all(0)
随机赋值所有信号
可以通过 randomize_all 方法随机赋值所有信号。"value_range" 参数用于指定随机值的范围,"exclude_signals" 参数用于指定不需要随机赋值的信号,"random_func" 参数用于指定随机函数。
adder_bundle.randomize_all()
信号赋值模式更改
信号赋值模式是 picker 中的概念,用于控制信号的赋值方式,请查阅 picker 文档以了解更多信息。
Bundle 中支持通过 set_write_mode 来改变整个 Bundle 的赋值模式。
同时,Bundle 提供了设置的快捷方法:set_write_mode_as_imme, set_write_mode_as_rise 与 set_write_mode_as_fall,分别用于设置 Bundle 的赋值模式为立即赋值、上升沿赋值与下降沿赋值。
消息支持
默认消息类型赋值
toffee 支持一个默认的消息类型,可以通过 assign 方法将一个字典赋值给 Bundle 中的信号。
adder_bundle.assign({
'a': 1,
'b': 2,
'cin': 0
})
Bundle 将会自动将字典中的值赋值给对应的信号,当需要将未指定的信号赋值成某个默认值时,可以通过 * 来指定默认值:
adder_bundle.assign({
'*': 0,
'a': 1,
})
子 Bundle 的默认消息赋值支持
如果希望通过默认消息类型同时赋值子 Bundle 中的信号,可以通过两种方式实现。当 assign 中的 multilevel 参数为 True 时,Bundle 支持多级字典赋值。
arithmetic_bundle.assign({
'selector': 1,
'adder': {
'*': 0,
'cin': 0
},
'multiplier': {
'a': 3,
'b': 4
}
}, multilevel=True)
当 multilevel 为 False 时,Bundle 支持通过 . 来指定子 Bundle 的赋值。
arithmetic_bundle.assign({
'*': 0,
'selector': 1,
'adder.cin': 0,
'multiplier.a': 3,
'multiplier.b': 4
}, multilevel=False)
默认消息类型读取
在 Bundle 中可以使用,as_dict 方法将 Bundle 当前的信号值转换为字典。其同样支持两种格式,当 multilevel 为 True 时,返回多级字典;当 multilevel 为 False 时,返回扁平化的字典。
> 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 中的信号。
一种方法是,在自定义消息结构中,实现 as_dict 函数,将自定义消息结构转换为字典,然后通过 assign 方法赋值给 Bundle。
另一种方法是,在自定义消息结构中,实现 __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)
当需要将 Bundle 中的信号值转换为自定义消息结构时,简易在自定义消息结构中实现 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 类除了对 DUT 的引脚进行封装外,还提供了基于数组的时序封装,可以适用于部分简单时序场景。Bundle 类提供了process_requests(data_list)函数,他接受一个数组输入,第i个时钟周期,会将data_list[i]对应的数据赋值给引脚。data_list中的数据可以是dict类型,或者callable(cycle, bundle_ins)类型的可调用对象。对于dict类型,特殊key有:
__funcs__: func(cycle, self) # 可调用对象,可以为函数数组[f1,f2,..]
__condition_func__: func(cycle, slef, cargs) # 条件函数,当改函数返回为true时,进行赋值,否则继续推进时钟
__condition_args__: cargs # 条件函数需要的参数
__return_bundles__: bundle # 需要本次dict赋值时返回的bundle数据,可以是list[bundle]
如果输入的dict中有__return_bundles__,则函数会返回该输入对应的 bundle 值,例如{"data": x, "cycle": y}。以 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
data.append(X)
return self.process_requests(data) # 推动时钟,赋值,返回结果
当调用add_list()后,返回的结果为:
[
{"data": {"a":5, "b":6, "cin": 0, "sum":11, "cout": 0}, "cycle":3},
{"data": {"a":7, "b":8, "cin": 0, "sum":15, "cout": 0}, "cycle":4}
]
异步支持
在 Bundle 中,为了方便的接收时钟信息,提供了 step 函数。当 Bundle 连接至 DUT 的任意一个信号时,step 函数会自动同步至 DUT 的时钟信号。
可以通过 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)
信号连接
信号连接规则
当定义好 Bundle 实例后,可以调用 all_signals_rule 方法,获取所有信号的连接规则,以帮助用户检查信号的连接规则是否符合预期。
adder_bundle.all_signals_rule()
信号可连接性检查
detect_connectivity 方法可以检查一个特定的信号名称是否可以连接到该 Bundle 中的某个信号。
adder_bundle.detect_connectivity('io_a')
detect_specific_connectivity 方法可以检查一个特定的信号名称是否可以连接到该 Bundle 中的某个特定的信号。
adder_bundle.detect_specific_connectivity('io_a', 'a')
如果需要检测子 Bundle 的信号连接性,可以通过 . 运算符来指定。
DUT 信号连接检查
未连接信号检查
detect_unconnected_signals 方法可以检查 DUT 中未连接到任何 Bundle 的信号。
Bundle.detect_unconnected_signals(adder)
重复连接检查
detect_multiple_connections 方法可以检查 DUT 中同时连接到多个 Bundle 的信号。
Bundle.detect_multiple_connections(adder)
其他实用操作
设置 Bundle 名称
可以通过 set_name 方法设置 Bundle 的名称。
adder_bundle.set_name('adder')
设置名称之后,将会得到更加直观的提示信息。
获取 Bundle 中所有信号
all_signals 信号会返回一个 generator,其中包含了包括子 Bundle 信号在内的所有信号。
for signal in adder_bundle.all_signals():
print(signal)
Bundle 的自动生成脚本
在很多情况下,DUT 的接口可能过于复杂,手动去编写 Bundle 的定义会变得非常繁琐。然而,Bundle 作为中间层,提供一个确切的信号名称定义是必要的。为了解决这个问题,toffee 提供了一个自动生成 Bundle 的脚本来从 DUT 的接口定义中生成 Bundle 的定义。
可以在 toffee 仓库目录下的 scripts 文件夹中找到 bundle_code_gen.py 脚本。该脚本可以通过解析 DUT 实例,以及指定的绑定规则自动生成 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 的定义。
使用时,指定 Bundle 的名称,DUT 实例,以及对应的生成规则便可生成 Bundle 的定义,还可以通过 max_width 参数来指定生成的代码的最大宽度。
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_(.*)')
生成好的代码可以直接或经过简单的修改后,复制到代码中使用。也可以作为子 Bundle 的定义,应用到其他 Bundle 中。