A 64-bit configuration written over a 32-bit bus exists briefly as half old and half new. Hardware sampling during that window sees a value software never intended.
Write low and high words into shadow registers.
Track which words have been written.
Commit with an explicit command or when the final required word arrives.
Copy the complete shadow atomically into the active configuration.
Each slot is one clock cycle, and a write takes effect at the edge that ends its slot. The two data lanes show one digit per word, high word first: 11 is the old value, 22 the new one, and 12 a torn mix.
Slot 1: the new low word is written. The naive register shows 12 from slot 2: new low word, old high word.
Slots 2 and 3: hardware sampling the naive value sees 12, a limit software never wrote. The shadow design still shows the old 11.
Slot 4: commit copies both shadow words at once, so the shadow design goes straight from 11 to 22 in slot 5.
The commit logic computes the post-write shadow value before updating active state. This explicitly supports a final word write and commit on the same edge instead of silently committing the previous shadow value.
Decode LIMIT_LO/HI and LIMIT_COMMIT from the canonical field map in CSR Contract and Address Map. The mapped single-request port issues commit separately; this helper's simultaneous inputs support an internal caller, not two software writes on one bus edge. Its incomplete commit retains validity and returns no active update, matching the map's OK no-op policy.
Here wr_lo and wr_hi are decoded, accepted write pulses. A word counts as written only when at least one byte strobe is set; zero-strobe writes change neither shadow data nor validity. Partial writes intentionally preserve unselected shadow bytes, including bytes from the previous committed value. This is a word-touch contract, not a requirement to replace every byte. A replacement-only contract would instead track all eight byte-valid bits before allowing commit.
module csr_shadow_limit (
input logic clk, rst_n,
input logic wr_lo, wr_hi, commit_pulse,
input logic [31:0] wdata,
input logic [3:0] wstrb,
output logic [63:0] active_q,
output logic [1:0] shadow_valid_q
);
logic [63:0] shadow_q, shadow_next;
logic [1:0] valid_next;
function automatic logic [31:0] merge_bytes(
input logic [31:0] old_data, new_data, input logic [3:0] strb
);
logic [31:0] merged;
merged = old_data;
for (int b = 0; b < 4; b++)
if (strb[b]) merged[b*8 +: 8] = new_data[b*8 +: 8];
return merged;
endfunction
always_comb begin
shadow_next = shadow_q;
valid_next = shadow_valid_q;
if (wr_lo && |wstrb) begin
shadow_next[31:0] = merge_bytes(shadow_q[31:0], wdata, wstrb);
valid_next[0] = 1'b1;
end
if (wr_hi && |wstrb) begin
shadow_next[63:32] = merge_bytes(shadow_q[63:32], wdata, wstrb);
valid_next[1] = 1'b1;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shadow_q <= '0;
shadow_valid_q <= '0;
active_q <= '0;
end else begin
shadow_q <= shadow_next;
shadow_valid_q <= valid_next;
if (commit_pulse && &valid_next) begin
active_q <= shadow_next;
shadow_valid_q <= '0;
end
end
end
endmodule
For counters wider than the bus, snapshot-on-read is often safer: reading the low word captures the full counter into a snapshot, and the high-word read returns the matching snapshot half.
Hardware can still sample between writes, and retries or reordered software sequences can violate the convention. A commit event creates an explicit atomic boundary.