Skip to main content

Testbench 自动生成流程

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

Testbench generate workflow
Testbench generate workflow

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

tb_top.sv
// -----------------------------------------
// 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
others.sv
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 系列注入的是模块内部代码,可以直接写 initialalways、assertion 和其他 testbench 逻辑。
  • 同一侧的 file 与 string 形式可以同时使用;若同时指定,-str 的内容会先插入,文件内容随后插入。
  • --inject-inner-str--inject-outer-str 本身支持多行字符串;testbench_gen 会按原样插入,不会自动重新排版。

示例 1:在模块外部插入一个额外的宏定义文件

tb_prelude.svh
`define TB_TIMEOUT 10000
`define TB_TRACE_ENABLE 1
add_values("verilua.tb_gen_flags", "--inject-outer-file", "tb_prelude.svh")

适用场景:需要把宏、include 或其他模块外定义放到 tb_top.svmodule 声明之前。

示例 2:直接以内联字符串插入一个简单宏

add_values("verilua.tb_gen_flags", "--inject-outer-str", "`define TB_TIMEOUT 10000")

适用场景:只有一行简单内容,不想额外维护文件。

示例 3:在模块内部插入一段监控逻辑文件

tb_hooks.sv
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.svendmodule 之前:

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.svendmodule 之前:

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 会按照以下优先级识别主时钟信号:

  1. 用户通过 --clock-signal 显式指定的信号名,可以是简单端口名,也可以是层次化路径,例如 top.clk_gen.clk
  2. 若未指定,则按优先级自动检测常用信号名(不区分大小写):clockclock_iclkclk_ii_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 自动生成的默认工作流。