Many completers: decoder and read mux
Give every completer its own PSEL from the address map, return the answer of the selected one only, decide what a hole answers, and see why one slow peripheral stalls them all.
- Lesson
- P3.6
- Time
- 30 min
- Runs on
- Your browser
- Check
- 5 questions
A timer and a UART shared one APB bus. The timer's designer decoded its registers from PADDR and PENABLE alone, reasoning that APB carries one transfer at a time anyway. Every write meant for another peripheral's register at offset 0x100 also landed in the timer's control register at 0x100. The timer kept restarting with odd periods, and only when the UART was busy.
p1.ignore_psel = 1. Completer 1 took all three writes to offset 0x100 and kept the last one, meant for completer 2.e3-e4 WRITE 0x0100 data=0x000000a0 strb=1111 waits=0 OKAY
e5-e6 WRITE 0x1100 data=0x000000b1 strb=1111 waits=0 OKAY
e7-e8 WRITE 0x2100 data=0x000000c2 strb=1111 waits=0 OKAY
e9-e10 READ 0x0100 data=0x000000a0 waits=0 OKAY
e11-e12 READ 0x1100 data=0x000000c2 waits=0 OKAY
e13-e14 READ 0x3000 data=0x00000000 waits=0 PSLVERR
CTRL: p0=000000a0 p1=000000c2 p2=000000c2One select per completer
Every completer on an APB bus sees the same PADDR, PWRITE, PWDATA, PSTRB and PENABLE. What sets them apart is the select: the requester, usually the bridge, drives one PSELx per completer, decoded from the address. A completer acts only in a transfer where its own PSELx is 1. The bench decodes PADDR[13:12] into three selects, and anything outside the three regions goes to a default responder.
| Address range | Selected | Offset the completer sees |
|---|---|---|
| 0x0000 to 0x0fff | completer 0 (PSELx 001) | PADDR[11:0] |
| 0x1000 to 0x1fff | completer 1 (PSELx 010) | PADDR[11:0] |
| 0x2000 to 0x2fff | completer 2 (PSELx 100) | PADDR[11:0] |
| anything else | the default responder | none: PSLVERR at once |
The bench's address map (apb_decoder.sv). The same three completer models, so offset 0x100 is a CTRL register in each.
A lit select is the one completer in the transfer; the pill is read data the requester takes.
- Cycle 0
paddr0x0100 decodes to completer 0: onlypsel0rises. - Cycle 2
The next transfer goes straight to 0x1100.
psel0falls andpsel1rises in the same cycle; there is no idle cycle in between. - Cycle 4
Completer 2 gets its write. The other two see the same
paddroffset 0x100 and the same data, and do nothing. - Cycle 7
A read of completer 0 completes. The multiplexor returns completer 0's
prdata, a0. - Cycle 9
A read of completer 1: same offset, its own value, b1.
- Cycle 10
0x3000 belongs to nobody. No
pselrises; the default responder is ready at once. - Cycle 11
It ends the transfer with
pslverr. The requester gets an error and no data.
The decoder chooses who acts; the multiplexor chooses who is heard. Both follow the transfer in progress.
The way back: a multiplexor keyed by the select
Each completer drives its own PREADY, PRDATA and PSLVERR, and those wires cannot simply be joined. The bridge picks the three answers of the completer whose PSELx is 1 in the current transfer. An unselected completer may drive anything; the bench's completers keep their last read value on PRDATA, as many register blocks do.
The twin dec.mux_stale = 1; keys the read multiplexor by the completer of the previous completed transfer. Nothing on the requester's side looks wrong, and the checker stays silent, but the data does not belong to the address.
e9-e10 READ 0x0100 data=0x00000000 waits=0 OKAY
e11-e12 READ 0x1100 data=0x000000a0 waits=0 OKAY
e13-e14 READ 0x3000 data=0x000000b1 waits=0 OKAYWith p1.ignore_psel = 1; the opposite half breaks: the decoder is right and one completer refuses to listen to it, as in the opening story.
Addresses nobody owns
The APB specification does not define a default completer. Whatever drives the bus must still end a transfer to an empty address, or the bus waits forever. The bench gives the hole its own select, psel_def, and a responder that answers at once with PSLVERR, so the checker still sees a complete transfer. The AXI-to-APB bridge of P3.7 does better: it decodes before it starts and answers DECERR without any APB transfer.
The decoder must also never raise two selects at once. The checker's onehot rule watches for it; forcing pselx to 011 during a write fires it at the setup edge and at the access edge.
One slow completer holds the bus
APB has no outstanding transfers. The requester starts the next transfer only after the current one completes, so a completer with wait states delays every completer queued behind it. Measured with p1.waits = 3;: a write to completer 1 takes edges 3 to 7, and the write to completer 0, which needs no wait at all, starts at edge 8.
e3-e7 WRITE 0x1100 data=0x000000b1 strb=1111 waits=3 OKAY
e8-e9 WRITE 0x0100 data=0x000000a0 strb=1111 waits=0 OKAYAsked in interviews: decoding an APB bus
Who generates PSELx?
The requester, normally the bridge: one select per completer, decoded from the address. Every other signal is shared by all completers.
How does the requester get the right PRDATA?
A multiplexor in the bridge selects PRDATA, PREADY and PSLVERR of the completer whose PSELx is 1 in the current transfer.
May a completer act without its PSEL?
No. PADDR and PENABLE are broadcast to every completer, so a completer that ignores PSEL acts on transfers meant for others.
What happens on an access to an unmapped address?
APB does not define it. The bridge must end the transfer: a default responder with PSLVERR, or, in an AXI bridge, DECERR without an APB transfer.
Why does one slow peripheral slow the others?
APB carries one transfer at a time. Nothing can start until the waiting transfer completes.
Try it
Three completers, each with a CTRL register at offset 0x100. Each write lands in exactly one of them, each read returns that completer's value, and the hole answers PSLVERR. The checker watches all four selects, the default responder's included.
Set watch = 1; at the top to print the selects on every edge. Then add p1.ignore_psel = 1; or dec.mux_stale = 1; and compare the last two lines with the twins above. p1.waits = 3; shows the stall.
e3-e4 WRITE 0x0100 data=0x000000a0 strb=1111 waits=0 OKAY e5-e6 WRITE 0x1100 data=0x000000b1 strb=1111 waits=0 OKAY e7-e8 WRITE 0x2100 data=0x000000c2 strb=1111 waits=0 OKAY e9-e10 READ 0x0100 data=0x000000a0 waits=0 OKAY e11-e12 READ 0x1100 data=0x000000b1 waits=0 OKAY e13-e14 READ 0x3000 data=0x00000000 waits=0 PSLVERR CTRL: p0=000000a0 p1=000000b1 p2=000000c2 checker: 3 writes, 3 reads, 0 wait edges, 0 rules broken, 0 notesRead 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_tasks.svh
// apb_tasks.svh: the requester side of the bench, as tasks for the stimulus (read-only).
// apb_write(addr, data, strb, prot) and apb_read(addr, prot) run one transfer and leave the bus
// IDLE; q_write / q_read queue transfers and apb_run() sends the queue back to back with PSEL held
// high (ACCESS goes straight to SETUP). Each transfer prints one log line at its completion edge:
// e3-e4 WRITE 0x0100 data=0x11223344 strb=1111 waits=0 OKAY
// (setup edge - completion edge). The result of a read is in prdata_q and pslverr_q; the results
// of a queue are in rdv[i] and errv[i]. `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge pclk) cycle = cycle + 1;
bit quiet = 0;
// Requester bugs, for the broken twins:
bit move_in_wait = 0; // PADDR moves by 4 in the first wait cycle
bit enable_stuck = 0; // PENABLE stays 1 into the next queued transfer (no setup cycle)
bit strb_on_read = 0; // PSTRB = 1111 on reads
bit skip_setup = 0; // PSEL and PENABLE rise together
int give_up = 24; // the bench stops waiting after this many wait cycles
logic [31:0] prdata_q = 0;
logic pslverr_q = 0;
int waits_q = 0;
bit stuck_now = 0;
bit q_wr [0:63];
logic [31:0] q_addr [0:63], q_data [0:63], rdv [0:63];
logic [3:0] q_strb [0:63];
logic [2:0] q_prot [0:63];
bit errv [0:63];
int qn = 0;
// A text waveform for the trace blocks: one line per rising edge with the values sampled there.
bit watch = 0;
int wave_edge = -1;
always @(posedge pclk) begin
wave_edge = wave_edge + 1;
`ifdef APB_DECODER
if (watch) $display("wave e%0d: psel=%b pselx=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
wave_edge, psel, pselx, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);
`else
if (watch) $display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
wave_edge, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);
`endif
end
// One transfer: SETUP for one cycle, then ACCESS until PREADY. last = 0 keeps PSEL high for the
// next transfer.
task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb,
input logic [2:0] prot, input bit last);
int s_edge, w;
paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0;
pstrb = wr ? strb : (strb_on_read ? 4'hf : 4'h0);
pprot = prot; psel = 1;
s_edge = cycle + 1;
if (skip_setup || stuck_now) penable = 1;
else begin
penable = 0;
@(posedge pclk); #1 penable = 1;
end
stuck_now = 0;
w = 0;
@(posedge pclk);
while (pready !== 1'b1 && w < give_up) begin
w++;
#1 if (move_in_wait && w == 1) paddr = addr + 4;
@(posedge pclk);
end
prdata_q = prdata; pslverr_q = (pslverr === 1'b1); waits_q = w;
#1;
if (!quiet) begin
if (w >= give_up) $display("e%0d-e%0d %0s 0x%04h no PREADY after %0d wait cycles, the bench gives up", s_edge, cycle, wr ? "WRITE" : "READ ", addr[15:0], w);
else if (wr) $display("e%0d-e%0d WRITE 0x%04h data=0x%08h strb=%b waits=%0d %0s", s_edge, cycle, addr[15:0], data, strb, w, pslverr_q ? "PSLVERR" : "OKAY");
else $display("e%0d-e%0d READ 0x%04h data=0x%08h waits=%0d %0s", s_edge, cycle, addr[15:0], prdata_q, w, pslverr_q ? "PSLVERR" : "OKAY");
end
if (last) begin
psel = 0; penable = 0; pwrite = 0; pstrb = 0;
end else begin
penable = enable_stuck ? 1 : 0;
stuck_now = enable_stuck;
end
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf, input logic [2:0] prot = 3'b000);
apb_xfer(1, addr, data, strb, prot, 1);
endtask
task automatic apb_read(input logic [31:0] addr, input logic [2:0] prot = 3'b000);
apb_xfer(0, addr, 0, 0, prot, 1);
endtask
// The queue: transfers sent back to back by apb_run().
task automatic q_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf, input logic [2:0] prot = 3'b000);
q_wr[qn] = 1; q_addr[qn] = addr; q_data[qn] = data; q_strb[qn] = strb; q_prot[qn] = prot; qn++;
endtask
task automatic q_read(input logic [31:0] addr, input logic [2:0] prot = 3'b000);
q_wr[qn] = 0; q_addr[qn] = addr; q_data[qn] = 0; q_strb[qn] = 0; q_prot[qn] = prot; qn++;
endtask
task automatic apb_run();
for (int i = 0; i < qn; i++) begin
apb_xfer(q_wr[i], q_addr[i], q_data[i], q_strb[i], q_prot[i], i == qn - 1);
rdv[i] = prdata_q; errv[i] = pslverr_q;
end
qn = 0;
endtask
task automatic idle(input int n);
repeat (n) @(posedge pclk);
#1;
endtask
// The completer's RAM, by bus address (the decoder bench picks the completer from bits 13:12).
function automatic logic [7:0] ram_byte(input int unsigned a);
`ifdef APB_DECODER
case (a[13:12]) 2'd1: ram_byte = p1.mem[a[7:0]]; 2'd2: ram_byte = p2.mem[a[7:0]]; default: ram_byte = p0.mem[a[7:0]]; endcase
`else
ram_byte = p0.mem[a[7:0]];
`endif
endfunction
// dump(addr, n): n bytes from addr, one 32-bit word per group, lane 0 (the lowest address) first.
task automatic dump(input int unsigned addr, input int n);
string s;
for (int a = addr & ~3; a < addr + n; a += 4) begin
if ((a - (addr & ~3)) % 16 == 0) begin
if (a != (addr & ~3)) $display("%0s", s);
s = $sformatf("0x%04h ", a[15:0]);
end
s = {s, $sformatf(" %h %h %h %h ", ram_byte(a), ram_byte(a + 1), ram_byte(a + 2), ram_byte(a + 3))};
end
$display("%0s", s);
endtask
// fifo_fill(n, first): put n words first, first+1, ... in completer 0's FIFO before the run.
task automatic fifo_fill(input int n, input logic [31:0] first);
for (int i = 0; i < n; i++) p0.fifo[(p0.fhead + p0.fcount + i) % 16] = first + i;
p0.fcount = p0.fcount + n;
endtask
// regs(): completer 0's registers as one line.
task automatic regs();
$display("regs: CTRL=0x%08h STATUS=%0d WCOUNT=%0d", p0.ctrl, p0.fcount, p0.wcount);
endtask
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
Check yourself
Remember
- The requester drives one
PSELxper completer from the address map; every other request signal is shared. - A completer acts only when its own
PSELxis 1. - A multiplexor returns
PREADY,PRDATAandPSLVERRfrom the completer selected in the current transfer. - APB defines no default completer; the bridge must still end a transfer to an empty address, with an error.
- One transfer at a time: a completer with wait states delays every transfer behind it.