编写可复用的验证组件
在硬件验证中,我们经常需要编写可复用的验证组件(如 Monitor、Scoreboard),以便在不同的测试环境和设计中复用。Verilua 提供了强大的数据结构(Bundle、AliasBundle)以及类型检查工具(TypeExpect),帮助我们构建真正可复用的组件。
问题:组件与设计紧耦合
假设我们有一个 Monitor 组件,用于监控两个信号:valid 和 value。它的实现可能是这样的:
local class = require "pl.class"
local texpect = require "verilua.TypeExpect"
local Monitor = class()
function Monitor:_init(signals)
texpect.expect_bdl(signals, "signals")
self.signals = signals
end
function Monitor:sample(cycles)
if self.signals.valid:is(1) then
print("[Monitor] get value =>", self.signals.value:get_hex_str(), "at", cycles)
end
end
return Monitor
这个组件期望传入一个包含 valid 和 value 两个信号的 Bundle。现在有两个不同的设计:
- DUT_A:信号名为
valid和value,层次路径为tb_top.path.to.mod - DUT_B:信号名为
vld和value_2,层次路径为tb_top.another.path.to.mod
如果直接将此组件用于 DUT_B,就必须修改组件内部代码(将 valid 改为 vld,value 改为 value_2),这样组件就失去了复用性。
解决方案一:使用 Bundle(仍不理想)
使用 Bundle 可以将信号打包,但信号名称仍然固定。在 DUT_A 中可以这样使用:
local signals_bdl = ([[
| valid
| value
]]):bdl {
hier = "tb_top.path.to.mod",
prefix = "",
is_decoupled = false
}
local mon = Monitor(signals_bdl)
但在 DUT_B 中,为了复用,我们必须创建一个名称完全匹配的 Bundle,这要求信号名恰好是 valid 和 value。如果 DUT_B 中信号名不同,就必须在测试代码中重新定义,或者修改 Monitor 的代码。Bundle 本身并不能解决信号名称差异的问题。
解决方案二:使用 AliasBundle(推荐)
AliasBundle 允许为信号创建别名,从而将实际信号名与组件内部使用的名称解耦。组件内部只关心别名,而外部测试代码负责将实际信号映射到这些别名上。
定义组件时声明所需信号的别名
local class = require "pl.class"
local texpect = require "verilua.TypeExpect"
local Monitor = class()
function Monitor:_init(signals)
-- 检查 signals 是一个 AliasBundle,并且包含必需的别名 valid 和 value
texpect.expect_abdl(signals, "signals", { "valid", "value" })
self.signals = signals
end
function Monitor:sample(cycles)
if self.signals.valid:is(1) then
print("[Monitor] get value =>", self.signals.value:get_hex_str(), "at", cycles)
end
end
return Monitor
关键点:
- 使用
texpect.expect_abdl(signals, "signals", { "valid", "value" })确保传入的是一个AliasBundle,并且其中必须包含别名为valid和value的信号。 - 组件内部通过别名
valid和value访问信号,与实际信号名无关。
在不同设计中映射实际信号到别名
DUT_A(信号名与别名恰好一致,可以直接映射):
local signals_bdl = ([[
| valid => valid
| value => value
]]):abdl {
hier = "tb_top.path.to.mod",
prefix = ""
}
-- 如果信号名和别名相同,也可以简写为:
-- local signals_bdl = ([[ | valid | value ]]):abdl { hier = "..." }
local mon = Monitor(signals_bdl)
DUT_B(信号名不同,通过 => 显式映射):
local signals_bdl = ([[
| vld => valid -- 实际信号 vld 映射到别名 valid
| value_2 => value -- 实际信号 value_2 映射到别名 value
]]):abdl {
hier = "tb_top.another.path.to.mod",
prefix = ""
}
local mon = Monitor(signals_bdl)
这样,Monitor 的代码无需任何修改,就能在 DUT_B 中复用。
特殊情况:某些信号不存在怎么办?
有时不同设计的信号配置不同,例如:
- DUT_A:有
valid和value两个信号 - DUT_B:只有
valid信号,没有value信号
我们的 Monitor 期望同时接收 valid 和 value,直接复用会失败。此时可以使用 虚拟信号(fake_chdl) 来解决。
虚拟信号是一个实现了 CallableHDL 接口的 Lua 对象,可以模拟真实信号的行为。通过将虚拟信号注入到 AliasBundle 中,我们可以满足组件对信号的依赖。
示例:在 DUT_B 中注入虚拟 value 信号
local Monitor = require "Monitor"
-- 创建 AliasBundle,包含 valid 信号,value 信号先留空(用 [value] 标记为可选)
local signals_bdl = ([[
| vld => valid
| [value]
]]):abdl {
hier = "tb_top.another.path.to.mod",
prefix = ""
}
-- 创建一个虚拟的 value 信号,总是返回 0
local fake_value = ("tb_top.u_dut.fake_value"):fake_chdl {
get = function(self)
return 0 -- 返回固定值
end,
set = function(self, value)
-- 忽略设置操作
end,
get_width = function(self)
return 32 -- 模拟 32 位信号
end,
is = function(self, value)
return value == 0
end
}
-- 将虚拟信号注入到 AliasBundle 的 value 别名中
signals_bdl.value = fake_value
local mon = Monitor(signals_bdl)
更复杂的虚拟信号示例
如果需要更真实的行为,可以模拟随机值或记录写入值:
local fake_value = ("tb_top.u_dut.fake_value"):fake_chdl {
get = function(self)
return math.random(0, 255) -- 返回 0-255 之间的随机值
end,
set = function(self, value)
self._last_value = value -- 记录最后一次写入的值
end,
get_width = function(self)
return 8
end,
is = function(self, value)
return value == self:get()
end
}
注意事项
- 虚拟信号的行为应合理:根据验证场景,虚拟信号应该返回有意义的默认值,避免影响验证逻辑。
- 必须实现
get_width方法:TypeExpect在检查位宽时可能会调用此方法,必须提供。 - 虚拟信号只是临时方案:长期来看,应考虑修改 DUT 或调整组件以支持可选信号。
- 在 AliasBundle 中标记可选信号:使用方括号
[signal_name]标记信号为可选,这样即使实际设计中不存在,也不会报错。
总结
编写可复用的验证组件,核心在于解耦:
- 使用 AliasBundle 将信号名称与组件内部逻辑解耦。
- 使用 TypeExpect 在组件入口处进行类型和信号存在性检查,提前暴露错误。
- 当某些设计缺少信号时,使用 fake_chdl 注入虚拟信号,保持组件兼容性。
遵循这些原则,您编写的验证组件可以在多个设计、多种场景(HVL、HSE、WAL)中无缝复用,极大提高验证效率。