An AXI agent, monitor and scoreboard
Drive five channels at once from one driver, rebuild transactions from beats that arrive out of order and interleaved, and check the data against the memory the bench expects.
- Lesson
- P2.17
- Time
- 40 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A UVM agent for AXI passed a thousand-item regression. Its driver sent AW, waited for AWREADY, then sent the beats, then waited for B, then asked for the next item: one transaction on the bus at a time, forever. Then the same memory controller met a real core that keeps six reads in flight with three IDs. The monitor attached every read beat to the most recent AR, filed the beats of three transactions into one, and the scoreboard reported a corrupt memory that was fine.
Each slot row is the oldest open read of one ID. A beat with RLAST completes it; a full row empties into a finished transaction.
- Cycle 2
Two beats of id 2, then a beat of id 3. The read of id 2 is still open; the monitor has three transactions filling at once.
- Cycle 5
The beat of id 3 carries RLAST: the 2-beat read of 0x108 is complete and is published first, although it was requested last.
- Cycle 6
Id 1 completes next with b0 b1: the slow read of 0x4000. Id 2's four beats are still arriving between the others.
- Cycle 9
The last two beats finish id 2 and then id 1's second read. Four transactions out of ten interleaved beats, none of them confused.
Measured from the lesson's bench: read beats 205 ns to 295 ns. The rule is one line: a read beat belongs to the oldest open read with its RID.
One process per channel
An AXI driver has five channels to serve and they do not take turns. The driver on this page runs six processes from run_phase. A dispatcher takes items from the sequencer; one process per request channel (AW, W, AR) drives whatever its queue holds; one per response channel (B, R) watches the handshakes. A write item goes to the AW and W queues at once, so its address and its first beat can be on the bus in the same cycle. A read item goes to the AR queue.
- get()
The dispatcher takes the item with
get(), which returns the sequence at once, and pushes it to the AW and W queues. Nothing on the bus has happened yet.Follow the calls, values and waits
- data write id 1: Sequence โ Dispatcher
- Two channels at once
The AW process and the W process each pop the item and drive their channel. The address and beat 0 handshake in the same cycle; the next item can follow on AW while the beats are still going.
Follow the calls, values and waits
- call write id 1: AW process โ axi_mem
- data write id 1: W process โ axi_mem
- The response by ID
The B process sees BVALID with BID 1, pops the oldest write of id 1 from its queue, stores BRESP and hands the item back with
put(). The sequence'sget_responsematches it by transaction id.Follow the calls, values and waits
- return B OKAY: axi_mem โ B process
- return B OKAY: B process โ Sequence
- The second witness
The monitor never talks to the driver. It rebuilds the same write from the wires and publishes it; the scoreboard updates the memory it expects, and compares every later read against it.
Follow the calls, values and waits
- data write id 1: axi_mem โ Monitor
- data write id 1: Monitor โ Scoreboard
Read the diagram as text
- Sequence: finish_item returns at get()
- Dispatcher: get(); push to the channel queues
- AW process: drives the oldest queued request
- W process: drives the beats, WLAST on the last
- axi_mem: the DUT, reorder on
- B process: matches BID to the oldest write of that ID; put()
- Monitor: rebuilds transactions from the wires
- Scoreboard: the memory it expects
- Sequence โ Dispatcher: item
- Dispatcher โ AW process: AW queue
- Dispatcher โ W process: W queue
- AW process โ axi_mem: AW
- W process โ axi_mem: W beats
- axi_mem โ B process: B
- B process โ Sequence: response
- axi_mem โ Monitor: the wires
- Monitor โ Scoreboard: transaction
The driver pipelines because get() does not wait for the bus; the responses find their way back because every queue is keyed by ID.
Two things make this driver different from the single-transfer driver of U6.1. It calls get() and put() rather than get_next_item() and item_done(), so finish_item returns as soon as the driver has the item and the sequence collects responses on a second thread. And it keeps a queue of outstanding writes and one of outstanding reads per ID, the only way to match a response to its request on this bus.
The monitor rebuilds transactions by ID
The monitor watches the five handshakes and nothing else. A W beat belongs to the oldest write whose address has arrived and whose data has not finished. AXI4 write data follows the order of the addresses and is never interleaved. A read beat belongs to the oldest open read with its RID. Same-ID data returns in order and different IDs may interleave. Two rules, two kinds of queue, one in-order list for writes and one list per ID for reads.
| ns | RID | RDATA | RLAST | Filed under | Published |
|---|---|---|---|---|---|
| 205 | 2 | a0 | 0 | read id 2 of 0x100, beat 0 | |
| 215 | 2 | a1 | 0 | read id 2 of 0x100, beat 1 | |
| 225 | 3 | a2 | 0 | read id 3 of 0x108, beat 0 | |
| 235 | 1 | b0 | 0 | read id 1 of 0x4000, beat 0 | |
| 245 | 2 | a2 | 0 | read id 2 of 0x100, beat 2 | |
| 255 | 3 | a3 | 1 | read id 3 of 0x108, beat 1 | read id=3 addr=0x108 beats=2 data a2 a3 |
| 265 | 1 | b1 | 1 | read id 1 of 0x4000, beat 1 | read id=1 addr=0x4000 beats=2 data b0 b1 |
| 275 | 1 | a0 | 0 | read id 1 of 0x100, beat 0 (the next open read of id 1) | |
| 285 | 2 | a3 | 1 | read id 2 of 0x100, beat 3 | read id=2 addr=0x100 beats=4 data a0 a1 a2 a3 |
| 295 | 1 | a1 | 1 | read id 1 of 0x100, beat 1 | read id=1 addr=0x100 beats=2 data a0 a1 |
The ten read beats of the example, measured. The requests were issued in the order id 1 (0x4000), id 2, id 1 (0x100), id 3; the transactions complete in the order 3, 1, 2, 1.
The memory under test was told to reorder between IDs and to interleave, and the slow region makes id 1's first read the longest. Read the log beside the table. The request order and the completion order differ, the beats of three transactions alternate, and every transaction the monitor publishes carries exactly its own beats. A monitor that attached beats to the most recent AR, the hook's bug, would have given id 3 the beat b0.
The scoreboard keeps the memory, and waits
The scoreboard is a uvm_subscriber on the monitor's analysis port with a memory of its own, an associative array of words. A write transaction updates it; a read transaction is compared with it beat by beat, and every difference is a mismatch with the address and both values in the message. Nothing in it knows about IDs or order. By the time a transaction reaches it, the monitor has already paired beats with requests, which is the ID-keyed pairing U8.2 builds for a tagged memory.
One decision in the sequence matters more than any line of the scoreboard: the reads are sent only after both write responses are in. AXI gives no ordering between a read and a write, even with the same ID. A read issued before the write's B may legally return the old or the new data. The scoreboard learns of the write only from its B, so it would compare against the old memory. The first version of this bench did that and reported two mismatches on a correct memory. Waiting for B is the manager's side of the ordering model of P2.12.
Asked in interviews: UVM agents for AXI
How do you structure an AXI driver?
One process per channel, with queues between them. A dispatcher takes items; the request processes drive AW, W and AR from their queues; the response processes match B and R to the outstanding items by ID. The driver uses get() and put() instead of get_next_item() and item_done(), so the sequence can keep several items in flight.
How does the monitor know which transaction a beat belongs to?
A W beat belongs to the oldest write whose AW has arrived and whose data is unfinished, because write data follows address order. A read beat belongs to the oldest open read with its RID, because same-ID data is in order and different IDs may interleave.
What does an AXI scoreboard key on?
On the address, with its own model of the memory, once the monitor has paired beats with requests by ID. A scoreboard that keys on arrival order breaks the first time two IDs reorder.
Why did your scoreboard report mismatches on a correct DUT?
Usually because a read was issued before the write it depends on completed. The protocol orders neither reads against writes nor different IDs, so the sequence must wait for B before a dependent read, or the scoreboard must model that freedom.
Where do IDs come from in the sequence?
From the item: an ID is a stream, so a sequence that wants parallelism uses several IDs and keeps same-ID requests that must stay ordered on one ID. The driver and the monitor never invent IDs; they only match on them.
Try it
Read the log by time. The two writes are on the bus together (their responses come at 95 ns and 155 ns, the second from the slow region), then four reads with three IDs go out in the same cycle. The R beat lines show the interleaving; the read lines show the monitor publishing each transaction at its RLAST, in the order 3, 1, 2, 1, while the requests went out 1, 2, 1, 3.
Then break it. Move send_reads() above collect(2) and watch the scoreboard report mismatches on a correct memory. Or make the monitor file read beats under the most recent AR instead of the oldest open read of its RID, and watch id 3 collect b0.
0 ns sequence sent 1: write id=1 addr=0x100 beats=4 0 ns sequence sent 2: write id=2 addr=0x4000 beats=2 95 ns monitor write id=1 addr=0x100 beats=4 data a0 a1 a2 a3 OKAY 95 ns sequence response 1: write id=1 addr=0x100 beats=4 data a0 a1 a2 a3 OKAY 155 ns monitor write id=2 addr=0x4000 beats=2 data b0 b1 OKAY 155 ns sequence response 2: write id=2 addr=0x4000 beats=2 data b0 b1 OKAY 155 ns sequence sent 3: read id=1 addr=0x4000 beats=2 155 ns sequence sent 4: read id=2 addr=0x100 beats=4 155 ns sequence sent 5: read id=1 addr=0x100 beats=2 155 ns sequence sent 6: read id=3 addr=0x108 beats=2 205 ns monitor R beat rid=2 data=a0 225 ns monitor R beat rid=3 data=a2 235 ns monitor R beat rid=1 data=b0 255 ns monitor read id=3 addr=0x108 beats=2 data a2 a3 OKAY 255 ns sequence response 3: read id=3 addr=0x108 beats=2 data a2 a3 OKAY 265 ns monitor read id=1 addr=0x4000 beats=2 data b0 b1 OKAY 285 ns monitor read id=2 addr=0x100 beats=4 data a0 a1 a2 a3 OKAY 295 ns monitor read id=1 addr=0x100 beats=2 data a0 a1 OKAY 295 ns sequence response 6: read id=1 addr=0x100 beats=2 data a0 a1 OKAY 335 ns scoreboard 2 writes, 4 reads, 0 mismatches UVM_ERROR : 0Read 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
Check yourself
Remember
- An AXI driver is one process per channel with queues between them;
get()andput()let the sequence keep several items in flight. - A response finds its request by ID: a queue of outstanding writes and a queue of outstanding reads per ID, in issue order.
- The monitor files a W beat under the oldest unfinished write and a read beat under the oldest open read of its RID; that rule survives reordering and interleaving.
- The scoreboard keeps the memory it expects and compares every read beat against it, after the monitor has paired beats with requests.
- A sequence that needs a read to see its write waits for the write's B first; a scoreboard cannot be more right than the ordering its stimulus respects.
Practice
- The AXI monitor Implement ยท Hard