Capstone: APB bug hunts
Run eleven faults of completers, the decoder and the requester through one bench with a rule checker and a register model. Learn which faults the wires reveal and which only the data does, then answer a mock interview.
- Lesson
- P3.10
- Time
- 35 min
- Runs on
- Your browser
- Check
- 5 questions
A UART driver lost characters only when the bus was busy. The receive FIFO's data register popped a character whenever its PSEL was high, and a slow bridge gave each read one wait state. One read took three characters and returned the first. Every handshake was legal, so the protocol checker stayed silent for a month.
act_every_cycle, then a read of STATUS.The four slots are the FIFO words, as each edge sees them.
- Cycle 0
Setup of the FIFO read: four words wait. The completer takes the read value (f0) here, and with
act_every_cycleit also pops at this edge. - Cycle 1
The wait cycle: PSEL is still high, so the broken completer pops again. Three words, then two.
- Cycle 2
The completion edge: the requester takes f0, which is right, and the completer pops a third time. A correct completer pops once, here.
- Cycle 5
STATUS reads 1 where 3 words should remain. Only a check of the data shows what the read did; the checker has nothing to say.
Measured on the capstone's bench (edges 3 to 8, p0.waits = 1, act_every_cycle on); fifo is the completer's word count as each edge sees it, drawn as a mask.
One bench, eleven faults
The bench is the decoder with three completers from P3.6 and the rule checker from P3.8 on the decoded bus. Beside them, a scoreboard keeps a model of every completer's RAM and CTRL. chk_write and chk_read run one transfer and compare the response and the data with the model; verdict prints the running counts. Each phase switches one fault on, runs a few transfers, and switches it off. A phase that leaves the hardware different from the model writes the word back, so the next phase starts clean.
| Fault | Who | The checker | The scoreboard |
|---|---|---|---|
| 0 none | silent | 11 checks, 0 mismatches | |
| 1 act_every_cycle | completer | silent | STATUS reads 0, expected 2: the FIFO read popped three words |
| 2 late_rdata | completer | silent | each read returns the previous read's value |
| 3 err_early | completer | two notes, no rule | a write to WCOUNT answered OKAY, expected PSLVERR |
| 4 ignore_strobes | completer | silent | a one-lane write clobbered the other three lanes |
| 5 ignore_psel | completer | silent | p1's CTRL took a write meant for p0 |
| 6 mux_stale | decoder | silent | three reads return the previous completer's data |
| 7 move_in_wait | requester | stable at edge 64 | the write landed one word higher |
| 8 enable_stuck | requester | setup and enable_drop at edge 74 | nothing: both writes landed |
| 9 strb_on_read | requester | strb_read at edges 79 and 80 | nothing: the completer ignores read strobes |
| 10 hang | completer | timeout at 97, stable at 103 | the read got no answer |
The fault matrix, measured by the example. Six faults are visible only in the data, three only on the wires, two in both.
Faults of the completer: only the data
Every completer fault in rows 1 to 6 keeps the handshake perfect. The requester drives setup and access correctly, the completer raises PREADY when it should, and the decoder selects one completer at a time. No rule on the wires has anything to say. The damage is in what the transfer did. A FIFO popped too often, the wrong completer took a write, a byte changed although its strobe was low, or the read data belonged to another transfer.
Two of them are worth reading twice. late_rdata returns, for every read, the value of the read before it. The first read of fault 2 returns the ID register, because the completer's previous read was the ID in fault 0. mux_stale returns the right data for the fourth read of fault 6. The previous transfer went to the same completer, so the stale select was right by accident, and a test that reads one completer at a time never sees the fault.
err_early is the one completer fault the checker notices at all, as a note. The completer raises PSLVERR in the wait cycle and drops it at the completion edge, which is the only edge where the requester looks. The write to a read-only register is reported OKAY, and only a model that knows WCOUNT is read-only says it should have failed.
Faults of the requester: the wires
Requester faults are rules of the protocol, so the checker names them at their edge. Two of them are harmless to this completer: it took both writes with PENABLE stuck high, and it ignores strobes on reads. A stricter completer could do something else with them, and the checker is what tells you before it does.
move_in_wait is caught twice. The checker names the edge where PADDR moved; the scoreboard finds the word that did not change and the neighbour that did. hang is caught twice too. The watchdog fires after sixteen wait cycles; the bench gives up after twenty and drops PSEL, which breaks stable; the scoreboard counts a read with no answer.
The lesson of the matrix is the same as P2.18's. A checker answers "did every transfer follow the rules?" and a scoreboard answers "did every transfer do what it should?". An APB bench needs both, because the faults that matter most to firmware are faults of the register block, and those keep the wires legal.
The mock interview
Asked in interviews: APB from end to end
How many cycles does an APB transfer take, and why?
At least two: one setup cycle and one access cycle, plus one cycle per wait state. APB is not pipelined, and PENABLE marks the second cycle so a simple completer can act on one edge.
When does a completer act on a write, and why only then?
At the edge where PSEL, PENABLE and PREADY are all high. Acting at every selected edge repeats side effects: a FIFO pops, a counter counts, a clear-on-read register clears, once per wait state.
May PSEL stay high between two transfers?
Yes, when the next transfer goes to the same completer: ACCESS goes straight to SETUP, PENABLE drops for one cycle and PSEL stays high.
What does PSLVERR mean for the register?
Only that the transfer failed. A write with an error may or may not have changed the register, and a read with an error may return data the requester still uses. PSLVERR counts only in the completion cycle.
How does an AXI write burst reach an APB register block?
Through a bridge that makes one APB transfer per beat, puts the beat's strobes on PSTRB, and returns SLVERR in BRESP if any beat got PSLVERR. An address that decodes to no completer gets DECERR without an APB transfer.
What would you put in an APB bench?
A rule checker bound to the bus, a register model with the access policy of every register, traffic with wait states, back-to-back transfers and errors, and a watchdog on PREADY.
Try it
Read the output as the table above: one block per fault, the checker's RULE lines and the scoreboard's mismatch lines, then the verdict with the running counts. The transfer log is off (quiet = 1) so only the two judges speak.
Then hunt. Turn on two faults at once in a new phase, say p2.late_rdata = 1; and dec.mux_stale = 1;. Read p0, p2 and p2 again, and predict each value before you run it. Or switch the scoreboard off for a phase (chk_read becomes apb_read) and count how many faults the checker alone would have shipped.
-- fault 0: none fault 0: 11 checks, 0 mismatches, 0 rules broken so far -- fault 1: act_every_cycle, a waited FIFO read pops at every selected edge mismatch: read 0x0104 returned 0x00000000, expected 0x00000002 fault 1: 13 checks, 1 mismatches, 0 rules broken so far -- fault 2: late_rdata, read data one cycle after the completion edge mismatch: read 0x1100 returned 0x41504234, expected 0x000000b1 mismatch: read 0x1014 returned 0x000000b1, expected 0x00000014 fault 2: 16 checks, 3 mismatches, 0 rules broken so far -- fault 3: err_early, PSLVERR in the wait cycle and gone at completion NOTE pslverr at edge 39: PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW) NOTE pslverr at edge 40: PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW) mismatch: write 0x0108 answered OKAY, expected PSLVERR fault 3: 17 checks, 4 mismatches, 0 rules broken so far -- fault 4: ignore_strobes, a byte write clobbers its word mismatch: read 0x2014 returned 0x00005500, expected 0x11225544 fault 4: 20 checks, 5 mismatches, 0 rules broken so far -- fault 5: ignore_psel, a completer that decodes PADDR without its PSEL mismatch: read 0x1100 returned 0x000000a5, expected 0x000000b1 fault 5: 23 checks, 6 mismatches, 0 rules broken so far -- fault 6: mux_stale, PRDATA from the completer of the previous transfer mismatch: read 0x0110 returned 0x000000a5, expected 0x41504234 mismatch: read 0x2014 returned 0x41504234, expected 0x11225544 mismatch: read 0x1100 returned 0x11225544, expected 0x000000b1 fault 6: 27 checks, 9 mismatches, 0 rules broken so far -- fault 7: move_in_wait, the requester moves PADDR in a wait state RULE stable broken at edge 64: PADDR changed from 0x30 to 0x34 inside a transfer mismatch: read 0x0030 returned 0x00000000, expected 0x00000077 mismatch: read 0x0034 returned 0x00000077, expected 0x00000000 fault 7: 31 checks, 11 mismatches, 1 rules broken so far -- fault 8: enable_stuck, PENABLE held into the next transfer RULE setup broken at edge 74: PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it RULE enable_drop broken at edge 74: PENABLE is still 1 in the cycle after a completed transfer fault 8: 33 checks, 11 mismatches, 3 rules broken so far -- fault 9: strb_on_read, strobes during a read RULE strb_read broken at edge 79: PSTRB is 1111 during a read RULE strb_read broken at edge 80: PSTRB is 1111 during a read fault 9: 34 checks, 11 mismatches, 5 rules broken so far -- fault 10: hang, a completer that never raises PREADY RULE timeout broken at edge 97: no PREADY after 16 wait cycles mismatch: read 0x2014 got no answer RULE stable broken at edge 103: PSEL dropped before PREADY fault 10: 35 checks, 12 mismatches, 7 rules broken so far checker: 14 writes, 22 reads, 28 wait edges, 7 rules broken, 2 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
- A rule checker answers "did every transfer follow the rules?"; a register model answers "did every transfer do what it should?". An APB bench needs both.
- Completer and decoder faults (acting twice, late read data, ignored strobes, a missing PSEL check, a stale read mux) keep the wires legal; only the data shows them.
- Requester faults (a moved address, PENABLE held, read strobes) are rules on the wires; the checker names their edge even when the completer copes.
- A completer acts once, at the edge where PSEL, PENABLE and PREADY are all high; every other edge of a transfer is for waiting.
- Read every completer back to back with another: a stale select is right by accident when consecutive reads go to the same completer.