Capstone: AXI bug hunts
Run one bench against ten broken things, read which detector catches which fault and why some faults leave no trace on the wires, and answer the ten questions this module prepared you for.
- Lesson
- P2.18
- Time
- 40 min
- Runs on
- Your browser
- Check
- 5 questions
A fabric shipped with its ordering enforcement switched off, "for performance". Every directed test passed, and the protocol checker on the manager port never said a word. The first time a DMA engine issued two reads with one ID, the first to a slow DDR controller and the second to a fast SRAM, the SRAM answered first. The engine filed that data as the DDR read, a descriptor pointed at the wrong buffer, and the bug took a month, because no handshake had broken a rule.
Both requests carry id 5. The manager can only file the first data that comes back under its oldest request with that ID.
- Cycle 1
The second request, to the fast m0, is accepted one cycle after the first. A fabric that enforces ordering would hold it until the slow read had answered. This one forwards it at once.
- Cycle 6
m0 answers first, and the fabric passes a1 to the manager under rid 5. The manager's oldest request with id 5 is the read of 0x8100, so a1 is filed as the first word of 0x8100.
- Cycle 9
Now m1 answers. b0 and b1, the real content of 0x8100, are filed as the read of 0x104. Every handshake was legal; two transactions swapped their data.
Measured through the bench's fabric with enforce_order = 0. The checker at the manager port sees nothing, because the two reads have the same length; only a scoreboard keyed by ID does.
One bench, ten things that can go wrong
The bench on this page holds everything the module built. The manager tasks of P2.1 to P2.9 drive the fabric of P2.10 with its two memories, and the rule checker of P2.16 watches the manager port. A scoreboard in your file writes known data, reads it back and compares beat by beat. Each memory and the fabric take switches, so one bench shows the correct behaviour and its broken twin.
The stimulus runs ten phases: correct traffic, five switches, four broken managers written by hand. Every phase prints the scoreboard's verdict, and the checker adds a line for every rule it sees broken, with the edge.
| Fault | What is broken | Symptom | Caught by |
|---|---|---|---|
| 1 ignore_strobes | The memory writes every lane of the bus word | A one-byte write at 0x101 wipes its three neighbours | Scoreboard only: word 0x100 reads 5a00, expected a05a |
| 2 drop_hold | The memory changes RDATA while its beat waits for RREADY | Beat 1 of the read arrives as a4 instead of a2 | Scoreboard only: the fabric re-registers the beat, so the manager port never sees it change |
| 3 reorder_same_id | The memory answers a fast same-ID read before a slow one | A 2-beat answer where a 4-beat one was due | Checker: rule rlast at edges 89, 94 and 96; scoreboard: 6 mismatches |
| 4 wrap_bug | A WRAP burst is addressed like INCR | Beats 2 and 3 of the read come from 0x210 and 0x214, which hold zeros | Scoreboard only: 2 mismatches, every handshake legal |
| 5 enforce_order off | The fabric forwards a same-ID request to a second target at once | The fast target answers first; two reads swap their data | Scoreboard only: 4 mismatches, no rule, the lengths were equal |
| 6 held request edited | The manager changes AWADDR while the fabric holds the request | The payload moves under a VALID that is still high | Checker: rule stable at edge 132 |
| 7 beat withdrawn | The manager drops WVALID before the fabric could take the beat | A one-cycle offer with no address behind it | Checker: rule hold at edge 150 |
| 8 4 KB crossing | A four-beat burst starts at 0xff8 | Beats 2 and 3 land at 0x1000 and 0x1004 | Checker: rule boundary at edge 151 (the fabric decoded the whole burst by its first address) |
| 9 VALID waits for READY | AWVALID waits for WREADY, while the fabric waits for the AW to route the beat | Nothing moves | No rule on the wires; the watchdog in the stimulus after 16 cycles |
The fault matrix, measured by the run below. Four faults break a rule; five leave the wires legal and change only the data, or stop them altogether.
The read that came back as another
Faults 3 and 5 are the same bug in two places: a same-ID response overtook an older one. In fault 3 the memory reorders; in fault 5 the fabric forwards a same-ID request to a second target without waiting, which is its duty to prevent. Responses with the same ID return in request order, whoever answers them.
The checker sees it only when the two transactions differ in length. In fault 3 the slow read has four beats and the fast one two. An RLAST arrives on beat 1 of what the checker takes for a 4-beat transaction, and rule rlast fires three times as the bursts misalign. In fault 5 both reads have two beats: every RLAST lands where a legal one would, the wires are perfect, and the data is wrong.
A scoreboard keyed by ID, the one P2.11 and P2.17 build, catches both. It expects the next finished transaction with id 5 to carry the data of the oldest request with id 5, and reports every beat that does not. check_collect in your file is that scoreboard reduced to one transaction.
What the fabric hides
Fault 2 is a subordinate that does not hold its read data while the beat waits. On the plain memory of P2.4 the checker names rule stable at the first stalled edge. Through the fabric it names nothing. The fabric takes the beat in one cycle, registers it, and offers its own copy, held perfectly while the manager stalls, with the corrupted value already inside.
A rule checker sees one port. Bind it at the manager port and it checks the fabric; bind a copy at each subordinate port and it checks the memories. The scoreboard sees the data wherever the fault was. Faults 1 and 4 are of the same kind: ignored strobes and a WRAP walked like INCR answer every handshake correctly, and only the bytes tell.
Three broken managers and a deadlock
The manager faults are the rules of P2.2 and P2.6 broken on purpose. Fault 6 offers a write to m0 with an ID that still has a response pending from m1, so the fabric holds it. Then it edits the address under the held VALID: rule stable. Fault 7 offers a write beat before any address and takes it back after one stalled edge: rule hold. Fault 8 sends four beats from 0xff8: rule boundary, with the last two beats in the next 4 KB page, which in a real system belongs to another subordinate.
Fault 9 is the one no checker can report. The manager offers a write beat and waits for WREADY before it sends the address. The fabric routes a beat by the address of its oldest unrouted write, so it waits for the AW before it raises WREADY. Each side waits for the other and nothing moves. The specification's caution is exactly this picture: a manager must not wait for a READY before asserting a VALID, because a subordinate may wait for that VALID. The only detector is a watchdog.
Run the bench, then make it yours. Flip a switch on fab.m1 instead of fab.m0, or give fault 5 two reads of different lengths and watch rule rlast appear. Bind a second checker at fab.s0_* and fault 2 is caught where it happens.
The mock interview
Five questions, drawn from the seventeen lessons before this one, in the words an interviewer uses. Answer each in two sentences before you open it.
Asked in interviews: five questions of this module
May write data arrive before its address, and when may B be sent?
Yes: AW and W are independent and a subordinate may wait for either. B may be sent only after the last W beat and, in AXI4, after the AW handshake, and there is one B per transaction.
Why must a burst not cross a 4 KB boundary, and where does a WRAP burst wrap?
Because the interconnect decodes by the start address and routes the whole burst to one subordinate, so a burst must never need two. A WRAP burst wraps at the boundary aligned to its total size, which is why it must start aligned and have 2, 4, 8 or 16 beats.
Why do responses with the same ID come back in order?
Because the manager has nothing else to match a response to its request; the ID is the only tag. Different IDs may return in any order, and read data of different IDs may even interleave.
Does the same ID on a read and a write order them?
No. There is no ordering between the read and write channels at all. A manager that needs its read to see its write waits for the write's B first.
What does a protocol checker catch that a scoreboard cannot, and the other way round?
The checker names a broken rule at its edge: a withdrawn offer, a changed payload, a wrong WLAST, an illegal burst. The scoreboard catches data that is legal on the wires and wrong in value: ignored strobes, a same-ID reorder of equal lengths, a beat corrupted behind a register. A deadlock needs a watchdog.
Try it
Ten phases, one per row of the fault matrix. Read each verdict line against the table: the checker names a rule for faults 3, 6, 7 and 8, the scoreboard's mismatch count moves for faults 1 to 5, and fault 9 ends with the watchdog's line.
Then run one fault alone. Comment out the others, give fault 5 reads of different lengths, or set fab.m0.drop_hold = 1 with fabric: false in your own copy of the bench and watch rule stable appear at the manager port, where the memory now sits.
-- fault 0: none e3 AW id=1 addr=0x100 len=3 size=2 INCR e4 W beat 0 data=0xa0 strb=1111 last=0 addr 0x100 lanes 0-3 e5 W beat 1 data=0xa1 strb=1111 last=0 addr 0x104 lanes 0-3 e6 W beat 2 data=0xa2 strb=1111 last=0 addr 0x108 lanes 0-3 e7 W beat 3 data=0xa3 strb=1111 last=1 addr 0x10c lanes 0-3 e12 B id=1 OKAY e13 AW id=2 addr=0x8100 len=3 size=2 INCR e14 W beat 0 data=0xb0 strb=1111 last=0 addr 0x8100 lanes 0-3 e15 W beat 1 data=0xb1 strb=1111 last=0 addr 0x8104 lanes 0-3 e16 W beat 2 data=0xb2 strb=1111 last=0 addr 0x8108 lanes 0-3 e17 W beat 3 data=0xb3 strb=1111 last=1 addr 0x810c lanes 0-3 e22 B id=2 OKAY e23 AR id=1 addr=0x100 len=3 size=2 INCR e28 R id=1 beat 0 data=0xa0 OKAY addr 0x100 lanes 0-3 e29 R id=1 beat 1 data=0xa1 OKAY addr 0x104 lanes 0-3 e30 R id=1 beat 2 data=0xa2 OKAY addr 0x108 lanes 0-3 e31 R id=1 beat 3 data=0xa3 OKAY last addr 0x10c lanes 0-3 e32 AR id=2 addr=0x8100 len=3 size=2 INCR e37 R id=2 beat 0 data=0xb0 OKAY addr 0x8100 lanes 0-3 e38 R id=2 beat 1 data=0xb1 OKAY addr 0x8104 lanes 0-3 e39 R id=2 beat 2 data=0xb2 OKAY addr 0x8108 lanes 0-3 e40 R id=2 beat 3 data=0xb3 OKAY last addr 0x810c lanes 0-3 fault 0: 8 beats checked, 0 mismatches, 0 rules broken so far -- fault 1: ignore_strobes, the memory writes every lane e41 AW id=1 addr=0x101 len=0 size=0 INCR e42 W beat 0 data=0x5a00 strb=0010 last=1 addr 0x101 lanes 1-1 e47 B id=1 OKAY e48 AR id=1 addr=0x100 len=0 size=2 INCR e53 R id=1 beat 0 data=0x5a00 OKAY last addr 0x100 lanes 0-3 mismatch: beat 0 read 0x5a00, expected 0xa05a fault 1: 9 beats checked, 1 mismatches, 0 rules broken so far -- fault 2: drop_hold, read data changes while the fabric waits e54 AR id=1 addr=0x104 len=1 size=2 INCR e61 R id=1 beat 0 data=0xa1 OKAY addr 0x104 lanes 0-3 e65 R id=1 beat 1 data=0xa4 OKAY last addr 0x108 lanes 0-3 mismatch: beat 1 read 0xa4, expected 0xa2 fault 2: 11 beats checked, 2 mismatches, 0 rules broken so far -- fault 3: reorder_same_id, a fast read overtakes a slow one with the same ID e66 AW id=3 addr=0x4000 len=3 size=2 INCR e67 W beat 0 data=0xc0 strb=1111 last=0 addr 0x4000 lanes 0-3 e68 W beat 1 data=0xc1 strb=1111 last=0 addr 0x4004 lanes 0-3 e69 W beat 2 data=0xc2 strb=1111 last=0 addr 0x4008 lanes 0-3 e70 W beat 3 data=0xc3 strb=1111 last=1 addr 0x400c lanes 0-3 e81 B id=3 OKAY e82 AR id=3 addr=0x4000 len=3 size=2 INCR e83 AR id=3 addr=0x104 len=1 size=2 INCR e88 R id=3 beat 0 data=0xa1 OKAY addr 0x4000 lanes 0-3 RULE rlast broken at edge 89: RLAST on beat 1 of the oldest read with id 3, whose ARLEN is 3 e89 R id=3 beat 1 data=0xa2 OKAY last addr 0x4004 lanes 0-3 mismatch: beat 0 read 0xa1, expected 0xc0 mismatch: beat 1 read 0xa2, expected 0xc1 e93 R id=3 beat 0 data=0xc0 OKAY addr 0x104 lanes 0-3 RULE rlast broken at edge 94: beat 1 of the oldest read with id 3 is its ARLEN but RLAST is 0 e94 R id=3 beat 1 data=0xc1 OKAY addr 0x108 lanes 0-3 e95 R id=3 beat 2 data=0xc2 OKAY addr 0x10c lanes 0-3 RULE rlast broken at edge 96: RLAST on beat 3 of the oldest read with id 3, whose ARLEN is 1 e96 R id=3 beat 3 data=0xc3 OKAY last addr 0x110 lanes 0-3 mismatch: beat 0 read 0xc0, expected 0xa1 mismatch: beat 1 read 0xc1, expected 0xa2 mismatch: beat 2 read 0xc2, expected 0xa3 mismatch: beat 3 read 0xc3, expected 0xa4 fault 3: 17 beats checked, 8 mismatches, 3 rules broken so far -- fault 4: wrap_bug, a WRAP burst addressed like INCR e97 AW id=4 addr=0x208 len=3 size=2 WRAP e98 W beat 0 data=0xd0 strb=1111 last=0 addr 0x208 lanes 0-3 e99 W beat 1 data=0xd1 strb=1111 last=0 addr 0x20c lanes 0-3 e100 W beat 2 data=0xd2 strb=1111 last=0 addr 0x200 lanes 0-3 e101 W beat 3 data=0xd3 strb=1111 last=1 addr 0x204 lanes 0-3 e106 B id=4 OKAY e107 AR id=4 addr=0x208 len=3 size=2 WRAP e112 R id=4 beat 0 data=0xd0 OKAY addr 0x208 lanes 0-3 e113 R id=4 beat 1 data=0xd1 OKAY addr 0x20c lanes 0-3 e114 R id=4 beat 2 data=0x0 OKAY addr 0x200 lanes 0-3 e115 R id=4 beat 3 data=0x0 OKAY last addr 0x204 lanes 0-3 mismatch: beat 2 read 0x0, expected 0xd2 mismatch: beat 3 read 0x0, expected 0xd3 fault 4: 21 beats checked, 10 mismatches, 3 rules broken so far -- fault 5: enforce_order off, the fabric lets a same-ID read overtake e116 AR id=5 addr=0x8100 len=1 size=2 INCR e117 AR id=5 addr=0x104 len=1 size=2 INCR e122 R id=5 beat 0 data=0xa1 OKAY addr 0x8100 lanes 0-3 e123 R id=5 beat 1 data=0xa2 OKAY last addr 0x8104 lanes 0-3 mismatch: beat 0 read 0xa1, expected 0xb0 mismatch: beat 1 read 0xa2, expected 0xb1 e127 R id=5 beat 0 data=0xb0 OKAY addr 0x104 lanes 0-3 e128 R id=5 beat 1 data=0xb1 OKAY last addr 0x108 lanes 0-3 mismatch: beat 0 read 0xb0, expected 0xa1 mismatch: beat 1 read 0xb1, expected 0xa2 fault 5: 25 beats checked, 14 mismatches, 3 rules broken so far -- fault 6: a manager changes the address of a request that is held e129 AW id=7 addr=0x8200 len=0 size=2 INCR e130 W beat 0 data=0xe0 strb=1111 last=1 addr 0x8200 lanes 0-3 RULE stable broken at edge 132: AW payload changed while the request waited e141 B id=7 OKAY e143 W beat 0 data=0xe0 strb=1111 last=1 addr 0x204 lanes 0-3 e148 B id=7 OKAY fault 6: 25 beats checked, 14 mismatches, 4 rules broken so far -- fault 7: a manager withdraws a write beat the fabric could not take yet RULE hold broken at edge 150: WVALID dropped before WREADY fault 7: 25 beats checked, 14 mismatches, 5 rules broken so far -- fault 8: a manager sends a burst across a 4 KB boundary RULE boundary broken at edge 151: AW: burst at 0xff8, 4 beats of 4 bytes, crosses a 4 KB boundary e151 AW id=8 addr=0xff8 len=3 size=2 INCR e152 W beat 0 data=0x40 strb=1111 last=0 addr 0xff8 lanes 0-3 e153 W beat 1 data=0x41 strb=1111 last=0 addr 0xffc lanes 0-3 e154 W beat 2 data=0x42 strb=1111 last=0 addr 0x1000 lanes 0-3 e155 W beat 3 data=0x43 strb=1111 last=1 addr 0x1004 lanes 0-3 e160 B id=8 OKAY fault 8: 25 beats checked, 14 mismatches, 6 rules broken so far -- fault 9: a manager waits for WREADY before it sends the address no handshake in 16 cycles: WVALID waits for WREADY, the fabric waits for the AW fault 9: 25 beats checked, 14 mismatches, 6 rules broken so far checker: 8 writes, 9 reads, 35 stalled edges, 6 rules brokenRead included file: axi_pkg.sv
`timescale 1ns/1ns
// axi_pkg.sv: the arithmetic of a burst (read-only).
// Every transfer n of a burst has an address and a set of byte lanes, computed from the start
// address, the burst length, the transfer size and the burst type exactly as the AXI specification
// defines them. The memory, the manager tasks and the checker all use these functions, so they
// never disagree about where a byte lands.
package axi_pkg;
localparam logic [1:0] FIXED = 2'b00, INCR = 2'b01, WRAP = 2'b10;
localparam logic [1:0] OKAY = 2'b00, EXOKAY = 2'b01, SLVERR = 2'b10, DECERR = 2'b11;
function automatic string burst_name(input logic [1:0] b);
case (b) FIXED: return "FIXED"; INCR: return "INCR"; WRAP: return "WRAP"; default: return "RESERVED"; endcase
endfunction
function automatic string resp_name(input logic [1:0] r);
case (r) OKAY: return "OKAY"; EXOKAY: return "EXOKAY"; SLVERR: return "SLVERR"; default: return "DECERR"; endcase
endfunction
// Bytes in one transfer: 1, 2, 4, ... 128.
function automatic int bytes_of(input logic [2:0] size);
return 1 << size;
endfunction
// The address of transfer n (n counts from 0).
// transfer 0 uses the start address as given, aligned or not;
// later transfers use the start address aligned down to the transfer size, plus n transfers;
// a WRAP burst returns to its wrap boundary (the start aligned down to the whole burst) when
// it reaches the end of that window; a FIXED burst repeats the start address.
function automatic int unsigned beat_addr(input int unsigned start, input int len, input logic [2:0] size,
input logic [1:0] burst, input int n);
int unsigned nb = 1 << size;
int unsigned aligned = (start / nb) * nb;
int unsigned window = nb * (len + 1);
int unsigned boundary = (start / window) * window;
int unsigned a;
if (burst == FIXED || n == 0) return start;
a = aligned + n * nb;
if (burst == WRAP && a >= boundary + window) a = a - window;
return a;
endfunction
// The byte lanes transfer n uses on a bus of bus_bytes bytes, packed as {upper, lower}.
// Transfer 0 of an unaligned burst (and every transfer of a FIXED burst) uses only the lanes from
// its address up to the end of its aligned transfer; every other transfer uses all the lanes of
// its size.
function automatic logic [15:0] beat_lanes(input int unsigned start, input int len, input logic [2:0] size,
input logic [1:0] burst, input int n, input int bus_bytes);
int unsigned nb = 1 << size;
int unsigned a = beat_addr(start, len, size, burst, n);
int unsigned aligned = (a / nb) * nb;
int unsigned word = (a / bus_bytes) * bus_bytes;
int lower = a - word;
int upper = (n == 0 || burst == FIXED) ? (aligned + nb - 1 - word) : (lower + nb - 1);
return {upper[7:0], lower[7:0]};
endfunction
// The strobe of transfer n: a 1 for every lane it uses (bit l is lane l).
function automatic logic [63:0] beat_strb(input int unsigned start, input int len, input logic [2:0] size,
input logic [1:0] burst, input int n, input int bus_bytes);
logic [15:0] lanes = beat_lanes(start, len, size, burst, n, bus_bytes);
logic [63:0] s = 0;
for (int l = lanes[7:0]; l <= lanes[15:8]; l++) s[l] = 1'b1;
return s;
endfunction
// True when the burst crosses a 4 KB boundary (only an INCR burst can).
function automatic bit crosses_4k(input int unsigned start, input int len, input logic [2:0] size, input logic [1:0] burst);
if (burst != INCR) return 0;
return (beat_addr(start, len, size, burst, 0) / 4096) != ((beat_addr(start, len, size, burst, len) + (1 << size) - 1) / 4096);
endfunction
endpackage
Read included file: axi_mem.sv
`timescale 1ns/1ns
// axi_mem.sv: an AXI4 subordinate with a byte-addressed memory behind it (read-only in this lesson).
// It accepts requests on AW and AR, write beats on W (before or after their AW), writes the bytes a
// burst addresses using the strobes, and answers on B and R after a latency. The switches below are
// plain variables: the stimulus sets them by name (mem.reorder = 1;) before it starts.
// rd_latency, wr_latency cycles from a request (or its last write beat) to its first response
// slow_latency, slow_above addresses at or above slow_above answer after slow_latency instead
// err_above addresses at or above err_above get SLVERR (and are not written)
// reorder answer transactions with DIFFERENT IDs as soon as each is ready;
// transactions with the same ID still answer in order
// interleave interleave the read data beats of ready transactions with different IDs
// excl_on support exclusive access (one monitor per ID), else answer OKAY
// posted apply a write to the memory only when its response is sent, so a read
// that arrives between the last beat and B still sees the old bytes
// trace print every byte the memory writes
// Broken twins, each off by default: early_b (B before the last write beat and before AW),
// ignore_strobes (write every lane of the bus word), drop_hold (RDATA changes while RVALID waits
// for RREADY), reorder_same_id (a later transaction with the same ID may answer first),
// wrap_bug (a WRAP burst addressed like INCR).
module axi_mem #(parameter int DATA_W = 32, parameter int ID_W = 4, parameter int DEPTH = 65536) (
input logic aclk,
input logic aresetn,
input logic [ID_W-1:0] awid,
input logic [31:0] awaddr,
input logic [7:0] awlen,
input logic [2:0] awsize,
input logic [1:0] awburst,
input logic awlock,
input logic [3:0] awcache,
input logic [2:0] awprot,
input logic awvalid,
output logic awready,
input logic [DATA_W-1:0] wdata,
input logic [DATA_W/8-1:0] wstrb,
input logic wlast,
input logic wvalid,
output logic wready,
output logic [ID_W-1:0] bid,
output logic [1:0] bresp,
output logic bvalid,
input logic bready,
input logic [ID_W-1:0] arid,
input logic [31:0] araddr,
input logic [7:0] arlen,
input logic [2:0] arsize,
input logic [1:0] arburst,
input logic arlock,
input logic [3:0] arcache,
input logic [2:0] arprot,
input logic arvalid,
output logic arready,
output logic [ID_W-1:0] rid,
output logic [DATA_W-1:0] rdata,
output logic [1:0] rresp,
output logic rlast,
output logic rvalid,
input logic rready
);
import axi_pkg::*;
localparam int BUS = DATA_W / 8;
localparam int N = 16; // transactions in flight per direction
// ---- switches ----
int rd_latency = 2, wr_latency = 2, slow_latency = 8;
int unsigned slow_above = 32'hffff_ffff, err_above = 32'hffff_ffff;
bit reorder = 0, interleave = 0, excl_on = 0, posted = 0, trace = 0;
bit early_b = 0, ignore_strobes = 0, drop_hold = 0, reorder_same_id = 0, wrap_bug = 0;
// ---- the memory ----
logic [7:0] mem [0:DEPTH-1];
initial for (int i = 0; i < DEPTH; i++) mem[i] = 8'h00;
int cycle = -1;
int seq = 0; // every request gets a sequence number: issue order
// ---- write side: accepted AW requests (a ring) and write beats (a ring) ----
logic [ID_W-1:0] aw_id [0:N-1];
int unsigned aw_addr [0:N-1];
int aw_len [0:N-1];
logic [2:0] aw_size [0:N-1];
logic [1:0] aw_burst [0:N-1];
logic aw_lock [0:N-1];
int aw_head = 0, aw_tail = 0, aw_count = 0;
localparam int WQ = 1024;
logic [DATA_W-1:0] w_data [0:WQ-1];
logic [BUS-1:0] w_strb [0:WQ-1];
logic w_last [0:WQ-1];
int w_head = 0, w_tail = 0, w_count = 0, w_bursts = 0; // w_bursts: complete bursts in the ring
// ---- write responses waiting for their latency ----
logic b_use [0:N-1];
logic [ID_W-1:0] b_id [0:N-1];
logic [1:0] b_rsp [0:N-1];
int b_at [0:N-1], b_seq [0:N-1];
logic b_driving = 0;
logic early_done = 0; // the early_b twin answers once per burst
// posted writes: the beats of a write kept until its response goes out
logic [DATA_W-1:0] p_data [0:N-1][0:255];
logic [BUS-1:0] p_strb [0:N-1][0:255];
int unsigned p_addr [0:N-1];
int p_len [0:N-1];
logic [2:0] p_size [0:N-1];
logic [1:0] p_burst [0:N-1];
logic p_pending [0:N-1];
// ---- read side: accepted AR requests with their progress ----
logic r_use [0:N-1];
logic [ID_W-1:0] r_id [0:N-1];
int unsigned r_addr [0:N-1];
int r_len [0:N-1];
logic [2:0] r_size [0:N-1];
logic [1:0] r_burst [0:N-1];
logic r_lock [0:N-1];
int r_at [0:N-1], r_seq [0:N-1], r_beat [0:N-1], r_served [0:N-1];
int r_cur = -1; // the transaction whose beat is on the wires
logic r_driving = 0;
// ---- exclusive monitors, one per ID ----
logic mon_use [0:(1<<ID_W)-1];
int unsigned mon_addr [0:(1<<ID_W)-1];
int unsigned mon_bytes [0:(1<<ID_W)-1];
initial begin
for (int i = 0; i < N; i++) begin b_use[i] = 0; r_use[i] = 0; p_pending[i] = 0; end
for (int i = 0; i < (1 << ID_W); i++) mon_use[i] = 0;
awready = 0; wready = 0; arready = 0; bvalid = 0; rvalid = 0; bid = 0; bresp = 0; rid = 0; rresp = 0; rlast = 0; rdata = 0;
end
function automatic int latency_of(input int unsigned a, input int base);
return (a >= slow_above) ? slow_latency : base;
endfunction
function automatic logic [1:0] burst_used(input logic [1:0] b);
return (wrap_bug && b == WRAP) ? INCR : b;
endfunction
// ---- selection policies ----
// The oldest of all pending entries, or the oldest of its ID: the in-order rule of the protocol.
function automatic bit b_oldest(input int i, input bit same_id_only);
for (int j = 0; j < N; j++)
if (b_use[j] && j != i && b_seq[j] < b_seq[i] && (!same_id_only || b_id[j] == b_id[i])) return 0;
return 1;
endfunction
function automatic bit r_oldest(input int i, input bit same_id_only);
for (int j = 0; j < N; j++)
if (r_use[j] && j != i && r_seq[j] < r_seq[i] && (!same_id_only || r_id[j] == r_id[i])) return 0;
return 1;
endfunction
function automatic bit b_eligible(input int i);
if (!b_use[i] || b_at[i] > cycle) return 0;
if (!reorder) return b_oldest(i, 0);
return reorder_same_id ? 1 : b_oldest(i, 1);
endfunction
function automatic bit r_eligible(input int i);
if (!r_use[i] || r_at[i] > cycle) return 0;
if (!reorder) return r_oldest(i, 0);
return reorder_same_id ? 1 : r_oldest(i, 1);
endfunction
function automatic int free_b();
for (int i = 0; i < N; i++) if (!b_use[i]) return i;
return -1;
endfunction
function automatic int free_r();
for (int i = 0; i < N; i++) if (!r_use[i]) return i;
return -1;
endfunction
function automatic int pick_b();
int best = -1;
for (int i = 0; i < N; i++) if (b_eligible(i) && (best < 0 || b_seq[i] < b_seq[best])) best = i;
return best;
endfunction
function automatic int pick_r();
int best = -1;
for (int i = 0; i < N; i++) if (r_eligible(i)) begin
if (best < 0) best = i;
else if (interleave ? (r_served[i] < r_served[best]) : (r_seq[i] < r_seq[best])) best = i;
end
return best;
endfunction
// ---- the bytes of one read beat, as the bus word that holds them ----
function automatic logic [DATA_W-1:0] read_word(input int unsigned a);
logic [DATA_W-1:0] d = 0;
int unsigned word = (a / BUS) * BUS;
for (int l = 0; l < BUS; l++) d[8*l +: 8] = (word + l < DEPTH) ? mem[word + l] : 8'h00;
return d;
endfunction
// ---- the write of one complete burst into the memory ----
task automatic write_burst(input int e);
logic [1:0] rsp = OKAY;
logic do_write = 1, ended = 0;
int unsigned a, word;
logic [15:0] lanes;
int n = 0, slot;
int unsigned total = (1 << aw_size[e]) * (aw_len[e] + 1);
slot = free_b();
if (aw_lock[e] && excl_on) begin
if (mon_use[aw_id[e]] && mon_addr[aw_id[e]] == aw_addr[e]) begin
rsp = EXOKAY;
$display("mem e%0d: exclusive write id=%0d at 0x%0h: the monitor matched, EXOKAY", cycle, aw_id[e], aw_addr[e]);
end else begin
do_write = 0;
$display("mem e%0d: exclusive write id=%0d at 0x%0h: no monitor, OKAY and nothing written", cycle, aw_id[e], aw_addr[e]);
end
end
while (!ended && n <= aw_len[e]) begin
a = beat_addr(aw_addr[e], aw_len[e], aw_size[e], burst_used(aw_burst[e]), n);
lanes = beat_lanes(aw_addr[e], aw_len[e], aw_size[e], burst_used(aw_burst[e]), n, BUS);
word = (a / BUS) * BUS;
if (a >= err_above) rsp = SLVERR;
else if (do_write && posted && slot >= 0) begin
p_data[slot][n] = w_data[w_head]; p_strb[slot][n] = w_strb[w_head];
end else if (do_write) begin
for (int l = 0; l < BUS; l++)
if ((w_strb[w_head][l] || ignore_strobes) && word + l < DEPTH) begin
mem[word + l] = w_data[w_head][8*l +: 8];
if (trace) $display("mem e%0d: beat %0d writes 0x%02h to 0x%0h (lane %0d)", cycle, n, w_data[w_head][8*l +: 8], word + l, l);
end
end
ended = w_last[w_head]; // a burst ends at its WLAST, whatever AWLEN said
w_head = (w_head + 1) % WQ; w_count = w_count - 1;
n = n + 1;
end
while (!ended) begin // beats past AWLEN (a broken manager) are dropped
ended = w_last[w_head];
w_head = (w_head + 1) % WQ; w_count = w_count - 1;
end
// A write clears every monitor that watches a byte it touched.
if (do_write)
for (int i = 0; i < (1 << ID_W); i++)
if (mon_use[i] && mon_addr[i] < aw_addr[e] + total && aw_addr[e] < mon_addr[i] + mon_bytes[i]) mon_use[i] = 0;
if (slot >= 0) begin
b_use[slot] = 1; b_id[slot] = aw_id[e]; b_rsp[slot] = rsp; b_seq[slot] = seq; seq = seq + 1;
b_at[slot] = cycle + latency_of(aw_addr[e], wr_latency);
if (posted && do_write) begin
p_pending[slot] = 1; p_addr[slot] = aw_addr[e]; p_len[slot] = n - 1; p_size[slot] = aw_size[e]; p_burst[slot] = aw_burst[e];
end
end
endtask
// A posted write reaches the memory when its response goes out.
task automatic apply_posted(input int i);
int unsigned a, word;
for (int n = 0; n <= p_len[i]; n++) begin
a = beat_addr(p_addr[i], p_len[i], p_size[i], burst_used(p_burst[i]), n);
word = (a / BUS) * BUS;
if (a < err_above)
for (int l = 0; l < BUS; l++)
if ((p_strb[i][n][l] || ignore_strobes) && word + l < DEPTH) begin
mem[word + l] = p_data[i][n][8*l +: 8];
if (trace) $display("mem e%0d: posted beat %0d writes 0x%02h to 0x%0h (lane %0d)", cycle, n, p_data[i][n][8*l +: 8], word + l, l);
end
end
p_pending[i] = 0;
endtask
int pick, extra;
always @(posedge aclk) begin
cycle = cycle + 1;
if (!aresetn) begin
aw_head = 0; aw_tail = 0; aw_count = 0; w_head = 0; w_tail = 0; w_count = 0; w_bursts = 0;
for (int i = 0; i < N; i++) begin b_use[i] = 0; r_use[i] = 0; end
b_driving = 0; r_driving = 0; r_cur = -1;
awready <= 0; wready <= 0; arready <= 0; bvalid <= 0; rvalid <= 0;
end else begin
// -- AW: take a request while there is room --
if (awvalid && awready) begin
aw_id[aw_tail] = awid; aw_addr[aw_tail] = awaddr; aw_len[aw_tail] = awlen; aw_size[aw_tail] = awsize;
aw_burst[aw_tail] = awburst; aw_lock[aw_tail] = awlock;
aw_tail = (aw_tail + 1) % N; aw_count = aw_count + 1;
end
// -- W: take a beat while there is room --
if (wvalid && wready) begin
w_data[w_tail] = wdata; w_strb[w_tail] = wstrb; w_last[w_tail] = wlast;
w_tail = (w_tail + 1) % WQ; w_count = w_count + 1;
if (wlast) w_bursts = w_bursts + 1;
end
// -- a complete burst with its request: write it and schedule B --
if (aw_count > 0 && w_bursts > 0) begin
write_burst(aw_head);
aw_head = (aw_head + 1) % N; aw_count = aw_count - 1; w_bursts = w_bursts - 1;
early_done = 0;
end else if (early_b && !early_done && w_count > 0 && w_bursts == 0) begin
// The broken twin: a response as soon as the first write beat arrived, before WLAST and,
// when the data came first, before the address.
pick = free_b();
if (pick >= 0) begin
b_use[pick] = 1; b_id[pick] = (aw_count > 0) ? aw_id[aw_head] : awid; b_rsp[pick] = OKAY; b_seq[pick] = seq; seq = seq + 1; b_at[pick] = cycle;
early_done = 1;
end
end
// -- B: finish a handshake, then put the next response on the wires --
if (b_driving && bready) b_driving = 0;
if (!b_driving) begin
pick = pick_b();
if (pick >= 0) begin
if (p_pending[pick]) apply_posted(pick);
b_driving = 1; bid <= b_id[pick]; bresp <= b_rsp[pick]; b_use[pick] = 0;
end
end
bvalid <= b_driving;
// -- AR: take a request while there is room --
if (arvalid && arready) begin
pick = free_r();
if (pick >= 0) begin
r_use[pick] = 1; r_id[pick] = arid; r_addr[pick] = araddr; r_len[pick] = arlen; r_size[pick] = arsize; r_burst[pick] = arburst;
r_lock[pick] = arlock; r_beat[pick] = 0; r_seq[pick] = seq; seq = seq + 1; r_served[pick] = -1;
r_at[pick] = cycle + latency_of(araddr, rd_latency);
if (arlock && excl_on) begin
mon_use[arid] = 1; mon_addr[arid] = araddr; mon_bytes[arid] = (1 << arsize) * (arlen + 1);
$display("mem e%0d: exclusive read id=%0d at 0x%0h: monitor set", cycle, arid, araddr);
end
end
end
// -- R: finish a handshake, then put the next beat on the wires --
if (r_driving && !rready && drop_hold) rdata <= rdata + 1; // the broken twin: data not held
if (r_driving && rready) begin
r_driving = 0;
if (r_beat[r_cur] > r_len[r_cur]) begin r_use[r_cur] = 0; r_cur = -1; end
else if (interleave) r_cur = -1;
end
if (!r_driving) begin
pick = (r_cur >= 0) ? r_cur : pick_r();
if (pick >= 0) begin
r_cur = pick; r_driving = 1; r_served[pick] = cycle;
extra = r_beat[pick];
rid <= r_id[pick];
rdata <= read_word(beat_addr(r_addr[pick], r_len[pick], r_size[pick], burst_used(r_burst[pick]), extra));
rresp <= (beat_addr(r_addr[pick], r_len[pick], r_size[pick], burst_used(r_burst[pick]), extra) >= err_above) ? SLVERR
: ((r_lock[pick] && excl_on) ? EXOKAY : OKAY);
rlast <= (extra == r_len[pick]);
r_beat[pick] = extra + 1;
end
end
rvalid <= r_driving;
awready <= (aw_count < N - 1);
wready <= (w_count < WQ - 2);
arready <= 1;
end
end
endmodule
Read included file: axi_checker.sv
`timescale 1ns/1ns
// axi_checker.sv: the rules of an AXI4 interface, checked at every rising edge (read-only).
// Each broken rule is named with the channel and the edge where it was seen:
// reset a VALID is 1 while reset is active;
// hold a VALID dropped before its READY: the offer was withdrawn;
// stable a channel's payload changed while its offer waited;
// known a payload is x while its VALID is 1;
// len a FIXED or WRAP burst longer than 16 beats;
// wrap a WRAP burst whose length is not 2, 4, 8 or 16 beats or whose address is not aligned;
// boundary a burst that crosses a 4 KB boundary;
// size a transfer wider than the data bus;
// strobe a write strobe on a lane the beat does not address;
// wlast WLAST on the wrong beat (not AWLEN), or missing after it;
// rlast RLAST on the wrong beat of the oldest read with that ID, or missing after it;
// border BVALID before any outstanding write has its last beat, or before its AW;
// bid, rid a response ID with no outstanding request of that ID;
// exokay EXOKAY for a request that was not exclusive;
// exclusive an exclusive request whose bytes are not a power of two up to 128, or not aligned to them.
// The rules no checker can see on the wires (VALID must not wait for READY; same-ID responses
// in the order of their requests) are checked by the data the tasks compare (lessons P2.2, P2.10).
module axi_checker #(parameter int DATA_W = 32, parameter int ID_W = 4) (
input logic aclk,
input logic aresetn,
input logic [ID_W-1:0] awid,
input logic [31:0] awaddr,
input logic [7:0] awlen,
input logic [2:0] awsize,
input logic [1:0] awburst,
input logic awlock,
input logic awvalid,
input logic awready,
input logic [DATA_W-1:0] wdata,
input logic [DATA_W/8-1:0] wstrb,
input logic wlast,
input logic wvalid,
input logic wready,
input logic [ID_W-1:0] bid,
input logic [1:0] bresp,
input logic bvalid,
input logic bready,
input logic [ID_W-1:0] arid,
input logic [31:0] araddr,
input logic [7:0] arlen,
input logic [2:0] arsize,
input logic [1:0] arburst,
input logic arlock,
input logic arvalid,
input logic arready,
input logic [ID_W-1:0] rid,
input logic [DATA_W-1:0] rdata,
input logic [1:0] rresp,
input logic rlast,
input logic rvalid,
input logic rready
);
import axi_pkg::*;
localparam int BUS = DATA_W / 8;
localparam int Q = 64;
int cycle = -1, writes = 0, reads = 0, stalls = 0, broken = 0;
logic [DATA_W-1:0] used;
function void report(input string rule, input string what);
broken++;
$display("RULE %s broken at edge %0d: %s", rule, cycle, what);
endfunction
// Previous values of every channel, for hold and stable.
logic p_awv = 0, p_awr = 0, p_wv = 0, p_wr = 0, p_bv = 0, p_br = 0, p_arv = 0, p_arr = 0, p_rv = 0, p_rr = 0;
logic [31:0] p_awaddr, p_araddr;
logic [7:0] p_awlen, p_arlen;
logic [2:0] p_awsize, p_arsize;
logic [1:0] p_awburst, p_arburst, p_bresp, p_rresp;
logic [ID_W-1:0] p_awid, p_arid, p_bid, p_rid;
logic [DATA_W-1:0] p_wdata, p_rdata;
logic [BUS-1:0] p_wstrb;
logic p_wlast, p_rlast, p_awlock, p_arlock;
// Write requests in order (AXI4 write data follows the order of the addresses).
int unsigned q_addr [0:Q-1];
int q_len [0:Q-1];
logic [2:0] q_size [0:Q-1];
logic [1:0] q_burst [0:Q-1];
int aw_n = 0; // requests accepted
int wb_n = 0; // write bursts started
int wb_done = 0; // write bursts ended (WLAST seen)
int w_beat = 0; // beats so far in the current burst
int b_n = 0; // responses given
// Beats recorded for bursts that ran ahead of their AW: strobes and count, checked when AW arrives.
logic [BUS-1:0] rec_strb [0:Q-1][0:255];
int rec_beats [0:Q-1];
logic rec_open [0:Q-1];
// Outstanding requests per ID, and the read requests per ID in order.
int aw_out [0:(1<<ID_W)-1], ar_out [0:(1<<ID_W)-1], aw_excl [0:(1<<ID_W)-1], ar_excl [0:(1<<ID_W)-1];
int rq_len [0:(1<<ID_W)-1][0:15];
int rq_head [0:(1<<ID_W)-1], rq_tail [0:(1<<ID_W)-1], r_beat [0:(1<<ID_W)-1];
initial for (int i = 0; i < (1 << ID_W); i++) begin aw_out[i] = 0; ar_out[i] = 0; aw_excl[i] = 0; ar_excl[i] = 0; rq_head[i] = 0; rq_tail[i] = 0; r_beat[i] = 0; end
initial for (int i = 0; i < Q; i++) begin rec_beats[i] = 0; rec_open[i] = 0; end
function automatic bit pow2_le128(input int unsigned n);
return (n == 1 || n == 2 || n == 4 || n == 8 || n == 16 || n == 32 || n == 64 || n == 128);
endfunction
task automatic check_request(input string ch, input int unsigned addr, input int len, input logic [2:0] size, input logic [1:0] burst, input logic lock);
int unsigned bytes = (1 << size) * (len + 1);
if ((burst == FIXED || burst == WRAP) && len > 15) report("len", $sformatf("%s: a %s burst of %0d beats (16 at most)", ch, burst_name(burst), len + 1));
if (burst == WRAP && !(len == 1 || len == 3 || len == 7 || len == 15)) report("wrap", $sformatf("%s: a WRAP burst of %0d beats (2, 4, 8 or 16)", ch, len + 1));
if (burst == WRAP && (addr % (1 << size)) != 0) report("wrap", $sformatf("%s: WRAP burst at 0x%0h is not aligned to its %0d-byte transfers", ch, addr, 1 << size));
if (burst == 2'b11) report("len", $sformatf("%s: burst type 2'b11 is reserved", ch));
if ((1 << size) > BUS) report("size", $sformatf("%s: %0d-byte transfers on a %0d-byte bus", ch, 1 << size, BUS));
if (crosses_4k(addr, len, size, burst)) report("boundary", $sformatf("%s: burst at 0x%0h, %0d beats of %0d bytes, crosses a 4 KB boundary", ch, addr, len + 1, 1 << size));
if (lock && (!pow2_le128(bytes) || (addr % bytes) != 0)) report("exclusive", $sformatf("%s: exclusive access of %0d bytes at 0x%0h (a power of two up to 128, aligned)", ch, bytes, addr));
endtask
task automatic check_strobe(input int k, input int n, input logic [BUS-1:0] strb);
logic [63:0] allowed = beat_strb(q_addr[k % Q], q_len[k % Q], q_size[k % Q], q_burst[k % Q], n, BUS);
if ((strb & ~allowed[BUS-1:0]) != 0)
report("strobe", $sformatf("W beat %0d of write %0d: strobe %b on lanes outside %b", n, k, strb, allowed[BUS-1:0]));
endtask
always @(posedge aclk) begin
cycle++;
if (!aresetn) begin
if (awvalid === 1 || wvalid === 1 || arvalid === 1) report("reset", "a manager VALID is 1 during reset");
if (bvalid === 1 || rvalid === 1) report("reset", "a subordinate VALID is 1 during reset");
end else begin
// hold, stable and known, per channel
if (p_awv && !p_awr && awvalid !== 1) report("hold", "AWVALID dropped before AWREADY");
if (p_awv && !p_awr && awvalid === 1 && (awaddr !== p_awaddr || awlen !== p_awlen || awsize !== p_awsize || awburst !== p_awburst || awid !== p_awid || awlock !== p_awlock)) report("stable", "AW payload changed while the request waited");
if (p_wv && !p_wr && wvalid !== 1) report("hold", "WVALID dropped before WREADY");
if (p_wv && !p_wr && wvalid === 1 && (wdata !== p_wdata || wstrb !== p_wstrb || wlast !== p_wlast)) report("stable", "W payload changed while the beat waited");
if (p_bv && !p_br && bvalid !== 1) report("hold", "BVALID dropped before BREADY");
if (p_bv && !p_br && bvalid === 1 && (bresp !== p_bresp || bid !== p_bid)) report("stable", "B payload changed while the response waited");
if (p_arv && !p_arr && arvalid !== 1) report("hold", "ARVALID dropped before ARREADY");
if (p_arv && !p_arr && arvalid === 1 && (araddr !== p_araddr || arlen !== p_arlen || arsize !== p_arsize || arburst !== p_arburst || arid !== p_arid || arlock !== p_arlock)) report("stable", "AR payload changed while the request waited");
if (p_rv && !p_rr && rvalid !== 1) report("hold", "RVALID dropped before RREADY");
if (p_rv && !p_rr && rvalid === 1 && (rdata !== p_rdata || rresp !== p_rresp || rid !== p_rid || rlast !== p_rlast)) report("stable", "R payload changed while the beat waited");
if (awvalid === 1 && ($isunknown(awaddr) || $isunknown(awlen) || $isunknown(awsize) || $isunknown(awburst) || $isunknown(awid))) report("known", "AW payload is x while AWVALID is 1");
if (wvalid === 1 && ($isunknown(wstrb) || $isunknown(wlast))) report("known", "W strobe or last is x while WVALID is 1");
else if (wvalid === 1) begin
used = '0;
for (int l = 0; l < BUS; l++) if (wstrb[l] === 1) used[8*l +: 8] = 8'hff;
if ($isunknown(wdata & used)) report("known", "W data is x on a strobed lane while WVALID is 1");
end
if (arvalid === 1 && ($isunknown(araddr) || $isunknown(arlen) || $isunknown(arsize) || $isunknown(arburst) || $isunknown(arid))) report("known", "AR payload is x while ARVALID is 1");
if (bvalid === 1 && ($isunknown(bresp) || $isunknown(bid))) report("known", "B payload is x while BVALID is 1");
if (rvalid === 1 && ($isunknown(rresp) || $isunknown(rid) || $isunknown(rlast))) report("known", "R payload is x while RVALID is 1");
if ((awvalid === 1 && awready !== 1) || (wvalid === 1 && wready !== 1) || (arvalid === 1 && arready !== 1) ||
(bvalid === 1 && bready !== 1) || (rvalid === 1 && rready !== 1)) stalls++;
// AW accepted
if (awvalid === 1 && awready === 1) begin
check_request("AW", awaddr, awlen, awsize, awburst, awlock);
q_addr[aw_n % Q] = awaddr; q_len[aw_n % Q] = awlen; q_size[aw_n % Q] = awsize; q_burst[aw_n % Q] = awburst;
aw_out[awid]++; if (awlock) aw_excl[awid]++;
// beats of this write that arrived before its address: check them now
if (rec_beats[aw_n % Q] > 0) begin
for (int n = 0; n < rec_beats[aw_n % Q]; n++) check_strobe(aw_n, n, rec_strb[aw_n % Q][n]);
if (!rec_open[aw_n % Q] && rec_beats[aw_n % Q] != awlen + 1)
report("wlast", $sformatf("write %0d sent %0d beats before its AW said AWLEN=%0d", aw_n, rec_beats[aw_n % Q], awlen));
if (!rec_open[aw_n % Q]) rec_beats[aw_n % Q] = 0;
end
aw_n++;
end
// W beat accepted
if (wvalid === 1 && wready === 1) begin
if (w_beat == 0) begin wb_n++; rec_beats[(wb_n - 1) % Q] = 0; rec_open[(wb_n - 1) % Q] = 1; end
if (wb_n <= aw_n) begin
check_strobe(wb_n - 1, w_beat, wstrb);
if (wlast === 1 && w_beat != q_len[(wb_n - 1) % Q]) report("wlast", $sformatf("WLAST on beat %0d of write %0d, AWLEN=%0d", w_beat, wb_n - 1, q_len[(wb_n - 1) % Q]));
if (wlast !== 1 && w_beat == q_len[(wb_n - 1) % Q]) report("wlast", $sformatf("beat %0d of write %0d is AWLEN=%0d but WLAST is 0", w_beat, wb_n - 1, q_len[(wb_n - 1) % Q]));
end else begin
rec_strb[(wb_n - 1) % Q][w_beat] = wstrb;
end
rec_beats[(wb_n - 1) % Q] = w_beat + 1;
w_beat++;
if (wlast === 1) begin w_beat = 0; wb_done++; rec_open[(wb_n - 1) % Q] = 0; if (wb_n <= aw_n) rec_beats[(wb_n - 1) % Q] = 0; end
end
// B offered or accepted
if (bvalid === 1 && !p_bv) begin
if (b_n >= aw_n && b_n >= wb_done) report("border", "BVALID with no write to answer: neither an AW nor a WLAST is outstanding");
else if (b_n >= wb_done) report("border", "BVALID before the last beat of the write it answers");
else if (b_n >= aw_n) report("border", "BVALID before the AW of the write it answers was accepted");
end
if (bvalid === 1 && bready === 1) begin
if (aw_out[bid] == 0) report("bid", $sformatf("B with id %0d, which has no outstanding write", bid));
else begin
aw_out[bid]--; writes++;
if (bresp === EXOKAY && aw_excl[bid] == 0) report("exokay", $sformatf("EXOKAY for write id %0d, which was not exclusive", bid));
if (aw_excl[bid] > 0 && aw_excl[bid] > aw_out[bid]) aw_excl[bid] = aw_out[bid];
end
b_n++;
end
// AR accepted
if (arvalid === 1 && arready === 1) begin
check_request("AR", araddr, arlen, arsize, arburst, arlock);
rq_len[arid][rq_tail[arid]] = arlen; rq_tail[arid] = (rq_tail[arid] + 1) % 16;
ar_out[arid]++; if (arlock) ar_excl[arid]++;
end
// R beat accepted
if (rvalid === 1 && rready === 1) begin
if (ar_out[rid] == 0) report("rid", $sformatf("R with id %0d, which has no outstanding read", rid));
else begin
if (rlast === 1 && r_beat[rid] != rq_len[rid][rq_head[rid]])
report("rlast", $sformatf("RLAST on beat %0d of the oldest read with id %0d, whose ARLEN is %0d", r_beat[rid], rid, rq_len[rid][rq_head[rid]]));
if (rlast !== 1 && r_beat[rid] == rq_len[rid][rq_head[rid]])
report("rlast", $sformatf("beat %0d of the oldest read with id %0d is its ARLEN but RLAST is 0", r_beat[rid], rid));
if (rresp === EXOKAY && ar_excl[rid] == 0) report("exokay", $sformatf("EXOKAY for read id %0d, which was not exclusive", rid));
r_beat[rid]++;
if (rlast === 1) begin
r_beat[rid] = 0; rq_head[rid] = (rq_head[rid] + 1) % 16; ar_out[rid]--; reads++;
if (ar_excl[rid] > ar_out[rid]) ar_excl[rid] = ar_out[rid];
end
end
end
end
p_awv = (awvalid === 1); p_awr = (awready === 1); p_wv = (wvalid === 1); p_wr = (wready === 1);
p_bv = (bvalid === 1); p_br = (bready === 1); p_arv = (arvalid === 1); p_arr = (arready === 1);
p_rv = (rvalid === 1); p_rr = (rready === 1);
p_awaddr = awaddr; p_awlen = awlen; p_awsize = awsize; p_awburst = awburst; p_awid = awid; p_awlock = awlock;
p_wdata = wdata; p_wstrb = wstrb; p_wlast = wlast; p_bresp = bresp; p_bid = bid;
p_araddr = araddr; p_arlen = arlen; p_arsize = arsize; p_arburst = arburst; p_arid = arid; p_arlock = arlock;
p_rdata = rdata; p_rresp = rresp; p_rid = rid; p_rlast = rlast;
end
final $display("checker: %0d writes, %0d reads, %0d stalled edges, %0d rules broken", writes, reads, stalls, broken);
endmodule
Read included file: axi_tasks.svh
// axi_tasks.svh: the manager side of the bench, as tasks for the stimulus (read-only).
// Data for a write burst goes in wd[0], wd[1], ... (one entry per beat, the lowest byte at the
// lowest address); the data of a read comes back in rd[0], rd[1], ... with its responses in rr[].
// Every task prints the beat log: one line per handshake, with the edge where it happened.
// `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge aclk) cycle = cycle + 1;
logic [63:0] wd [0:255];
logic [63:0] rd [0:255];
logic [1:0] rr [0:255];
logic [63:0] rdq [0:15][0:255]; // read data per ID, for reads collected out of order
logic [1:0] rrq [0:15][0:255];
int rdn [0:15]; // beats received per ID
logic [1:0] last_bresp = 0;
int last_beats = 0;
// What the task is driving right now, for the waveform scenes: the address and lanes of the beat.
int unsigned beat_a = 0;
logic [7:0] beat_m = 0;
// quiet = 1 silences the beat log (a predict item can then ask for a dump line instead).
bit quiet = 0;
// Strobe override: when strb_force is 1, every write beat uses strb_val as it is.
bit strb_force = 0;
logic [7:0] strb_val = 8'hff;
// Outstanding read requests per ID, so the tasks know the lanes of the beats that come back.
int unsigned arq_addr [0:15][0:7];
int arq_len [0:15][0:7];
logic [2:0] arq_size [0:15][0:7];
logic [1:0] arq_burst [0:15][0:7];
int arq_head [0:15], arq_tail [0:15];
initial for (int i = 0; i < 16; i++) begin arq_head[i] = 0; arq_tail[i] = 0; rdn[i] = 0; end
// READY patterns for the response channels: bit (cycle % 16) of the pattern drives the signal.
bit rpat_on = 0, bpat_on = 0;
logic [15:0] rpat = 16'hffff, bpat = 16'hffff;
always @(posedge aclk) begin
#1;
if (rpat_on) rready = rpat[cycle % 16];
if (bpat_on) bready = bpat[cycle % 16];
end
// A text waveform for the lesson's trace blocks: with watch = 1, one line per rising edge with the
// values sampled at that edge (cycle k of a trace is what edge k sees).
bit watch = 0;
int wave_edge = -1;
always @(posedge aclk) begin
wave_edge = wave_edge + 1;
if (watch) $display("wave e%0d: aw=%b%b w=%b%b b=%b%b ar=%b%b r=%b%b awaddr=%h wdata=%h wstrb=%b wlast=%b bid=%h bresp=%b araddr=%h rid=%h rdata=%h rresp=%b rlast=%b",
wave_edge, awvalid, awready, wvalid, wready, bvalid, bready, arvalid, arready, rvalid, rready,
awaddr, wdata, wstrb, wlast, bid, bresp, araddr, rid, rdata, rresp, rlast);
end
// Drive RREADY (ch = 0) or BREADY (ch = 1) from a 16-bit pattern; 16'hffff restores always ready.
task automatic ready_pattern(input int ch, input logic [15:0] pat);
if (ch == 0) begin rpat = pat; rpat_on = 1; rready = pat[cycle % 16]; end
else begin bpat = pat; bpat_on = 1; bready = pat[cycle % 16]; end
endtask
// The write address channel: offer the request and hold it until it is taken.
task automatic aw_issue(input logic [ID_W-1:0] id, input int unsigned addr, input int len, input logic [2:0] size,
input logic [1:0] burst, input logic lock = 0);
awid = id; awaddr = addr; awlen = len[7:0]; awsize = size; awburst = burst; awlock = lock; awvalid = 1;
do @(posedge aclk); while (!awready);
#1 if (!quiet) $display("e%0d AW id=%0d addr=0x%0h len=%0d size=%0d %0s%0s", cycle, id, addr, len, size, burst_name(burst), lock ? " exclusive" : "");
awvalid = 0; awaddr = 'x; awlen = 'x; awsize = 'x; awburst = 'x; awid = 'x; awlock = 0;
endtask
// The write data channel: the beats of one burst from wd[first..], each placed in its lanes.
task automatic w_burst(input int unsigned addr, input int len, input logic [2:0] size, input logic [1:0] burst, input int first = 0);
logic [15:0] lanes;
for (int n = 0; n <= len; n++) begin
lanes = beat_lanes(addr, len, size, burst, n, BUS_BYTES);
beat_a = beat_addr(addr, len, size, burst, n);
beat_m = strb_force ? strb_val : beat_strb(addr, len, size, burst, n, BUS_BYTES);
wdata = wd[first + n] << (8 * lanes[7:0]);
wstrb = beat_m[BUS_BYTES-1:0];
wlast = (n == len);
wvalid = 1;
do @(posedge aclk); while (!wready);
#1 if (!quiet) $display("e%0d W beat %0d data=0x%0h strb=%b last=%0d addr 0x%0h lanes %0d-%0d", cycle, n, wdata, wstrb, wlast, beat_a, lanes[7:0], lanes[15:8]);
end
wvalid = 0; wdata = 'x; wstrb = 'x; wlast = 0; beat_m = 0;
endtask
// Wait for the write response of one transaction with this ID.
task automatic b_wait(input logic [ID_W-1:0] id);
do @(posedge aclk); while (!(bvalid && bready && bid == id));
last_bresp = bresp; // sampled at the edge, before the subordinate moves on
#1 if (!quiet) $display("e%0d B id=%0d %0s", cycle, id, resp_name(last_bresp));
endtask
// A complete write. order 0 offers the address, then the first beat one cycle later whether or
// not the address has been taken yet (a manager must never make WVALID wait for AWREADY); 1 offers
// the beats first and the address after beat 0; 2 offers the address and beat 0 in the same cycle.
// Returns after B, with the response in last_bresp.
task automatic axi_write(input logic [ID_W-1:0] id, input int unsigned addr, input int len, input logic [2:0] size,
input logic [1:0] burst, input int order = 0, input logic lock = 0);
case (order)
1: fork
w_burst(addr, len, size, burst);
begin @(posedge aclk); #1 aw_issue(id, addr, len, size, burst, lock); end
join
2: fork
aw_issue(id, addr, len, size, burst, lock);
w_burst(addr, len, size, burst);
join
default: fork
aw_issue(id, addr, len, size, burst, lock);
begin @(posedge aclk); #1 w_burst(addr, len, size, burst); end
join
endcase
b_wait(id);
endtask
// The read address channel: offer the request and remember it for the beats that come back.
task automatic ar_issue(input logic [ID_W-1:0] id, input int unsigned addr, input int len, input logic [2:0] size,
input logic [1:0] burst, input logic lock = 0);
arq_addr[id][arq_tail[id]] = addr; arq_len[id][arq_tail[id]] = len; arq_size[id][arq_tail[id]] = size;
arq_burst[id][arq_tail[id]] = burst; arq_tail[id] = (arq_tail[id] + 1) % 8;
arid = id; araddr = addr; arlen = len[7:0]; arsize = size; arburst = burst; arlock = lock; arvalid = 1;
do @(posedge aclk); while (!arready);
#1 if (!quiet) $display("e%0d AR id=%0d addr=0x%0h len=%0d size=%0d %0s%0s", cycle, id, addr, len, size, burst_name(burst), lock ? " exclusive" : "");
arvalid = 0; araddr = 'x; arlen = 'x; arsize = 'x; arburst = 'x; arid = 'x; arlock = 0;
endtask
// Collect read beats until n transactions have ended (their RLAST). Each beat lands in rdq[rid][k]
// with only the bytes of the transfer (lane-shifted down), and rd[] holds the last transaction.
task automatic r_collect(input int n);
int done = 0, h;
logic [15:0] lanes;
logic [63:0] bytes;
logic [ID_W-1:0] id;
logic [DATA_W-1:0] data;
logic [1:0] resp;
logic last;
while (done < n) begin
do @(posedge aclk); while (!(rvalid && rready));
id = rid; data = rdata; resp = rresp; last = rlast; // sampled at the edge, before the subordinate moves on
h = arq_head[id];
lanes = beat_lanes(arq_addr[id][h], arq_len[id][h], arq_size[id][h], arq_burst[id][h], rdn[id], BUS_BYTES);
beat_a = beat_addr(arq_addr[id][h], arq_len[id][h], arq_size[id][h], arq_burst[id][h], rdn[id]);
beat_m = beat_strb(arq_addr[id][h], arq_len[id][h], arq_size[id][h], arq_burst[id][h], rdn[id], BUS_BYTES);
bytes = (data >> (8 * lanes[7:0])) & ((64'h1 << (8 * (lanes[15:8] - lanes[7:0] + 1))) - 1);
rdq[id][rdn[id]] = bytes; rrq[id][rdn[id]] = resp;
#1 if (!quiet) $display("e%0d R id=%0d beat %0d data=0x%0h %0s%0s addr 0x%0h lanes %0d-%0d", cycle, id, rdn[id], bytes, resp_name(resp),
last ? " last" : "", beat_a, lanes[7:0], lanes[15:8]);
rdn[id] = rdn[id] + 1;
if (last) begin
for (int k = 0; k < rdn[id]; k++) begin rd[k] = rdq[id][k]; rr[k] = rrq[id][k]; end
last_beats = rdn[id]; rdn[id] = 0; arq_head[id] = (h + 1) % 8; done = done + 1;
end
end
beat_m = 0;
endtask
// A complete read: AR, then every beat, into rd[] and rr[].
task automatic axi_read(input logic [ID_W-1:0] id, input int unsigned addr, input int len, input logic [2:0] size,
input logic [1:0] burst, input logic lock = 0);
ar_issue(id, addr, len, size, burst, lock);
r_collect(1);
endtask
// The table of a burst before it runs: the address, lanes and strobe of every beat.
task automatic beat_table(input int unsigned addr, input int len, input logic [2:0] size, input logic [1:0] burst);
logic [15:0] lanes;
logic [63:0] s;
int unsigned a;
$display("burst at 0x%0h: %0d beat%0s of %0d byte%0s, %0s, bus %0d bytes", addr, len + 1, len ? "s" : "", 1 << size, size ? "s" : "",
burst_name(burst), BUS_BYTES);
$display(" beat address lanes strobe");
for (int n = 0; n <= len; n++) begin
lanes = beat_lanes(addr, len, size, burst, n, BUS_BYTES);
s = beat_strb(addr, len, size, burst, n, BUS_BYTES);
a = beat_addr(addr, len, size, burst, n);
$display(" %4d 0x%05h %0d-%0d %b", n, a[19:0], lanes[7:0], lanes[15:8], s[BUS_BYTES-1:0]);
end
if (crosses_4k(addr, len, size, burst)) $display(" this burst crosses a 4 KB boundary");
endtask
// One byte of the memory that owns an address (the fabric's second memory above 0x8000).
function automatic logic [7:0] mem_byte(input int unsigned a);
`ifdef AXI_MEM1
if (a >= 32'h8000) return `AXI_MEM1.mem[a];
`endif
return `AXI_MEM.mem[a];
endfunction
task automatic set_byte(input int unsigned a, input logic [7:0] v);
`ifdef AXI_MEM1
if (a >= 32'h8000) `AXI_MEM1.mem[a] = v;
else `AXI_MEM.mem[a] = v;
`else
`AXI_MEM.mem[a] = v;
`endif
endtask
// The memory, n bytes from addr, one bus word per group.
task automatic dump(input int unsigned addr, input int n);
string line;
int unsigned a = (addr / 16) * 16;
while (a < addr + n) begin
line = $sformatf("0x%05h ", a[19:0]);
for (int i = 0; i < 16; i++) begin
if (i % BUS_BYTES == 0) line = {line, " "};
line = {line, $sformatf(" %02h", mem_byte(a + i))};
end
$display("%s", line);
a = a + 16;
end
endtask
// Fill n bytes from addr with first, first+1, ... so untouched bytes stay recognisable.
task automatic mem_fill(input int unsigned addr, input int n, input logic [7:0] first = 8'h00);
for (int i = 0; i < n; i++) set_byte(addr + i, first + i[7:0]);
endtask
// Drive nothing for n cycles.
task automatic idle(input int n);
repeat (n) @(posedge aclk);
#1;
endtask
Read included file: axi_fabric.sv
`timescale 1ns/1ns
// axi_fabric.sv: a small interconnect (read-only in this lesson).
// One manager port in; behind an address decoder, two memories and a default subordinate:
// m0 answers 0x0000 to 0x7fff, m1 answers 0x8000 to 0xffff, any other address gets DECERR.
// The fabric appends one ID bit for the manager port (the memories see IDs one bit wider, with
// prefix 0, and the bit is removed from BID and RID on the way back), routes every write beat to
// the target of its address (a beat that arrives before its address waits at the fabric), and
// keeps same-ID responses in order by holding a request whose ID still has responses outstanding
// from a different target. SLICES register stages sit on every channel of the manager port; each
// adds one cycle in its direction. Switches, set by name from the stimulus (fab.trace = 1;):
// enforce_order 1: hold a request until its ID's responses from the other target are back
// trace print every routing decision
// The memories are fab.m0 and fab.m1 (fab.m1.rd_latency = 8; makes m1 the slow one).
module vr_slice #(parameter int W = 8, parameter int N = 1) (
input logic clk,
input logic rst_n,
input logic in_valid,
output logic in_ready,
input logic [W-1:0] in_data,
output logic out_valid,
input logic out_ready,
output logic [W-1:0] out_data
);
generate if (N == 0) begin : g_wire
assign out_valid = in_valid;
assign in_ready = out_ready;
assign out_data = in_data;
end else begin : g_pipe
logic [N:1] full;
logic [N+1:1] acc; // stage k can take an entry this cycle
logic [W-1:0] data [1:N];
always @* begin
acc[N+1] = out_ready;
for (int k = N; k >= 1; k--) acc[k] = !full[k] || acc[k+1];
end
assign in_ready = acc[1];
assign out_valid = full[N];
assign out_data = data[N];
always @(posedge clk) begin
if (!rst_n) full <= '0;
else for (int k = N; k >= 1; k--) if (acc[k]) begin
full[k] <= (k == 1) ? in_valid : full[k-1];
data[k] <= (k == 1) ? in_data : data[k-1];
end
end
end endgenerate
endmodule
module axi_fabric #(parameter int DATA_W = 32, parameter int ID_W = 4, parameter int SLICES = 0) (
input logic aclk,
input logic aresetn,
input logic [ID_W-1:0] awid,
input logic [31:0] awaddr,
input logic [7:0] awlen,
input logic [2:0] awsize,
input logic [1:0] awburst,
input logic awlock,
input logic [3:0] awcache,
input logic [2:0] awprot,
input logic awvalid,
output logic awready,
input logic [DATA_W-1:0] wdata,
input logic [DATA_W/8-1:0] wstrb,
input logic wlast,
input logic wvalid,
output logic wready,
output logic [ID_W-1:0] bid,
output logic [1:0] bresp,
output logic bvalid,
input logic bready,
input logic [ID_W-1:0] arid,
input logic [31:0] araddr,
input logic [7:0] arlen,
input logic [2:0] arsize,
input logic [1:0] arburst,
input logic arlock,
input logic [3:0] arcache,
input logic [2:0] arprot,
input logic arvalid,
output logic arready,
output logic [ID_W-1:0] rid,
output logic [DATA_W-1:0] rdata,
output logic [1:0] rresp,
output logic rlast,
output logic rvalid,
input logic rready
);
import axi_pkg::*;
localparam int BUS = DATA_W / 8;
localparam int unsigned M1_BASE = 32'h8000, TOP = 32'h1_0000;
localparam int AW_W = ID_W + 32 + 8 + 3 + 2 + 1, W_W = DATA_W + BUS + 1, B_W = ID_W + 2, R_W = ID_W + DATA_W + 2 + 1;
bit enforce_order = 1, trace = 0;
int cycle = -1;
// ---- the manager port after the register slices (a_* is what the fabric itself sees) ----
logic a_awvalid, a_awready, a_wvalid, a_wready, a_bvalid, a_bready, a_arvalid, a_arready, a_rvalid, a_rready;
logic [AW_W-1:0] a_aw, a_ar;
logic [W_W-1:0] a_w;
logic [B_W-1:0] a_b;
logic [R_W-1:0] a_r;
vr_slice #(.W(AW_W), .N(SLICES)) sl_aw (.clk(aclk), .rst_n(aresetn), .in_valid(awvalid), .in_ready(awready),
.in_data({awid, awaddr, awlen, awsize, awburst, awlock}), .out_valid(a_awvalid), .out_ready(a_awready), .out_data(a_aw));
vr_slice #(.W(W_W), .N(SLICES)) sl_w (.clk(aclk), .rst_n(aresetn), .in_valid(wvalid), .in_ready(wready),
.in_data({wdata, wstrb, wlast}), .out_valid(a_wvalid), .out_ready(a_wready), .out_data(a_w));
vr_slice #(.W(B_W), .N(SLICES)) sl_b (.clk(aclk), .rst_n(aresetn), .in_valid(a_bvalid), .in_ready(a_bready),
.in_data(a_b), .out_valid(bvalid), .out_ready(bready), .out_data({bid, bresp}));
vr_slice #(.W(AW_W), .N(SLICES)) sl_ar (.clk(aclk), .rst_n(aresetn), .in_valid(arvalid), .in_ready(arready),
.in_data({arid, araddr, arlen, arsize, arburst, arlock}), .out_valid(a_arvalid), .out_ready(a_arready), .out_data(a_ar));
vr_slice #(.W(R_W), .N(SLICES)) sl_r (.clk(aclk), .rst_n(aresetn), .in_valid(a_rvalid), .in_ready(a_rready),
.in_data(a_r), .out_valid(rvalid), .out_ready(rready), .out_data({rid, rdata, rresp, rlast}));
wire [ID_W-1:0] a_awid = a_aw[AW_W-1 -: ID_W], a_arid = a_ar[AW_W-1 -: ID_W];
wire [31:0] a_awaddr = a_aw[AW_W-ID_W-1 -: 32], a_araddr = a_ar[AW_W-ID_W-1 -: 32];
wire a_wlast = a_w[0];
// ---- the two memories, with IDs one bit wider ----
logic [ID_W:0] s0_awid, s0_bid, s0_arid, s0_rid, s1_awid, s1_bid, s1_arid, s1_rid;
logic [31:0] s0_awaddr, s0_araddr, s1_awaddr, s1_araddr;
logic [7:0] s0_awlen, s0_arlen, s1_awlen, s1_arlen;
logic [2:0] s0_awsize, s0_arsize, s1_awsize, s1_arsize;
logic [1:0] s0_awburst, s0_arburst, s0_bresp, s0_rresp, s1_awburst, s1_arburst, s1_bresp, s1_rresp;
logic s0_awlock, s0_arlock, s1_awlock, s1_arlock;
logic s0_awvalid, s0_awready, s0_wvalid, s0_wready, s0_wlast, s0_bvalid, s0_bready, s0_arvalid, s0_arready, s0_rvalid, s0_rready, s0_rlast;
logic s1_awvalid, s1_awready, s1_wvalid, s1_wready, s1_wlast, s1_bvalid, s1_bready, s1_arvalid, s1_arready, s1_rvalid, s1_rready, s1_rlast;
logic [DATA_W-1:0] s0_wdata, s0_rdata, s1_wdata, s1_rdata;
logic [BUS-1:0] s0_wstrb, s1_wstrb;
axi_mem #(.DATA_W(DATA_W), .ID_W(ID_W + 1)) m0 (.aclk(aclk), .aresetn(aresetn),
.awid(s0_awid), .awaddr(s0_awaddr), .awlen(s0_awlen), .awsize(s0_awsize), .awburst(s0_awburst), .awlock(s0_awlock), .awcache(4'b0), .awprot(3'b0), .awvalid(s0_awvalid), .awready(s0_awready),
.wdata(s0_wdata), .wstrb(s0_wstrb), .wlast(s0_wlast), .wvalid(s0_wvalid), .wready(s0_wready),
.bid(s0_bid), .bresp(s0_bresp), .bvalid(s0_bvalid), .bready(s0_bready),
.arid(s0_arid), .araddr(s0_araddr), .arlen(s0_arlen), .arsize(s0_arsize), .arburst(s0_arburst), .arlock(s0_arlock), .arcache(4'b0), .arprot(3'b0), .arvalid(s0_arvalid), .arready(s0_arready),
.rid(s0_rid), .rdata(s0_rdata), .rresp(s0_rresp), .rlast(s0_rlast), .rvalid(s0_rvalid), .rready(s0_rready));
axi_mem #(.DATA_W(DATA_W), .ID_W(ID_W + 1)) m1 (.aclk(aclk), .aresetn(aresetn),
.awid(s1_awid), .awaddr(s1_awaddr), .awlen(s1_awlen), .awsize(s1_awsize), .awburst(s1_awburst), .awlock(s1_awlock), .awcache(4'b0), .awprot(3'b0), .awvalid(s1_awvalid), .awready(s1_awready),
.wdata(s1_wdata), .wstrb(s1_wstrb), .wlast(s1_wlast), .wvalid(s1_wvalid), .wready(s1_wready),
.bid(s1_bid), .bresp(s1_bresp), .bvalid(s1_bvalid), .bready(s1_bready),
.arid(s1_arid), .araddr(s1_araddr), .arlen(s1_arlen), .arsize(s1_arsize), .arburst(s1_arburst), .arlock(s1_arlock), .arcache(4'b0), .arprot(3'b0), .arvalid(s1_arvalid), .arready(s1_arready),
.rid(s1_rid), .rdata(s1_rdata), .rresp(s1_rresp), .rlast(s1_rlast), .rvalid(s1_rvalid), .rready(s1_rready));
// ---- the decoder and the ordering bookkeeping ----
function automatic int decode(input int unsigned a);
return (a >= TOP) ? 2 : (a >= M1_BASE) ? 1 : 0;
endfunction
int wid_t [0:(1<<ID_W)-1], wid_n [0:(1<<ID_W)-1]; // target and count of outstanding writes per ID
int rid_t [0:(1<<ID_W)-1], rid_n [0:(1<<ID_W)-1]; // the same for reads
initial for (int i = 0; i < (1 << ID_W); i++) begin wid_n[i] = 0; rid_n[i] = 0; wid_t[i] = 0; rid_t[i] = 0; end
// Registered copies of the state: the readies the manager and the targets sample are computed
// from these, so they change only at the edge, never in the middle of a cycle.
int wid_t_r [0:(1<<ID_W)-1], wid_n_r [0:(1<<ID_W)-1], rid_t_r [0:(1<<ID_W)-1], rid_n_r [0:(1<<ID_W)-1];
initial for (int i = 0; i < (1 << ID_W); i++) begin wid_n_r[i] = 0; rid_n_r[i] = 0; wid_t_r[i] = 0; rid_t_r[i] = 0; end
logic paw_v_r = 0, par_v_r = 0, pw_v_r = 0, pb_v_r = 0, pr_v_r = 0, d_aw_v_r = 0, d_b_v_r = 0, d_ar_v_r = 0;
int paw_t_r = 0, par_t_r = 0, pw_t_r = 0, r_sel_r = -1, b_rr_r = 0, wdir_n_r = 0;
function automatic bit w_ok(input logic [ID_W-1:0] id, input int t);
return !enforce_order || wid_n_r[id] == 0 || wid_t_r[id] == t;
endfunction
function automatic bit r_ok(input logic [ID_W-1:0] id, input int t);
return !enforce_order || rid_n_r[id] == 0 || rid_t_r[id] == t;
endfunction
// ---- one request register per direction (the fabric's own cycle), the W route queue ----
logic paw_v = 0, par_v = 0, pw_v = 0;
int paw_t = 0, par_t = 0, pw_t = 0;
logic [AW_W-1:0] paw, par;
logic [W_W-1:0] pw;
int wdir_q [0:15];
int wdir_head = 0, wdir_tail = 0, wdir_n = 0;
// the default subordinate: one write and one read at a time
logic d_aw_v = 0, d_b_v = 0, d_ar_v = 0;
logic [ID_W-1:0] d_aw_id, d_b_id, d_ar_id;
int d_ar_len, d_beat;
// responses presented to the manager port
logic pb_v = 0, pr_v = 0;
logic [B_W-1:0] pb;
logic [R_W-1:0] pr;
int r_sel = -1, b_rr = 0;
// The target of the request register accepts it: this cycle it may be replaced.
wire aw_done = !paw_v_r || (paw_t_r == 0 ? s0_awready : paw_t_r == 1 ? s1_awready : 1'b1);
wire ar_done = !par_v_r || (par_t_r == 0 ? s0_arready : par_t_r == 1 ? s1_arready : 1'b1);
wire w_done = !pw_v_r || (pw_t_r == 0 ? s0_wready : pw_t_r == 1 ? s1_wready : 1'b1);
// The request readies read the per-ID counts, so they are recomputed every cycle (order_ver) and
// whenever the request or the target's ready changes.
int order_ver = 0;
initial begin a_awready = 0; a_arready = 0; end
always @(a_awvalid, a_aw, a_awid, a_awaddr, aw_done, d_aw_v_r, d_b_v_r, wdir_n_r, order_ver, enforce_order)
a_awready = aw_done && w_ok(a_awid, decode(a_awaddr)) && (decode(a_awaddr) != 2 || (!d_aw_v_r && !d_b_v_r)) && wdir_n_r < 15;
always @(a_arvalid, a_ar, a_arid, a_araddr, ar_done, d_ar_v_r, order_ver, enforce_order)
a_arready = ar_done && r_ok(a_arid, decode(a_araddr)) && (decode(a_araddr) != 2 || !d_ar_v_r);
assign a_wready = w_done && wdir_n_r > 0;
// Responses: take a target's response when the manager-side register is free or being emptied now.
wire b_free = !pb_v_r || a_bready;
wire r_free = !pr_v_r || a_rready;
assign s0_bready = b_free && s0_bvalid && (b_rr_r == 0 || !s1_bvalid);
assign s1_bready = b_free && s1_bvalid && (b_rr_r == 1 || !s0_bvalid);
wire d_b_take = b_free && d_b_v_r && !s0_bvalid && !s1_bvalid;
int r_pick;
always @* begin
if (r_sel_r >= 0) r_pick = r_sel_r;
else r_pick = s0_rvalid ? 0 : s1_rvalid ? 1 : d_ar_v_r ? 2 : -1;
end
assign s0_rready = r_free && (r_pick == 0);
assign s1_rready = r_free && (r_pick == 1);
wire d_r_take = r_free && (r_pick == 2);
always @(posedge aclk) begin
cycle = cycle + 1;
if (!aresetn) begin
paw_v = 0; par_v = 0; pw_v = 0; pb_v = 0; pr_v = 0; d_aw_v = 0; d_b_v = 0; d_ar_v = 0; r_sel = -1; wdir_n = 0; wdir_head = 0; wdir_tail = 0;
for (int i = 0; i < (1 << ID_W); i++) begin wid_n[i] = 0; rid_n[i] = 0; end
end else begin
// -- requests the targets accept this edge free their register --
if (paw_v && (paw_t == 0 ? s0_awready : paw_t == 1 ? s1_awready : 1'b1)) paw_v = 0;
if (par_v && (par_t == 0 ? s0_arready : par_t == 1 ? s1_arready : 1'b1)) par_v = 0;
if (pw_v && (pw_t == 0 ? s0_wready : pw_t == 1 ? s1_wready : 1'b1)) begin
pw_v = 0;
if (pw_t == 2 && pw[0]) begin d_b_v = 1; d_b_id = d_aw_id; d_aw_v = 0; end
end
// -- a new AW from the manager --
if (a_awvalid && a_awready) begin
paw_v = 1; paw = a_aw; paw_t = decode(a_awaddr);
wid_t[a_awid] = paw_t; wid_n[a_awid] = wid_n[a_awid] + 1;
wdir_q[wdir_tail] = paw_t; wdir_tail = (wdir_tail + 1) % 16; wdir_n = wdir_n + 1;
if (paw_t == 2) begin d_aw_v = 1; d_aw_id = a_awid; end
if (trace) $display("fab e%0d: AW id=%0d addr=0x%0h -> %0s as id %0d", cycle, a_awid, a_awaddr,
paw_t == 2 ? "DECERR" : paw_t ? "m1" : "m0", {1'b0, a_awid});
end
// -- a new AR from the manager --
if (a_arvalid && a_arready) begin
par_v = 1; par = a_ar; par_t = decode(a_araddr);
rid_t[a_arid] = par_t; rid_n[a_arid] = rid_n[a_arid] + 1;
if (par_t == 2) begin d_ar_v = 1; d_ar_id = a_arid; d_ar_len = a_ar[AW_W-ID_W-32-1 -: 8]; d_beat = 0; end
if (trace) $display("fab e%0d: AR id=%0d addr=0x%0h -> %0s as id %0d", cycle, a_arid, a_araddr,
par_t == 2 ? "DECERR" : par_t ? "m1" : "m0", {1'b0, a_arid});
end else if (a_arvalid && trace && enforce_order && !r_ok(a_arid, decode(a_araddr)))
$display("fab e%0d: AR id=%0d addr=0x%0h held: %0d read%0s of id %0d still outstanding at %0s", cycle, a_arid, a_araddr,
rid_n[a_arid], rid_n[a_arid] == 1 ? "" : "s", a_arid, rid_t[a_arid] ? "m1" : "m0");
// -- a new W beat from the manager, routed to the oldest unfinished write --
if (a_wvalid && a_wready) begin
pw_v = 1; pw = a_w; pw_t = wdir_q[wdir_head];
if (a_wlast) begin wdir_head = (wdir_head + 1) % 16; wdir_n = wdir_n - 1; end
end
// -- B: take one from a target, hand it to the manager --
if (pb_v && a_bready) begin pb_v = 0; wid_n[pb[B_W-1 -: ID_W]] = wid_n[pb[B_W-1 -: ID_W]] - 1; end
if (s0_bvalid && s0_bready) begin pb_v = 1; pb = {s0_bid[ID_W-1:0], s0_bresp}; b_rr = 1; end
else if (s1_bvalid && s1_bready) begin pb_v = 1; pb = {s1_bid[ID_W-1:0], s1_bresp}; b_rr = 0; end
else if (d_b_take) begin pb_v = 1; pb = {d_b_id, DECERR}; d_b_v = 0; end
if (trace && pb_v && !a_bvalid) $display("fab e%0d: B id=%0d from %0s", cycle, pb[B_W-1 -: ID_W], s0_bvalid && s0_bready ? "m0" : s1_bvalid && s1_bready ? "m1" : "the default subordinate");
// -- R: take beats from the selected target until its RLAST --
if (pr_v && a_rready) begin
pr_v = 0;
if (pr[0]) rid_n[pr[R_W-1 -: ID_W]] = rid_n[pr[R_W-1 -: ID_W]] - 1;
end
if (r_pick == 0 && s0_rvalid && s0_rready) begin
pr_v = 1; pr = {s0_rid[ID_W-1:0], s0_rdata, s0_rresp, s0_rlast}; r_sel = s0_rlast ? -1 : 0;
if (trace) $display("fab e%0d: R id=%0d from m0%0s", cycle, s0_rid[ID_W-1:0], s0_rlast ? " (last)" : "");
end else if (r_pick == 1 && s1_rvalid && s1_rready) begin
pr_v = 1; pr = {s1_rid[ID_W-1:0], s1_rdata, s1_rresp, s1_rlast}; r_sel = s1_rlast ? -1 : 1;
if (trace) $display("fab e%0d: R id=%0d from m1%0s", cycle, s1_rid[ID_W-1:0], s1_rlast ? " (last)" : "");
end
else if (d_r_take) begin
pr_v = 1; pr = {d_ar_id, {DATA_W{1'b0}}, DECERR, (d_beat == d_ar_len)};
if (d_beat == d_ar_len) begin d_ar_v = 0; r_sel = -1; end else begin d_beat = d_beat + 1; r_sel = 2; end
end
end
// -- the registered copies the readies are computed from --
paw_v_r <= paw_v; par_v_r <= par_v; pw_v_r <= pw_v; pb_v_r <= pb_v; pr_v_r <= pr_v;
paw_t_r <= paw_t; par_t_r <= par_t; pw_t_r <= pw_t; r_sel_r <= r_sel; b_rr_r <= b_rr; wdir_n_r <= wdir_n;
d_aw_v_r <= d_aw_v; d_b_v_r <= d_b_v; d_ar_v_r <= d_ar_v;
for (int i = 0; i < (1 << ID_W); i++) begin wid_n_r[i] <= wid_n[i]; wid_t_r[i] <= wid_t[i]; rid_n_r[i] <= rid_n[i]; rid_t_r[i] <= rid_t[i]; end
order_ver <= order_ver + 1;
// -- drive the targets and the manager port from the registers --
s0_awvalid <= paw_v && paw_t == 0; s1_awvalid <= paw_v && paw_t == 1;
{s0_awid, s0_awaddr, s0_awlen, s0_awsize, s0_awburst, s0_awlock} <= {1'b0, paw};
{s1_awid, s1_awaddr, s1_awlen, s1_awsize, s1_awburst, s1_awlock} <= {1'b0, paw};
s0_arvalid <= par_v && par_t == 0; s1_arvalid <= par_v && par_t == 1;
{s0_arid, s0_araddr, s0_arlen, s0_arsize, s0_arburst, s0_arlock} <= {1'b0, par};
{s1_arid, s1_araddr, s1_arlen, s1_arsize, s1_arburst, s1_arlock} <= {1'b0, par};
s0_wvalid <= pw_v && pw_t == 0; s1_wvalid <= pw_v && pw_t == 1;
{s0_wdata, s0_wstrb, s0_wlast} <= pw;
{s1_wdata, s1_wstrb, s1_wlast} <= pw;
a_bvalid <= pb_v; a_b <= pb;
a_rvalid <= pr_v; a_r <= pr;
end
endmodule
Check yourself
Remember
- A rule checker answers "did every handshake follow the rules?" from one port; a scoreboard answers "did every byte arrive once, in order, unchanged?" from the data. A bench needs both.
- Same-ID responses must return in request order whoever answers them; when the lengths are equal the wires stay legal and only a scoreboard keyed by ID sees the swap.
- A fabric re-registers what it forwards, so a subordinate's stability bug is invisible at the manager port. Bind a checker where the fault can happen.
- Ignored strobes, a WRAP walked like INCR and a reordered response leave no trace on the wires. Read back what you wrote.
- A VALID that waits for a READY cannot be reported by any rule on the wires. It deadlocks against a subordinate that waits for the VALID, and only a watchdog sees it.
Practice
- Catch every broken subordinate Verify · Hard
- Break the subordinates Break it · Medium
- Break the managers Break it · Medium
- The beat that landed in the wrong word Debug · Medium
- The subordinate that reordered Fix · Medium
- A beat-to-transaction monitor Implement · Hard