Zacas: atomic compare-and-swap - #1880
Open
davidharrishmc wants to merge 7 commits into
Open
davidharrishmc wants to merge 7 commits into
davidharrishmc wants to merge 7 commits into
Conversation
davidharrishmc
force-pushed
the
dh/zacas
branch
from
September 16, 2026 13:29
e1a992c to
b855414
Compare
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>
davidharrishmc
force-pushed
the
dh/zacas
branch
from
September 18, 2026 10:45
068ec46 to
8e1af6a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the Zacas extension:
amocas.w,amocas.d,amocas.q, andamocas.b/amocas.hwhere Zabha is enabled. Every form passes its architectural test and runs clean against ImperasDV in lockstep on bothrv64gcandrv32gc.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.don RV32 andamocas.qon RV64 operate on twice XLEN.LLENnow widens to2*XLENwhen Zacas is enabled, exactly the way quad-precision floats widen it, so the pair forms reuse the load/store path thatflqalready exercises. The wide compare and select happen in the LSU where the read data is alreadyLLENwide, and the loaded high half writes rd+1 through the register file's pair write port.Other changes
ZACAS_SUPPORTEDandZABHA_SUPPORTED, each with an assertion that it requires Zaamo. Enabled onrv64gcandrv32gc. The Zabha parameter and assertion text matches Zabha support #1877 verbatim so the two branches merge without conflict.amocas.won RV64 compares only the low 32 bits of rd, so the comparison is masked to the access width.LLENequalled the larger ofXLENandFLEN, which stops being true once Zacas widens it. Both operands are now extended toLLEN.cpu/Zabha=Tandcpu/Zacas=Tadded to therv64gcandrv32gcImperasDV settings.Testing
amocas.b,amocas.hamocas.wamocas.damocas.qLint 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
zmopbranch of the arch-test fork. They declare both extensions for the cvw cores and raise the memory region's atomic support fromAMOArithmetic, 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