A slave that commits a write only when AW and W arrive in the same cycle passes every test that sends them together. Give it a master that sends the address two cycles before the data, and the first write never answers: FAIL: write 1 to 0x00 (AW first) got no B response in 50 cycles. This lesson connects the topic's slave to a small CSR block and follows one write and one read through every channel, cycle by cycle, to a register bit and back.
Follow the accent line left to right and back. The slave owns the protocol; the CSR block owns the meaning of each bit.
Slave: pairs AW and W with aw_seen and w_seen, then raises csr_we for exactly one cycle. It turns werr and rerr into SLVERR.
CSR block: applies the strobes, the W1C rule and the read-only rule on that one cycle. It knows nothing about valid or ready.
Hardware: reads CONTROL on ctrl_o and sets STATUS bits with one-cycle pulses on event_i.
The slave is axilite_regs from A Minimal AXI-Lite Slave with its two registers moved out. Its pairing flags, readies and response registers are unchanged, so every timing fact from that lesson still holds. The CSR block uses the field equations from Field Access Types and the set-wins rule from Hardware/Software Arbitration.
Offset | Register | Read | Write |
|---|---|---|---|
0x00 | CONTROL | Current value | RW: only strobed bytes change; drives |
0x04 | STATUS | Bits [3:0], sticky | W1C: a 1 in a strobed byte clears that bit; a hardware event on the same edge wins |
0x08 | ID | 0xC5A0_0100 | RO: ignored, answers OKAY |
Any other word | none | Zero, SLVERR | No effect, SLVERR |
The port is four facts. csr_we = aw_seen && w_seen && !s_bvalid is the commit cycle, and on that edge the CSR updates and BVALID rises together. The read address goes to the CSR combinationally, and the slave captures csr_rdata on the AR handshake edge, so a read returns the value from before that edge. An unmapped word raises werr or rerr, and the slave answers SLVERR, this topic's code for a hole inside one slave. DECERR stays with the default slave.
// The AXI-Lite slave from A Minimal AXI-Lite Slave with its two registers moved out:
// the same pairing flags, readies and response registers, plus a CSR port.
module axil_csr_slave #(
parameter int AW = 8
) (
input logic clk,
input logic rst_n,
input logic s_awvalid,
output logic s_awready,
input logic [AW-1:0] s_awaddr,
input logic s_wvalid,
output logic s_wready,
input logic [31:0] s_wdata,
input logic [3:0] s_wstrb,
output logic s_bvalid,
input logic s_bready,
output logic [1:0] s_bresp,
input logic s_arvalid,
output logic s_arready,
input logic [AW-1:0] s_araddr,
output logic s_rvalid,
input logic s_rready,
output logic [31:0] s_rdata,
output logic [1:0] s_rresp,
// CSR port: csr_we is high for exactly one cycle per paired write
output logic csr_we,
output logic [AW-1:0] csr_waddr,
output logic [31:0] csr_wdata,
output logic [3:0] csr_wstrb,
input logic csr_werr,
output logic [AW-1:0] csr_raddr,
input logic [31:0] csr_rdata,
input logic csr_rerr
);
localparam [1:0] OKAY = 2'b00;
localparam [1:0] SLVERR = 2'b10;
logic aw_seen, w_seen;
logic [AW-1:0] awaddr_q;
logic [31:0] wdata_q;
logic [3:0] wstrb_q;
logic [1:0] bresp_q, rresp_q;
assign s_awready = !aw_seen && !s_bvalid;
assign s_wready = !w_seen && !s_bvalid;
assign s_arready = !s_rvalid;
assign s_bresp = bresp_q;
assign s_rresp = rresp_q;
assign csr_we = aw_seen && w_seen && !s_bvalid; // pair first, then commit once
assign csr_waddr = awaddr_q;
assign csr_wdata = wdata_q;
assign csr_wstrb = wstrb_q;
assign csr_raddr = s_araddr;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
aw_seen <= 1'b0;
w_seen <= 1'b0;
awaddr_q <= '0;
wdata_q <= '0;
wstrb_q <= '0;
s_bvalid <= 1'b0;
bresp_q <= OKAY;
s_rvalid <= 1'b0;
rresp_q <= OKAY;
s_rdata <= '0;
end else begin
if (s_awvalid && s_awready) begin
aw_seen <= 1'b1;
awaddr_q <= s_awaddr;
end
if (s_wvalid && s_wready) begin
w_seen <= 1'b1;
wdata_q <= s_wdata;
wstrb_q <= s_wstrb;
end
if (csr_we) begin
bresp_q <= csr_werr ? SLVERR : OKAY;
s_bvalid <= 1'b1;
aw_seen <= 1'b0;
w_seen <= 1'b0;
end
if (s_bvalid && s_bready)
s_bvalid <= 1'b0;
if (s_arvalid && s_arready) begin
s_rdata <= csr_rerr ? '0 : csr_rdata;
rresp_q <= csr_rerr ? SLVERR : OKAY;
s_rvalid <= 1'b1;
end
if (s_rvalid && s_rready)
s_rvalid <= 1'b0;
end
end
endmodule
// Three registers: CONTROL (RW, 0x00), STATUS (W1C, set by hardware, 0x04), ID (RO, 0x08).
// Any other word answers an error, which the slave turns into SLVERR.
module csr_block #(
parameter int AW = 8,
parameter logic [31:0] ID_VALUE = 32'hC5A0_0100
) (
input logic clk,
input logic rst_n,
input logic we,
input logic [AW-1:0] waddr,
input logic [31:0] wdata,
input logic [3:0] wstrb,
output logic werr,
input logic [AW-1:0] raddr,
output logic [31:0] rdata,
output logic rerr,
output logic [31:0] ctrl_o, // to the hardware
input logic [3:0] event_i, // from the hardware: one-cycle event pulses
output logic irq_o
);
logic [31:0] control_q, mask;
logic [3:0] status_q, w1c;
logic [AW-3:0] wword, rword; // word index: byte address / 4
assign wword = waddr[AW-1:2];
assign rword = raddr[AW-1:2];
always_comb
for (int i = 0; i < 4; i++) mask[i*8 +: 8] = {8{wstrb[i]}};
assign werr = (wword > 2);
assign w1c = (we && wword == 1) ? (wdata[3:0] & mask[3:0]) : 4'b0000;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
control_q <= '0;
status_q <= '0;
end else begin
if (we && wword == 0)
control_q <= (control_q & ~mask) | (wdata & mask); // only strobed bytes change
status_q <= (status_q & ~w1c) | event_i; // W1C, hardware set wins
end
end
always_comb begin
rerr = 1'b0;
case (rword)
0: rdata = control_q;
1: rdata = {28'h0, status_q};
2: rdata = ID_VALUE;
default: begin rdata = '0; rerr = 1'b1; end
endcase
end
assign ctrl_o = control_q;
assign irq_o = |status_q;
endmoduleCONTROL holds 0x0000_0011. The master writes 0x0000_5A00 with WSTRB = 0010, so only byte 1 may change. It sends W first and the address three cycles later, and BREADY is held high.
A channel transfers at the edge that ends a slot in which its VALID and READY are both high.
Slot 1: WVALID and WREADY are high, so W transfers. The slave holds the data and strobes; CONTROL does not move.
Slots 2 and 3: WREADY is low because w_seen is set. AWREADY stays high: the slave is waiting for the address and blocks nothing.
Slot 4: AW transfers. Slot 5: both readies are low and csr_we is high, so the pair commits at the end of this slot.
Slot 6: CONTROL becomes 0x5A11: byte 1 took 0x5A and byte 0 kept 0x11. BVALID rises in the same slot, B transfers, and both readies return in slot 7.
A hardware event set STATUS bit 2 before this window, so STATUS reads 0x4. The master reads STATUS, then offers a read of 0x10, which no register owns, while the first response is still stalled.
RRESP is shown in hex: 0 is OKAY and 2 is SLVERR.
Slot 1: AR for 0x04 transfers. The slave captures csr_rdata on this edge.
Slots 2 and 3: RVALID is high with RDATA 4 and RRESP OKAY, but RREADY is low, so R stalls and holds. The second ARVALID is already up, and ARREADY is low because ARREADY = !RVALID.
Slot 4: RREADY is high and the STATUS read retires. Slot 5: ARREADY returns and the held request for 0x10 transfers.
Slot 6: RDATA 0 with RRESP 2, SLVERR. The read did not hang and did not return stale data.
Reading STATUS does not clear it; only a W1C write does. The bench's directed part checks that: event, read 1, write 1 to STATUS, read 0.
The bench is a simple AXI-Lite master with a scoreboard. It drives on falling edges and samples handshakes on rising edges, with RREADY and BREADY random every cycle. Its reference model is a copy of the map: CONTROL, the four STATUS bits and the ID constant.
When a write lands: the model applies a write on the edge where BVALID rises, the port fact above. It applies event_i on every edge, after any W1C, so set wins exactly as in the RTL.
What is checked: every B and R beat, stalled or not, against the model; ctrl_o and irq_o every cycle; and the expected value in each directed case.
Traffic: directed cases first (ID read, AW-first full write, W-first partial write, event then W1C, write to ID, unmapped write and read). Then 1000 random writes and 1000 random reads run in parallel, with random strobes, random order and a gap of 1 to 3 cycles between AW and W, 20% unmapped addresses, and random hardware events.
$ vvp -n axil_csr.vvp
PASS: 1005 writes (375 AW first, 300 W first, 330 same cycle; 944 partial; 183 SLVERR), 1006 reads (226 SLVERR), 946 B and 968 R stall cycles, 731 events, 14 set/clear collisionsThe 14 collisions are edges where a W1C write and a hardware event hit the same STATUS bit. That is the case a directed test rarely reaches and a register bug hides in.
The mutant below is the slave from the opening sentence. It has no pairing flags: both readies are high whenever no response is pending, and it commits only when AWVALID and WVALID are high in the same cycle.
// bad-example: no pairing flags, so a half that arrives alone is accepted and lost
assign s_awready = !s_bvalid;
assign s_wready = !s_bvalid;
assign csr_we = s_awvalid && s_wvalid && !s_bvalid; // both halves in one cycle
assign csr_waddr = s_awaddr;
assign csr_wdata = s_wdata;
assign csr_wstrb = s_wstrb;$ vvp -n mut.vvp
FATAL: axil_csr_mutant.sv:270: FAIL: write 1 to 0x00 (AW first) got no B response in 50 cycles
Time: 600000 Scope: tb_axil_csr.axi_writeThe first directed write fails, 600 ns into the run. AW transferred alone, so the address was taken and dropped. W transferred two cycles later and was dropped too. Both handshakes completed, so the master had no reason to retry, and B never came.
Slot 4. W is held from the end of slot 1 and AW from the end of slot 2, so csr_we is high in slot 3, the pair commits at the end of slot 3, and CONTROL and BVALID change in slot 4.
Bit 0 is 1. The CSR computes (status & ~w1c) | event_i, so the new event survives the clear. The model applies the W1C first and the event second on that edge, the same order.
The two blocks and the master-plus-scoreboard bench described above.
// The AXI-Lite slave from A Minimal AXI-Lite Slave with its two registers moved out:
// the same pairing flags, readies and response registers, plus a CSR port.
module axil_csr_slave #(
parameter int AW = 8
) (
input logic clk,
input logic rst_n,
input logic s_awvalid,
output logic s_awready,
input logic [AW-1:0] s_awaddr,
input logic s_wvalid,
output logic s_wready,
input logic [31:0] s_wdata,
input logic [3:0] s_wstrb,
output logic s_bvalid,
input logic s_bready,
output logic [1:0] s_bresp,
input logic s_arvalid,
output logic s_arready,
input logic [AW-1:0] s_araddr,
output logic s_rvalid,
input logic s_rready,
output logic [31:0] s_rdata,
output logic [1:0] s_rresp,
// CSR port: csr_we is high for exactly one cycle per paired write
output logic csr_we,
output logic [AW-1:0] csr_waddr,
output logic [31:0] csr_wdata,
output logic [3:0] csr_wstrb,
input logic csr_werr,
output logic [AW-1:0] csr_raddr,
input logic [31:0] csr_rdata,
input logic csr_rerr
);
localparam [1:0] OKAY = 2'b00;
localparam [1:0] SLVERR = 2'b10;
logic aw_seen, w_seen;
logic [AW-1:0] awaddr_q;
logic [31:0] wdata_q;
logic [3:0] wstrb_q;
logic [1:0] bresp_q, rresp_q;
assign s_awready = !aw_seen && !s_bvalid;
assign s_wready = !w_seen && !s_bvalid;
assign s_arready = !s_rvalid;
assign s_bresp = bresp_q;
assign s_rresp = rresp_q;
assign csr_we = aw_seen && w_seen && !s_bvalid; // pair first, then commit once
assign csr_waddr = awaddr_q;
assign csr_wdata = wdata_q;
assign csr_wstrb = wstrb_q;
assign csr_raddr = s_araddr;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
aw_seen <= 1'b0;
w_seen <= 1'b0;
awaddr_q <= '0;
wdata_q <= '0;
wstrb_q <= '0;
s_bvalid <= 1'b0;
bresp_q <= OKAY;
s_rvalid <= 1'b0;
rresp_q <= OKAY;
s_rdata <= '0;
end else begin
if (s_awvalid && s_awready) begin
aw_seen <= 1'b1;
awaddr_q <= s_awaddr;
end
if (s_wvalid && s_wready) begin
w_seen <= 1'b1;
wdata_q <= s_wdata;
wstrb_q <= s_wstrb;
end
if (csr_we) begin
bresp_q <= csr_werr ? SLVERR : OKAY;
s_bvalid <= 1'b1;
aw_seen <= 1'b0;
w_seen <= 1'b0;
end
if (s_bvalid && s_bready)
s_bvalid <= 1'b0;
if (s_arvalid && s_arready) begin
s_rdata <= csr_rerr ? '0 : csr_rdata;
rresp_q <= csr_rerr ? SLVERR : OKAY;
s_rvalid <= 1'b1;
end
if (s_rvalid && s_rready)
s_rvalid <= 1'b0;
end
end
endmodule
// Three registers: CONTROL (RW, 0x00), STATUS (W1C, set by hardware, 0x04), ID (RO, 0x08).
// Any other word answers an error, which the slave turns into SLVERR.
module csr_block #(
parameter int AW = 8,
parameter logic [31:0] ID_VALUE = 32'hC5A0_0100
) (
input logic clk,
input logic rst_n,
input logic we,
input logic [AW-1:0] waddr,
input logic [31:0] wdata,
input logic [3:0] wstrb,
output logic werr,
input logic [AW-1:0] raddr,
output logic [31:0] rdata,
output logic rerr,
output logic [31:0] ctrl_o, // to the hardware
input logic [3:0] event_i, // from the hardware: one-cycle event pulses
output logic irq_o
);
logic [31:0] control_q, mask;
logic [3:0] status_q, w1c;
logic [AW-3:0] wword, rword; // word index: byte address / 4
assign wword = waddr[AW-1:2];
assign rword = raddr[AW-1:2];
always_comb
for (int i = 0; i < 4; i++) mask[i*8 +: 8] = {8{wstrb[i]}};
assign werr = (wword > 2);
assign w1c = (we && wword == 1) ? (wdata[3:0] & mask[3:0]) : 4'b0000;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
control_q <= '0;
status_q <= '0;
end else begin
if (we && wword == 0)
control_q <= (control_q & ~mask) | (wdata & mask); // only strobed bytes change
status_q <= (status_q & ~w1c) | event_i; // W1C, hardware set wins
end
end
always_comb begin
rerr = 1'b0;
case (rword)
0: rdata = control_q;
1: rdata = {28'h0, status_q};
2: rdata = ID_VALUE;
default: begin rdata = '0; rerr = 1'b1; end
endcase
end
assign ctrl_o = control_q;
assign irq_o = |status_q;
endmodule
Tool: Icarus Verilog 13.0, run on 2026-10-17. Commands: iverilog -g2012 -s tb_axil_csr -o axil_csr.vvp axil_csr_tb.sv; vvp -n axil_csr.vvp (the slave, the CSR block and the bench exactly as listed). Result: PASS: 1005 writes (375 AW first, 300 W first, 330 same cycle; 944 partial; 183 SLVERR), 1006 reads (226 SLVERR), 946 B and 968 R stall cycles, 731 events, 14 set/clear collisions.
Mutant (no pairing flags, commit only when AWVALID and WVALID are high in one cycle): FATAL ... FAIL: write 1 to 0x00 (AW first) got no B response in 50 cycles, at 600 ns. Two further mutants, not shown on the page, were also caught: CONTROL ignoring WSTRB (read and ctrl_o mismatches) and clear-wins W1C (22 mismatches). A slave whose WREADY waits for the AW handshake passes this bench, as the page says.
Waveforms: node scripts/tools/gen-waves.mjs memory-buses:axi-lite (entries axil_csr_write and axil_csr_read, with expect strings), from Icarus simulations of the same two modules.
Scope and limits: one outstanding transaction per channel, as this slave allows; one fixed random seed in the course run; the bench checks behaviour, not timing closure of the combinational CSR read path.