Testbench 自动生成流程
Verilua 中,Testbench 是全自动生成的 testbench_gen 工具实现(安装 Verilua 的时候会编译生成 testbench_gen),其核心功能包括顶层模块的例化、时钟驱动、波形控制等。具体工作流程如下图所示:

testbench_gen 底层使用的是 slang 进行 RTL 代码的解析。常用命令行参数如下:
| 参数 | 说明 |
|---|---|
--tbtop <module_name> | 指定生成的 testbench 顶层模块名,默认值为 tb_top。 |
--dut-name <instance_name> | 指定 DUT 实例名,默认值为 u_<top>。 |
--out-dir <directory> | 指定生成文件的输出目录。 |
--clock-signal <signal_name> | 指定主时钟信号名;未指定时会自动检测。 |
--reset-signal <signal_name> | 指定复位信号名;未指定时会自动检测。 |
--period <period_value> | 指定内部自动生成时钟的周期,默认值为 20。 |
--inject-inner-file <file> | 将文件内容注入到生成的 testbench 模块内部底部,即 endmodule 之前。 |
--inject-inner-str <string> | 将字符串内容注入到生成的 testbench 模块内部底部,即 endmodule 之前。 |
--inject-outer-file <file> | 将文件内容注入到生成的 testbench module 声明之前。 |
--inject-outer-str <string> | 将字符串内容注入到生成的 testbench module 声明之前。 |
--filelist <file/filelist> | 指定输入文件或 filelist。 |
--regen | 强制重新生成 testbench。 |
--verbose | 输出更详细的日志信息。 |
完整可用参数请以 testbench_gen --help 输出为准。
生成结果
默认情况下,testbench_gen 会生成两个文件:
tb_top.sv:主 Testbench 文件。others.sv:用户自定义模块的占位文件。
在 Verilua 的默认工作流中,这两个文件会生成到当前 target 的构建目录,而不是源码目录。构建目录的命名和位置可参考 verilua.build_dir_name / verilua.build_dir_path 的说明;默认情况下,该目录通常是 ./build/<simulator>/<top module name>。Verilua 调用 testbench_gen 时会自动把这个构建目录传给 --out-dir。如果直接手动运行 testbench_gen,未指定 --out-dir 时则默认输出到当前目录。
tb_top.sv
tb_top.sv 包含 DUT 的例化、时钟/复位生成、波形控制接口、DPI 函数声明等。每次重新生成 testbench 时,该文件都会被覆盖,因此不应直接修改,而应通过 --custom-code 系列选项插入自定义代码。
others.sv
others.sv 是一个空模块,用于放置用户自定义的 RTL 代码,例如额外的监控模块或测试逻辑。如果该文件已存在,testbench_gen 不会覆盖它,因此可以安全修改。
部分内容示例如下(假设 DUT 为 Design):
// -----------------------------------------
// user custom code (outer)
// use `--inject-outer-file/-iof <file>` or
// `--inject-outer-str/-ios <string>`.
// -----------------------------------------
module tb_top(
`ifdef SIM_VERILATOR
input wire clock,
input wire reset,
output wire [63:0] cycles_o
`endif
);
// ... 内部逻辑
// -----------------------------------------
// reg/wire declaration
// -----------------------------------------
reg inc;
reg[7:0] test;
wire[7:0] value;
// -----------------------------------------
// DUT module instantiate
// -----------------------------------------
Design u_Design (
.clock (clock),
.reset (reset),
.inc (inc),
.test (test),
.value (value)
);
// -----------------------------------------
// other user code...
// -----------------------------------------
Others u_others(
.clock(clock),
.reset(reset)
);
// -----------------------------------------
// user custom code (inner)
// use `--inject-inner-file/-iif <file>` or
// `--inject-inner-str/-iis <string>`.
// -----------------------------------------
endmodule
module Others (
input wire clock,
input wire reset
);
// 在这里添加您自己的逻辑,例如:
// always @(posedge clock) begin
// // ...
// end
endmodule
使用自定义代码插入
当您需要在生成的 tb_top.sv 中插入额外的 SystemVerilog 代码时,可以使用下面 4 个参数:
| 参数 | 传入形式 | 实际插入位置 | 典型用途 |
|---|---|---|---|
--inject-outer-file <file> / -iof | 文件 | module <tbtop>(...) 之前 | 宏定义(如 define)、include、package import、辅助模块声明 |
--inject-outer-str <string> / -ios | 字符串 | module <tbtop>(...) 之前 | 很短的宏定义或预处理语句 |
--inject-inner-file <file> / -iif | 文件 | testbench 模块内部、endmodule 之前 | initial / always / assertion / monitor 逻辑 |
--inject-inner-str <string> / -iis | 字符串 | testbench 模块内部、endmodule 之前 | 很短的内联调试逻辑 |
插入位置关系如下:
// --inject-outer-str / --inject-outer-file inserted here
module tb_top(...);
// generated declarations
// generated DUT instantiation
// generated helper logic
// --inject-inner-str / --inject-inner-file inserted here
endmodule
使用建议:
- 需要插入多行代码、单独维护文件、或者包含复杂
initial/always块时,优先使用--inject-inner-file或--inject-outer-file。 - 只需要插入一两行简短宏定义或调试语句时,使用
--inject-inner-str或--inject-outer-str更方便。 outer系列注入的是模块外部代码,适合预处理宏、include、package import、辅助 module/function/task 声明;不适合直接写依赖 testbench 内部信号的逻辑。inner系列注入的是模块内部代码,可以直接写initial、always、assertion 和其他 testbench 逻辑。- 同一侧的 file 与 string 形式可以同时使用;若同时指定,
-str的内容会先插入,文件内容随后插入。 --inject-inner-str和--inject-outer-str本身支持多行字符串;testbench_gen会按原样插入,不会自动重新排版。
示例 1:在模块外部插入一个额外的宏定义文件
`define TB_TIMEOUT 10000
`define TB_TRACE_ENABLE 1
add_values("verilua.tb_gen_flags", "--inject-outer-file", "tb_prelude.svh")
适用场景:需要把宏、include 或其他模块外定义放到 tb_top.sv 的 module 声明之前。
示例 2:直接以内联字符串插入一个简单宏
add_values("verilua.tb_gen_flags", "--inject-outer-str", "`define TB_TIMEOUT 10000")
适用场景:只有一行简单内容,不想额外维护文件。
示例 3:在模块内部插入一段监控逻辑文件
initial begin
$display("[tb] simulation start");
end
always @(posedge clock) begin
if (!reset) begin
// user monitor logic
end
end
add_values("verilua.tb_gen_flags", "--inject-inner-file", "tb_hooks.sv")
适用场景:插入多行 initial / always / assertion / monitor 逻辑。
示例 4:直接以内联字符串插入一个简短的 initial 语句
add_values("verilua.tb_gen_flags", "--inject-inner-str", 'initial $display("[tb] ready");')
适用场景:只想临时加一条很短的调试输出。
示例 5:在 xmake.lua 中传入多行 --inject-inner-str
下面这个例子已经实际验证过,会把一个完整的多行 initial 块插入到 tb_top.sv 的 endmodule 之前:
add_values("verilua.tb_gen_flags", "--inject-inner-str", [[
initial begin
$display("[tb] xmake multiline inner");
end
]])
示例 6:在 xmake.lua 中传入多行 --inject-outer-str
下面这个例子已经实际验证过,会把多行宏定义插入到 module tb_top(...) 之前:
add_values("verilua.tb_gen_flags", "--inject-outer-str", [[
`define TB_XMAKE_OUTER 1
`define TB_XMAKE_OUTER_2 2
]])
示例 7:直接命令行传入多行 --inject-inner-str
下面这个例子已经实际验证过,会把一个完整的多行 initial 块插入到 tb_top.sv 的 endmodule 之前:
testbench_gen \
--top MinimalTop \
--out-dir ./build/verilator/MinimalTop \
--inject-inner-str $'initial begin\n $display("[tb] direct multiline inner");\n $display("[tb] line2");\nend' \
minimal_top.sv
生成结果中的对应片段如下:
initial begin
$display("[tb] direct multiline inner");
$display("[tb] line2");
end
示例 8:直接命令行传入多行 --inject-outer-str
下面这个例子已经实际验证过,会把多行宏定义插入到 module tb_top(...) 之前:
testbench_gen \
--top MinimalTop \
--out-dir ./build/verilator/MinimalTop \
--inject-outer-str $'`define TB_DIRECT_OUTER 1\n`define TB_DIRECT_OUTER_2 2' \
minimal_top.sv
生成结果中的对应片段如下:
`define TB_DIRECT_OUTER 1
`define TB_DIRECT_OUTER_2 2
module tb_top(...);
注意事项:
- 如果直接命令行调用
testbench_gen,--inject-inner-str和--inject-outer-str需要根据所用 shell 正确处理引号、反引号和换行;复杂内容通常更建议使用 file 形式。 - 在
xmake.lua中,verilua.tb_gen_flags现在可以正确透传包含空格和换行的--inject-inner-str/--inject-outer-str参数。 - 如果内容较长、需要复用、或者需要避免 shell 引号问题,依然更推荐
--inject-inner-file <file>和--inject-outer-file <file>。
时钟策略
默认情况下,testbench_gen 会自动在 RTL 侧生成时钟信号。这是 Verilua 的一项性能优化设计,将时钟生成放在 RTL 层面可以避免使用 Lua 任务来驱动时钟信号,从而减少任务切换开销,提高仿真性能。
内部时钟自动生成
RTL 侧时钟生成(默认):
- 时钟信号由 SystemVerilog 的
always块直接驱动。 - 无需 Lua 任务参与时钟切换。
- 仿真开销最小,性能最好。
可以通过 verilua.tb_gen_flags 传递 --period 参数来配置时钟周期。该参数仅在启用内部时钟时有效。例如:
add_values("verilua.tb_gen_flags", "--period", "15") -- 周期 15ns
生成的 testbench 会使用该值产生 50% 占空比的时钟。
多时钟配置
当设计包含多个时钟信号时,通常需要禁用 testbench_gen 的内部时钟自动生成功能,然后在用户代码中手动管理所有时钟信号。这样可以完全控制每个时钟的频率和相位。
testbench_gen 会按照以下优先级识别主时钟信号:
- 用户通过
--clock-signal显式指定的信号名,可以是简单端口名,也可以是层次化路径,例如top.clk_gen.clk。 - 若未指定,则按优先级自动检测常用信号名(不区分大小写):
clock、clock_i、clk、clk_i、i_clk。
如果设计中存在多个匹配的时钟信号,testbench_gen 会选择优先级最高的信号作为主时钟。对于多时钟设计,建议始终显式指定 --clock-signal。
在 xmake.lua 中禁用内部时钟生成:
set_values("verilua.no_internal_clock", "1")
假设设计有以下时钟信号:
sys_clk:系统主时钟uart_clk:UART 时钟mem_clk:内存时钟
步骤 1:在 xmake.lua 中配置
add_values("verilua.tb_gen_flags", "--clock-signal", "sys_clk")
set_values("verilua.no_internal_clock", "1")
步骤 2:在测试代码中手动生成所有时钟
local sys_clk = dut.sys_clk:chdl()
local uart_clk = dut.uart_clk:chdl()
local mem_clk = dut.mem_clk:chdl()
fork {
-- System clock: 100MHz (period 10ns)
function()
while true do
sys_clk:set(1)
await_time_ns(5)
sys_clk:set(0)
await_time_ns(5)
end
end,
-- UART clock: 9.6MHz (period ~= 104.167ns)
function()
while true do
uart_clk:set(1)
await_time_ns(52)
uart_clk:set(0)
await_time_ns(52)
end
end,
-- Memory clock: 200MHz (period 5ns)
function()
while true do
mem_clk:set(1)
await_time_ns(2.5)
mem_clk:set(0)
await_time_ns(2.5)
end
end
}
选择建议
推荐使用内部时钟的场景:
- 单时钟设计,追求最佳仿真性能。
- 标准 50% 占空比时钟即可满足需求。
- 对仿真速度有较高要求。
需要禁用内部时钟的场景:
- 多时钟设计,需要分别控制多个时钟信号。
- 需要非标准时钟波形,例如可变占空比或复杂调制。
- 存在时钟门控、动态调频或精确跨时钟域控制需求。
传递额外参数
Testbench 只在 HVL 场景下才需要自动生成。在 xmake.lua 中可以通过 add_values("verilua.tb_gen_flags", ...) 传递其他 flags,例如:
add_values("verilua.tb_gen_flags", "--ignore-unknown-modules", "--verbose")
强制重新生成
如果需要强制重新生成 testbench(即使文件已存在),可以使用 -r 或 --regen 参数:
add_values("verilua.tb_gen_flags", "-r")
这在修改了端口或参数后很有用。
常见问题
Q: 我的顶层模块端口名不是 clock,生成的 testbench 中时钟信号会是什么?
A: 您需要在 xmake.lua 中通过 add_values("verilua.tb_gen_flags", "--clock-signal", "your_clk_name") 指定正确的时钟信号名。如果不指定,testbench_gen 会尝试自动检测,但可能匹配不到。
Q: 我可以自定义生成的 testbench 模块名吗?
A: 可以。使用 --tbtop <module_name> 参数,例如 --tbtop my_tb。默认值为 tb_top。
Q: DUT 实例名可以自定义吗?
A: 可以。使用 --dut-name <name> 参数,例如 --dut-name my_dut_instance。默认值为 u_<顶层模块名>。
Q: 我想在生成的 testbench 中添加一些自定义的 initial 块,怎么办?
A: 您可以将这些代码放入一个文件中,然后通过 --inject-inner-file 参数注入进来。该文件的内容会被插入到 testbench 的 endmodule 之前。
Q: others.sv 的作用是什么?我可以修改它吗?
A: others.sv 是一个预留给用户的模块文件,您可以在其中添加任何额外的 RTL 代码。它不会被 testbench_gen 覆盖,因此您可以安全地修改它。每次重新生成时,它都会保留您的修改。
Q: 为什么我指定了 --clock-signal 但生成的 testbench 中时钟仍然不对?
A: 请确保指定的信号名正确(大小写敏感),并且该信号确实是顶层模块的输入端口。如果信号位于子模块中,您可能需要指定层次化路径,例如 top.sub.clk。另外,如果信号在设计中存在但未在顶层端口暴露,您可能需要使用 --ignore-unknown-modules 或其他编译选项来允许内部信号访问。
Q: 运行 testbench_gen 时 slang 报错找不到 include 的头文件(如 'my_header.svh': No such file or directory),怎么办?
A: testbench_gen 底层使用 slang 解析 RTL,头文件搜索路径需要通过 -I(或 --include-directory / +incdir)显式指定,而不是依赖 Verilog .f filelist 里的 +incdir+。
在 xmake.lua 中,通过 verilua.tb_gen_flags 传入 include 路径:
add_values("verilua.tb_gen_flags", "-I", path.join(os.scriptdir(), "include"))
注意:testbench_gen 自带的 filelist 解析器只按行读取文件列表,不会解析 .f 文件中的 +incdir+ 等指令。因此即便 filelist 里写了 +incdir+,仍需通过 -I 或 --include-directory 单独传给 testbench_gen。
相关文档
- xmake 参数说明:查看
verilua.tb_gen_flags、构建目录和仿真器参数的完整说明。 - 时钟驱动策略:了解
--clock-signal、内部时钟和手动驱动之间的关系。 - 编写 xmake.lua:查看如何在工程里组织
testbench_gen相关配置。 - HVL 示例:对照最小工程理解 testbench 自动生成的默认工作流。