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.

One read, three popsA read of the FIFO register with one wait state on the completer with act_every_cycle, then a read of STATUS.
PSELPENABLEPRDATAPREADYRequesterone read of 0x10cCompleter p0FIFO of 4 words

The four slots are the FIFO words, as each edge sees them.

012345clkpselpenablepaddr10c104preadyprdata0f01fifo1111011100110001
  1. Cycle 0

    Setup of the FIFO read: four words wait. The completer takes the read value (f0) here, and with act_every_cycle it also pops at this edge.

  2. Cycle 1

    The wait cycle: PSEL is still high, so the broken completer pops again. Three words, then two.

  3. 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.

  4. 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.

FaultWhoThe checkerThe scoreboard
0 nonesilent11 checks, 0 mismatches
1 act_every_cyclecompletersilentSTATUS reads 0, expected 2: the FIFO read popped three words
2 late_rdatacompletersilenteach read returns the previous read's value
3 err_earlycompletertwo notes, no rulea write to WCOUNT answered OKAY, expected PSLVERR
4 ignore_strobescompletersilenta one-lane write clobbered the other three lanes
5 ignore_pselcompletersilentp1's CTRL took a write meant for p0
6 mux_staledecodersilentthree reads return the previous completer's data
7 move_in_waitrequesterstable at edge 64the write landed one word higher
8 enable_stuckrequestersetup and enable_drop at edge 74nothing: both writes landed
9 strb_on_readrequesterstrb_read at edges 79 and 80nothing: the completer ignores read strobes
10 hangcompletertimeout at 97, stable at 103the 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
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

RUNIcarus Verilog ยท in your browser
Edit the code, then run it
Expected: -- 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 notes
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_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

Predict the outputFive words wait in the FIFO (a0 to a4). The completer acts at every edge its PSEL is high and inserts two wait states. The requester reads the FIFO once, then reads STATUS (the number of words left). What line is printed?
`timescale 1ns/1ps
module tb;
  logic pclk = 0, presetn = 0;
  always #5 pclk = ~pclk;
  logic        psel = 0, penable = 0, pwrite = 0;
  logic [31:0] paddr = 0, pwdata = 0;
  logic [3:0]  pstrb = 0;
  logic [2:0]  pprot = 0;
  logic        pready, pslverr;
  logic [31:0] prdata, got;
  apb_periph p0 (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
                 .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr));
  `include "apb_tasks.svh"
  initial begin
    repeat (2) @(posedge pclk); #1 presetn = 1;
    @(posedge pclk); #1;
    quiet = 1;
    fifo_fill(5, 32'ha0);                  // five words in the FIFO: a0, a1, a2, a3, a4
    p0.waits = 2; p0.act_every_cycle = 1;  // two wait states, and the broken completer
    apb_read(32'h10c); got = prdata_q;     // read the FIFO
    apb_read(32'h104);                     // read STATUS: the words left
    $display("FIFO read 0x%0h, STATUS %0d", got, prdata_q);
    $finish;
  end
endmodule
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_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
Spot the bugThe same traffic runs four times, each with one fault switched on: a strobed write, a read and a FIFO read, two wait states each. Which fault does the rule checker report?
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_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
ConceptIn fault 6 the decoder takes PRDATA from the completer of the previous transfer. Why does the fourth read, the second read of 0x1100, return the right value?
ConceptThe checker counts no broken rule in fault 3, where the completer raises PSLVERR in the wait cycle and drops it at completion. What finds that the write should have failed?
Read the waveformThe completer has late_rdata: PRDATA is registered at the completion edge. 0x100 holds b1 and 0x14 holds 14. What does the requester take for the read of 0x14, and at which cycle?
Two reads on a completer with late read data
0123pselpenablepaddr10014preadyprdata0b1

Edges 7 to 10 of a run on one completer; one value per cycle, sampled at the edge that ends the cycle.

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.