Skip to main content

String Literal Constructor Pattern (SLCP)

SLCP(String Literal Constructor Pattern,字符串字面量构造器模式)是 Verilua 中的一种核心设计模式,通过扩展 Lua 的 string 库,使得用户可以直接使用字符串字面量来构建各种 Verilua 数据结构,而无需显式 require 相应的模块。

设计动机

在传统的 Lua 编程中,构建一个对象通常需要先 require 对应的模块:

local CallableHDL = require "LuaCallableHDL"
local Bundle = require "LuaBundle"

local clock = CallableHDL("clock", "tb_top.clock")
local bdl = Bundle({"valid", "ready"}, "", "tb_top.dut", "my_bundle", true)

这种方式虽然清晰,但在硬件验证场景中存在一些不便:

  1. 导入繁琐:每个测试文件都需要导入多个模块
  2. 代码冗长:构造函数的参数列表可能很长
  3. 降低可读性:真正的业务逻辑被大量的模板代码淹没

SLCP 的设计目标就是解决这些问题,让用户能够以更简洁、更直观的方式构建 Verilua 数据结构。

核心原理

SLCP 利用了 Lua 语言的一个重要特性:所有字符串共享同一个 metatable

在 Lua 中,字符串类型的 metatable 的 __index 字段指向 string 表。这意味着:

-- 这两种写法是等价的
local upper = string.upper("hello")
local upper = ("hello"):upper()

基于这个特性,Verilua 向 string 表添加了一系列方法,使得所有字符串字面量都可以调用这些方法来构建各种数据结构:

-- 向 string 表添加方法
string.chdl = function(hierpath, hdl)
return CallableHDL(hierpath, "", hdl)
end

-- 然后就可以这样使用
local clock = ("tb_top.clock"):chdl()

支持的构造器

下表列出了 SLCP 支持的所有构造器方法:

方法返回类型说明
<str>:hdl()ComplexHandleRaw获取 VPI 底层句柄
<str>:hdl_safe()ComplexHandleRaw安全获取 VPI 句柄(不存在返回 -1)
<str>:chdl()CallableHDL构建可调用信号句柄
<str>:fake_chdl{}CallableHDL构建虚拟信号句柄
<str>:bdl{...}Bundle构建信号组
<str>:bundle{...}Bundle:bdl{...}
<str>:abdl{...}AliasBundle构建带别名的信号组
<str>:ehdl()EventHandle构建事件句柄
<str>:bv()BitVec构建位向量
<str>:bit_vec()BitVec:bv()
<str>:auto_bundle{}Bundle自动构建信号组

详细用法

CallableHDL 构造

CallableHDL 是 Verilua 中最常用的信号句柄类型,用于读写硬件信号。

-- 基本用法
local clock = ("tb_top.clock"):chdl()
local reset = ("tb_top.reset"):chdl()

-- 访问深层信号
local data = ("tb_top.u_dut.path.to.data"):chdl()

-- 使用信号
clock:posedge() -- 等待上升沿
local value = data:get() -- 读取值
data:set(0x123) -- 设置值

Bundle 构造

Bundle 用于将多个相关信号组织在一起,特别适合处理接口信号。

-- 基本 Bundle
local io = ("valid | ready | data | addr"):bdl {
hier = "tb_top.u_dut",
prefix = "io_",
name = "IO Bundle"
}

-- 访问信号
io.valid:set(1)
io.data:set(0xABCD)

-- Decoupled Bundle(类似 Chisel 的 DecoupledIO)
local axi_ch = ("valid | ready | data | id"):bdl {
hier = "tb_top.u_dut",
prefix = "axi_w_",
is_decoupled = true,
name = "AXI Write Channel"
}

-- 检查握手
if axi_ch:fire() then
print("Transaction completed!")
end

多行格式(推荐用于较多信号的情况):

local bus = ([[
| valid
| ready
| address
| data
| strobe
| resp
]]):bdl {
hier = "tb_top.u_bus",
prefix = "bus_",
is_decoupled = true,
name = "Bus Interface"
}

可选信号(使用方括号语法):

-- 使用方括号标记可选信号
local io = ([[
| valid
| ready
| data
| [debug_info] -- 可选信号,不存在时不会报错
]]):bdl {
hier = "tb_top.u_dut",
prefix = "io_"
}

-- 旧方法(仍然有效)
local io_old = ("valid | ready | data | debug_info"):bdl {
hier = "tb_top.u_dut",
prefix = "io_",
optional_signals = {"debug_info"}
}

AliasBundle 构造

AliasBundle 允许为信号创建别名,提高代码可读性。

-- 使用 => 创建别名
local ctrl = ([[
| io_in_start => start
| io_in_stop => stop
| io_out_done => done
| io_out_error => error
]]):abdl {
hier = "tb_top.u_ctrl",
name = "Control Signals"
}

-- 通过别名访问
ctrl.start:set(1)
if ctrl.done:get() == 1 then
print("Operation completed!")
end

-- 支持多个别名(用 / 分隔)
local sig = ([[
| some_very_long_signal_name => short/alias1/alias2
]]):abdl {
hier = "tb_top"
}

-- 以下访问方式都是等价的
sig.short:get()
sig.alias1:get()
sig.alias2:get()

-- 可选信号(使用方括号语法)
local ctrl_with_opt = ([[
| io_in_start => start
| io_in_stop => stop
| [io_debug_port => debug]
]]):abdl {
hier = "tb_top.u_ctrl"
}

-- 旧方法(仍然有效)
local ctrl_old = ([[
| io_in_start => start
| io_in_stop => stop
| io_debug_port => debug
]]):abdl {
hier = "tb_top.u_ctrl",
optional_signals = {"debug"}
}

支持字符串插值:

local ch = ([[
| channel_{n}_valid => valid
| channel_{n}_data => data
]]):abdl {
hier = "tb_top.u_router",
n = 3 -- 将 {n} 替换为 3
}
-- 实际信号路径: tb_top.u_router.channel_3_valid

BitVec 构造

BitVec 用于处理大位宽的数据。

-- 从十六进制字符串构造
local bv = ("deadbeef"):bv(128) -- 128 位的 BitVec

-- 操作位字段
local field = bv:get_bitfield(0, 31)
bv:set_bitfield(32, 63, 0x12345678)

-- 转换为字符串
local hex_str = bv:get_bitfield_hex_str(0, 127)

EventHandle 构造

EventHandle 用于任务间的同步与通信。

-- 创建事件句柄
local tx_done = ("tx_complete"):ehdl()
local rx_ready = ("rx_ready"):ehdl(1) -- 带 ID

-- 在一个任务中等待事件
fork {
function()
tx_done:wait()
print("TX completed!")
end
}

-- 在另一个任务中触发事件
tx_done:fire()

虚拟信号构造

fake_chdl 用于创建不存在于实际设计中的虚拟信号,便于测试和调试。

local fake_sig = ("virtual.signal"):fake_chdl {
get = function(self)
return 42 -- 总是返回 42
end,
set = function(self, value)
print("Setting virtual signal to", value)
end,
is = function(self, value)
return value == 42
end
}

-- 像普通信号一样使用
assert(fake_sig:get() == 42)
fake_sig:set(100) -- 打印 "Setting virtual signal to 100"

自动 Bundle 构造

auto_bundle 可以根据模式自动匹配信号创建 Bundle。

-- 使用 SignalDB 自动发现信号
local io_bdl = ("tb_top.u_dut"):auto_bundle {
startswith = "io_in_", -- 匹配以 io_in_ 开头的信号
}

-- 更多匹配选项
local data_bdl = ("tb_top.u_dut"):auto_bundle {
endswith = "_data", -- 匹配以 _data 结尾的信号
}

local wide_bdl = ("tb_top.u_dut"):auto_bundle {
filter = function(name, width)
return width >= 32 -- 只包含位宽 >= 32 的信号
end
}

其他字符串扩展方法

除了数据结构构造器,SLCP 还提供了一些实用的字符串操作方法:

字符串渲染

local template = "Hello {{name}}, your score is {{score}}!"
local result = template:render({
name = "Alice",
score = 100
})
-- result: "Hello Alice, your score is 100!"

数值转换

-- 支持多种进制
local dec = ("42"):number() -- 42
local hex = ("0x2A"):number() -- 42
local bin = ("0b101010"):number() -- 42

子串检查

local s = "hello world"
assert(s:contains("world") == true)
assert(s:contains("moon") == false)

后缀移除

local s = "signal_valid"
local result = s:strip("_valid") -- "signal"

枚举定义

local Color = ("Color"):enum_define {
Red = 1,
Green = 2,
Blue = 3
}

assert(Color.Red == 1)
assert(Color(1) == "Red") -- 反向查找

TCC 编译

SLCP 还支持在运行时编译 C 代码:

local lib = ([[
int add(int a, int b) {
return a + b;
}
]]):tcc_compile({
{sym = "add", ptr = "int (*)(int, int)"}
})

local result = lib.add(1, 2) -- 3

最佳实践

1. 优先使用 SLCP

对于简单的信号访问,优先使用 SLCP 而不是传统的 require 方式:

-- 推荐
local clock = ("tb_top.clock"):chdl()

-- 不推荐(除非需要更多控制)
local CallableHDL = require "LuaCallableHDL"
local clock = CallableHDL("clock", "tb_top.clock")

2. 使用多行格式提高可读性

对于包含多个信号的 Bundle,使用多行格式:

-- 推荐
local bus = ([[
| req_valid
| req_ready
| req_addr
| req_data
| resp_valid
| resp_data
]]):bdl { hier = "tb_top.u_bus" }

-- 不推荐
local bus = ("req_valid|req_ready|req_addr|req_data|resp_valid|resp_data"):bdl { hier = "tb_top.u_bus" }

3. 合理使用别名

当信号名称过长或不够直观时,使用 abdl 创建别名:

local ctrl = ([[
| auto_generated_very_long_signal_name_for_start => start
| auto_generated_very_long_signal_name_for_done => done
]]):abdl { hier = "tb_top.u_ctrl" }

-- 使用简洁的别名
ctrl.start:set(1)
ctrl.done:expect(1)

4. 结合 dut 使用

SLCP 可以与 dut 代理表配合使用:

-- 使用 dut 获取路径,再用 SLCP 构建 Bundle
local hier = dut.u_top.u_bus:get_local_path()
local bus = ("valid | ready | data"):bdl { hier = hier }

总结

SLCP 是 Verilua 的核心设计模式之一,它通过巧妙地利用 Lua 的语言特性,为用户提供了一种简洁、直观的方式来构建各种数据结构。理解并善用这一模式,可以显著提高验证代码的可读性和开发效率。