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.