Skip to main content

信号句柄(CallableHDL)

创建 CallableHDL

使用 dut 创建(推荐)

使用 dut 来创建 chdl 需要完整地使用 dut 表示出一个信号的 hierarchy path(这里的 dut 表示的是 Testbench 的模块名称,默认是 tb_top),例如:

local signal = dut.path.to.signal:chdl()

通常对于一些 DUT 的顶层信号,可以使用 dut.xxx 表示,例如:

local signal = dut.clock:chdl()
local signal2 = dut.reset:chdl()

对于 DUT 的内部信号,可以使用 dut.u_<top_module_name>.<internal_signal_name> 表示,具体原因可以查看此处的说明,代码例子如下:

local signal = dut.u_Design.value:chdl()
基于 dut 灵活创建 chdl

Lua 中,可以使用 string 来灵活访问某个 table 的子变量,例如:

local t = {a0 = 1, a1 = 2, b0 = 3, b1 = 4}
local a = t.a0
local b = t.b0

local aa = t["a0"]
local bb = t["b0"]

assert(a == aa)
assert(b == bb)

for i = 0, 1 do
if i == 0 then
assert(t["a" .. i] == t.a0)
else
assert(t["a" .. i] == t.a1)
end
end

可以看到利用字符串拼接等方法访问一些有规律的子变量,这提供了更灵活的方式来访问子变量。

dut 本质上也是一个 table,因此也可以使用 string 来访问 dut 的子变量,例如:

local signals = {}
for i = 1, 5 do
table.insert(signals, dut.path.to["some_module_" .. i].value:chdl())
end

上面这个例子中我们使用 for 循环批量获得了 5 个 chdl 对象,这些对象分别对应 dut 中 some_module_1 到 some_module_5 这五个有规律的模块的 value 字段,并将其放入了一个 table 中。同样地,对于信号名也可以这么操作:

local signals = {}
for i = 1, 5 do
table.insert(signals, dut.path.to["some_value_" .. i]:chdl())
end

使用 class 创建

本质上 CallableHDL 是一个 class,因此可以使用类似 class 的方式创建,例如:

local CallableHDL = require "verilua.handles.LuaCallableHDL"

local chdl = CallableHDL("tb_top.clock", "clock")

-- equivalent to

local chdl = dut.clock:chdl()

CallableHDL 接收两个参数,第一个是信号的完整 hierarchy path,第二个则是 chdl 的名称。

使用 string literal 创建(推荐)

Lua 允许重载 string 的 metatable,Verilua 基于这个机制实现了一种简化的方式来创建 chdl,例如:

local chdl = ("tb_top.clock"):chdl()

这种方式在 Verilua 中被称为 String Literal Constructor Pattern(SLCP),使用 SLCP 的好处在于可以不用像 class 构建那样提前使用 requireLuaCallableHDL 加载到代码中,而是在使用时根据后缀的方法名(:chdl())来直接创建 chdl。除了 chdl 之外,其他的数据结构同样也支持使用 SLCP 的方式来创建,详见 字符串字面量构造模式(SLCP)

信号名与接口名重名时

dut.path.to.signal 这种写法把信号名当字段查,而 setset_uncheckedrandomizeforcereleasefreezegetnamedump 这些接口名优先命中。RTL 里真有同名信号时,dut 路径拿到的是接口函数:

type(dut.u_top.set) -- "function",不是信号

用字符串路径构造 chdl,信号名只是路径的一部分,不参与名字解析:

local sig = ("tb_top.u_top.set"):chdl()

-- 已经持有父节点时,用 get_local_path() 拼出完整路径
local u_top = dut.u_top
local sig = (u_top:get_local_path() .. ".set"):chdl()

拿到 chdl 之后和普通信号一样:

sig:set(1)
sig:randomize()
sig.value = 0x11

Bundle 上的重名会在构造时报错,处理方式一样。如果想保留结构化的访问方式,可以用 AliasBundle 把它映射成不重名的别名。

CallableHDL 接口

CallableHDL 支持多种信号操作接口,且相关接口函数的功能会与具体信号的位宽有关,因此在使用时需要注意一些细节。

Verilua 中根据信号的位宽不同,隐式地将 chdl 分为了三种类型:

  1. Single:位宽 <= 32 bit 的信号
  2. Double:位宽 > 32 bit 且 <= 64 bit 的信号
  3. Multi:位宽 > 64 bit 的信号
在后续的 API 介绍的内容中,如果没有特别写出针对这三种情况的说明,那么默认情况下相关 API 的行为都是一致的
之所以会有区分是为了尽可能针对位宽实施更多性能上的优化
beat 的概念

硬件信号的位宽是没有限制的,但是在 Lua 中普通的 number 类型能够保存 32 bit 的数值,因此 Verilua 以 32 bit 为单位来表示一部分的信号的值,这个单位称为 beat。例如:1 ~ 32 bit 的信号可以用 1 个 beat 来表示,33 ~ 64 bit 的信号可以用 2 个 beat 来表示,以此类推。beat 的概念在后续的 API 介绍中会经常遇到。

可以使用 <chdl>.beat_num 来获得 beat 数。

信号读取

  1. <chdl>:get()

    返回一个 Lua 的 number 类型的数值,表示当前的信号值。

  2. <chdl>:get64()

    返回一个 Lua 的 number 类型的数值,表示当前的信号值。

  3. <chdl>:get_bitvec()

    返回一个 BitVec,关于 BitVec 可以查看 位向量(BitVec) 的文档。

  4. <chdl>:get_str(fmt)

    获得当前信号的数值,并以 String 的类型返回,接受一个 fmt 参数,用于指定返回的字符串的格式,可以是 HexStrBinStrDecStr

    local signal = dut.value:chdl()

    local value = signal:get()
    assert(value == 0x123)

    local value_hex_str = signal:get_str(HexStr)
    assert(value_hex_str == "123")

    local value_bin_str = signal:get_str(BinStr)
    assert(value_bin_str == "100100011")

    local value_dec_str = signal:get_str(DecStr)
    assert(value_dec_str == "291")
    这里的 HexStrBinStrDecStr 是 Verilua 预定义的全局变量,可以直接使用
  5. <chdl>:get_hex_str()

    获得当前信号的数值,并以 Hex String 的类型返回。

    local signal = dut.value:chdl()

    local value = signal:get()
    assert(value == 0x123)

    local value_hex_str = signal:get_hex_str()
    assert(value_hex_str == "123")
  6. <chdl>:get_dec_str()

    类似 get_hex_str,但是返回的是 Decimal String 类型的字符串。

  7. <chdl>:get_bin_str()

    类似 get_hex_str,但是返回的是 Binary String 类型的字符串。

信号赋值

写入默认是延迟的,写完马上 get() 读到的还是旧值。每个方法都有立即生效的 _imm 版本,区别见立即赋值和普通赋值的区别

API作用立即版本
set(v)常规写入,根据位宽校验 v 的类型与大小set_imm
.value = v属性写法,等价于 set(v).value_imm = v
set_unchecked(v)跳过校验的写入,用于逐拍写的热点路径set_imm_unchecked
set_bits(s, e, v)只改 [e:s] 这段位,其余位保持不变,位段宽度上限 64 bitset_bits_imm
set_bits_hex_str(s, e, hex_str)同上,但值用十六进制字符串给,位段可以超过 64 bitset_bits_imm_hex_str
set_str(str)字符串写入,由前缀定进制:0x 走十六进制,0b 走二进制,其余按十进制set_imm_str
set_hex_str(str)进制已知时直接指定,另有 set_bin_str / set_dec_strset_imm_hex_str
randomize()写入一个随机值randomize_imm
force(v)强制驱动,同 SystemVerilog 的 force,值上限 64 bit(或按 beat 给一个数组)force_imm
force_hex_str(str)用十六进制字符串强制驱动,值可以超过 64 bitforce_imm_hex_str
release()解除 force / freezerelease_imm
freeze()把信号钉在当前值,用 release() 解除freeze_imm
set_cached(v)值和上次写入相同就跳过本次写,用 reset_set_cached() 清缓存set_imm_cached

数组信号额外有下标和整体写入:

API作用立即版本
arr[i]取第 i 个元素的句柄,从 0 数,同一下标返回同一个句柄
set_all(values)values 依次写入整个数组set_all_imm
set_all_unchecked(values)跳过校验的整数组写入set_all_imm_unchecked
local arr = dut.u_top.array_signal:chdl()
arr[0]:set(0x11)
arr[0].value = 0x22
arr:set_all { 0x11, 0x22, 0x33, 0x44 }
  1. <chdl>:set(value)

    value 赋值给当前信号。

    local signal = dut.value:chdl()
    local value = 0x123
    signal:set(value)
  2. <chdl>:set_unchecked(value)

    set 类似,但是不会检查 value 的正确性,省去了 assert 的开销。适合在热路径(例如每拍都需要驱动的信号)中使用;功能性测试建议使用 set

  3. <chdl>:set_str(str)

    一个通用的字符串赋值方式,可以接收一个 Lua string 类型的字符串,具体的字符串类型由 str 的前两位来区分,例如:

    local signal = dut.value:chdl()

    -- set hex string
    signal:set_str("0x123")

    -- set binary string
    signal:set_str("0b01011")

    -- set decimal string
    signal:set_str("123")

    0x 表示为 Hex String,0b 表示为 Binary String,其他的字符串则表示为 Decimal String。

  4. <chdl>:set_hex_str(str)

    使用 Hex String 赋值,str 是一个 Lua string 类型的字符串。

    local signal = dut.value:chdl()

    signal:set_hex_str("123")

    clock:posedge()

    signal:expect(0x123)
  5. <chdl>:set_bin_str(value)

    使用 Binary String 赋值,value 是一个 Lua string 类型的字符串。

    local signal = dut.value:chdl()

    signal:set_bin_str("1010")

    clock:posedge()

    signal:expect(0xA)
  6. <chdl>:set_dec_str(value)

    使用 Decimal String 赋值,value 是一个 Lua string 类型的字符串。

    local signal = dut.value:chdl()

    signal:set_dec_str("12")

    clock:posedge()

    signal:expect(12)
  7. <chdl>:randomize()

    对当前信号进行随机赋值,这在验证中很常用。

  8. <chdl>:set_bits(s, e, v)

    设置信号的值 v,并且只在 se 之间的位宽上赋值。v 可以是 Lua number 类型的数值,也可以是一个 uint64_t 类型的 cdata,因此位段宽度(e - s + 1)不能超过 64 bit。

    local signal = dut.value:chdl()

    local value = 0x123
    signal:set_bits(0, 7, value)
  9. <chdl>:set_bits_hex_str(s, e, hex_str)

    set_bits 一样只写 se 这段位,但值用十六进制字符串给,所以位段可以超过 64 bit:

    local wide = ("tb_top.u_top.wide_signal"):chdl() -- 128 bit
    wide:set_bits_hex_str(0, 127, "1234567890abcdef1234567890abcdef")
  10. <chdl>:force(value)

    强制赋值(与 release 配合使用),与 SystemVerilog 中的 force 关键字相同。除了 force 这个属性上的区别之外,其他和 set 一样。

    Verilator 需要 ≥ 5.050 且 forceable

    在 Verilator 上使用 force / release 时:

    1. forceable:目标信号必须标记为 forceable,例如通过 verilua.verilator_config.vlt
    set_values("verilua.verilator_config", [[
    forceable -module "top" -var "ready"
    ]])
    1. 版本:需要 Verilator ≥ 5.050。xmake rule 在 control file 里出现 forceable 时会在构建期检查版本;过低会直接 build fail,避免 force 被当成普通写而无声失败。

    详见 Verilator Control Files

    local signal = dut.value:chdl()
    local value = 0x123
    signal:force(value)

    -- ...

    signal:release()
  11. <chdl>:release()

    释放赋值(与 force 配合使用),与 SystemVerilog 中的 release 关键字相同。对于使用了 force 的信号,需要使用 release 来释放赋值,否则会导致信号的值不会更新。

  12. <chdl>:set_imm(value)

    立即赋值版本的 set,除了立即赋值的属性之外,其他和 set 一样。

    立即赋值和普通赋值的区别

    set 是延迟赋值:调用时只把值放入 pending 队列,不会立刻更新 HDL;Verilua 会在 VPI 的 cbReadWriteSynch 阶段统一写入,因此后续进入 cbReadOnlySynch(例如 await_rd())时可以读到新值。set() 的生效点不由时钟边沿决定。

    set_imm 是立即赋值:调用时直接写入 HDL,随后读取同一个信号通常能立刻看到新值。

    阶段示意:

    Lua code VPI ReadWrite VPI ReadOnly
    ──────── ───────────── ────────────
    signal:set(0x42) -> flush pending set -> get() == 0x42
    get() == 旧值

    signal:set_imm(0x42)
    get() == 0x42

    代码示例:

    set() 延迟到 ReadWrite 阶段统一写入:

    local signal = dut.value:chdl() -- assume that the initial value is 0x00

    signal:set(0x123)
    assert(signal:get() == 0x00) -- set() 不会立即生效

    await_rd()
    assert(signal:get() == 0x123) -- ReadOnly 阶段可见;此阶段禁止 HDL 写操作

    set_imm() 立即写入,适合需要马上读回的场景:

    local signal = dut.value:chdl()

    signal:set_imm(0x100)
    assert(signal:get() == 0x100) -- set_imm() 立即生效

    同一阶段内多次调用 set(),只有最后一次写入的值会在 pending 队列 flush 后可见:

    signal:set(0x11)
    signal:set(0x22)
    signal:set(0x33) -- flush 后可见的是 0x33
    常见陷阱

    陷阱:set 后立即 get 拿到旧值

    signal:set(0xFF)
    local v = signal:get() -- v == 旧值,不是 0xFF!
    -- 如果需要写入后立即读回,请使用 set_imm
  13. set_imm_* 立即赋值变体

    每个赋值方法均有对应的 set_imm_* 立即赋值变体,行为与普通版本完全相同,但立即生效(参见立即赋值和普通赋值的区别):

    set_imm_uncheckedset_imm_strset_imm_hex_strset_imm_bin_strset_imm_dec_strrandomize_immset_bits_immset_bits_imm_hex_strforce_immrelease_immfreeze_immset_imm_cached

  14. <chdl>:freeze()

    冻结当前的信号值,在后续的仿真中,该信号的值将保持不变。需要调用 <chdl>:release() 来取消冻结。

  15. <chdl>:set_cached(value)

    带缓存的赋值:value 与上一次通过 set_cached 写入的值相同时直接返回,不产生 VPI 调用。适合每拍都要驱动、但取值很少变化的信号。

    local signal = dut.value:chdl()

    signal:set_cached(1) -- 实际写入
    signal:set_cached(1) -- 跳过

    signal:set(2) -- 绕过缓存写入,缓存仍记录为 1
    signal:reset_set_cached() -- 清空缓存,下一次 set_cached 必定写入
    warning

    缓存只跟踪 set_cached / set_imm_cached 自己写入的值。如果信号还被 setforce 等其他方式改动,需要调用 reset_set_cached() 使缓存失效。

debug 相关

  1. <chdl>:dump()

    用于将信号的值(主要是以 Hex String 的形式)输出到控制台,可以用于查看信号的值,打印的内容如下所示:

    Terminal
    [tb_top.value] => 0x01
  2. <chdl>:dump_str()

    会将原本 <chdl>:dump() 的输出的内容作为一个返回值进行返回,因此 <chdl>:dump() 也等价于 print(<chdl>:dump_str())

  3. <chdl>:get_width()

    获得信号的位宽,也可以直接访问 <chdl>.width 来获得。

  4. <chdl>:__len()

    chdl 重载了 Lua 的 metatable 的 __len 方法,可以直接使用 #<chdl> 来获得信号的位宽。下面的三种方式来获得信号位宽是等价的:

    local signal = dut.value:chdl()
    assert(#signal == 32)
    assert(signal:get_width() == 32)
    assert(signal.width == 32)

验证相关

  1. <chdl>:expect(value)

    用于断言信号的值,在验证中很常用,如果信号的值与期望值相等则什么也不会发生,如果不相等则会打印报错信息并停止仿真。错误信息格式如下所示:

    Terminal
    [tb_top.value] expect => 10, but got => 0

    value 的值是一个 Lua 的 number 类型的值。

  2. <chdl>:expect_not(value)

    <chdl>:expect(value) 类似,但是如果信号的值与期望值相等则会打印报错信息并停止仿真。

  3. <chdl>:expect_hex_str(hex_value_str)

    比较信号的值是否为指定的 Hex String 值,如果不相等则会打印报错信息并停止仿真。

    local signal = dut.value:chdl()

    signal:expect_hex_str("123")
  4. <chdl>:expect_bin_str(bin_value_str)

    比较信号的值是否为指定的 Binary String 值,如果不相等则会打印报错信息并停止仿真。

    local signal = dut.value:chdl()

    signal:expect_bin_str("1010")
  5. <chdl>:expect_dec_str(dec_value_str)

    比较信号的值是否为指定的 Decimal String 值,如果不相等则会打印报错信息并停止仿真。

    local signal = dut.value:chdl()

    signal:expect_dec_str("12")
  6. <chdl>:expect_not_hex_str / expect_not_bin_str / expect_not_dec_str

    分别与 expect_hex_strexpect_bin_strexpect_dec_str 作用相反。

  7. <chdl>:is(value)

    判断信号的值是否等于某个值,如果等于则返回 true,否则返回 false

    value 的值是一个 Lua 的 number 类型的值。

  8. <chdl>:is_not(value)

    <chdl>:is(value) 的作用相反。

  9. <chdl>:is_hex_str(hex_value_str) / is_bin_str(bin_value_str) / is_dec_str(dec_value_str)

    判断信号的值是否等于某个 Hex / Binary / Decimal String 值,等于则返回 true,否则返回 false

    local signal = dut.value:chdl()

    local correct = signal:is_hex_str("123")
    local correct = signal:is_bin_str("1010")
    local correct = signal:is_dec_str("12")

信号回调管理

信号回调管理函数只能作用在信号位宽为 1 的信号上
  1. <chdl>:posedge(times, func)

    用于等待信号的上升沿到来,timesfunc 是可选的两个参数,times 表示等待的次数,func 表示回调函数。

    local clock = dut.clock:chdl()

    clock:posedge() -- wait for one posedge
    clock:posedge(10) -- wait for 10 posedges

    clock:posedge(10, function (c)
    -- `func` will be called every time posedge arrives, and the argument `c` is the count of the posedge
    print("posedge count => ", c)
    end)
  2. <chdl>:negedge(times, func)

    <chdl>:posedge(times, func) 类似,但是在等待信号的下降沿到来。

  3. <chdl>:posedge_until(max_limit, func)

    在每一轮检查中,Verilua 会先调用一次 func;若返回 true 则停止并返回 true,若返回 false 才会继续等待下一个时钟上升沿并进入下一轮。若达到 max_limit 轮后 func 仍未返回 true,则返回 false

    注意:第一次调用 func 发生在进入等待之前,因此如果 func(1) 立即返回 true<chdl>:posedge_until(...) 会直接返回 true,此时可能还没有发生任何 posedge

    local clock = dut.clock:chdl()

    local ok = clock:posedge_until(100, function ()
    return dut.u_Design.ready:chdl():get() == 1
    end)
    assert(ok, "ready never asserted within 100 cycles")
  4. <chdl>:negedge_until(max_limit, func)

    <chdl>:posedge_until(max_limit, func) 类似,但是在等待信号的下降沿到来。调用顺序也相同:会先立即调用一次 func,只有返回 false 时才会继续等待下一个 negedge

具备自动格式识别的赋值接口

上述的 API 介绍中可以看到针对信号赋值有许多的函数,有时候用户可能需要一种更灵活的方式来赋值信号,能够根据此时输入的值的格式来调用合适的赋值接口函数,Verilua 通过重载 Lua metatable 的 __newindex 方法来实现了这一点。

调用的格式为:<chdl>.value = <value>,其中 <value> 是一个任意能够表示数值的值,注意到这里等号左边的 .value,这是为了能够触发 __newindex 元方法,因此在调用时需要加上。

目前 <value> 支持的格式包括:

  • Lua number 类型的数值;

    <chdl>.value = 123
    <chdl>.value = 0x123
  • Lua string 类型的字符串(对于 Hex 和 Binary 需要带上前缀);

    <chdl>.value = "123"
    <chdl>.value = "0x123"
    <chdl>.value = "0b01011"
  • Lua table(或者叫 list),里面的元素为 number 类型;

    <chdl>.value = {0x123, 0x456, 0x789}
  • LuaJIT 的 cdata,包括:uint64_tuint32_t[]

    <chdl>.value = 0x123ULL -- uint64_t cdata

    local vec = ffi.new("uint32_t[?]", 4) -- uint32_t[] cdata
    vec[1] = 0x123 -- Notice: the index is 1-based
    vec[2] = 0x456
    vec[3] = 0x789
    <chdl>.value = vec
    对于 uint32_t[] 的赋值,需要用户手动使用 ffi 创建,因此建议还是采用 Lua table 的方式
  • Lua boolean 类型的值;

    <chdl>.value = true
    <chdl>.value = false
可以在一些性能不敏感的场景下使用这种自动识别格式的赋值方式,例如在一些模块的 UT 测试中,这样可以简化业务代码

具备自动格式识别的比较接口

在实际的验证中,往往需要对信号的值进行比较,来判断信号的值是否符合预期,但是信号的数值表示方式在 chdl 中有多种类型,例如 Lua 的 number、string、table、cdata 等,这些类型的比较方式也是不同的,用户可能需要一种能够根据输入的值的格式来进行数值比较的接口,因此 Verilua 通过重载 Lua metatable 的 __eq 方法来实现了这一点。

调用的格式为:<chdl> == <value_wrapper>(<value>),其中 <value> 是一个任意能够表示数值的值,为了能够触发 __eq 元方法,需要给 <value> 包上一个 <value_wrapper>

<value_wrapper> 有三种可以选择,都是全局可用的一个全局变量,分别是:

  • v:最普通的 <value_wrapper>,只是用来触发 __eq 元方法,不对 <value> 做额外的处理;

  • vv:verbose 版本的 v,在比较信号的值时,如果比较失败,那么会在命令行中打印出 log 信息,便于调试,报错信息如下所示:

    Terminal
    [tb_top.value] expect => 0132, but got => 0032
  • vs:verbose + stop 版本的 v,在比较信号的值时,如果比较失败,那么会在命令行中打印出 log 信息,同时还会触发 assert 断言报错停止仿真,因此 vs 也等价于 assert(<chdl> == vv(<value>))

目前 <value> 支持的格式包括(以 v 这个 <value_wrapper> 为例):

  • Lua number 类型的数值;

    local correct = <chdl> == v(123)
    local correct = <chdl> == v(0x123)
  • Lua string 类型的字符串(对于 Hex 和 Binary 需要带上前缀);

    local correct = <chdl> == v("0x123")
    local correct = <chdl> == v("0b01011")
  • Lua table(或者叫 list),里面的元素为 number 类型;

    local correct = <chdl> == v({0x123, 0x456, 0x789})
  • LuaJIT 的 cdata,包括:uint64_tuint32_t[]

    local correct = <chdl> == v(0x123ULL) -- uint64_t cdata

    local vec = ffi.new("uint32_t[?]", 4) -- uint32_t[] cdata
    vec[1] = 0x456 -- Notice: the index is 1-based
    vec[2] = 0x789
    vec[3] = 0x000
    local correct = <chdl> == v(vec)
    对于 uint32_t[] 的赋值,需要用户手动使用 ffi 创建,因此建议还是采用 Lua table 的方式
  • Lua boolean 类型的值;

    local correct = <chdl> == v(true)
    local correct = <chdl> == v(false)
  • BitVec 类型的值;

    local correct = <chdl> == v(BitVec(123))
    local correct = <chdl> == v(BitVec("123"))
可以在一些性能不敏感的场景下使用这种自动识别格式的比较方式,例如在一些模块的 UT 测试中,这样可以简化业务代码

CallableHDL 接口(Array)

TODO: 针对 Array 类型的信号,Verilua 的 chdl 由独立的 API 进行赋值。

相关文档