Wait states and PREADY

Stretch a transfer with PREADY, know which signals must hold still while it waits, and say exactly when read data is valid.

Lesson
P3.3
Time
30 min
Runs on
Your browser
Check
5 questions

Every register of a new peripheral read back the value of the read before it. The peripheral registered PRDATA at the completion edge, so the data arrived one cycle after the requester had taken it. Its unit test read each register twice in a row and passed.

Read data one cycle lateCTRL holds 0x11. The requester reads CTRL, then the ID register. The completer has the broken twin late_rdata.
PSELPENABLEPREADYPRDATAAPB bridgetakes PRDATA at completionPeripheralregisters PRDATA late

The token is what the bridge takes: PRDATA at the completion edge.

012345678910clkpselpenablepwritepaddr0100110preadyprdata01141504234
  1. Cycle 4

    The write of 0x11 to CTRL completes.

  2. Cycle 6

    The read of CTRL completes. PRDATA is still 0: the bridge takes 0.

  3. Cycle 7

    CTRL's value appears now, one cycle late, while the bus is already in the setup of the next read.

  4. Cycle 8

    The read of the ID register completes and takes 0x11, the value of CTRL.

  5. Cycle 9

    The ID value arrives after its transfer has ended. Nobody takes it.

Measured on the bench with p0.late_rdata = 1. No rule on the wires is broken; every value is simply one transfer behind.

PREADY stretches the access

In the access phase the completer drives PREADY. While it is 0 the transfer waits: the bus stays in ACCESS, one cycle per wait state, and the transfer completes at the first edge where it is 1. A completer may add any number of wait states, from zero upwards.

A write and a read with two wait states eachThe completer holds PREADY low for two access cycles in every transfer.
PENABLEPWDATAPREADYPRDATAAPB bridgeholds everythingPeripheraltwo wait states

The bridge is ringed while the completer holds PREADY low.

01234567891011clkpselpenablepwritepwdata0beef0preadyprdata0beef
  1. Cycle 3

    Setup of the write of 0xbeef.

  2. Cycle 4

    Access, PREADY = 0: the first wait state. Every signal the bridge drives keeps its value.

  3. Cycle 6

    Second wait state over: PREADY = 1 and the write completes. One transfer, four edges.

  4. Cycle 8

    The read's data is on PRDATA already, but PREADY is 0. The bridge must not take it yet.

  5. Cycle 10

    The read completes and the bridge takes 0xbeef here, at the only edge that counts.

Measured on the bench with p0.waits = 2. The checker counts four wait edges.

PREADY matters only when PENABLE is 1. In IDLE and SETUP it may take any value, so a completer that never needs a wait state can tie it high. The bench's twin p0.ready_in_setup = 1 drives it high outside the access phase: the transfers and the checker's report are identical.

Hold still

While PREADY is 0 the requester keeps everything it set in SETUP. The completer may sample any of these signals at any edge of the transfer and must see one consistent request.

Held while PREADY is 0For
PADDR, PWRITE, PSELx, PENABLEevery transfer
PPROT, PAUSERevery transfer, when present
PWDATA, PSTRB, PWUSERwrites
PRDATA, PSLVERRnot held: completer outputs, valid only at completion

The same values must also not change between SETUP and the first access cycle.

A requester that moves the address during a wait state writes somewhere else. The completer acts on what it sees at the completion edge, and by then the address has moved.

The write meant for 0x000 landed at 0x004. The checker names stable at the edge the address moved.
p0.waits = 2;
move_in_wait = 1;                   // PADDR moves by 4 in the first wait cycle
apb_write(32'h000, 32'h55667788);
dump(32'h000, 8);
// RULE stable broken at edge 5: PADDR changed from 0x0 to 0x4 inside a transfer
// 0x0000   00 00 00 00  88 77 66 55

When read data is valid

PRDATA has to be correct at one edge only: the completion edge. Before that the completer may drive anything, and the requester must not look. The bench completer fetches its data in SETUP and holds it, as most register blocks do, but a completer may also produce the data in the last wait cycle.

Data ready by the completion edge

apb_write(32'h100, 32'h11);
apb_write(32'h004, 32'h22);
apb_read(32'h100);
apb_read(32'h004);
apb_read(32'h110);

Reads return 0x11, 0x22 and 0x41504234.

Data registered at the completion edge

p0.late_rdata = 1;
apb_write(32'h100, 32'h11);
apb_write(32'h004, 32'h22);
apb_read(32'h100);
apb_read(32'h004);
apb_read(32'h110);

Reads return 0, 0x11 and 0x22: each one the value of the read before.

No rule on the wires catches this, because PRDATA has no rule of its own. A bench catches it by reading registers whose values differ, in an order where a stale value cannot pass for the right one.

A wait with no end

The specification puts no upper limit on wait states. A completer that never raises PREADY holds its transfer, and the bridge cannot start another one. Every peripheral on that APB bus becomes unreachable, and the manager that started the access waits too. The bench checker has a watchdog that is not a protocol rule. After 16 wait cycles it names timeout.

The bench gives up after 24 wait cycles. A real requester cannot, which is why its abandoned transfer breaks stable here.
p0.hang = 1;
apb_read(32'h100);
// RULE timeout broken at edge 19: no PREADY after 16 wait cycles
// e3-e28 READ  0x0100 no PREADY after 24 wait cycles, the bench gives up
// RULE stable broken at edge 29: PSEL dropped before PREADY

Systems guard against this outside APB, with a timeout in the bridge or the interconnect that answers an error upstream. Lesson P3.4 shows how errors reach the requester.

Where you will meet it

Every completer that reads a slow memory, crosses a clock domain or waits for a hardware block uses wait states. A bench for it runs every register with zero, one and many wait states, holds every request signal still, and reads values that differ from register to register.

Asked in interviews: wait states
  1. How does an APB completer insert wait states?

    It drives PREADY low in the access phase. The transfer completes at the first access edge where PREADY is high; each low cycle adds one wait state, with no upper limit in the protocol.

  2. What must the requester keep stable during wait states?

    PADDR, PWRITE, PSELx, PENABLE and PPROT, plus PWDATA and PSTRB for a write, and any user request signals. They also must not change between SETUP and the first access cycle.

  3. Must PREADY be low in the setup phase?

    No. PREADY is a don't-care whenever PENABLE is low, so a completer with no wait states can tie it high.

  4. When must PRDATA be valid?

    Only in the cycle the read completes, when PSEL, PENABLE and PREADY are all high. The requester samples it at that edge and ignores it at any other.

Try it

A transfer with n wait states takes n + 2 edges: one setup, n waits, one completion. The same read of the ID register takes two edges with no wait states and seven with five, and returns the same value both times. Wait states cost time and change nothing else.

Add move_in_wait = 1; before the first write: the checker names stable in every transfer that waits, and the write goes to 0x104, the read-only STATUS register, which answers with an error. Replace it with p0.late_rdata = 1;: the read of 0x100 returns 0 and the next read returns 0xbeef.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: e3-e6 WRITE 0x0100 data=0x0000beef strb=1111 waits=2 OKAY e7-e10 READ 0x0100 data=0x0000beef waits=2 OKAY e11-e12 READ 0x0110 data=0x41504234 waits=0 OKAY e13-e19 READ 0x0110 data=0x41504234 waits=5 OKAY checker: 1 writes, 3 reads, 9 wait edges, 0 rules broken, 0 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

Check yourself

Predict the outputThe completer has three wait states and drives PREADY high whenever PENABLE is low. One read and one write run with the transfer log off. What does the checker print?
`timescale 1ns/1ps
module tb;
  logic pclk = 0, presetn = 0;
  always #5 pclk = ~pclk;                 // rising edges at 5, 15, 25, ...
  logic        psel = 0, penable = 0, pwrite = 0, pwakeup = 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;
  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));
  apb_checker chk (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
                   .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .pslverr(pslverr));
  `include "apb_tasks.svh"

  initial begin
    p0.waits = 3; p0.ready_in_setup = 1;
    repeat (2) @(posedge pclk); #1 presetn = 1;   // reset released after edge 1
    @(posedge pclk); #1;                          // the first transfer may start after edge 2
    quiet = 1;
    apb_read(32'h110);
    apb_write(32'h100, 32'h1);
    repeat (3) @(posedge pclk);
    $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_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
Spot the bugThe completer has two wait states. Each option writes the CTRL register at 0x100. Which option breaks a rule?
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
ConceptDuring the wait states of a read, which of these signals may change?
ConceptA completer ties PREADY to 1. Which statement is true?
Read the waveformIn the measured trace with two wait states, at which edge does the bridge take the read data?
The read of 0x100 with two wait states
01234pselpenablepreadyprdata0beef

Edges 7 to 11 of the figure, shown as cycles 0 to 4.

Remember

  • PREADY low in the access phase adds a wait state; the transfer completes at the first access edge where PREADY is 1, after any number of waits.
  • While the transfer waits, PADDR, PWRITE, PSELx, PENABLE, PPROT and, for writes, PWDATA and PSTRB stay exactly as they were set in SETUP.
  • PREADY is a don't-care when PENABLE is low; a completer without wait states may tie it high.
  • PRDATA must be valid only at the completion edge; a completer whose data comes a cycle late passes every rule and returns stale values.
  • APB has no wait-state limit: a completer that never answers stalls the whole bus, so systems add a timeout outside the protocol.