An engineer attaches a debugger to a running chip and opens a window showing the interrupt-status registers. Interrupts start going missing. Close the window and they come back. The debugger is not writing anything. It is only reading, and in this design some reads change the register they read.
What can a register access mean besides "store this value" and "give me that value", and what goes wrong with each meaning?
The obvious model of a register is a memory location: a write stores a value, a read returns it, and reading has no effect. Many registers do behave like that. But registers are the interface between software and hardware that is changing on its own, and pure storage cannot express "tell me what happened, and let me acknowledge it". So registers acquire access types in which reads or writes have side effects, and every side effect is a trap for someone who assumed a register was memory.
Type | A write does | A read does | Trap |
|---|---|---|---|
RO (read-only) | Nothing (or an error) | Returns hardware state | Software writes it and assumes it changed |
RW (read-write) | Stores the value | Returns the stored value | Read-modify-write races with hardware that also updates it |
W1C (write 1 to clear) | Clears each bit written as 1; bits written as 0 are untouched | Returns status | Read-modify-write writes back 1s and clears events it never saw |
W1S (write 1 to set) | Sets each bit written as 1 | Returns the value | As W1C, mirrored |
RC (read to clear) | Nothing | Returns status and clears it | Any extra read loses events: debuggers, speculative or repeated reads |
Read with side effect | Varies | Pops a FIFO, acknowledges an interrupt | A read that is retried or duplicated consumes data twice |
WO (write-only) | Triggers an action (start, reset) | Returns 0 or garbage | Software reads it back to check, and learns nothing |
W1C exists because of the read-modify-write trap. With a plain RW status register, software would read the status, clear the bit it handled and write the rest back; any bit hardware set in between would be overwritten with 0 and lost. With W1C, software writes 1 only to the bits it handled, and every other bit, including any that just appeared, is untouched.
Four event sources set four status bits. Software clears a bit by writing 1 to it. The one subtle case is an event that fires in the same cycle software clears that bit: the set must win, or the event is lost.
// A write-1-to-clear (W1C) status register. Hardware sets a bit when its event
// fires; software clears a bit by writing 1 to it (writing 0 leaves it alone). If an
// event fires in the same cycle software clears that bit, the set wins, so the new
// event is never lost.
module tb;
logic clk = 0;
always #5 clk = ~clk;
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
logic [3:0] event_in = 0; // hardware events, one bit per source
logic wr_en = 0; // software write strobe
logic [3:0] wdata = 0;
logic [3:0] status = 0;
always @(posedge clk)
status <= (status & ~(wr_en ? wdata : 4'b0)) | event_in; // clear, then set: set wins
int cyc = 0;
always @(posedge clk) cyc <= cyc + 1;
always @(posedge clk) begin
event_in <= 0; wr_en <= 0; wdata <= 0;
case (cyc)
1: event_in <= 4'b0001; // event 0
2: event_in <= 4'b0100; // event 2
4: begin wr_en <= 1; wdata <= 4'b0001; end // software clears bit 0 only
6: begin wr_en <= 1; wdata <= 4'b0100; event_in <= 4'b0100; end // clear bit 2 as event 2 fires again
8: begin wr_en <= 1; wdata <= 4'b0100; end // clear bit 2 again
default: ;
endcase
end
int errors = 0;
always @(posedge clk) begin
if (cyc == 6 && status != 4'b0100) errors++; // bit 0 cleared, bit 2 untouched
if (cyc == 8 && status != 4'b0100) errors++; // the clear and the new event collided: still set
if (cyc == 10 && status != 4'b0000) errors++; // the second clear empties it
end
initial begin
#130;
if (errors == 0) $display("PASS: writing 1 clears only that bit, and an event in the clearing cycle is kept");
else $display("FAIL: %0d checks failed", errors);
$finish;
end
endmoduleThe single update line does it: clear the bits written as 1, then OR in this cycle's events, so a simultaneous event survives the clear.
How to read it, slot by slot:
Slots 1 and 2: events 0 and 2 fire (event_in 1, then 4). The register updates on the next edge, so status is 1 in slot 2 and 5 (bits 0 and 2) in slot 3.
Slot 4: software writes 1 (wr_en, wdata 1). In slot 5 status is 4: bit 0 cleared, bit 2 untouched, although software wrote 0 to it.
Slot 6: software writes 4 to clear bit 2, and in the same cycle event 2 fires again (event_in 4). In slot 7 status is still 4: the new event survived the clear.
Slot 8: software clears bit 2 again, with no new event; status is 0 from slot 9.
A free-running 64-bit counter must be read as two 32-bit halves, and it can carry from the low half into the high half between the two reads.
Counter = 0x0000_0001_FFFF_FFFF
Read low half: 0xFFFF_FFFF
(counter increments to 0x0000_0002_0000_0000)
Read high half: 0x0000_0002
Software sees 0x0000_0002_FFFF_FFFF wrong by 2^32 - 1, about 4.3 billion ticksFix 1: read high, low, high. If the two high reads differ, a carry happened in between; read again.
Fix 2: a shadow register. Reading the low half copies the high half into a shadow register at that instant; the next read returns the shadow. Both halves come from the same moment. (The shadow is itself a read side effect, so a stray read of the low half alone is harmless but changes what the next high read returns.)
Interrupt status registers are almost always W1C, for the reason above.
FIFO data registers in UARTs and similar peripherals often pop on read: reading the receive register takes the oldest byte.
Register description languages (such as IP-XACT and SystemRDL) name the access types so that RTL, documentation, software headers and verification models are generated from one description.
The opening story (illustrative): the interrupt-status register was RC. Every refresh of the debugger window read it, clearing whatever was pending, so the interrupt handler later found nothing. The same design also lost interrupts under a compiler change that made the driver read the register twice. The fix was to make status W1C and keep reads free of side effects. As a rule, a read should never destroy information.
Bit 1 is overwritten with 0 by the write-back and the event is lost. With W1C, software would write 0b0001 and bit 1 would survive.
So that an event in the same cycle as a clear of that bit leaves the bit set; clearing after setting would lose it.
A carry happened between the reads; read the low half again (and the high half), and use the pair whose high halves agree.
Registers have access types (RO, RW, W1C, RC, side-effect reads), and each side effect is a trap for code that treats registers as memory.
W1C status avoids read-modify-write races; hardware must let a same-cycle event win over a clear.
Reads should not destroy information, and wide counters need an atomic read method.