Skip to content

frontend: Fix reg next_id launch deadlock#153

Open
DanielKellerM wants to merge 2 commits into
develfrom
frontend/reg-deadlock-fix
Open

frontend: Fix reg next_id launch deadlock#153
DanielKellerM wants to merge 2 commits into
develfrom
frontend/reg-deadlock-fix

Conversation

@DanielKellerM

Copy link
Copy Markdown
Collaborator

What

Fixes a launch deadlock in the register frontend's next_id read — introduced by the SystemRDL/PeakRDL migration (#73) and inherited by devel/#148 — and adds the first directed reg-frontend testbench.

Root cause

next_id is a PeakRDL external register: reading it launches a transfer and the read stalls (blocking) until the backend arbiter grants (arb_ready). When the launch isn't granted in its single active cycle, the reg_top latches external_pending and masks its own CPUIF req — so next_id.req becomes a one-cycle pulse. The #134 gate rd_ack = req & ~req_is_wr & arb_ready still references that maskable req, so once req drops, rd_ack can never fire, external_pending never clears, and the whole reg block freezes.

Fix

Reconstruct the canonical (pre-#73 reggen) held read-strobe with a per-[reg][stream] pending latch, entirely in the CPUIF-agnostic launch layer:

  • nxt_read_seen = the single-cycle req pulse; nxt_read_pending_q sets on seen & ~arb_ready, clears on pending & arb_ready
  • arb_valid = |(seen | pending) — holds the arbiter request across the stall (satisfies rr_arb_tree LockIn)
  • rd_ack = (seen | pending) & arb_ready — driven by the latch, not the maskable req

next_id/status/done_id stay external (no RDL change, no CPUIF change), which preserves the canonical blocking read-stall exactly: the fast path accepts in cycle 0 (zero added latency), the slow path holds until the grant. Because the read blocks, SW physically cannot issue a second next_id read while one is pending, so the 1-deep latch is sufficient and no launch is ever dropped. Template-only, +30/-7.

Also fixes a latent multi-stream bug: stream_idx_o was keyed on the maskable req and reverted to 0 mid-stall; it is now keyed on (seen | pending).

Test

New self-checking test/tb_idma_reg_frontend.sv (APB DUT + controllable backend stub + idma_transfer_id_gen + a deadlock watchdog) with an idma_sim_tb_idma_reg_frontend make target — the first directed reg-frontend test in the repo. Cases: basic launch, backpressure/deadlock regression, multi-stream stream_idx hold, back-to-back monotonic ids.

  • With the fix: PASS — 26 checks (NumStreams=1), 31 checks (NumStreams=2), 0 errors.
  • Non-vacuity: reverting the template to pre-fix trips the watchdog (DEADLOCK: next_id read did not complete within 2000 cycles) at exactly the external_pending freeze — proving the test catches the bug.

Notes

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@DanielKellerM DanielKellerM left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix

Comment thread src/frontend/reg/tpl/idma_reg.sv.tpl Outdated
Comment on lines +102 to +107
// launch-stall pending state (see gen_nxt_read_pending in gen_core_regs). The PeakRDL
// reg_top masks the CPUIF read req the cycle after an ungranted external read
// (external_pending), so next_id[c].req is a single-cycle pulse. nxt_read_pending holds
// the launch across the stall so arb_valid/rd_ack/stream_idx_o stay asserted until the
// arbiter grants (matching the canonical reggen .re strobe); else the launch is dropped
// and the reg block deadlocks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove verbose comments

Comment on lines +108 to +109
logic [NumRegs-1:0][NumStreams-1:0] nxt_read_seen;
logic [NumRegs-1:0][NumStreams-1:0] nxt_read_pending_q;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain why we need extra signals.

Comment thread src/frontend/reg/tpl/idma_reg.sv.tpl Outdated
Comment on lines +187 to +190
// launch-stall: hold the arbiter request and the reg read-ack across the CPUIF read stall
// until the arbiter accepts the launch. nxt_read_seen is the single-cycle req pulse;
// nxt_read_pending_q holds it until arb_ready. rd_ack is driven from these below (see
// gen_hw2reg_connections), not from the maskable req — matching the canonical held handshake.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove verbose comments

Comment thread test/tb_idma_reg_frontend.sv Outdated
Comment on lines +8 to +19
// Self-checking testbench for the iDMA register frontend `idma_reg32_3d`
// (config-bus CPUIF = apb4-flat, the default). It drives the APB config slave
// directly, owns the transfer-id counter via `idma_transfer_id_gen`, and models
// a controllable backend stub on `req_ready_i` so the launch handshake can be
// stalled at will.
//
// The core regression is the next_id launch deadlock: PeakRDL's external-register
// handshake masks the CPUIF read req the cycle after an ungranted next_id read
// (external_pending), so without the pending latch the launch rd_ack never fires,
// external_pending never clears, and the whole reg block freezes. A cycle watchdog
// `$fatal`s if any next_id (APB) read stalls beyond a generous bound, so an unfixed
// template trips the watchdog while the fixed one completes cleanly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbose comments. This should be a clear header

Comment thread idma.mk Outdated
Comment on lines +389 to +396
# Self-checking register-frontend regression. Drives idma_reg32_3d's apb4-flat
# config slave, owns the id counter (idma_transfer_id_gen), and models a
# controllable backend on req_ready_i. Test 2 is the next_id launch-deadlock gate:
# a per-read watchdog $fatals if a next_id read freezes under backpressure. Runs
# once with NumStreams=1 and once with NumStreams=2 (multi-stream stream_idx hold).
# Requires the apb4-flat reg top (the default IDMA_REG_CPUIF) — compile.tcl regens
# it. Run with the Questa SEPP wrapper:
# make idma_sim_tb_idma_reg_frontend VSIM="questa-2023.4 vsim"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove verbose comments

The PeakRDL external-register handshake masks the CPUIF read req the cycle
after an ungranted next_id read (external_pending), collapsing next_id.req to
a single-cycle pulse. The launch rd_ack (req & ~req_is_wr & arb_ready) then can
never fire once req is masked, external_pending never clears, and the whole reg
block freezes.

Hold the launch across the stall in nxt_read_pending_q: set on the seen read,
clear on arb_ready, and drive arb_valid, rd_ack and stream_idx_o from the latch
instead of the maskable req. This replicates the canonical reggen held-handshake
(the stable .re strobe) exactly: the read stalls until the arbiter grants,
completes the cycle arb_ready is high with no added latency, and returns this
launch's id. status/done_id are untouched (their ungated rd_ack never latches
external_pending).
The register frontend had no directed testbench. tb_idma_reg_frontend drives
idma_reg32_3d's apb4-flat config slave, owns the transfer-id counter via
idma_transfer_id_gen, and models a controllable backend on req_ready_i so the
next_id launch handshake can be stalled at will.

A per-read cycle watchdog fatals if a next_id APB read fails to complete under
backpressure, so the test detects the launch deadlock: it passes against the
pending-latch fix and fatals (DEADLOCK) against the pre-fix template. Covers a
basic launch, the backpressure/deadlock regression, multi-stream stream_idx
hold under stall (NumStreams=2), and back-to-back monotonic ids. Wired into the
idma_test Bender target and a new idma_sim_tb_idma_reg_frontend make target that
runs both NumStreams=1 and NumStreams=2 in Questa.
@DanielKellerM

Copy link
Copy Markdown
Collaborator Author

Addressed the review — dropped the verbose comment blocks in idma_reg.sv.tpl, the tb_idma_reg_frontend.sv header, and the idma.mk target, keeping a one-line rationale for the pending-latch signals. Comment-only vs the previous push (no logic change); the full rationale is in the commit messages.

@DanielKellerM DanielKellerM force-pushed the frontend/reg-deadlock-fix branch from d87610c to 13ba549 Compare July 8, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants