Skip to content

BUG-004: RTL $finish races cocotb regression shutdown (SimFailure)

Field Value
Severity CRITICAL
Component RTL
File(s) rtl/system_top.sv
Status Fixed
Date 2026-07-19

Symptom

run_riscv_program failed every run with:

error_type="SimFailure" error_msg="Simulator shut down prematurely"

Reproduced identically whether run alone (TESTCASE=run_riscv_program) or as part of the full suite — always the last test, always this error, at a sim time consistent with halt + a small fixed offset.

Root Cause

system_top.sv had a debug block that called $finish directly from RTL, 20 cycles after halt asserted:

`ifndef SYNTHESIS
logic [4:0] finish_cnt;
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)    finish_cnt <= 5'd0;
    else if (halt) finish_cnt <= finish_cnt + 5'd1;
end
always @(posedge clk) begin
    if (finish_cnt == 5'd20) $finish;
end
`endif

But cocotb owns simulation lifecycle in this testbench: run_riscv_program() drains only HALT_DRAIN_CYCLES = 10 cycles after halt, does its register checks, returns, and the cocotb regression manager then performs its own end-of-regression shutdown (which itself calls $finish via VPI). In every observed run, RTL's $finish and cocotb's own shutdown landed in the same simulation timestep, and RTL's always @(posedge clk) block won the race — killing the vvp process before cocotb could cleanly report, which cocotb reports as SimFailure: Simulator shut down prematurely.

This block was a leftover from an earlier flow (the Dockerfile's plain make clean sim) that predates today's cocotb-driven testbenches. No other RTL depends on it — halt is otherwise just an observation port read directly by wait_for_halt() in the testbench — and every other passing test in the suite already terminates cleanly via cocotb's own shutdown without RTL's help.

Fix

Removed the debug $finish block entirely; halt remains as an observation-only output port.

     assign halt = instr_valid && (active_instruction == HALT_WORD);
-
-    `ifndef SYNTHESIS
-    logic [4:0] finish_cnt;
-    always_ff @(posedge clk or negedge rst_n) begin
-        if (!rst_n)    finish_cnt <= 5'd0;
-        else if (halt) finish_cnt <= finish_cnt + 5'd1;
-    end
-    always @(posedge clk) begin
-        if (finish_cnt == 5'd20) $finish;
-    end
-    `endif

Notes

RTL should never call $finish in a cocotb-driven simulation — simulation lifecycle belongs entirely to the Python regression manager. Any future debug/standalone (non-cocotb) Verilog harness that needs self-termination should implement it in its own top-level testbench file, not in synthesizable RTL guarded only by `ifndef SYNTHESIS.