Add VPI callback support to --main (#7145).

Fixes #7145.
This commit is contained in:
Wilson Snyder 2026-02-28 09:42:28 -05:00
parent d40036239b
commit 1e6c1ab106
12 changed files with 138 additions and 129 deletions

View File

@ -13,6 +13,7 @@ Verilator 5.047 devel
**Other:**
* Add VPI callback support to --main (#7145).
* Fix wide conditional short circuiting (#7155).

View File

@ -114,7 +114,7 @@ CPPFLAGS += $(OPT)
# On macOS, specify all weak symbols as dynamic_lookup.
# Otherwise, you get undefined symbol errors.
ifeq ($(UNAME_S),Darwin)
LDFLAGS += -Wl,-U,__Z15vl_time_stamp64v,-U,__Z13sc_time_stampv
LDFLAGS += -Wl,-U,__Z15vl_time_stamp64v,-U,__Z13sc_time_stampv,-U,vlog_startup_routines
endif
# Allow upper level user makefiles to specify flags they want.

View File

@ -716,7 +716,7 @@ public:
// cppcheck-suppress uninitVar // m_value
VerilatedVpiCbHolder(uint64_t id, const s_cb_data* cbDatap, const VerilatedVpioVar* varop)
: m_id{id}
, m_cbData{*cbDatap}
, m_cbData{*cbDatap} // vpi_register_cb checks cbDatap is non-null
, m_varo{varop} {
m_value.format = cbDatap->value ? cbDatap->value->format : vpiSuppressVal;
m_cbData.value = &m_value;

View File

@ -55,13 +55,21 @@ private:
// Heavily commented output, as users are likely to look at or copy this code
puts("#include \"verilated.h\"\n");
if (v3Global.opt.vpi()) puts("#include \"verilated_vpi.h\"\n");
puts("#include \"" + EmitCUtil::topClassName() + ".h\"\n");
if (v3Global.opt.debugRuntimeTimeout()) {
puts("\n");
puts("#include <csignal>\n");
}
puts("\n");
puts("\n//======================\n\n");
if (v3Global.opt.vpi()) {
puts("// User VPI code adds to this array to get startup main() callbacks\n");
puts("extern \"C\" void (*vlog_startup_routines[])() VL_ATTR_WEAK;\n");
puts("\n");
}
puts("//======================\n\n");
if (v3Global.opt.debugRuntimeTimeout()) {
puts("void alarmHandler(int signum) {\n");
@ -92,18 +100,43 @@ private:
+ EmitCUtil::topClassName() + "{contextp.get(), \"" + topName + "\"}};\n");
puts("\n");
if (v3Global.opt.vpi()) {
puts("// Hook VPI startup routines and invoke callback\n");
puts("if (vlog_startup_routines) {\n");
puts(/**/ "for (auto routinep = &vlog_startup_routines[0]; *routinep; routinep++)"
" (*routinep)();\n");
puts("}\n");
puts("VerilatedVpi::callCbs(cbStartOfSimulation);\n");
puts("\n");
}
puts("// Simulate until $finish\n");
puts("while (VL_LIKELY(!contextp->gotFinish())) {\n");
if (v3Global.opt.vpi()) {
puts(/**/ "// VPI callbacks\n");
puts(/**/ "VerilatedVpi::callTimedCbs();\n");
puts(/**/ "VerilatedVpi::callCbs(cbNextSimTime);\n"); // Before next event queue
puts(/**/ "VerilatedVpi::callCbs(cbAtStartOfSimTime);\n"); // Before time queue
}
puts(/**/ "// Evaluate model\n");
puts(/**/ "topp->eval();\n");
if (v3Global.opt.vpi()) {
puts(/**/ "// VPI callbacks\n");
puts(/**/ "VerilatedVpi::callValueCbs();\n");
puts(/**/ "VerilatedVpi::callCbs(cbAtEndOfSimTime);\n"); // After nonblocking events
puts(/**/ "VerilatedVpi::callCbs(cbReadWriteSynch);\n"); // After a specified time
puts(/**/ "VerilatedVpi::callCbs(cbReadOnlySynch);\n"); // After cbReadWriteSynch
}
puts(/**/ "// Advance time\n");
if (v3Global.rootp()->delaySchedulerp() || v3Global.opt.timing()) {
puts("if (!topp->eventsPending()) break;\n");
}
if (v3Global.rootp()->delaySchedulerp()) {
puts("contextp->time(topp->nextTimeSlot());\n");
const std::string nextSlot = v3Global.rootp()->delaySchedulerp() ? "topp->nextTimeSlot()"
: "contextp->time() + 1";
if (v3Global.opt.vpi()) {
puts("contextp->time(std::min("s + nextSlot + ", VerilatedVpi::cbNextDeadline()));\n");
} else {
puts("contextp->timeInc(1);\n");
puts("contextp->time("s + nextSlot + ");\n");
}
puts("}\n");
@ -116,6 +149,7 @@ private:
puts("// Execute 'final' processes\n");
puts("topp->final();\n");
if (v3Global.opt.vpi()) puts("VerilatedVpi::callCbs(cbEndOfSimulation);\n");
puts("\n");
if (v3Global.opt.coverage()) {

View File

@ -159,7 +159,7 @@ void vpi_compat_bootstrap(void) {
// We're able to call vpi_main() here on Verilator/Xcelium,
// but Icarus complains (rightfully so)
s_cb_data cb_data;
s_cb_data cb_data{};
s_vpi_time vpi_time;
vpi_time.high = 0;

19
test_regress/t/t_vpi_empty.py Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
# Checks that --vpi without a C file doesn't break builds
test.compile(verilator_flags2=["--binary --vpi"])
test.execute()
test.passes()

View File

@ -0,0 +1,9 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t;
initial $finish;
endmodule

View File

@ -12,21 +12,10 @@
// Setup multiple one-time callbacks with different time delays.
// Ensure they are not called before the delay has elapsed.
#ifdef IS_VPI
#include "vpi_user.h"
#include <cstdlib>
#else
#include "verilated.h"
#include "verilated_vpi.h"
#include VM_PREFIX_INCLUDE
#endif
// These require the above. Comment prevents clang-format moving them
#include "TestSimulator.h"
#include "TestVpi.h"
@ -45,7 +34,7 @@ typedef struct {
static cb_stats CallbackStats[cbAtEndOfSimTime + 1];
bool got_error = false;
int errors = 0;
static vpiHandle ValueHandle, ToggleHandle, ClockHandle;
@ -80,28 +69,40 @@ bool cb_time_is_delay(int cb_name) {
return false;
}
vpiHandle test_vpi_register_cb(p_cb_data cb_datap) {
if (verbose)
vpi_printf(const_cast<char*>("- test_vpi_register_cb(%s @%d)\n"),
strFromVpiCallbackReason(cb_datap->reason),
(cb_datap->time ? cb_datap->time->low : 0));
return vpi_register_cb(cb_datap);
}
// forward declaration
static PLI_INT32 TheCallback(s_cb_data* data);
static PLI_INT32 AtEndOfSimTimeCallback(s_cb_data* data) {
s_vpi_time t;
CHECK_RESULT(data->reason, cbAtEndOfSimTime);
cb_stats* stats = &CallbackStats[data->reason];
t.type = vpiSimTime;
vpi_get_time(0, &t);
if (verbose) vpi_printf(const_cast<char*>("- [@%d] AtEndOfSimTime Callback\n"), t.low);
if (verbose)
vpi_printf(const_cast<char*>("- [@%d] AtEndOfSimTime Callback (count=%d)\n"), t.low,
stats->count);
CHECK_RESULT(stats->count < stats->number_of_exp_times, 1);
CHECK_RESULT(t.low, stats->exp_times[stats->count]);
stats->count += 1;
s_cb_data cb_data;
s_cb_data cb_data{};
s_vpi_time time = {vpiSimTime, 0, 417, 0}; // non-zero time to check that it's ignored
cb_data.time = &time;
cb_data.reason = cbNextSimTime;
cb_data.cb_rtn = TheCallback;
vpiHandle Handle = vpi_register_cb(&cb_data);
vpiHandle Handle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(Handle);
return 0;
@ -116,16 +117,17 @@ static PLI_INT32 TheCallback(s_cb_data* data) {
vpi_get_time(0, &t);
if (verbose) {
vpi_printf(const_cast<char*>("- [@%d] %s Callback\n"), t.low,
cb_reason_to_string(data->reason));
vpi_printf(const_cast<char*>("- [@%d] %s Callback (count=%d)\n"), t.low,
cb_reason_to_string(data->reason), stats->count);
}
CHECK_RESULT(stats->count < stats->number_of_exp_times, 1);
CHECK_RESULT(t.low, stats->exp_times[stats->count]);
stats->count += 1;
if (stats->count >= stats->number_of_exp_times) return 0;
s_cb_data cb_data;
s_cb_data cb_data{};
PLI_UINT32 next_time;
if (data->reason == cbNextSimTime) {
@ -150,14 +152,14 @@ static PLI_INT32 TheCallback(s_cb_data* data) {
s_vpi_time time = {vpiSimTime, 0, next_time, 0};
cb_data.time = &time;
vpiHandle Handle = vpi_register_cb(&cb_data);
vpiHandle Handle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(Handle);
return 0;
}
static PLI_INT32 StartOfSimulationCallback(s_cb_data* data) {
s_cb_data cb_data;
s_cb_data cb_data{};
s_vpi_time timerec = {vpiSimTime, 0, 0, 0};
s_vpi_time t;
@ -179,32 +181,33 @@ static PLI_INT32 StartOfSimulationCallback(s_cb_data* data) {
CallbackStats[cbAtStartOfSimTime].number_of_exp_times = 3;
timerec.low = 5;
cb_data.reason = cbAtStartOfSimTime;
vpiHandle ASOSHandle = vpi_register_cb(&cb_data);
vpiHandle ASOSHandle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(ASOSHandle);
CallbackStats[cbReadWriteSynch].exp_times = new PLI_UINT32[3]{6, 16, 21};
CallbackStats[cbReadWriteSynch].number_of_exp_times = 3;
timerec.low = 6;
cb_data.reason = cbReadWriteSynch;
vpiHandle RWHandle = vpi_register_cb(&cb_data);
vpiHandle RWHandle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(RWHandle);
CallbackStats[cbReadOnlySynch].exp_times = new PLI_UINT32[3]{7, 17, 22};
CallbackStats[cbReadOnlySynch].number_of_exp_times = 3;
timerec.low = 7;
cb_data.reason = cbReadOnlySynch;
vpiHandle ROHandle = vpi_register_cb(&cb_data);
vpiHandle ROHandle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(ROHandle);
CallbackStats[cbNextSimTime].exp_times = new PLI_UINT32[9]{5, 6, 7, 15, 16, 17, 20, 21, 22};
CallbackStats[cbNextSimTime].number_of_exp_times = 9;
CallbackStats[cbNextSimTime].exp_times
= new PLI_UINT32[10]{5, 6, 7, 10, 15, 16, 17, 20, 21, 22};
CallbackStats[cbNextSimTime].number_of_exp_times = 10;
timerec.low = 8;
cb_data.reason = cbNextSimTime;
vpiHandle NSTHandle = vpi_register_cb(&cb_data);
vpiHandle NSTHandle = test_vpi_register_cb(&cb_data);
CHECK_RESULT_NZ(NSTHandle);
CallbackStats[cbAtEndOfSimTime].exp_times = new PLI_UINT32[8]{5, 6, 7, 15, 16, 17, 20, 21};
CallbackStats[cbAtEndOfSimTime].number_of_exp_times = 8;
CallbackStats[cbAtEndOfSimTime].exp_times = new PLI_UINT32[9]{5, 6, 7, 10, 15, 16, 17, 20, 21};
CallbackStats[cbAtEndOfSimTime].number_of_exp_times = 9;
return (0);
}
@ -223,23 +226,26 @@ static int EndOfSimulationCallback(p_cb_data cb_data) {
CHECK_RESULT(CallbackStats[cbStartOfSimulation].count, 1);
CHECK_RESULT(CallbackStats[cbAtStartOfSimTime].count, 3);
CHECK_RESULT(CallbackStats[cbNextSimTime].count, 9);
CHECK_RESULT(CallbackStats[cbNextSimTime].count, 10);
CHECK_RESULT(CallbackStats[cbReadWriteSynch].count, 3);
CHECK_RESULT(CallbackStats[cbReadOnlySynch].count, 3);
CHECK_RESULT(CallbackStats[cbAtEndOfSimTime].count, 8);
CHECK_RESULT(CallbackStats[cbAtEndOfSimTime].count, 9);
CHECK_RESULT(CallbackStats[cbEndOfSimulation].count, 1);
if (!got_error) printf("*-* All Finished *-*\n");
if (!errors) printf("*-* All Finished *-*\n");
return 0;
}
// cver entry
static void VPIRegister(void) {
if (verbose) vpi_printf(const_cast<char*>("- VPIRegister callback\n"));
// Clear stats
for (int cb = 1; cb <= cbAtEndOfSimTime; cb++) CallbackStats[cb].count = 0;
CallbackStats[cbStartOfSimulation].exp_times = new PLI_UINT32(0);
CallbackStats[cbEndOfSimulation].exp_times = new PLI_UINT32(22);
s_cb_data cb_data;
CallbackStats[cbEndOfSimulation].exp_times = new PLI_UINT32(100);
s_cb_data cb_data{};
s_vpi_time timerec = {vpiSuppressTime, 0, 0, 0};
cb_data.time = &timerec;
@ -248,80 +254,12 @@ static void VPIRegister(void) {
cb_data.obj = 0;
cb_data.reason = cbStartOfSimulation;
cb_data.cb_rtn = StartOfSimulationCallback;
vpi_register_cb(&cb_data);
test_vpi_register_cb(&cb_data);
cb_data.reason = cbEndOfSimulation;
cb_data.cb_rtn = EndOfSimulationCallback;
vpi_register_cb(&cb_data);
test_vpi_register_cb(&cb_data);
}
#ifdef IS_VPI
// icarus entry
// simulator entry
void (*vlog_startup_routines[])(void) = {VPIRegister, 0};
#else
int main(int argc, char** argv, char** env) {
double sim_time = 100;
const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext};
bool cbs_called;
contextp->commandArgs(argc, argv);
// contextp->debug(9);
const std::unique_ptr<VM_PREFIX> topp{new VM_PREFIX{contextp.get(),
// Note null name - we're flattening it out
""}};
topp->clk = 1;
// StartOfSimulationCallback(nullptr);
VPIRegister();
VerilatedVpi::callCbs(cbStartOfSimulation);
topp->clk = 0;
topp->eval();
while (contextp->time() < sim_time && !contextp->gotFinish()) {
VerilatedVpi::callTimedCbs();
VerilatedVpi::callCbs(cbNextSimTime);
VerilatedVpi::callCbs(cbAtStartOfSimTime);
topp->eval();
VerilatedVpi::callValueCbs();
VerilatedVpi::callCbs(cbReadWriteSynch);
VerilatedVpi::callCbs(cbReadOnlySynch);
VerilatedVpi::callCbs(cbAtEndOfSimTime);
const uint64_t next_time = VerilatedVpi::cbNextDeadline();
if (next_time != -1) contextp->time(next_time);
if (verbose)
vpi_printf(const_cast<char*>("- [@%" PRId64 "] time change\n"), contextp->time());
if (next_time == -1 && !contextp->gotFinish()) {
if (got_error) {
vl_stop(__FILE__, __LINE__, "TOP-cpp");
} else {
VerilatedVpi::callCbs(cbEndOfSimulation);
contextp->gotFinish(true);
}
}
// Count updates on rising edge, so cycle through falling edge as well
topp->clk = !topp->clk;
topp->eval();
topp->clk = !topp->clk;
}
if (!contextp->gotFinish()) {
vl_fatal(__FILE__, __LINE__, "main", "%Error: Timeout; never got a $finish");
}
topp->final();
exit(0L);
}
#endif

View File

@ -12,12 +12,11 @@ import vltest_bootstrap
test.scenarios('simulator')
test.compile(make_top_shell=False,
make_main=False,
make_pli=True,
verilator_flags2=["--exe --vpi", test.pli_filename],
verilator_flags2=["--binary --vpi", test.pli_filename],
iv_flags2=["-g2005-sv -D USE_VPI_NOT_DPI -DIVERILOG"],
v_flags2=["+define+USE_VPI_NOT_DPI"])
test.execute(use_libvpi=True)
test.execute(check_finished=True, use_libvpi=True)
test.passes()

View File

@ -5,10 +5,17 @@
// SPDX-License-Identifier: CC0-1.0
module t (/*AUTOARG*/
// Inputs
input clk
);
module t;
logic clk;
initial begin
clk = 0;
#10;
while ($time < 100) begin
clk = !clk;
#10;
end
end
reg [31:0] count /*verilator public_flat_rd */;
@ -21,4 +28,4 @@ module t (/*AUTOARG*/
count <= count + 2;
end
endmodule : t
endmodule

View File

@ -81,7 +81,7 @@ int _mon_check_unimpl(p_cb_data cb_data) {
handle = vpi_register_cb(NULL);
CHECK_RESULT(handle, 0);
s_cb_data cb_data_s;
s_cb_data cb_data_s{};
cb_data_s.reason = 0; // Bad
handle = vpi_register_cb(&cb_data_s);
CHECK_RESULT(handle, 0);

View File

@ -120,7 +120,9 @@ if(NOT CMAKE_CXX_COMPILER_ID MATCHES MSVC)
endif()
if(APPLE)
add_link_options(-Wl,-U,__Z15vl_time_stamp64v,-U,__Z13sc_time_stampv)
add_link_options(
-Wl,-U,__Z15vl_time_stamp64v,-U,__Z13sc_time_stampv,-U,vlog_startup_routines
)
endif()
define_property(