Skip to content

Zacas: atomic compare-and-swap - #1880

Open
davidharrishmc wants to merge 7 commits into
openhwfoundation:rva23from
davidharrishmc:dh/zacas
Open

davidharrishmc wants to merge 7 commits into
openhwfoundation:rva23from
davidharrishmc:dh/zacas

Conversation

@davidharrishmc

@davidharrishmc davidharrishmc commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Implements the Zacas extension: amocas.w, amocas.d, amocas.q, and amocas.b/amocas.h where Zabha is enabled. Every form passes its architectural test and runs clean against ImperasDV in lockstep on both rv64gc and rv32gc.

The three problems worth explaining

amocas needs a third source operand. It reads rs1 for the address, rs2 for the swap value, and rd for the compare value, but the register file has two read ports. Rather than add a port, the instruction stalls one cycle in Decode and borrows the rs2 port to read rd. The borrowed cycle captures the compare value and the normal cycle reads the swap value, so both operands arrive through one port. This also handles the pair forms for free, since the two reads come off the same pair output on successive cycles.

A failed comparison still writes. The specification allows an unsuccessful amocas either to skip the memory write or to write back the value it loaded. Writing the old value back was the better fit here. The alternative, cancelling the write, needs a late signal into the cache state machine: unlike a failed store-conditional, whose outcome is known from the reservation registers before the access is issued, a compare-and-swap only resolves after the data comes back, by which point the read-modify-write is already in flight. Writing the old value back makes a failed amocas an ordinary AMO whose result happens to equal what was read, so the store path, byte masks, and fault logic are untouched. The cost is that a failed swap dirties the line; the comparison result is a named signal, so a cancel can be added later without restructuring.

The pair forms need a wide path, which already existed. amocas.d on RV32 and amocas.q on RV64 operate on twice XLEN. LLEN now widens to 2*XLEN when Zacas is enabled, exactly the way quad-precision floats widen it, so the pair forms reuse the load/store path that flq already exercises. The wide compare and select happen in the LSU where the read data is already LLEN wide, and the loaded high half writes rd+1 through the register file's pair write port.

Other changes

  • Register file. The paired form holds registers as pairs and supports a pair write. Its array previously started at index 1, leaving the pair holding x0 and x1 without storage, so x1 read out of range; it now starts at 0 with x0 forced to read as zero. Half-register writes write only the addressed half rather than reading the pair back and merging, which drops a read port.
  • Parameters. ZACAS_SUPPORTED and ZABHA_SUPPORTED, each with an assertion that it requires Zaamo. Enabled on rv64gc and rv32gc. The Zabha parameter and assertion text matches Zabha support #1877 verbatim so the two branches merge without conflict.
  • Reserved encodings. The pair forms require even rd and rs2; odd encodings raise an illegal instruction.
  • Width-masked comparison. amocas.w on RV64 compares only the low 32 bits of rd, so the comparison is masked to the access width.
  • LSU floating-point write mux. Two branches assumed LLEN equalled the larger of XLEN and FLEN, which stops being true once Zacas widens it. Both operands are now extended to LLEN.
  • Lockstep configs. cpu/Zabha=T and cpu/Zacas=T added to the rv64gc and rv32gc ImperasDV settings.
  • Trace wrapper. It reaches the register file by hierarchical name, so it now selects the correct half of a pair, and it reports both registers of a pair write. Without the latter, a pair amocas left rd+1 invisible to lockstep even though the architectural state was correct.

Testing

form rv64gc rv32gc
amocas.b, amocas.h arch test passes, lockstep clean arch test passes
amocas.w arch test passes, lockstep clean arch test passes, lockstep clean
amocas.d arch test passes, lockstep clean pair form; arch test passes, lockstep clean
amocas.q pair form; arch test passes, lockstep clean n/a

Lint is clean on all configurations, and the regression shows no new failures. Note that enabling Zacas routes every register access through the paired register file, so the regression passing is also the evidence that it is a correct drop-in.

The matching arch-test configuration changes are on the zmop branch of the arch-test fork. They declare both extensions for the cvw cores and raise the memory region's atomic support from AMOArithmetic, which does not permit compare-and-swap, so Sail was rejecting every amocas on a platform-attribute check before it reached the core.

🤖 Generated with Claude Code

davidharrishmc and others added 7 commits September 18, 2026 03:36
Adds src/ieu/regfile2.sv, a regfile variant parameterized with
ZACAS_SUPPORTED, parked on this branch so it stays out of the way.

Note: this declares a second module named regfile, so it collides with
src/ieu/regfile.sv and breaks Verilator elaboration while both files are
present. Rename the module or replace the original before building.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the ZACAS_SUPPORTED and ZABHA_SUPPORTED parameters, the Zaamo
dependency assertions for both, and folds the paired register file into
regfile.sv, replacing regfile2.sv.

Register file fixes needed to make the paired form usable:
  - The pair array started at index 1, so the pair holding x0 and x1 had no
    storage and x1 read out of range. It now starts at 0 with x0 forced to
    read as zero.
  - Half-register writes now write only the addressed half instead of
    reading the pair back and merging, which drops a read port.
  - The third read port takes its own address (a4) rather than sharing the
    write address a3, since the amocas compare operand is read in Decode
    while a3 is the Writeback destination.
  - Nonblocking assignments, and distinct generate block labels.

The parameter and assertion text matches openhwfoundation#1877 verbatim so
the branches merge without conflict.

Verified: lint clean on every config, and the full regression passes with
the paired register file active on rv64gc and rv32gc, so it is a correct
drop-in for ordinary code. The only failures are the two that predate this
work (the unbuilt sv57 vectors and WALLY-trap-01 from the issue1538 merge).

amocas decode and the load-store compare-and-swap are not implemented yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amocas atomically compares the loaded value against rd and swaps in rs2 on
a match. It needs a third source operand, so rather than add a read port
the instruction stalls one cycle in Decode and borrows the rs2 port to read
rd, capturing the value on its way to the Memory stage.

On a mismatch the old value is written back, which the spec permits, so the
ordinary AMO store path is reused unchanged and no late write-suppress
signal reaches the cache.

The comparison is masked to the access width, since amocas.w on RV64
compares only the low 32 bits of rd.

LLEN now widens to 128 when Zacas is enabled on RV64, the same way quad
floats widen it, so amocas.q has a path when it is implemented. That
exposed two branches of the LSU floating-point write mux that assumed LLEN
equalled the larger of XLEN and FLEN; both operands are now extended to
LLEN.

The RVVI tracer reaches into the register file by hierarchical name, so it
now selects the correct half of the pair.

Implemented: amocas.w on both widths, amocas.d on RV64, and amocas.b/h
where Zabha is enabled. The pair forms (amocas.d on RV32, amocas.q) remain
reserved and trap.

Verified on rv64gc and rv32gc: every implemented form passes its arch test
and runs clean in ImperasDV lockstep. Lint clean on all configs, and the
regression shows no new failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amocas.d on RV32 and amocas.q on RV64 name even register pairs and operate
on twice XLEN bits. Both pairs come off the same register file pair output
on successive cycles: the borrowed Decode cycle reads the rd pair (the
compare value) and the normal cycle reads the rs2 pair (the swap value).

The wide compare and select happen in the LSU, where the read data is
already LLEN wide, and feed the same write path quad floats use. The loaded
high half writes rd+1 through the register file pair write port. Encodings
with an odd rd or rs2 are reserved and trap.

Two bugs found while testing:
  - The pair flag keyed on funct3 alone, but funct3 100 is also lbu, so
    ordinary byte loads were treated as pair operations and hung the core.
    It is now qualified by the opcode.
  - The captured compare value took the pair's low half, which is the wrong
    register when a scalar amocas has an odd rd. The low half now comes
    from the selected read port.

The RVVI tracer reports only one register write per instruction, so a pair
write left rd+1 invisible and lockstep flagged it even though the
architectural state was right. It now reports both.

Verified on rv64gc and rv32gc: every amocas form passes its arch test and
runs clean in ImperasDV lockstep. Lint clean on all configs, and the
regression shows no new failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Address review of the amocas implementation:

  - controller: name the new signals for what they are.  CASFunctD is a
    legal amocas encoding by funct5 and funct3, CASPairSizeD is the funct3
    naming the 2*XLEN size, and AMOCASD / AMOCASPairD add the opcode.  The
    intermediate width signal is gone.  Correct the comment: word and
    doubleword always exist, only byte and halfword need Zabha.  The amo
    and amocas decode lines produce identical controls, so merge them.

  - regfile: read each pair once into a raw signal and derive both the pair
    output and the selected half from it, so there are truly two read ports.
    Note why dropping a2's low bit is sufficient: the pair forms require an
    even register.

  - amoalu: amocas takes the free funct7 encoding 00101 in the existing
    case, so the separate if disappears, and one a/b mux now serves amocas
    alongside amomin and amomax.

  - Share the comparison.  It is computed once in the LSU, sized by the full
    funct3 so the pair forms are covered by the same logic, and feeds both
    the AMO ALU and the pair mux.  The pair result reuses the AMO ALU output
    for its low half and only selects the high half, which also means only
    the high half of the swap pair needs carrying down the pipeline rather
    than the whole pair.  AMOCASM is no longer needed downstream.

Verified: lint clean, every amocas form still passes on rv64gc and rv32gc,
and all seven run clean in lockstep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compare value leaves the read port when the instruction advances, so
it was captured in Decode and then piped through an Execute register. One
register serves both: capture during the borrowed cycle and hold until the
instruction reaches Execute. A following amocas only reaches Decode after
this one has left, so it cannot capture over it.

The swap value needs no Decode register at all, since its low half rides
the ordinary rs2 path to WriteDataM and its high half is still on the port
when the instruction advances.

Verified: lint clean, every amocas form passes on rv64gc and rv32gc, and
all seven run clean in lockstep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amocas reads rd, and for the pair forms rd+1 and rs2+1, straight from the
register file during the borrowed Decode cycle, so the forwarding network
never sees them. An older instruction still in Execute or Memory would
leave a stale value. Hold the instruction in Decode until that producer
reaches writeback, where the negedge register file write makes the value
visible to the Decode read. The borrowed read now happens on the first
cycle the operands are actually ready rather than unconditionally.

The arch tests do not cover this: rd receives the loaded value whether or
not the comparison matches, so a stale compare operand only changes memory.
A probe that reads memory back after each amocas diverges from ImperasDV
with the stall disabled (the swap does not happen) and is clean with it.

Verified: lint clean, every amocas form passes on rv64gc and rv32gc, and
all seven plus the hazard probe run clean in lockstep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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