SV 生成(SVBuilder)
SVBuilder 是 Verilua 提供的 SystemVerilog 生成工具,用于生成 SVA 断言/cover 语句和 covergroup(功能覆盖率)。它复用 Verilua testbench 中已有的信号 handle,结合 Lua 的循环、条件、模板能力,批量生成可嵌入 RTL 的 SV 文本,由仿真器原生引擎执行。
什么时候使用
以下场景适合使用 SVBuilder:
- 你已经有一个 Verilua testbench,里面定义了 DUT 的信号 handle(
dut.clk、dut.req等),想复用这些 handle 生成 SVA,而不是另外手写一套 XMR 路径 - 需要批量生成结构相似的断言,比如多个 AXI channel 的握手检查、多个寄存器的读写断言、多 beat burst 的逐拍检查
- 断言逻辑包含可参数化的模式,希望用模板引擎(循环、条件)动态生成,而不是手写重复代码
- 希望在定义时就发现 SVA 语法/语义错误,而不是等到仿真运行时才报错
不适合的场景:
- 只有少量固定的断言,直接写 SV 更简单
- 不使用 Verilua 做 testbench,没有可复用的 handle
对比:手写 SV vs SVBuilder
示例 1:信号路径自动提取
手写 SV 时,你需要手动拼写完整的层次路径:
// 手写:每个信号都要写完整 XMR 路径
sequence handshake();
top.u_soc.u_axi_master.awvalid ##1 top.u_soc.u_axi_master.awready;
endsequence
property aw_valid_hold();
top.u_soc.u_axi_master.awvalid && !top.u_soc.u_axi_master.awready
|-> ##1 top.u_soc.u_axi_master.awvalid;
endproperty
assert property (aw_valid_hold);
用 SVBuilder,信号路径从 testbench 已有的 handle 自动提取。路径变了(比如模块重命名)只需改 testbench 一处,生成的 SVA 随之更新:
-- SVBuilder:复用 testbench 里的 handle,路径自动解析
ctx:add "sequence" {
name = "handshake",
expr = "$(valid) ##1 $(ready)",
envs = { valid = dut.awvalid, ready = dut.awready },
}
ctx:add "property" {
name = "aw_valid_hold",
expr = "$(valid) && !$(ready) |-> ##1 $(valid)",
envs = { valid = dut.awvalid, ready = dut.awready },
}
ctx:add "assert" {
name = "chk_aw",
expr = "$(prop:aw_valid_hold)",
}
示例 2:批量生成
对 3 个 AXI channel 各生成一条 valid-hold 断言。手写需要重复 3 次:
// 手写:每个 channel 重复一遍
property aw_valid_hold();
top.dut.awvalid && !top.dut.awready |-> ##1 top.dut.awvalid;
endproperty
chk_aw: assert property (aw_valid_hold);
property w_valid_hold();
top.dut.wvalid && !top.dut.wready |-> ##1 top.dut.wvalid;
endproperty
chk_w: assert property (w_valid_hold);
property ar_valid_hold();
top.dut.arvalid && !top.dut.arready |-> ##1 top.dut.arvalid;
endproperty
chk_ar: assert property (ar_valid_hold);
SVBuilder 只需一个循环。新增 channel 在 channels 表里加一行即可,断言逻辑只写一份,不会漏改某个 channel:
local channels = {
{ name = "aw", valid = dut.awvalid, ready = dut.awready },
{ name = "w", valid = dut.wvalid, ready = dut.wready },
{ name = "ar", valid = dut.arvalid, ready = dut.arready },
}
for _, ch in ipairs(channels) do
ctx:add "property" {
name = ch.name .. "_valid_hold",
expr = "$(valid) && !$(ready) |-> ##1 $(valid)",
envs = { valid = ch.valid, ready = ch.ready },
}
ctx:add "assert" {
name = "chk_" .. ch.name,
expr = "$(prop:" .. ch.name .. "_valid_hold)",
}
end
示例 3:模板渲染能力
当断言逻辑本身包含重复模式时,可以用 expr 内置的模板引擎(# 开头的 Lua 代码行 + $(expr) 内联求值)直接在表达式内部展开。
例如,检查一个 4-beat burst 传输中每一拍的 data 都不为零:
// 手写:每个 beat 单独列出
sequence burst_nonzero();
(top.dut.wdata[0] != 0) ##1
(top.dut.wdata[1] != 0) ##1
(top.dut.wdata[2] != 0) ##1
(top.dut.wdata[3] != 0);
endsequence
SVBuilder 用模板循环生成,beat 数量变化时只改循环边界,信号 handle 和循环变量可以混用:
ctx:add "sequence" {
name = "burst_nonzero",
expr = [[
# for i = 0, 3 do
# if i > 0 then
##1
# end
($(wdata)[$(i)] != 0)
# end
]],
envs = { wdata = dut.wdata },
}
生成结果:
sequence burst_nonzero(); (top.dut.wdata[0] != 0) ##1 (top.dut.wdata[1] != 0) ##1 (top.dut.wdata[2] != 0) ##1 (top.dut.wdata[3] != 0); endsequence
初始化
local ctx = require "verilua.sv.SVBuilder"
SVBuilder 是一个模块级单例。使用前建议调用 ctx:clean() 确保状态干净。
快速示例
local ctx = require "verilua.sv.SVBuilder"
-- 设置默认时钟
ctx:default_clocking(dut.clk, "posedge")
-- 定义 sequence(自动提取 handle 的 fullpath)
ctx:add "sequence" {
name = "handshake",
expr = "$(req) ##1 $(ack)",
envs = { req = dut.req, ack = dut.ack },
}
-- 定义 property,引用已有 sequence
ctx:add "property" {
name = "req_then_ack",
expr = "$(seq:handshake) |-> !$(ovf)",
envs = { ovf = dut.overflow },
}
-- 生成 assert
ctx:add "assert" {
name = "chk_handshake",
expr = "$(prop:req_then_ack)",
}
-- 输出生成的 SVA 文本
print(ctx:generate())
输出:
default clocking @(posedge top.dut.clk); endclocking
sequence handshake(); top.dut.req ##1 top.dut.ack; endsequence
property req_then_ack(); handshake |-> !top.dut.overflow; endproperty
// 1/1
property _GEN_chk_handshake_PROPERTY(); req_then_ack; endproperty
chk_handshake: assert property (_GEN_chk_handshake_PROPERTY);
API 参考
ctx:add(typ)(params)
柯里化调用,typ 为 "sequence" | "property" | "cover" | "assert" | "covergroup" | "raw"。
ctx:add "sequence" { name = "s1", expr = "...", envs = {...} }
ctx:add "property" { name = "p1", expr = "..." }
ctx:add "cover" { name = "c1", expr = "...", cov_type = "sequence" }
ctx:add "assert" { name = "a1", expr = "..." }
ctx:add "covergroup" { name = "cg1", expr = "..." }
ctx:add "raw" { name = "helper", expr = "logic sample_en;" }
参数(params):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✓ | 语句名称,整个 context 内唯一 |
expr | string | ✓ | SVA/SV 表达式,支持 $(var) 模板语法 |
envs | table | 模板变量表,key→value 映射 | |
formal_args | string | 仅 sequence/property 有效:形参列表,如 "logic req, logic ack",直接插入括号内 | |
cov_type | string | 仅 cover 有效:"property"(默认)或 "sequence" | |
sample_event | string | 仅 covergroup 有效:覆盖默认采样事件 |
返回值:
"sequence"→ 返回Sequencehandle(可通过$(seq:name)引用)"property"→ 返回Propertyhandle(可通过$(prop:name)引用)"covergroup"→ 返回Covergrouphandle,.name为 covergroup 类型名,.inst_name为生成的实例名(_GEN_<name>_inst);可通过$(cov:name)在add "raw"的 expr 里引用(渲染为inst_name)"cover"/"assert"/"raw"→ 返回nil
ctx:default_clocking(signal, edge_type, overwrite?)
设置默认采样时钟。
ctx:default_clocking(dut.clk, "posedge")
ctx:default_clocking(dut.clk, "negedge", true) -- overwrite 已有设置
ctx:default_clocking("top.dut.clk", "posedge") -- 无 handle 时直接给层次路径
signal:CallableHDL/ProxyTableHandle(1-bit),或层次路径字符串(如"top.dut.clk")edge_type:"posedge"或"negedge"overwrite:是否覆盖已有的 default clocking(默认false)
返回 self,支持链式调用。
ctx:with_global_envs(envs)
注册全局模板变量,对后续所有 add 调用生效。
ctx:with_global_envs { clk = dut.clk, rst = dut.rst }
返回 self,支持链式调用。
ctx:set_lint(enable)
启用或禁用 sv_lint 自动检查(默认开启)。
ctx:set_lint(false) -- 关闭 lint
ctx:set_lint(true) -- 重新开启
返回 self,支持链式调用。
ctx:set_coverage_report(enable)
启用或禁用 covergroup 的 final coverage 报告(默认开启)。
ctx:set_coverage_report(false)
返回 self,支持链式调用。
ctx:set_block_comments(enable)
控制每个 add 块的 begin/end 标记及 Lua 源码位置,默认关闭。不影响原有 cover / assert 的 // i/n 编号注释。
ctx:set_block_comments(true) -- 输出名称、类型和 Lua 调用位置
ctx:set_block_comments(false) -- 不输出块标记
设置对已收集的语句同样生效,clean() 后保留。返回 self,支持链式调用。
ctx:generate()
返回生成的完整 SVA 文本字符串。等价于 tostring(ctx)。
ctx:clean()
清空已收集的语句、命名空间、模板变量和默认时钟,保留 lint、覆盖率报告及块注释设置。返回 self。
命名空间引用:$(seq:name) / $(prop:name) / $(cov:name)
通过 ctx:add "sequence" 或 ctx:add "property" 定义的语句,必须通过命名空间前缀引用:
ctx:add "sequence" { name = "hs", expr = "$(req) ##1 $(ack)", envs = {...} }
-- 引用 sequence:必须用 seq: 前缀
ctx:add "property" { name = "p1", expr = "$(seq:hs) |-> $(valid)", envs = {...} }
-- 引用 property:必须用 prop: 前缀
ctx:add "assert" { name = "a1", expr = "$(prop:p1)" }
裸引用 $(hs) 会报错并提示正确写法:
[SVBuilder] cannot reference sequence `hs` as a flat `$(hs)`;
use the `seq:` prefix, e.g. `$(seq:hs)`
带 formal_args 的 sequence 实参传递
如果 sequence 声明了 formal_args,在 expr 里引用时需要在 $(seq:name) 后面直接跟括号传入实参。$(seq:name) 渲染为 sequence 名称,括号和其中的模板表达式由引擎继续展开:
ctx:add "sequence" {
name = "handshake",
formal_args = "logic req, logic ack",
expr = "req ##1 ack",
}
-- 在引用处传入实参:$(seq:handshake) 渲染为 "handshake",
-- ($(req), $(ack)) 继续由模板引擎展开
ctx:add "assert" {
name = "chk",
expr = "$(seq:handshake)($(req), $(ack)) |-> !$(err)",
envs = { req = dut.req, ack = dut.ack, err = dut.err },
}
-- 生成: handshake(top.dut.req, top.dut.ack) |-> !top.dut.err
如果有 formal_args 的 sequence 被裸 $(seq:name) 引用(不传实参),SV 是非法的,sv_lint 会在 add 阶段报出 slang 错误:
[SVBuilder] lint error in 'chk'
error: too few arguments for 'handshake'; expected 2 but 0 were provided
这样设计的好处:
- 引用点一眼能看出类型(sequence 还是 property)
- sequence/property 不会和普通 env 变量命名冲突
- 有形参的 sequence 缺实参时,lint 在定义阶段即报错,不会等到仿真器运行时才发现
模板语法
expr 字段支持完整的模板引擎(基于 penlight template):
内联表达式 $(expr)
expr = "$(req) ##1 $(ack)"
-- req/ack 从 envs 或 global_envs 中查找,handle 自动渲染为 fullpath
Lua 代码行(# 开头)
expr = [[
# for i = 0, 3 do
$(bus)[$i] ##1 $(ack)
# end
]]
自定义转义符
如果 # 或 $ 和你的表达式冲突,可以通过 envs 里的特殊字段修改:
envs = {
_escape = "%", -- Lua 代码行前缀改为 %
_inline_escape = "@", -- 内联表达式改为 @{...}
_brackets = "{}", -- 括号改为 {}
}
Handle 自动渲染
envs 中的 CallableHDL 和 ProxyTableHandle 会自动渲染为其 fullpath(XMR 路径):
envs = { req = dut.req } -- dut.req 是 ProxyTableHandle
-- $(req) 渲染为 "top.dut.req"
嵌套表访问
envs = { bus = { data = dut.data, valid = dut.valid } }
-- $(bus.data) 渲染为 dut.data 的 fullpath
-- $(bus.valid) 渲染为 dut.valid 的 fullpath
sv_lint 集成
SVBuilder 在每次 add 调用后自动对生成的 SVA 进行 lint 检查(基于 slang),能捕获语法和语义错误。例如:
- 缺分号、括号不匹配、缺
endproperty/endsequence ##[5:2]范围反转- 引用未定义的标识符 / sequence / property
- 重复定义同名 sequence/property
- 在 sequence 中使用 property 表达式
- 递归 sequence 定义
|->/|=>误用在 sequence 体内(implication 只能用在 property 中)
报错格式:
[SVBuilder] lint error in 'chk_handshake'
source: /project/tb/checks.lua:137
dump: <build_dir>/svbuilder/lint/sv_builder_lint.sv
<build_dir>/svbuilder/lint/sv_builder_lint.sv:4:8: error: sequence range minimum '5' is greater than specified maximum '2'
##[5:2] top.dut.ack
^ ~
<build_dir>/svbuilder/lint/sv_builder_lint.sv:2:10: note: while expanding sequence 'handshake'
sequence handshake(); top.dut.req
^
报错包含两层定位信息:
source:调用ctx:add(...)的 Lua 文件和行号dump:送进 slang 的生成 SV 文件;后续诊断中的行号、列号和 caret 都对应这个文件
lint 失败时,会把送进 slang 的那段 SV 写到 <build_dir>/svbuilder/lint/sv_builder_lint.sv。<build_dir> 优先取 xmake 导出的 VL_BUILD_DIR;直接运行 Lua 时回退到当前目录下的 .svbuilder/。同一个构建目录只保留最近一次 lint 失败的输入。报错里 dump: 后面就是路径,诊断里的文件名也指向它,行号和文件内容对齐。失败的那条不会进入 generate()。
关闭 lint
在不需要 lint 的场景(如单元测试使用 fake handle)可以关闭:
ctx:set_lint(false)
完整示例:批量生成 AXI 握手断言
local ctx = require "verilua.sv.SVBuilder"
ctx:default_clocking(dut.aclk, "posedge")
-- 批量为多个 AXI channel 生成握手断言
local channels = {
{ name = "aw", valid = dut.awvalid, ready = dut.awready },
{ name = "w", valid = dut.wvalid, ready = dut.wready },
{ name = "ar", valid = dut.arvalid, ready = dut.arready },
}
for _, ch in ipairs(channels) do
-- valid 拉高后必须保持到 ready
ctx:add "property" {
name = ch.name .. "_valid_hold",
expr = "$(valid) && !$(ready) |-> ##1 $(valid)",
envs = { valid = ch.valid, ready = ch.ready },
}
ctx:add "assert" {
name = "chk_" .. ch.name .. "_valid_hold",
expr = "$(prop:" .. ch.name .. "_valid_hold)",
}
end
-- 输出完整 SVA 文本,可写入文件后 bind 到 RTL
local sva_text = ctx:generate()
Covergroup 生成
SVBuilder 同样支持生成 SystemVerilog covergroup,用于功能覆盖率收集。
基本用法
local ctx = require "verilua.sv.SVBuilder"
ctx:clean()
ctx:default_clocking(dut.clk, "posedge")
ctx:add "covergroup" {
name = "cg_axi_cmd",
expr = [[
cp_burst: coverpoint $(burst) {
bins fixed = {0};
bins incr = {1};
bins wrap = {2};
illegal_bins reserved = {3};
}
cp_size: coverpoint $(size) {
bins byte = {3'b000};
bins half = {3'b001};
bins word = {3'b010};
}
cross cp_burst, cp_size;
]],
envs = { burst = dut.awburst, size = dut.awsize },
}
生成结果:
covergroup cg_axi_cmd @(posedge top.dut.clk);
cp_burst: coverpoint top.dut.awburst {
bins fixed = {0};
bins incr = {1};
bins wrap = {2};
illegal_bins reserved = {3};
}
cp_size: coverpoint top.dut.awsize {
bins byte = {3'b000};
bins half = {3'b001};
bins word = {3'b010};
}
cross cp_burst, cp_size;
endgroup
cg_axi_cmd _GEN_cg_axi_cmd_inst = new();
final begin
$display("[COVERAGE] cg_axi_cmd: %.2f%%", _GEN_cg_axi_cmd_inst.get_coverage());
end
在上述过程中,SVBuilder 自动处理:
- 外层包裹:
covergroup name @(event); ... endgroup - 实例化:
name _GEN_name_inst = new; - Coverage 报告:
final begin ... $display ... end
用户只需写 covergroup 体(coverpoint + bins + cross + option)。
Coverage 报告控制
默认自动生成 final block 打印每个 covergroup 的覆盖率。可通过 set_coverage_report(false) 禁用:
ctx:set_coverage_report(false) -- 禁用 final block 生成
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✓ | covergroup 名称(必须唯一) |
expr | string | ✓ | covergroup 体(coverpoint/bins/cross/option),支持 $(var) 模板语法 |
sample_event | string | ✗ | 覆盖默认采样事件(默认复用 default_clocking);也支持 "with function sample(...)" 声明显式采样形参 |
envs | table | ✗ | 模板变量,handle 自动解析为 XMR 路径 |
覆盖采样事件
默认使用 default_clocking 设置的时钟。可通过 sample_event 参数针对单个 covergroup 定义新采样事件:
ctx:add "covergroup" {
name = "cg_alt_clk",
sample_event = "negedge top.dut.alt_clk", -- 覆盖默认时钟
expr = [[
coverpoint top.dut.status { bins idle = {0}; bins busy = {1}; }
]],
}
显式采样(with function sample)
sample_event 写成 with function sample(...) 时,covergroup 头不带 @(),需要在外部显式调用 sample() 实现信号采样。这些形参在该 covergroup 体内可直接调用。
add "covergroup" 返回一个 Covergroup handle,.inst_name 字段持有生成的实例名。采样调用点要自己放进 SV,最方便是写进 add "raw",有两种方式引用实例名:
$(cov:name):直接在 raw expr 里通过cov:命名空间引用(推荐).inst_name:把 handle 的.inst_name字段放进envs,通过普通模板变量引用
在 covergroup 后添加采样 raw,生成结果会先定义并实例化 covergroup,再输出采样调用:
ctx:add "raw" {
name = "sample_decls",
expr = "logic clk;\nlogic [3:0] burst;\nlogic [7:0] len;",
}
local cov = ctx:add "covergroup" {
name = "cg_axi_cmd",
sample_event = "with function sample(bit [3:0] burst, bit [7:0] len)",
expr = [[
cp_burst: coverpoint burst { bins incr = {4'd1}; }
cp_len: coverpoint len { bins short = {[0:15]}; bins long = {[16:255]}; }]],
}
-- 方式一:$(cov:cg_axi_cmd) 直接在 expr 里引用(推荐)
ctx:add "raw" {
name = "cg_axi_sample_driver",
expr = [[always @(posedge clk) begin
$(cov:cg_axi_cmd).sample(burst, len);
end]],
}
-- 方式二:把 .inst_name 放进 envs,作为普通模板变量
-- ctx:add "raw" {
-- name = "cg_axi_sample_driver",
-- expr = [[always @(posedge clk) begin
-- $(cg_inst).sample($(args));
-- end]],
-- envs = { cg_inst = cov.inst_name, args = "burst, len" },
-- }
例子里第一个 raw 只是让示例自带声明;实际工程里 clk / burst / len 由 testbench_gen 生成的 tb_top 提供,只要能解析,sv_lint 就不会在 add 阶段报未声明标识符。生成结果:
logic clk;
logic [3:0] burst;
logic [7:0] len;
covergroup cg_axi_cmd with function sample(bit [3:0] burst, bit [7:0] len);
cp_burst: coverpoint burst { bins incr = {4'd1}; }
cp_len: coverpoint len { bins short = {[0:15]}; bins long = {[16:255]}; }
endgroup
cg_axi_cmd _GEN_cg_axi_cmd_inst = new();
always @(posedge clk) begin
_GEN_cg_axi_cmd_inst.sample(burst, len);
end
final begin
$display("[COVERAGE] cg_axi_cmd: %.2f%%", _GEN_cg_axi_cmd_inst.get_coverage());
end
.inst_name 是普通字符串,用 .. 直接拼接也可以。
使用模板批量生成 coverpoint
ctx:add "covergroup" {
name = "cg_channels",
expr = [[
# for i = 0, 3 do
cp_ch$(i): coverpoint top.dut.ch$(i)_data {
bins low = {[0:15]};
bins high = {[16:31]};
}
# end
]],
}
sv_lint 检查
sv_lint 同样检查 covergroup,能在定义时捕获:
- 重复的 bins/coverpoint 名称
- cross 引用不存在的 coverpoint
- coverpoint 表达式类型错误(如
real类型) - 范围倒置(如
{[7:0]},报-Wreversed-range警告)
Raw preamble(辅助逻辑)
add "raw" 可插入 typedef、function、logic 和 always 等 SV 代码。把声明放在引用它们的语句前;后续 raw 保留在添加位置。完整规则见输出格式。
-- bare clock/reset:在开头的 raw 里声明,生成与 lint 时会排在 default clocking 前
ctx:add "raw" {
name = "fcov_helper",
expr = [[
logic clock;
logic reset;
logic sample_en;
logic [2:0] size;
logic [1:0] be_class;
always @(posedge clock or posedge reset) begin
if (reset) sample_en <= 1'b0;
else begin
sample_en <= req_fire;
size <= req_size;
be_class <= be_class_fn(req_be, req_size);
end
end
]],
}
ctx:default_clocking("clock", "posedge") -- 也可用 dut.clk handle / 层次路径字符串
ctx:add "covergroup" {
name = "cg_req",
expr = [[
cp_size: coverpoint size iff (sample_en) {
bins B1 = {3'd0};
bins B2 = {3'd1};
}
cp_be: coverpoint be_class iff (sample_en) {
bins Zero = {2'd0};
bins Partial = {2'd1};
bins Full = {2'd2};
}
]],
}
expr原样保留换行(不会做 SVA 那套空白压缩)- 支持
$(var)模板与envs - 参与
sv_lint:检查当前语句及此前成功添加的全部语句,包括 covergroup 定义和实例;失败的语句不会加入 builder - 与其他语句的相对位置遵循
add调用顺序,见输出格式
Lua 到 SV 的覆盖同步
背景
Verilua 里很大一部分验证逻辑天然在 Lua 侧:Lua 产生激励、Lua monitor 解析事务、Lua reference model 算期望值。这些组件看到的是事务级信息,有些覆盖点用几行 Lua 判断就够了,比如一个事务的 beat 数超过 4、读写交替且地址跨 4KB 边界。
SV covergroup 的采样事件来自时钟和信号,看不到 Lua 的判定结果。覆盖点在 Lua 里已经成立,覆盖率报告里却没有记录。
covergroup 的写法见 Covergroup 生成。这里要补的是同步:Lua 判定命中后,把这次命中记进 SV 的 covergroup。
通道
命中时机由 Lua 决定,所以 covergroup 不能挂在时钟事件上,要用显式采样(with function sample(...)),再用 export "DPI-C" 包一层交给 Lua 调用:
covergroup cg_lua_cov with function sample(int unsigned bin_id);
cp_bin: coverpoint bin_id {
bins burst_len_gt4 = {0};
bins rd_wr_alt = {1};
}
endgroup
cg_lua_cov cg_lua_cov_inst = new();
export "DPI-C" function verilua_cov_sample;
function void verilua_cov_sample(int unsigned bin_id);
cg_lua_cov_inst.sample(bin_id);
endfunction
Lua 侧用 SymbolHelper 取到这个符号,判定命中哪个 bin 后调用;scope 的取值见 调用 C 函数:
local SymbolHelper = require "verilua.utils.SymbolHelper"
local cov_sample = SymbolHelper.try_ffi_cast("void verilua_cov_sample(unsigned int bin_id);")
-- DPI export 在声明它的 module 作用域里执行,作用域会跨调度点重置,所以每次调用前设一次
sim.set_dpi_scope("tb_top") -- 参数是声明这个 export 的 module 作用域,省略时用 cfg.top
cov_sample(bin_id)
这样 Lua 判定的命中和 SV 自己采样的 bin 落在同一份覆盖率数据里,和直接在 SV 里 sample() 没有区别。
这段 SV 必须放进 tb_top:DPI export 要在 module 作用域里。tb_top 每次 build 都会重新生成,所以用 --inject-inner-file 注入,见下文 注入方式:文件与字符串;仓库里 tests/test_lua_cov_bridge/ 是一个可运行的例子。
用 SVBuilder 生成
covergroup 本体交给 SVBuilder,sample_event 用 with function sample(...) 不挂时钟;export "DPI-C" 包一层放进 add "raw",用 $(cov:name) 引用生成的实例名:
local cov = ctx:add "covergroup" {
name = "cg_lua_cov",
sample_event = "with function sample(int unsigned bin_id)",
expr = [[ cp_bin: coverpoint bin_id {
bins burst_len_gt4 = {0};
bins rd_wr_alt = {1};
}]],
}
ctx:add "raw" {
name = "lua_cov_sample_export",
expr = [[export "DPI-C" function verilua_cov_sample;
function void verilua_cov_sample(int unsigned bin_id);
$(cov:cg_lua_cov).sample(bin_id);
endfunction]],
}
verilua_cov_sample 只在 Lua 侧被调用,Verilator 会当作没有引用把 DPI export 链接掉,需要显式保留:
add_ldflags("-u verilua_cov_sample")
另外,Verilator 会静默丢弃仿真初始化完成前的 DPI export 调用,第一次采样要等第一个时钟沿之后。
推荐用法
生成通常不需要在每次仿真时都执行。推荐通过环境变量控制,只在需要重新生成时才运行:
if os.getenv("SV_GEN") == "1" then
fork {
function()
local ctx = require "verilua.sv.SVBuilder"
ctx:clean()
ctx:default_clocking(dut.clk, "posedge")
-- 定义 sequences / properties / assertions ...
ctx:add "sequence" { name = "hs", expr = "$(req) ##1 $(ack)", envs = { req = dut.req, ack = dut.ack } }
ctx:add "assert" { name = "chk_hs", expr = "$(seq:hs) |-> $(valid)", envs = { valid = dut.valid } }
-- 写入文件,后续通过 testbench_gen --iif 注入 tb_top
local output_path = require("verilua.LuaUtils").get_scriptdir() .. "/svbuilder_output.sv"
local f = io.open(output_path, "w")
f:write(ctx:generate())
f:close()
sim.finish()
end
}
end
sim.finish() 在生成完成后立即结束仿真,避免继续执行后续 testbench 逻辑(此时只需要生成文件,不需要跑真正的测试)。
运行时通过环境变量触发:
SV_GEN=1 xmake run -P tests/test_xxx
这样做的好处:
- 生成逻辑和 testbench 在同一个进程里,能直接复用所有 handle
- 正常仿真时不会执行生成代码,零开销
- 生成的
.sv文件可以纳入版本控制,如有变化可以 diff 审查
如果不想每次手动跑两遍(先 SV_GEN=1 xmake r 生成、再 xmake b 注入),可以用 before_build + nosim 在每次 build 前自动重新生成,见 自动生成:build 前用 nosim 刷新。
集成到 RTL
生成的 .sv 文件通过 verilua.tb_gen_flags 传给 testbench_gen,用 --iif(inject-inner-file)将内容注入到 tb_top module 内部。生成内容内部使用 handle 渲染出的 XMR 路径(如 tb_top.clk),在 tb_top 内部上下文中完全合法。
在 xmake.lua 里配置:
local sv_file = path.join(os.scriptdir(), "svbuilder_output.sv")
if os.isfile(sv_file) and not os.getenv("SV_GEN") then
set_values("verilua.tb_gen_flags", { "--iif", sv_file })
end
这样 testbench_gen 会把 svbuilder_output.sv 的内容插入到生成的 tb_top.sv 末尾(endmodule 之前)。
生成的 tb_top.sv 效果如下:
module tb_top(...);
// ... testbench_gen 自动生成的内容(时钟、DUT 例化等)...
// --- SVBuilder 生成的内容(通过 --iif 注入) ---
default clocking @(posedge tb_top.clk); endclocking
sequence req_to_ack(); tb_top.req ##[1:2] tb_top.ack; endsequence
property handshake_valid(); (tb_top.req && tb_top.ack) |-> ##1 tb_top.valid; endproperty
// 1/2
property _GEN_chk_handshake_valid_PROPERTY(); handshake_valid; endproperty
chk_handshake_valid: assert property (_GEN_chk_handshake_valid_PROPERTY);
// 2/2
property _GEN_cov_handshake_PROPERTY(); tb_top.req && tb_top.ack; endproperty
cov_handshake: cover property (_GEN_cov_handshake_PROPERTY);
endmodule
SV_GEN=1 时不加 --iif flag,不然 testbench_gen 尝试 compile svbuilder_output.sv 时 tb_top 还不存在会报错。
注入方式:文件与字符串
testbench_gen 提供两组注入选项,正名和短名都可用:
--inject-inner-file(--iif)/--inject-outer-file(--iof):注入一个文件的内容--inject-inner-str(--iis)/--inject-outer-str(--ios):直接注入字符串
inner 落在 testbench module 内部、endmodule 之前;outer 落在 module 声明之前,适合放宏定义、include、package import。
SVBuilder 的输出是多行 SV,通常还会纳入版本控制,用文件形式更合适:可以单独 diff 审查,编辑器也能按 SV 语法高亮;需要放宏或 import 时换成 --inject-outer-file。
set_values("verilua.tb_gen_flags", { "--inject-inner-file", sv_file })
路径可以是 path.join(os.scriptdir(), ...) 拼出的绝对路径,也可以直接给相对路径,按 project dir 解析(Verilua 规则调用 testbench_gen 前会切到 project dir)。
注入的文件只交给 testbench_gen,不要再写进 add_files,否则同一段内容会被编译两次。
testbench_gen 判断能否跳过重新生成时,只比对命令行(含注入文件的路径)、others.sv 和各 RTL 源文件,不读注入文件的内容。所以只改了被注入的 .sv(例如重跑一次 SV_GEN=1),它会打 up-to-date 并复用旧的 tb_top.sv,新内容不会进仿真。
要强制刷新 tb_top.sv:删掉 build/<sim>/<top>/tb_top.sv,或在 verilua.tb_gen_flags 里加 --regen(每次 build 都重新生成)。
完整的选项表和插入位置示意见 testbench 生成。
自动生成:build 前用 nosim 刷新
nosim 是 Verilua 的不仿真后端,用 slang 做 elaborate,不推进仿真时间,也不读写信号值。
手动流程要先生成、再 build 一次,因为 SVBuilder 的输出只能在运行阶段产生,注入的是下一次编译的 tb_top.sv。用 before_build + nosim 可以让 xmake build 一步到位:build 前先用 slang(nosim)跑一遍生成逻辑刷新输出文件,再让同一次 build 的 testbench_gen 注入。
原理:
- 生成逻辑仍写在
verilua.lua_main里,由SV_GEN环境变量控制。 - 在
xmake.lua注册before_build:当前后端不是 nosim 时,用子进程 xmake 以SIM=nosim SV_GEN=1先build+run一次刷新输出文件;输出路径写成SV_OUT传给生成进程,只在这一处定义。 before_build早于 Verilua 规则的testbench_gen(后者在on_build阶段执行),所以刚生成的文件能在同一次 build 里被--iif注入tb_top.sv。- 子进程设置独立的
XMAKE_CONFIGDIR,否则会和外层 build 争用project.lock死锁。 - 生成进程带
SV_GEN=1时不加--iif,避免去注入一个还没生成的文件。
xmake.lua:
local prj_dir = os.scriptdir()
local sv_file = path.join(prj_dir, "svbuilder_output.sv")
target("my_test", function()
add_rules("verilua")
on_config(function(target)
local sim = os.getenv("SIM") or "verilator"
-- 按 SIM 选择 @nosim / @verilator / @vcs / ...
end)
add_files("rtl/*.sv")
set_values("verilua.top", "Top")
set_values("verilua.lua_main", path.join(prj_dir, "main.lua"))
-- 生成进程(SV_GEN=1)不注入,其余后端把生成内容注入 tb_top
if not os.getenv("SV_GEN") then
add_values("verilua.tb_gen_flags", "--iif", sv_file)
end
before_build(function(target)
if (os.getenv("SIM") or "verilator") == "nosim" then
return
end
local gen_envs = {
SIM = "nosim",
SV_GEN = "1",
SV_OUT = sv_file,
-- 独立 config dir:避免与外层 build 争用 project.lock
XMAKE_CONFIGDIR = path.join(prj_dir, ".xmake", ".xmake-nosim"),
}
local target_name = target:name()
os.execv("xmake", { "build", "-P", prj_dir, target_name }, { envs = gen_envs })
os.execv("xmake", { "run", "-P", prj_dir, target_name }, { envs = gen_envs })
end)
end)
main.lua 用同一个 gate,输出路径直接从环境变量读取:
if os.getenv("SV_GEN") == "1" then
fork {
function()
local ctx = require "verilua.sv.SVBuilder"
ctx:clean()
ctx:default_clocking(dut.clk, "posedge")
-- ... 定义 sequence / property / assert / covergroup ...
local out = assert(os.getenv("SV_OUT"))
local f = assert(io.open(out, "w"))
f:write(ctx:generate())
f:close()
sim.finish()
end,
}
end
之后每次只需要一条命令,注入的始终是本次 build 开始时生成的内容:
xmake build my_test
注意:
- 生成进程用 slang(nosim),
dut.*handle 可用,但<chdl>:get()之类会报错。 - nosim 与真实后端的 build 目录默认分开(
build/<sim>/...),互不覆盖。 xmake build会改写输出文件;若它已纳入版本控制,构建后需要自行确认 diff。
输出格式
generate() 保留所有 add 调用的顺序,不按语句类型重新分组:
- 开头连续添加的
raw辅助声明 default clocking声明(如果设置了)- 其余语句按
add顺序输出;每个 covergroup 的定义和实例化保持相邻 finalcoverage 报告(若启用)
即使先调用 default_clocking,开头的 raw 仍排在 clocking 前,让其中声明的 bare clock / reset 可供时钟使用。若只有 raw,clocking 放在这些块之后;其他 raw 与相邻语句保持 add 顺序。
cover / assert 的 // i/n 编号仅统计这两类语句,在其他类型穿插时仍连续递增。Lint 使用相同的语句顺序。
默认不输出块标记。调用 ctx:set_block_comments(true) 后,每次 add 的完整产物会带有名称和类型标记,begin 行末还会附上 Lua 调用位置:
// ---- pulse_seen (sequence) begin ---- @ monitor.lua:42
sequence pulse_seen(); pulse; endsequence
// ---- pulse_seen (sequence) end ----
启用时,covergroup 的定义和实例化共用一对标记;default clocking 和末尾报告不加标记。路径使用 Lua 调试信息中的源码名;缺少调试信息时省略位置。名称和源码位置中的换行只在注释内替换为空格。
各块之间用空行分隔,可直接嵌入 SystemVerilog module 中使用。