The transfer: setup, access and the state machine

Walk an APB transfer through IDLE, SETUP and ACCESS, find the one edge where the completer acts, and run transfers back to back.

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

A UART driver lost every other received byte, and two of every three on chips where the peripheral needed a wait state. The receive FIFO popped an entry at every edge where its select was high, instead of once per transfer. A read returned the oldest byte and silently threw the next ones away.

One FIFO read, three entries goneThe FIFO holds four words. The completer has one wait state and the broken twin act_every_cycle: it pops at every edge where PSEL is 1.
PSELPENABLEPREADYPRDATAAPB bridgereads the FIFO onceUART FIFOfour entries

The slots are the FIFO entries still stored, measured at each edge.

012345678clkpselpenablepreadyprdata0a0entries00001111011100110001
  1. Cycle 3

    Setup of the read. The FIFO holds four entries. The broken completer pops one at this edge, because PSEL is 1.

  2. Cycle 4

    Access, with a wait state: PREADY is 0. The data a0 is on PRDATA, and another entry goes, because PSEL is still 1.

  3. Cycle 5

    The transfer completes: PSEL, PENABLE and PREADY are all 1. The bridge takes a0, and the completer pops a third time.

  4. Cycle 6

    One read, three entries gone, one left. A correct completer pops once, at edge 5, and leaves three.

Measured on the bench with p0.act_every_cycle = 1 and p0.waits = 1.

Three states

The specification describes the bus as a small state machine driven by the requester. IDLE is the default. A transfer moves the bus to SETUP, where PSEL is 1 and PENABLE is 0, for exactly one cycle. The next edge always moves it to ACCESS, where PENABLE is 1. The bus stays in ACCESS while PREADY is 0. When PREADY is 1 it leaves ACCESS: back to IDLE, or straight to SETUP when another transfer follows.

The operating states of APBA write followed by a read, walked through the three states.
The operating states of APBA write followed by a read, walked through the three states. state IDLE PSEL = 0 state SETUP PSEL = 1, PENABLE= 0, one cycle state ACCESS PSEL = 1, PENABLE= 1, until PREADY completer Completer holds PREADY low towait transfernext edgePREADY = 1, anothertransferPREADY = 1, notransferPREADY
  1. Request

    The bridge has a write. It drives the address, the direction and the data, and raises PSEL: SETUP.

    Follow the calls, values and waits
    1. data transfer: IDLE → SETUP
  2. Enable

    One edge later PENABLE rises: ACCESS. The address, data and controls do not change.

    Follow the calls, values and waits
    1. data transfer: SETUP → ACCESS
  3. Wait

    The completer holds PREADY at 0 for as long as it needs. The bus stays in ACCESS and nothing changes.

  4. Next transfer

    PREADY is 1, the write completes, and a read is waiting. PENABLE drops, PSEL stays: SETUP again, with no idle cycle.

    Follow the calls, values and waits
    1. data transfer: ACCESS → SETUP
  5. Done

    The read completes with nothing behind it: PSEL falls and the bus is IDLE.

    Follow the calls, values and waits
    1. data transfer: ACCESS → IDLE
Read the diagram as text
  • IDLE: PSEL = 0
  • SETUP: PSEL = 1, PENABLE = 0, one cycle
  • ACCESS: PSEL = 1, PENABLE = 1, until PREADY
  • Completer: holds PREADY low to wait
  • IDLE → SETUP: transfer
  • SETUP → ACCESS: next edge
  • ACCESS → SETUP: PREADY = 1, another transfer
  • ACCESS → IDLE: PREADY = 1, no transfer
  • Completer → ACCESS: PREADY

The requester moves the bus between states; the completer can only keep it in ACCESS.

StatePSELPENABLEWhat happens
IDLE00nothing; the other signals are don't-care
SETUP10address, direction, write data and strobes are valid; exactly one cycle
ACCESS11the same values, held; the transfer completes at the edge where PREADY is 1

The signals set in SETUP must not change until the transfer completes (lesson P3.3).

The completion edge

There is exactly one edge per transfer where PSEL, PENABLE and PREADY are all 1. At that edge a write takes effect and the requester takes PRDATA and PSLVERR. A completer may prepare earlier, for instance fetch the read data during SETUP, as the bench completer does. What it must not do is act twice: anything with a side effect belongs to the completion edge alone. That covers a FIFO pop, a write counter, a read-to-clear status or a write-one-to-clear bit.

Acts at the completion edge

p0.waits = 1;
fifo_fill(4, 32'ha0);
apb_read(32'h10c);   // a0
apb_read(32'h10c);   // a1
// ... a write, then a FIFO read and a STATUS read

Reads return a0, a1, a2; STATUS = 1, WCOUNT = 1.

Acts at every edge PSEL is 1

p0.waits = 1; p0.act_every_cycle = 1;
fifo_fill(4, 32'ha0);
apb_read(32'h10c);   // a0
apb_read(32'h10c);   // a3
// ... the same write and reads

Reads return a0, a3, 0; STATUS = 0, WCOUNT = 3.

The broken completer breaks no rule on the wires: every signal the checker watches is correct. Only the data and the counters show it, which is why a register test must read side-effect registers after waited transfers. One write counted three times is the same bug as one read popping three entries.

Back to back

A requester with another transfer for the same completer keeps PSEL high and drops PENABLE for one cycle: the new SETUP. There is no IDLE cycle between them, and no way to skip the SETUP. With nothing to do, the requester drops PSEL and the bus goes IDLE.

A write and a read back to back, then a read after an idle cycleWrite 5 to CTRL, read it back with PSEL held, wait one cycle, read the ID register.
PSELPENABLEPREADYIDLEPSEL = 0SETUPone cycleACCESSuntil PREADY

The ringed block is the state of the bus at that edge.

012345678910clkpselpenablepwritepaddr0100110preadyprdata0541504234
  1. Cycle 3

    SETUP of the write to 0x100.

  2. Cycle 4

    ACCESS, and PREADY is 1: the write completes.

  3. Cycle 5

    SETUP of the read. PSEL never fell; PENABLE did. This is the ACCESS-to-SETUP transition.

  4. Cycle 6

    The read completes with 5 on PRDATA.

  5. Cycle 7

    Nothing queued: IDLE for one cycle.

  6. Cycle 8

    A new transfer from IDLE: SETUP with the ID address, then ACCESS and completion at edge 9.

Measured on the bench. Two transfers back to back take four cycles; IDLE between transfers is allowed and never required.

Two requester bugs break this. A requester that holds PENABLE into the next transfer gives that transfer no setup cycle: the checker names setup and enable_drop. One that raises PSEL and PENABLE together skips SETUP from IDLE, and a completer that prepares its read data in SETUP returns stale data.

PENABLE held into the next transfer

enable_stuck = 1;
q_write(32'h100, 32'h1);
q_write(32'h014, 32'h2);
apb_run();

RULE setup broken at edge 5: PENABLE is 1 without a setup cycle before it. RULE enable_drop broken at edge 5.

PSEL and PENABLE together

apb_write(32'h100, 32'h1);
apb_read(32'h100);
idle(1);
skip_setup = 1;
apb_read(32'h110);   // the ID register

RULE setup broken at edge 8. e8-e8 READ 0x0110 data=0x00000001: the CTRL value, not the ID.

Where you will meet it

A completer's state machine and a requester's sequencer both follow these three states. A bench for either runs single transfers and back-to-back ones, with and without wait states, and reads every register with a side effect afterwards.

Asked in interviews: the APB transfer
  1. Describe the APB state machine.

    IDLE (PSEL low), SETUP (PSEL high, PENABLE low) for exactly one cycle, then ACCESS (PENABLE high) until PREADY is high. From ACCESS the bus returns to IDLE, or goes straight to SETUP when another transfer follows.

  2. When does a completer perform a write?

    At the edge where PSEL, PENABLE and PREADY are all high. It may decode earlier, but side effects happen once, at that edge.

  3. Can PSEL stay high between two transfers?

    Yes, for two transfers to the same completer. PENABLE must still drop for one cycle, so the second transfer has its own setup cycle.

  4. What breaks if a completer acts whenever PSEL is high?

    Every side effect repeats once per cycle of the transfer: a FIFO read pops two entries, or more with wait states. The wires look correct, so only the data shows it.

Try it

Each FIFO read pops one entry at its completion edge: a0, a1, then a2 after the write. STATUS, read last, says one entry is left, and WCOUNT counted the one write once. Every transfer takes three edges: setup, one wait, completion.

Add p0.act_every_cycle = 1; to the first line and run again. The second read returns a3 and the third returns 0, STATUS reads 0 and WCOUNT is 3. The checker still reports 0 rules broken: the wires were right and the completer was wrong.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: e3-e5 READ 0x010c data=0x000000a0 waits=1 OKAY e6-e8 READ 0x010c data=0x000000a1 waits=1 OKAY e9-e11 WRITE 0x0100 data=0x00000001 strb=1111 waits=1 OKAY e12-e14 READ 0x010c data=0x000000a2 waits=1 OKAY e15-e17 READ 0x0104 data=0x00000001 waits=1 OKAY regs: CTRL=0x00000001 STATUS=1 WCOUNT=1 checker: 1 writes, 4 reads, 5 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 two wait states and the broken twin act_every_cycle, so its write counter goes up by four for this one write. The transfer log is 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 = 2; p0.act_every_cycle = 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_write(32'h100, 32'h9);
    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 bugEach option reads the FIFO register at 0x10c twice. Which option breaks a rule of the transfer?
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
ConceptHow long does the SETUP state last?
ConceptA completer has a write-one-to-clear status register. At which edge should a write clear the bits?
Read the waveformIn the measured back-to-back trace, at which edge does the bus go from ACCESS straight to SETUP?
A write, a read back to back, an idle cycle, a read
012345678910pselpenablepready

One value per edge, from edge 0.

Remember

  • The bus is IDLE, then SETUP (PSEL = 1, PENABLE = 0) for exactly one cycle, then ACCESS (PENABLE = 1) until PREADY is 1.
  • A transfer completes at the one edge where PSEL, PENABLE and PREADY are all 1; writes and side effects happen there, once.
  • Back to back, PSEL stays high and PENABLE drops for one cycle: ACCESS goes straight to SETUP, never skipping it.
  • A completer that acts at every selected edge pops, counts or clears once per cycle; the wires stay correct and only the data shows it.
  • Holding PENABLE into the next transfer, or raising it together with PSEL, removes the setup cycle; the checker names setup and enable_drop.