The AXI-to-APB bridge
Follow an AXI burst through a bridge into APB transfers: one per beat, strobes carried across, errors mapped back, unmapped addresses refused, and every cycle counted.
- Lesson
- P3.7
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A bridge was written for 32-bit registers that are only ever written whole. It drove PSTRB to 1111 on every write and ignored WSTRB. Then a CPU did a byte store to the top byte of a GPIO word, and the three bytes below it, other pins' settings, became zero. The APB side looked perfect: one clean write, OKAY.
br.drop_strb = 1: the word at 0x10 held 11 11 11 11; the AXI write carries one byte, ee, at 0x13. Cycle 0 is the W handshake at edge 9.- Cycle 0
The AXI beat arrives with
wstrb1000: only lane 3, the byte at 0x13, is valid. - Cycle 1
The bridge starts the APB write with
pstrb1111. Lanes 0 to 2 ofpwdataare zero. - Cycle 2
The write completes. Memory at 0x10 is now 00 00 00 ee.
- Cycle 3
BRESP OKAY. Neither side broke a rule; the bytes are simply gone.
Measured dump: 0x00010 00 00 00 ee. With the strobes carried across it reads 11 11 11 ee.
Two protocols, one transfer at a time
APB peripherals are usually reached through a bridge: an AXI subordinate on one side and the APB requester on the other. The bench's bridge takes one AXI transaction at a time and turns every beat into one APB transfer. The beat's address, from the burst arithmetic of P2.6, is rounded down to the word, and the beat's WSTRB becomes PSTRB. When a read and a write wait at the same edge, this bridge serves the read first. That is its own choice, and the AXI ordering model of P2.12 allows it.
Pills: an AXI beat taken by the bridge, then the same data written on APB.
- Cycle 0
AW is taken: a 2-beat write of words at 0x10.
- Cycle 1
Beat 0 is taken from W. The bridge has room for one beat only.
- Cycle 2
APB setup for 0x10 with 11111111. Beat 1 is already offered on W and must wait:
wreadyis 0. - Cycle 3
APB access with
pready1: the first word is written. - Cycle 4
Now the bridge takes beat 1.
- Cycle 5
Setup for 0x14 with 22222222.
- Cycle 6
The second word is written. That was the last beat.
- Cycle 7
B is sent with OKAY: one response for the whole AXI write.
Three edges per beat here: take the W beat, APB setup, APB access. The AXI side could have moved a beat on every edge.
Reads have the same shape. Measured for a 2-beat read of 0x110: AR at edge 3, the APB transfers at edges 4 to 5 and 7 to 8, R beats at edges 6 and 9. The bridge holds back the next request only while it is busy. The throughput is set by APB: at best one word every two cycles, plus the bridge's own cycle per beat.
Strobes and narrow beats
A narrow AXI beat uses some lanes of the bus. The bridge sends the aligned word address and the beat's strobes, so the completer writes exactly those bytes. A byte write to 0x13 becomes PADDR 0x10 with PSTRB 1000, and the dump shows the three neighbours untouched. A narrow read is the same in reverse: a 4-beat read of half-words from 0x20 makes four APB reads at 0x20, 0x20, 0x24 and 0x24. Each returns the whole word, and the AXI manager takes the lanes of its beat.
e14 R id=1 beat 0 data=0x2211 OKAY addr 0x20 lanes 0-1
e17 R id=1 beat 1 data=0x4433 OKAY addr 0x22 lanes 2-3
e20 R id=1 beat 2 data=0x6655 OKAY addr 0x24 lanes 0-1
e23 R id=1 beat 3 data=0x8877 OKAY last addr 0x26 lanes 2-3Reading a word twice is harmless for RAM. For a register whose read has a side effect, a FIFO pop for instance, it is not, so drivers access such registers with full-width single transfers.
Errors in both directions
The APB specification maps PSLVERR to BRESP for writes and RRESP for reads. AXI gives a write one response and a read one response per beat, so the bridge must summarise a write: this one answers SLVERR when any beat failed. Addresses with no APB completer behind the bridge never reach APB at all: the bridge decodes them first and answers DECERR, the AXI code for "no subordinate here".
| AXI request | APB side | AXI response (measured) |
|---|---|---|
| Write 2 beats at 0x100: CTRL, then read-only STATUS | OKAY, then PSLVERR | BRESP SLVERR, and CTRL holds 5a |
| Read 2 beats at 0x110: ID, then an unmapped offset | OKAY, then PSLVERR | RRESP OKAY, SLVERR |
| Read 1 beat at 0x3000 | no transfer | RRESP DECERR |
The first row is the P3.4 rule again: an error response does not undo the beats that succeeded.
Asked in interviews: AXI-to-APB bridges
What does a 4-beat AXI burst become on APB?
Four APB transfers, one per beat, each with setup and access. APB has no bursts.
How is a byte write carried across?
PADDR is the aligned word address and PSTRB is the beat's WSTRB, so only the addressed byte changes.
How is PSLVERR reported to AXI?
As SLVERR: in the RRESP of the failing read beat, or in the single BRESP of the write.
What does the bridge answer for an address with no APB completer?
DECERR, decided before any APB transfer starts.
Why is the bridge slow?
APB takes at least two cycles per transfer and one transfer at a time; a simple bridge also spends a cycle per beat on the AXI side.
Try it
The AXI beat log is the P2 bench's; the APB side has its own checker, which prints the second summary. Seven APB transfers serve five AXI transactions: two for the first write, one for the byte, two for the failing write, two for the failing read, none for 0x3000. The four AXI stalled edges are W beats waiting while the bridge was busy on APB.
Set apb_watch = 1; (and watch = 1; for the AXI side) to print every edge of both buses. Add br.drop_strb = 1; as the first line to reproduce the opening bug in the dump.
e3 AW id=1 addr=0x10 len=1 size=2 INCR e4 W beat 0 data=0x11111111 strb=1111 last=0 addr 0x10 lanes 0-3 e7 W beat 1 data=0x22222222 strb=1111 last=1 addr 0x14 lanes 0-3 e10 B id=1 OKAY e11 AW id=1 addr=0x13 len=0 size=0 INCR e12 W beat 0 data=0xee000000 strb=1000 last=1 addr 0x13 lanes 3-3 e15 B id=1 OKAY 0x00010 11 11 11 ee 22 22 22 22 00 00 00 00 00 00 00 00 e16 AW id=2 addr=0x100 len=1 size=2 INCR e17 W beat 0 data=0x5a strb=1111 last=0 addr 0x100 lanes 0-3 e20 W beat 1 data=0x1 strb=1111 last=1 addr 0x104 lanes 0-3 e23 B id=2 SLVERR BRESP SLVERR, CTRL = 0000005a e24 AR id=3 addr=0x110 len=1 size=2 INCR e27 R id=3 beat 0 data=0x41504234 OKAY addr 0x110 lanes 0-3 e30 R id=3 beat 1 data=0x0 SLVERR last addr 0x114 lanes 0-3 RRESP OKAY SLVERR e31 AR id=4 addr=0x3000 len=0 size=2 INCR e32 R id=4 beat 0 data=0x0 DECERR last addr 0x3000 lanes 0-3 RRESP DECERR, APB transfers 7 checker: 3 writes, 2 reads, 4 stalled edges, 0 rules broken checker: 5 writes, 2 reads, 0 wait edges, 0 rules broken, 0 notesRead 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_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: apb_periph.sv
`timescale 1ns/1ns
// apb_periph.sv: an APB4 completer (read-only). Offsets are paddr[11:0]:
// 0x000-0x0ff RAM, 256 bytes, written lane by lane under PSTRB
// 0x100 CTRL read/write, byte-strobed
// 0x104 STATUS read-only: the number of words in the FIFO
// 0x108 WCOUNT read-only: counts completed writes (a zero-strobe write counts too)
// 0x10c FIFO a write pushes PWDATA, a read pops the oldest word (0 when empty)
// 0x110 ID read-only, 0x41504234 ("APB4")
// With err_on (the default) the completer answers PSLVERR for an unmapped offset, an unaligned
// PADDR, a write to a read-only register and, with priv_ctrl, an unprivileged access to CTRL;
// a transfer that gets PSLVERR changes nothing here (IHI 0024E 3.4 allows either choice).
// Switches (set from the stimulus):
// waits wait states in every access (PREADY low for that many access cycles)
// ready_in_setup drive PREADY high outside the access phase (a don't-care: the spec allows it)
// hang never raise PREADY
// err_on, priv_ctrl
// Broken twins:
// act_every_cycle acts at every edge PSEL is high, not once at completion
// late_rdata PRDATA registered at completion, so it arrives one cycle late
// err_early PSLVERR during setup and wait cycles, low at completion
// ignore_strobes writes every lane of a RAM word or CTRL
// ignore_psel decodes PADDR and acts in access cycles without looking at its PSEL
module apb_periph #(parameter NAME = "p0") (
input logic pclk,
input logic presetn,
input logic psel,
input logic penable,
input logic pwrite,
input logic [31:0] paddr,
input logic [31:0] pwdata,
input logic [3:0] pstrb,
input logic [2:0] pprot,
output logic pready,
output logic [31:0] prdata,
output logic pslverr);
int waits = 0;
bit ready_in_setup = 0, hang = 0, err_on = 1, priv_ctrl = 0, trace = 0;
bit act_every_cycle = 0, late_rdata = 0, err_early = 0, ignore_strobes = 0, ignore_psel = 0;
logic [7:0] mem [0:255];
logic [31:0] ctrl = 0, wcount = 0;
logic [31:0] fifo [0:15];
int fhead = 0, fcount = 0;
int wcnt = 0; // wait cycles spent in this access
logic [31:0] prdata_q = 0;
initial for (int i = 0; i < 256; i++) mem[i] = 0;
localparam int RAM = 0, CTRL = 1, STATUS = 2, WCOUNT = 3, FIFO = 4, ID = 5, NONE = 6;
function automatic int region(input logic [11:0] a);
if (a < 12'h100) region = RAM;
else if (a[11:2] == 10'h040) region = CTRL;
else if (a[11:2] == 10'h041) region = STATUS;
else if (a[11:2] == 10'h042) region = WCOUNT;
else if (a[11:2] == 10'h043) region = FIFO;
else if (a[11:2] == 10'h044) region = ID;
else region = NONE;
endfunction
function automatic bit bad(input logic wr, input logic [11:0] a, input logic [2:0] prot);
int r;
r = region(a);
bad = (r == NONE) || (a[1:0] != 0) || (wr && (r == STATUS || r == WCOUNT || r == ID))
|| (priv_ctrl && r == CTRL && !prot[0]);
endfunction
function automatic logic [31:0] value(input logic [11:0] a);
case (region(a))
RAM: value = {mem[{a[7:2], 2'd3}], mem[{a[7:2], 2'd2}], mem[{a[7:2], 2'd1}], mem[{a[7:2], 2'd0}]};
CTRL: value = ctrl;
STATUS: value = fcount;
WCOUNT: value = wcount;
FIFO: value = fcount > 0 ? fifo[fhead] : 32'h0;
ID: value = 32'h41504234;
default: value = 32'h0;
endcase
endfunction
wire sel = ignore_psel ? 1'b1 : (psel === 1'b1);
wire access = sel && (penable === 1'b1);
wire err = err_on && bad(pwrite, paddr[11:0], pprot);
assign pready = hang ? 1'b0 : (!access ? (ready_in_setup ? 1'b1 : 1'b0) : (wcnt >= waits));
wire done = access && pready;
assign pslverr = err_early ? (sel && !done && err) : (done && err);
// The read value is taken at the setup edge (PSEL 1, PENABLE 0) and held on PRDATA until the next
// read, the way most register blocks do it (the bus multiplexor decides who is heard);
// late_rdata registers it at completion instead.
logic [31:0] rdata_r = 0;
always @(posedge pclk) if (sel && penable !== 1'b1 && pwrite === 1'b0) rdata_r <= value(paddr[11:0]);
assign prdata = late_rdata ? prdata_q : rdata_r;
wire act = act_every_cycle ? sel : done;
always @(posedge pclk or negedge presetn) begin
if (!presetn) begin
ctrl <= 0; wcount <= 0; wcnt <= 0;
end else begin
wcnt <= (access && !pready) ? wcnt + 1 : 0;
if (done && !pwrite) prdata_q <= value(paddr[11:0]);
if (act && !err) begin
if (pwrite) begin
wcount <= wcount + 1;
case (region(paddr[11:0]))
RAM: for (int l = 0; l < 4; l++) if (pstrb[l] || ignore_strobes) mem[{paddr[7:2], 2'b00} + l] <= pwdata[8*l +: 8];
CTRL: for (int l = 0; l < 4; l++) if (pstrb[l] || ignore_strobes) ctrl[8*l +: 8] <= pwdata[8*l +: 8];
FIFO: if (fcount < 16) begin fifo[(fhead + fcount) % 16] <= pwdata; fcount <= fcount + 1; end
default: ;
endcase
if (trace) $display(" %0s e%0d: write 0x%03h <= 0x%08h strb=%b", NAME, ($time - 5) / 10, paddr[11:0], pwdata, pstrb);
end else begin
if (region(paddr[11:0]) == FIFO && fcount > 0) begin fhead <= (fhead + 1) % 16; fcount <= fcount - 1; end
if (trace) $display(" %0s e%0d: read 0x%03h -> 0x%08h", NAME, ($time - 5) / 10, paddr[11:0], value(paddr[11:0]));
end
end else if (act && trace) $display(" %0s e%0d: error, %0s 0x%03h ignored", NAME, ($time - 5) / 10, pwrite ? "write" : "read", paddr[11:0]);
end
end
endmodule
Read included file: apb_checker.sv
`timescale 1ns/1ns
// apb_checker.sv: the rules of an APB interface, checked at every rising edge (read-only).
// reset PSEL is 1 while PRESETn is low;
// known PSEL is x, or a control signal, a strobed write byte or PREADY in access is x;
// setup PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it;
// setup_len a setup cycle not followed by an access cycle;
// stable PSEL, PADDR, PWRITE, PPROT, PSTRB or the write data changed inside a transfer,
// or PSEL or PENABLE dropped before PREADY;
// enable_drop PENABLE still 1 in the cycle after a completed transfer;
// strb_read PSTRB not zero during a read;
// onehot more than one PSELx at once;
// timeout no PREADY after TIMEOUT wait cycles (a liveness watchdog, not a protocol rule).
// A NOTE (not a broken rule): PSLVERR is 1 outside the completion cycle, which the spec allows
// and recommends against (IHI 0024E 3.4).
module apb_checker #(parameter int NSEL = 1, parameter int TIMEOUT = 16) (
input logic pclk,
input logic presetn,
input logic [NSEL-1:0] psel,
input logic penable,
input logic pwrite,
input logic [31:0] paddr,
input logic [31:0] pwdata,
input logic [3:0] pstrb,
input logic [2:0] pprot,
input logic pready,
input logic pslverr);
int broken = 0, notes = 0, writes = 0, reads = 0, waits = 0, edge_n = -1, stall = 0;
logic p_any = 0, p_en = 0, p_rdy = 0, p_wr = 0;
logic [NSEL-1:0] p_sel = 0;
logic [31:0] p_addr = 0, p_wdata = 0;
logic [3:0] p_strb = 0;
logic [2:0] p_prot = 0;
task automatic report(input string rule, input string msg);
broken++;
$display("RULE %0s broken at edge %0d: %0s", rule, edge_n, msg);
endtask
task automatic note(input string rule, input string msg);
notes++;
$display("NOTE %0s at edge %0d: %0s", rule, edge_n, msg);
endtask
always @(posedge pclk) begin
logic any, en, rdy, bytes_x;
edge_n = edge_n + 1;
any = (|psel) === 1'b1; en = (penable === 1'b1); rdy = (pready === 1'b1);
if (presetn !== 1'b1) begin
if (any) report("reset", "PSEL is 1 while PRESETn is low");
any = 0; en = 0;
end else begin
bytes_x = 0;
for (int l = 0; l < 4; l++) if (pstrb[l] === 1'b1 && (^pwdata[8*l +: 8]) === 1'bx) bytes_x = 1;
if ((^psel) === 1'bx) report("known", "PSEL is x");
else if (any && (^{paddr, pwrite, penable, pstrb, pprot}) === 1'bx) report("known", "a control signal is x while PSEL is 1");
else if (any && pwrite === 1'b1 && bytes_x) report("known", "a strobed byte of PWDATA is x");
else if (any && en && pready === 1'bx) report("known", "PREADY is x in an access cycle");
if (any && en && !(p_any && (!p_en || !p_rdy)))
report("setup", "PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it");
if (p_any && !p_en && !(any && en))
report("setup_len", "the setup cycle was not followed by an access cycle");
if (p_any && (!p_en || !p_rdy) && any) begin
if (psel !== p_sel) report("stable", $sformatf("PSEL changed from %b to %b inside a transfer", p_sel, psel));
if (paddr !== p_addr) report("stable", $sformatf("PADDR changed from 0x%0h to 0x%0h inside a transfer", p_addr, paddr));
if (pwrite !== p_wr) report("stable", "PWRITE changed inside a transfer");
if (pprot !== p_prot) report("stable", "PPROT changed inside a transfer");
if (pstrb !== p_strb) report("stable", "PSTRB changed inside a transfer");
if (p_wr && pwdata !== p_wdata) report("stable", $sformatf("PWDATA changed from 0x%0h to 0x%0h inside a transfer", p_wdata, pwdata));
if (p_en && !en) report("stable", "PENABLE dropped before PREADY");
end
if (p_any && p_en && !p_rdy && !any) report("stable", "PSEL dropped before PREADY");
if (p_any && p_en && p_rdy && en) report("enable_drop", "PENABLE is still 1 in the cycle after a completed transfer");
if (any && pwrite === 1'b0 && pstrb !== 4'b0000) report("strb_read", $sformatf("PSTRB is %b during a read", pstrb));
if ($countones(psel) > 1) report("onehot", $sformatf("PSELx is %b: two completers selected", psel));
if (any && en && rdy) begin
if (pwrite === 1'b1) writes++; else reads++;
stall = 0;
end else if (any && en) begin
waits++; stall++;
if (stall == TIMEOUT) report("timeout", $sformatf("no PREADY after %0d wait cycles", TIMEOUT));
end
if (pslverr === 1'b1 && !(any && en && rdy))
note("pslverr", "PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW)");
end
p_any = any; p_en = en; p_rdy = rdy; p_sel = psel; p_addr = paddr; p_wr = (pwrite === 1'b1);
p_wdata = pwdata; p_strb = pstrb; p_prot = pprot;
end
final $display("checker: %0d writes, %0d reads, %0d wait edges, %0d rules broken, %0d notes", writes, reads, waits, broken, notes);
endmodule
Read included file: apb_decoder.sv
`timescale 1ns/1ns
// apb_decoder.sv: the select decoder and the read multiplexor of an APB bus (read-only).
// 0x0000-0x0fff completer 0 0x1000-0x1fff completer 1 0x2000-0x2fff completer 2
// anything else the default responder (select psel_def) answers at once with PSLVERR
// PSELx follows PADDR while the bus PSEL is 1. PREADY, PRDATA and PSLVERR come from the
// completer whose PSELx is 1 (the select of the transfer in progress).
// Broken twin: mux_stale takes PRDATA and PSLVERR from the completer of the previous completed
// transfer (a select captured at completion instead of decoded from the current PADDR).
module apb_decoder (
input logic pclk,
input logic presetn,
input logic psel,
input logic penable,
input logic [31:0] paddr,
output logic [2:0] pselx,
output logic psel_def,
input logic [2:0] pready_x,
input logic [2:0] pslverr_x,
input logic [31:0] prdata0,
input logic [31:0] prdata1,
input logic [31:0] prdata2,
output logic pready,
output logic [31:0] prdata,
output logic pslverr);
bit mux_stale = 0;
wire [1:0] hit = (paddr[31:14] != 0) ? 2'd3 : paddr[13:12];
assign pselx = (psel === 1'b1 && hit != 3) ? (3'b001 << hit) : 3'b000;
assign psel_def = (psel === 1'b1 && hit == 3); // the default responder's select
logic [1:0] last_hit = 0;
always @(posedge pclk) if (psel === 1'b1 && penable === 1'b1 && pready === 1'b1) last_hit <= hit;
wire [1:0] dsel = mux_stale ? last_hit : hit;
assign pready = (hit == 3) ? 1'b1 : pready_x[hit];
assign prdata = (dsel == 0) ? prdata0 : (dsel == 1) ? prdata1 : (dsel == 2) ? prdata2 : 32'h0;
assign pslverr = (dsel == 3) ? (psel === 1'b1 && penable === 1'b1) : pslverr_x[dsel];
endmodule
Read included file: apb_bridge.sv
`timescale 1ns/1ns
// apb_bridge.sv: an AXI4 to APB bridge (read-only), one transaction at a time.
// Every AXI beat becomes one APB transfer to the aligned word of the beat's address
// (beat_addr from axi_pkg), with the beat's WSTRB as PSTRB. PSLVERR on any beat of a write gives
// BRESP = SLVERR; on a read beat it gives RRESP = SLVERR for that beat. Addresses at or above
// APB_TOP decode to nothing: the bridge answers DECERR without any APB transfer. When a read and
// a write request wait at the same edge, the read goes first (this bridge's policy).
// Broken twin: drop_strb drives PSTRB = 1111 on every write.
module apb_bridge #(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 [2:0] awprot,
input logic awvalid,
output logic awready,
input logic [31:0] wdata,
input logic [3: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 [2:0] arprot,
input logic arvalid,
output logic arready,
output logic [ID_W-1:0] rid,
output logic [31:0] rdata,
output logic [1:0] rresp,
output logic rlast,
output logic rvalid,
input logic rready,
output logic psel,
output logic penable,
output logic pwrite,
output logic [31:0] paddr,
output logic [31:0] pwdata,
output logic [3:0] pstrb,
output logic [2:0] pprot,
input logic pready,
input logic [31:0] prdata,
input logic pslverr);
import axi_pkg::*;
localparam logic [31:0] APB_TOP = 32'h3000;
localparam int IDLE = 0, WBEAT = 1, SETUP = 2, ACCESS = 3, BRESP = 4, RBEAT = 5;
bit drop_strb = 0, trace = 0;
int st = IDLE;
logic wr = 0, dec = 0;
logic [ID_W-1:0] id = 0;
logic [31:0] a0 = 0, cur = 0, wd_q = 0, rd_q = 0;
logic [7:0] len = 0, beat = 0;
logic [2:0] size = 0, prot = 0;
logic [1:0] burst = 0, resp = 0, rr_q = 0;
logic [3:0] ws_q = 0;
assign arready = (st == IDLE);
assign awready = (st == IDLE) && !(arvalid === 1'b1);
assign wready = (st == WBEAT);
assign bvalid = (st == BRESP);
assign bid = id; assign bresp = resp;
assign rvalid = (st == RBEAT);
assign rid = id; assign rdata = rd_q; assign rresp = rr_q; assign rlast = (beat == len);
assign psel = (st == SETUP || st == ACCESS);
assign penable = (st == ACCESS);
assign pwrite = wr;
assign paddr = cur;
assign pwdata = wr ? wd_q : 32'h0;
assign pstrb = wr ? (drop_strb ? 4'hf : ws_q) : 4'h0;
assign pprot = prot;
always @(posedge aclk or negedge aresetn) begin
if (!aresetn) st <= IDLE;
else case (st)
IDLE:
if (arvalid === 1'b1) begin
wr <= 0; id <= arid; a0 <= araddr; len <= arlen; size <= arsize; burst <= arburst; prot <= arprot;
beat <= 0; dec <= (araddr >= APB_TOP);
cur <= beat_addr(araddr, arlen, arsize, arburst, 0) & ~32'h3;
if (araddr >= APB_TOP) begin rd_q <= 0; rr_q <= DECERR; st <= RBEAT; end
else st <= SETUP;
end else if (awvalid === 1'b1) begin
wr <= 1; id <= awid; a0 <= awaddr; len <= awlen; size <= awsize; burst <= awburst; prot <= awprot;
beat <= 0; dec <= (awaddr >= APB_TOP); resp <= (awaddr >= APB_TOP) ? DECERR : OKAY;
st <= WBEAT;
end
WBEAT:
if (wvalid === 1'b1) begin
wd_q <= wdata; ws_q <= wstrb;
cur <= beat_addr(a0, len, size, burst, beat) & ~32'h3;
if (dec) begin
if (beat == len) st <= BRESP; else beat <= beat + 1;
end else st <= SETUP;
end
SETUP: st <= ACCESS;
ACCESS:
if (pready === 1'b1) begin
if (trace) $display(" bridge e%0d: beat %0d %0s 0x%04h%0s", ($time - 5) / 10, beat, wr ? "write" : "read", cur, pslverr === 1'b1 ? " PSLVERR" : "");
if (wr) begin
if (pslverr === 1'b1) resp <= SLVERR;
if (beat == len) st <= BRESP;
else begin beat <= beat + 1; st <= WBEAT; end
end else begin
rd_q <= prdata; rr_q <= (pslverr === 1'b1) ? SLVERR : OKAY; st <= RBEAT;
end
end
BRESP: if (bready === 1'b1) st <= IDLE;
RBEAT:
if (rready === 1'b1) begin
if (beat == len) st <= IDLE;
else begin
beat <= beat + 1;
cur <= beat_addr(a0, len, size, burst, beat + 1) & ~32'h3;
if (dec) begin rd_q <= 0; rr_q <= DECERR; end
else st <= SETUP;
end
end
default: st <= IDLE;
endcase
end
endmodule
Check yourself
Remember
- An AXI-to-APB bridge turns every AXI beat into one APB transfer; APB has no bursts.
- The beat's address is rounded down to the word and its
WSTRBbecomesPSTRB, so narrow and byte writes stay narrow. PSLVERRbecomes SLVERR: in the RRESP of that read beat, or in the single BRESP of a write.- Addresses with no APB completer get DECERR from the bridge, without an APB transfer.
- APB sets the pace: at least two cycles per transfer and one transfer at a time, with backpressure on the AXI side.