Skip to main content

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):

字段类型必填说明
namestring✓语句名称,整个 context 内唯一
exprstring✓SVA/SV 表达式,支持 $(var) 模板语法
envstable模板变量表,key→value 映射
formal_argsstring仅 sequence/property 有效:形参列表,如 "logic req, logic ack",直接插入括号内
cov_typestring仅 cover 有效:"property"(默认)或 "sequence"
sample_eventstring仅 covergroup 有效:覆盖默认采样事件

返回值:

  • "sequence" → 返回 Sequence handle(可通过 $(seq:name) 引用)
  • "property" → 返回 Property handle(可通过 $(prop:name) 引用)
  • "covergroup" → 返回 Covergroup handle,.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 自动处理:

  1. 外层包裹:covergroup name @(event); ... endgroup
  2. 实例化:name _GEN_name_inst = new;
  3. 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 生成

参数说明​

参数类型必填说明
namestring✓covergroup 名称(必须唯一)
exprstring✓covergroup 体(coverpoint/bins/cross/option),支持 $(var) 模板语法
sample_eventstring✗覆盖默认采样事件(默认复用 default_clocking);也支持 "with function sample(...)" 声明显式采样形参
envstable✗模板变量,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]],
}
warning

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
tip

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,否则同一段内容会被编译两次。

warning

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 注入。

原理:

  1. 生成逻辑仍写在 verilua.lua_main 里,由 SV_GEN 环境变量控制。
  2. 在 xmake.lua 注册 before_build:当前后端不是 nosim 时,用子进程 xmake 以 SIM=nosim SV_GEN=1 先 build + run 一次刷新输出文件;输出路径写成 SV_OUT 传给生成进程,只在这一处定义。
  3. before_build 早于 Verilua 规则的 testbench_gen(后者在 on_build 阶段执行),所以刚生成的文件能在同一次 build 里被 --iif 注入 tb_top.sv。
  4. 子进程设置独立的 XMAKE_CONFIGDIR,否则会和外层 build 争用 project.lock 死锁。
  5. 生成进程带 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 调用的顺序,不按语句类型重新分组:

  1. 开头连续添加的 raw 辅助声明
  2. default clocking 声明(如果设置了)
  3. 其余语句按 add 顺序输出;每个 covergroup 的定义和实例化保持相邻
  4. final coverage 报告(若启用)

即使先调用 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 中使用。