Skip to content

Wait for wfi in one place; record traps only when taken - #1941

Open
davidharrishmc wants to merge 7 commits into
openhwfoundation:mainfrom
davidharrishmc:dh/wfi-wait
Open

davidharrishmc wants to merge 7 commits into
openhwfoundation:mainfrom
davidharrishmc:dh/wfi-wait

Conversation

@davidharrishmc

Copy link
Copy Markdown
Contributor

What

A waiting wfi now stalls the whole pipeline through StallW, as an LSU stall does, instead of stalling M and flushing W. Trap CSRs and trap HPM events are recorded only when the trap is taken.

This PR contains #1925's commits (merged, not rebased) and should merge after or together with #1925. It replaces #1930 (closed) and covers #1926, which was reverted by #1939. It supersedes #1923, #1933 and #1935.

Wait design

A waiting wfi stays in M. WaitM stalls the whole pipeline, so the instruction in W keeps forwarding.

  • privdec:
    • WaitM = wfiM & ~IntPendingM & ~TWTimeoutM
    • One flop, WaitedM <= StallM & (WaitM | WaitedM), records that the wfi in M has waited.
    • One counter, WaitCount <= StallM ? WaitCount + (WaitM & ~msb) : 0, sets the mstatus.TW limit. WFI_TIMEOUT_BIT is unchanged. The counter holds while the wfi stays in M and clears when M advances.
  • hazard: StallWCause |= WaitM & ~FlushWCause, FlushWCause = TrapM, StallM = StallW
  • trap: ValidIntsM = (Committed | WaitedM) ? 0 : EnabledIntsM. InterruptM no longer gates on wfi.
  • Removed: WFIStallM, WFIInterruptedM, StallMCause, LatestUnstalledW, wfiW and the ~wfiM | wfiW gate.

Behavior:

  • Interrupt already enabled and pending when the wfi reaches M: the trap is taken on the wfi, and the wfi does not retire.
  • wfi woken by a locally enabled interrupt: the wfi retires. If the interrupt is enabled, it is taken with mepc = pc + 4 (wfi_mepc_val).
  • TW limit reached (below M with TW = 1, or U mode with S): illegal-instruction trap with mepc = the wfi. The wfi does not retire.

The logic is written so that Zawrs only adds wrs terms to WaitM, the counter and the timeouts.

Bugs fixed

Testing

Tests in tests/coverage, listed in coverage64gc:

check (rv64gc) result
Verilator: the 8 tests above, plus divflushstall all pass
wfitimeout runs to completion (it has no self-check)
coverage64gc SUCCESS
Lockstep wfiForward, csrwfiInt 0 mismatches
ACT InterruptsSm/S, rv64gc and rv32gc 32/32 each
lint-wally clean

On main, wfitimeoutnext, wfiBackToBack, wfiForward, csrwfiInt and wfitimeoutint fail.

wfiTW, wfitimeoutint and wfitimeoutnext are waived in lockstep. ImperasDV traps a wfi below M with TW = 1 at once, while Wally waits a bounded time. Both are legal: mstatus_tw_always_illegal allows the first and mstatus_tw_op the second.

Synthesis was measured on the combined wfi + Zawrs version (sky130, rv64gc core, 200 MHz). Compared with main + Zawrs:

  • Hazard + privdec + trap area is 8.6% smaller.
  • No path through StallW, StallM, FlushW, StallD or InterruptM gets slower.

It has not been re-measured for this wfi-only version.

🤖 Generated with Claude Code

davidharrishmc and others added 7 commits October 5, 2026 08:12
A divide mispredicted as a jump raises FlushE in its DONE cycle; when StallM
held it in Execute the divider FSM went IDLE and restarted with stale
operands.  Reset the FSMs only when Execute advances; add an opt-in
testbench ExternalStall injector and the divflushstall test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
…v.sv on rv32gc

Use FlushE & ~StallE, matching flopenrc, which clears only when enabled.
rv64gc divides on the FPU, so add divflushstall32 for rv32gc to exercise
div.sv.  RV32 self-checking tests live in tests/coverage32 (built by
'make coverage', run by the self-checked coverage32gc suite) so the
nightly lockstep run over tests/coverage on rv64gc does not pick them up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
StallE equals StallM whenever FlushE is asserted: FlushECause masks the divider's own StallECause, and
LatestUnstalledE implies ~StallE.  FlushE & ~StallM is therefore the same condition, and it needs no new
StallE port through fpu, fdivsqrt, mdu, the core and testbench_fp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
The net now has two drivers, the RVVI wrapper and the stall injector, so name
it after the core port it drives. rvvitbwrapper keeps its RVVIStall port.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
A waiting wfi now stalls the whole pipeline through StallW (WaitM), like an
LSU stall, instead of stalling M and flushing W.  One flop (WaitedM)
records that the wfi in M has waited, and one counter of waiting cycles,
held while the wfi stays in M and cleared when M advances, provides the
mstatus.TW time limit (WFI_TIMEOUT_BIT, unchanged).

This replaces WFIStallM, WFIInterruptedM, StallMCause, LatestUnstalledW,
wfiW and the wfiM | wfiW interrupt gate, and fixes:
- the instruction after a woken wfi used a stale operand, because W was
  flushed every waiting cycle and forwarding stopped (openhwfoundation#1933);
- an interrupt already enabled and pending when a wfi executed, e.g. just
  enabled by a CSR write or xRET, was taken after the wfi retired; it is
  now taken on the wfi (openhwfoundation#1935).  A wfi woken by an interrupt still retires
  and the interrupt is taken with mepc = pc + 4;
- a trapping wfi both retired and trapped: FlushWCause = TrapM (openhwfoundation#1926,
  reverted on main by openhwfoundation#1939, restored here with its wfiBackToBack test and
  the corrected ecall/ebreak comments);
- the next instruction took a spurious TW trap when a wfi woke on the
  cycle its count reached the limit (openhwfoundation#1923); the count no longer depends
  on TrapM, so it cannot change while a trap waits for StallW;
- a TW timeout coinciding with an enabled interrupt wrote mcause = 5
  without the interrupt bit: WaitedM gates ValidIntsM, so CauseM agrees.

The wait logic is written so that Zawrs only adds wrs terms to WaitM, the
counter and the timeouts.

Tests: wfitimeoutnext, wfiBackToBack, wfiForward, csrwfiInt, wfitimeoutint
and wfiTW.  wfiTW, wfitimeoutint and wfitimeoutnext are waived in lockstep:
ImperasDV traps a wfi below M with mstatus.TW = 1 at once while Wally waits
a bounded time, and both are legal (mstatus_tw_always_illegal,
mstatus_tw_op).  The FPGA debug lists and wave.do drop the removed names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
From openhwfoundation#1930.  xEPC, xCAUSE and xTVAL were written every cycle TrapM was
high, while mstatus, the privilege mode and the PC wait for ~StallW.  If
the trap changes during a stall (an M timer interrupt overtaking a
delegated illegal instruction under ExternalStall), scause/sepc/stval were
overwritten for a trap that was never taken.  MTrapM and STrapM, and HPM
events 22 (interrupts) and 23 (exceptions), are now gated with
TrapM & ~StallW at each use, so each trap is recorded and counted once.

openhwfoundation#1930 also gated the WFI counter reset with ~StallW.  That part is not
needed: the wait counter no longer uses TrapM.  It holds while the wait
instruction stays in M and clears when M advances, which for a trap is
exactly when the trap is taken.

Tests: trapcsrstall and hpmtrapcount (they use the openhwfoundation#1925 ExternalStall
injector).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: David Harris <David_Harris@hmc.edu>
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.

1 participant