Capstone: AHB bug hunts

Run one bench against eleven broken things, read which detector catches each fault and why most of them leave every wire legal, and answer the questions this module prepared you for.

Lesson
P4.12
Time
40 min
Runs on
Your browser
Check
5 questions

A system with two SRAMs behind an AHB decoder passed every test that read one memory at a time. Then a DMA engine alternated reads between the two, back to back, and every read of the first memory returned whatever the second had sent last. Every handshake was legal and the protocol checker said nothing. The read multiplexor followed the select of the address phase, which by then belonged to the next transfer.

Two reads, and a multiplexor that looks one transfer aheadA read of 0x100 in m0, then a read of 0x1100 in m1, back to back. The multiplexor selects by the subordinate of the address phase on the wires instead of the one in its data phase.
HRDATA m0HRDATA m1HRDATAManagertwo readsMultiplexormux_addr_phasem00x0000 to 0x0fffm10x1000 to 0x1fff

msel is the subordinate the multiplexor listens to (0 is m0, 1 is m1); done marks the edges where a read's data phase ends.

0123clkhtransIDLENONSEQIDLEhaddr13001001100mselrdata00a0a0a0a0rdata1ee00b1b1b1b1hrdataee000ee00b1b1b1b1done
  1. Cycle 1

    The read of 0x100 is taken: its data phase is the next cycle, and the data will come from m0.

  2. Cycle 2

    Memory m0 drives a0a0a0a0, but the address phase now holds 0x1100, and this multiplexor follows it to m1, which still drives ee00 from its last read. The manager takes ee00 as the content of 0x100.

  3. Cycle 3

    The read of 0x1100 ends with b1b1b1b1, right by luck: the address phase still points at m1.

Measured on the lesson's bench (fault 5, edges 53 to 56) with the waveform printer and the multiplexor's select printed mid-cycle. The select must be registered on HREADY so it follows the data phase.

One bench, eleven things that can go wrong

The bench on this page holds everything the module built. The manager tasks of P4.1 to P4.7 drive the fabric of P4.8: two memories, the default subordinate, and the APB bridge with the P3 completer behind it. The rule checker of P4.10 watches the bus. A scoreboard in your file reads back what was written and counts every word that differs. The stimulus runs twelve phases, one correct and eleven broken, and each ends with the verdict line.

FaultWhat is brokenSymptomCaught by
1 one_cycle_errorm0 answers ERROR with HREADY high in its first cycleThe write to 0x104 behind it is taken at that edge; the manager had no cycle to cancel itChecker: rule err_shape at edge 21; the data agrees with the manager
2 wdata_in_addrm1 takes HWDATA in the address phase0x1200 holds 0 and 0x1204 holds the data of 0x1200Scoreboard only: 2 mismatches
3 act_on_idlem0 treats IDLE as a transferThe IDLE after the write, at the same address with HWDATA 0, wipes 0x300Scoreboard only
4 ignore_sizem1 writes every lane of a byte writeA byte at 0x1301 leaves 0000ee00 in the wordScoreboard only
5 mux_addr_phaseThe read multiplexor follows the address phaseThe read of 0x100 returns m1's ee00Scoreboard only
6 mux_no_hreadyThe multiplexor select is registered during wait statesThe read of 0x100 ends one wait early, with m1's HREADY and dataScoreboard only
7 ignore_hreadym1 takes an address phase while HREADY is lowThe write the manager cancelled after an ERROR lands, with deadbeefScoreboard only
8 address movedThe manager moves the next address during a wait state0x66 lands at 0x514 instead of 0x510Checker: rule hold at edge 72; scoreboard
9 WRAP8 as WRAP4The manager wraps a WRAP8 burst inside 16 bytes0x620 is never writtenChecker: rule seq_addr at edges 84 and 88; scoreboard
10 1 KB crossingAn INCR4 from 0xff8Beats 2 and 3 reach m1 as SEQ with no NONSEQ before themChecker: rule kb1 at edge 96; the data lands
11 APB hangThe completer behind the bridge never raises PREADYThe bridge holds HREADY low and the whole bus waitsNo rule on the wires: the watchdog after 24 cycles

The fault matrix, measured by the run below. Four faults break a rule; seven leave every wire legal and change only the data, or stop the bus.

The multiplexor is part of the protocol

Faults 5 and 6 sit in the interconnect, not in a subordinate. The decoder selects a subordinate in the address phase; the multiplexor must return that subordinate's HRDATA, HRESP and HREADYOUT in the data phase, one HREADY later. So its select is registered at every edge where HREADY is high, and only there (IHI 0033C 4.3). Fault 5 drops the register: the multiplexor listens to the subordinate of the next transfer, the hook's bug. Fault 6 keeps the register but loads it at every edge. During m0's wait states the select moves on to m1, whose HREADYOUT is high, so the read of 0x100 ends early with m1's data.

A select that moved during the wait
01234htransIDLENONSEQIDLEhaddr11001001100mselhreadyhrdatab1b1b1b1a0a0a0a0b1b1b1b1

Fault 6, edges 57 to 61. m0 has two wait states to give; after one, the select already points at m1, and HREADY comes from m1.

Neither fault breaks a rule on the manager's wires: every value is legal, it is just the wrong subordinate's. Fault 7 is the subordinate's side of the same rule. A subordinate may sample HSEL, the address and the control only when HREADY is high (IHI 0033C 4.2). Memory m1 took the write to 0x1400 during the first cycle of an ERROR, while the manager was cancelling it.

What the checker sees, and what it cannot

The checker names four faults at their edge: the one-cycle ERROR, the moved address, the wrong WRAP and the 1 KB crossing, all rules of P4.10. Two of them also corrupt data, and two do not. In fault 1 the manager kept the transfer because the memory never gave it a first ERROR cycle to react to. In fault 10 each beat goes to the subordinate its own address decodes to, and these memories treat every beat alone. A subordinate that counts beats or prefetches would see a SEQ arrive with no NONSEQ. That is why the 1 KB rule exists: regions are at least 1 KB on 1 KB boundaries, so a legal burst never needs two subordinates (IHI 0033C 4.2).

Fault 11 is the one no rule can report. AHB has no timeout. A subordinate that never ends its data phase stops every transfer behind it, because HREADY is shared, and here the subordinate is an APB peripheral two bridges away. Only a watchdog sees it.

Run the bench, then make it yours. Comment out the faults you have understood, flip a switch on fab.m1 instead of fab.m0, or give fault 6 a read of m1 first and watch the symptom change.

The mock interview

Five questions from the eleven lessons before this one, in the words an interviewer uses. Answer each in two sentences before you open it.

Asked in interviews: five questions of this module
  1. What does pipelined mean on AHB?

    The address phase of one transfer overlaps the data phase of the one before. Write data follows its address by at least one cycle, and a wait state in one data phase holds the next address phase on the wires.

  2. Why does a subordinate need HREADY as an input when it drives HREADYOUT?

    Because the bus HREADY says when the data phase in progress ends, possibly another subordinate's. A subordinate may sample an address phase only when HREADY is high; otherwise it takes a transfer that has not been accepted and may be cancelled.

  3. Why is the ERROR response two cycles?

    Because the next address phase is already on the wires. The first cycle holds it and warns the manager; in the second the manager may drive IDLE, so a transfer it no longer wants is never taken.

  4. Why must the read multiplexor be keyed by the data phase?

    Because the data and the response belong to the transfer in its data phase, while the decoder's select already points at the next address. The select is registered on HREADY high so it changes exactly when the data phase does.

  5. What does a protocol checker catch that a scoreboard cannot, and the other way round?

    The checker names a broken rule at its edge: a moved address, a one-cycle ERROR, a wrong SEQ address, a 1 KB crossing. The scoreboard catches the faults that keep every wire legal: data taken in the wrong phase, lanes HSIZE did not select, a multiplexor listening to the wrong subordinate.

Try it

Twelve phases, one per row of the fault matrix plus the correct one. Read each verdict line against the table: the checker's count moves for faults 1, 8, 9 and 10, the mismatch count for faults 2 to 9, and fault 11 ends with the watchdog's line.

Then hunt on your own. Turn on two faults at once and see which detector reports first, or set fab.trace = 1 before fault 5 to watch the decoder route every address phase.

RUNIcarus Verilog ยท in your browser
Edit the code, then run it
Expected: -- fault 0: none e4 A NONSEQ WRITE addr=0x100 size=2 SINGLE e5 D write addr=0x100 data=0xa0a0a0a0 OKAY e5 A NONSEQ WRITE addr=0x1100 size=2 SINGLE e6 D write addr=0x1100 data=0xb1b1b1b1 OKAY e6 A NONSEQ WRITE addr=0x2100 size=2 SINGLE e8 D write addr=0x2100 data=0xc2c2c2c2 OKAY e10 A NONSEQ READ addr=0x100 size=2 SINGLE e11 D read addr=0x100 data=0xa0a0a0a0 OKAY e13 A NONSEQ READ addr=0x1100 size=2 SINGLE e14 D read addr=0x1100 data=0xb1b1b1b1 OKAY e16 A NONSEQ READ addr=0x2100 size=2 SINGLE e18 D read addr=0x2100 data=0xc2c2c2c2 OKAY fault 0: 3 words checked, 0 mismatches, 0 rules broken so far -- fault 1: one_cycle_error, m0 answers ERROR in one cycle e20 A NONSEQ WRITE addr=0xf00 size=2 SINGLE RULE err_shape broken at edge 21: ERROR with HREADY high in its first cycle: an ERROR takes two cycles e21 D write addr=0xf00 data=0xdead ERROR e21 A NONSEQ WRITE addr=0x104 size=2 SINGLE e22 D write addr=0x104 data=0x77 OKAY e24 A NONSEQ READ addr=0x104 size=2 SINGLE e25 D read addr=0x104 data=0x77 OKAY fault 1: 4 words checked, 0 mismatches, 1 rules broken so far -- fault 2: wdata_in_addr, m1 takes HWDATA in the address phase e27 A NONSEQ WRITE addr=0x1200 size=2 SINGLE e28 D write addr=0x1200 data=0x11111111 OKAY e28 A NONSEQ WRITE addr=0x1204 size=2 SINGLE e29 D write addr=0x1204 data=0x22222222 OKAY e31 A NONSEQ READ addr=0x1200 size=2 SINGLE e32 D read addr=0x1200 data=0x0 OKAY mismatch: 0x1200 read 0x0, expected 0x11111111 e34 A NONSEQ READ addr=0x1204 size=2 SINGLE e35 D read addr=0x1204 data=0x11111111 OKAY mismatch: 0x1204 read 0x11111111, expected 0x22222222 fault 2: 6 words checked, 2 mismatches, 1 rules broken so far -- fault 3: act_on_idle, m0 writes on IDLE e37 A NONSEQ WRITE addr=0x300 size=2 SINGLE e38 D write addr=0x300 data=0x33333333 OKAY e42 A NONSEQ READ addr=0x300 size=2 SINGLE e43 D read addr=0x300 data=0x0 OKAY mismatch: 0x300 read 0x0, expected 0x33333333 fault 3: 7 words checked, 3 mismatches, 1 rules broken so far -- fault 4: ignore_size, m1 writes every lane of a byte write e45 A NONSEQ WRITE addr=0x1300 size=2 SINGLE e46 D write addr=0x1300 data=0x44332211 OKAY e48 A NONSEQ WRITE addr=0x1301 size=0 SINGLE e49 D write addr=0x1301 data=0xee OKAY e51 A NONSEQ READ addr=0x1300 size=2 SINGLE e52 D read addr=0x1300 data=0xee00 OKAY mismatch: 0x1300 read 0xee00, expected 0x4433ee11 fault 4: 8 words checked, 4 mismatches, 1 rules broken so far -- fault 5: mux_addr_phase, the multiplexor follows the address phase e54 A NONSEQ READ addr=0x100 size=2 SINGLE e55 D read addr=0x100 data=0xee00 OKAY e55 A NONSEQ READ addr=0x1100 size=2 SINGLE e56 D read addr=0x1100 data=0xb1b1b1b1 OKAY mismatch: 0x100 read 0xee00, expected 0xa0a0a0a0 fault 5: 10 words checked, 5 mismatches, 1 rules broken so far -- fault 6: mux_no_hready, the multiplexor select moves during a wait state e58 A NONSEQ READ addr=0x100 size=2 SINGLE e60 D read addr=0x100 data=0xb1b1b1b1 OKAY e60 A NONSEQ READ addr=0x1100 size=2 SINGLE e61 D read addr=0x1100 data=0xb1b1b1b1 OKAY mismatch: 0x100 read 0xb1b1b1b1, expected 0xa0a0a0a0 fault 6: 12 words checked, 6 mismatches, 1 rules broken so far -- fault 7: ignore_hready, m1 takes an address phase while HREADY is low e63 A NONSEQ WRITE addr=0x3000 size=2 SINGLE e64 X cancelled NONSEQ WRITE addr=0x1400 e65 D write addr=0x3000 data=0xdeadbeef ERROR e67 A NONSEQ READ addr=0x1400 size=2 SINGLE e68 D read addr=0x1400 data=0xdeadbeef OKAY mismatch: 0x1400 read 0xdeadbeef, expected 0x53525150 fault 7: 13 words checked, 7 mismatches, 1 rules broken so far -- fault 8: a manager moves its address during a wait state e70 A NONSEQ WRITE addr=0x500 size=2 SINGLE RULE hold broken at edge 72: HADDR changed from 0x510 to 0x514 during a wait state e73 D write addr=0x500 data=0x55 OKAY e73 A NONSEQ WRITE addr=0x514 size=2 SINGLE e76 D write addr=0x514 data=0x66 OKAY e78 A NONSEQ READ addr=0x510 size=2 SINGLE e79 D read addr=0x510 data=0x0 OKAY mismatch: 0x510 read 0x0, expected 0x66 fault 8: 14 words checked, 8 mismatches, 2 rules broken so far -- fault 9: a manager wraps a WRAP8 burst like WRAP4 e81 A NONSEQ WRITE addr=0x634 size=2 WRAP8 e82 D write addr=0x634 data=0xe0 OKAY e82 A SEQ WRITE addr=0x638 size=2 WRAP8 e83 D write addr=0x638 data=0xe1 OKAY e83 A SEQ WRITE addr=0x63c size=2 WRAP8 RULE seq_addr broken at edge 84: SEQ at 0x630; the beat after 0x63c in a WRAP8 burst of 4-byte transfers is 0x620 e84 D write addr=0x63c data=0xe2 OKAY e84 A SEQ WRITE addr=0x630 size=2 WRAP8 e85 D write addr=0x630 data=0xe3 OKAY e85 A SEQ WRITE addr=0x634 size=2 WRAP8 e86 D write addr=0x634 data=0xe4 OKAY e86 A SEQ WRITE addr=0x638 size=2 WRAP8 e87 D write addr=0x638 data=0xe5 OKAY e87 A SEQ WRITE addr=0x63c size=2 WRAP8 RULE seq_addr broken at edge 88: SEQ at 0x630; the beat after 0x63c in a WRAP8 burst of 4-byte transfers is 0x620 e88 D write addr=0x63c data=0xe6 OKAY e88 A SEQ WRITE addr=0x630 size=2 WRAP8 e89 D write addr=0x630 data=0xe7 OKAY e91 A NONSEQ READ addr=0x620 size=2 SINGLE e92 D read addr=0x620 data=0x0 OKAY mismatch: 0x620 read 0x0, expected 0xe3 fault 9: 15 words checked, 9 mismatches, 4 rules broken so far -- fault 10: a manager sends an INCR4 across 0x1000 e94 A NONSEQ WRITE addr=0xff8 size=2 INCR4 e95 D write addr=0xff8 data=0xd0 OKAY e95 A SEQ WRITE addr=0xffc size=2 INCR4 RULE kb1 broken at edge 96: SEQ at 0x1000 crosses the 1 KB boundary of the burst that started at 0xff8 e96 D write addr=0xffc data=0xd1 OKAY e96 A SEQ WRITE addr=0x1000 size=2 INCR4 e97 D write addr=0x1000 data=0xd2 OKAY e97 A SEQ WRITE addr=0x1004 size=2 INCR4 e98 D write addr=0x1004 data=0xd3 OKAY e100 A NONSEQ READ addr=0x1000 size=2 SINGLE e101 D read addr=0x1000 data=0xd2 OKAY fault 10: 16 words checked, 9 mismatches, 5 rules broken so far -- fault 11: the APB completer behind the bridge never answers e103 A NONSEQ READ addr=0x2100 size=2 SINGLE no HREADY for 24 cycles: the read of 0x2100 waits on APB, and so does the whole bus fault 11: 16 words checked, 9 mismatches, 5 rules broken so far checker: 41 transfers, 33 wait edges, 2 errors, 5 rules broken
Read included file: ahb_pkg.sv
`timescale 1ns/1ns
// ahb_pkg.sv: the encodings and the arithmetic of a burst (read-only).
// The memory, the driver, the checker and the lesson text all use these functions, so they never
// disagree about the address of a beat or the lanes it uses.
package ahb_pkg;
  localparam logic [1:0] IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
  localparam logic [2:0] SINGLE = 3'b000, INCR = 3'b001, WRAP4 = 3'b010, INCR4 = 3'b011,
                         WRAP8 = 3'b100, INCR8 = 3'b101, WRAP16 = 3'b110, INCR16 = 3'b111;
  localparam logic OKAY = 1'b0, ERROR = 1'b1;

  function automatic string trans_name(input logic [1:0] t);
    case (t) IDLE: return "IDLE"; BUSY: return "BUSY"; NONSEQ: return "NONSEQ"; default: return "SEQ"; endcase
  endfunction
  function automatic string burst_name(input logic [2:0] b);
    case (b)
      SINGLE: return "SINGLE"; INCR: return "INCR"; WRAP4: return "WRAP4"; INCR4: return "INCR4";
      WRAP8: return "WRAP8"; INCR8: return "INCR8"; WRAP16: return "WRAP16"; default: return "INCR16";
    endcase
  endfunction
  function automatic string resp_name(input logic r);
    return r ? "ERROR" : "OKAY";
  endfunction
  // s when c is 1, else the empty string (a string-typed result prints the same on every simulator).
  function automatic string opt(input bit c, input string s);
    if (c) return s;
    return "";
  endfunction

  // Beats in a burst: 1 for SINGLE, 4, 8 or 16 for the fixed bursts, 0 for INCR (undefined length).
  function automatic int beats_of(input logic [2:0] b);
    case (b) SINGLE: return 1; INCR: return 0; WRAP4, INCR4: return 4; WRAP8, INCR8: return 8; default: return 16; endcase
  endfunction
  function automatic bit is_wrap(input logic [2:0] b);
    return b == WRAP4 || b == WRAP8 || b == WRAP16;
  endfunction

  // The address of the beat after the one at a: the size in bytes further on; a wrapping burst
  // returns to the start of its window (beats x size bytes, aligned to that total) at the window's end.
  function automatic int unsigned next_addr(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
    int unsigned nb = 1 << size;
    int unsigned window = nb * beats_of(b);
    int unsigned base;
    if (!is_wrap(b)) return a + nb;
    base = (a / window) * window;
    if (a + nb >= base + window) return base;
    return a + nb;
  endfunction

  // The address of beat n of a burst that starts at start.
  function automatic int unsigned beat_addr(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int unsigned a = start;
    for (int k = 0; k < n; k++) a = next_addr(a, size, b);
    return a;
  endfunction

  // The lanes a transfer at address a uses on a bus of bus_bytes bytes, packed as {upper, lower}.
  // Every AHB transfer is aligned to its size, so it uses size bytes from its own lane upwards.
  function automatic logic [15:0] beat_lanes(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    int lower = a % bus_bytes;
    int upper = lower + (1 << size) - 1;
    if (upper > bus_bytes - 1) upper = bus_bytes - 1;
    return {upper[7:0], lower[7:0]};
  endfunction

  // The strobe of a transfer: a 1 for every lane it uses (bit l is lane l).
  function automatic logic [63:0] beat_strb(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    logic [15:0] lanes = beat_lanes(a, size, bus_bytes);
    logic [63:0] s = 0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) s[l] = 1'b1;
    return s;
  endfunction

  function automatic bit aligned(input int unsigned a, input logic [2:0] size);
    return (a % (1 << size)) == 0;
  endfunction

  // True when an incrementing burst of n beats (n is used only for INCR) crosses a 1 KB boundary.
  function automatic bit crosses_1k(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int beats = beats_of(b) ? beats_of(b) : n;
    if (is_wrap(b) || beats < 2) return 0;
    return (start / 1024) != ((start + beats * (1 << size) - 1) / 1024);
  endfunction
endpackage
Read included file: ahb_mem.sv
`timescale 1ns/1ns
// ahb_mem.sv: an AHB subordinate with a byte-addressed memory behind it (read-only in this lesson).
// It registers an address phase at an edge where HSEL is 1, HTRANS is NONSEQ or SEQ and HREADY is 1,
// then runs the data phase: wait states with HREADYOUT low, a write takes HWDATA (and HWSTRB) at the
// edge that ends the data phase, a read drives HRDATA for the cycle that ends it. IDLE and BUSY get a
// zero-wait OKAY and change nothing. The switches below are plain variables: the stimulus sets them by
// name (mem.waits = 2;).
//   waits          wait states inserted in every data phase
//   waits_at[k]    wait states for the k-th transfer this memory takes (from 0; -1 = use waits)
//   err_above      transfers at or above this address get the two-cycle ERROR (and write nothing)
//   excl_on        AHB5 exclusive access: one monitor per HMASTER, HEXOKAY on success
//   trace          print every transfer the memory takes and every byte it writes
// Broken twins, each off by default:
//   ignore_hready    registers an address phase while HREADY is low (another subordinate is stretching),
//                    so it acts on a transfer the manager may still change or cancel
//   wdata_in_addr    takes HWDATA in the address phase, so a write stores the previous transfer's data
//   act_on_idle      treats IDLE and BUSY as transfers (waits, reads, writes HWDATA); this twin is an
//                    AHB-Lite design without an HWSTRB input, so it writes the lanes HSIZE selects
//   one_cycle_error  answers ERROR in one cycle (HRESP high with HREADYOUT high)
//   ignore_size      writes every lane of the bus word, whatever HSIZE and HWSTRB say
module ahb_mem #(parameter int DATA_W = 32, parameter int DEPTH = 65536) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic              hsel,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  input  logic              hready,
  output logic              hreadyout,
  output logic              hresp,
  output logic [DATA_W-1:0] hrdata,
  output logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;

  // ---- switches ----
  int waits = 0;
  int waits_at [0:63];
  int unsigned err_above = 32'hffff_ffff;
  bit excl_on = 0, trace = 0;
  bit ignore_hready = 0, wdata_in_addr = 0, act_on_idle = 0, one_cycle_error = 0, ignore_size = 0;

  // ---- the memory ----
  logic [7:0] mem [0:DEPTH-1];
  int cycle = -1;
  int taken = 0;                              // transfers taken so far

  // ---- the data phase in progress ----
  bit              d_valid = 0, d_write = 0, d_err = 0, d_exok = 0, d_xfail = 0;
  int unsigned     d_addr = 0;
  logic [2:0]      d_size = 0;
  logic [1:0]      d_trans = 0;
  logic [DATA_W-1:0] d_wd = 0;                // HWDATA seen in the address phase (the wdata_in_addr twin)
  int              d_cnt = 0;                 // wait states still to insert
  bit              e_two = 0;                 // the ERROR response is in its second cycle

  // ---- exclusive monitors, one per HMASTER ----
  bit          mon_use [0:15];
  int unsigned mon_addr [0:15];
  int          mon_bytes [0:15];

  initial begin
    for (int i = 0; i < DEPTH; i++) mem[i] = 8'h00;
    for (int k = 0; k < 64; k++) waits_at[k] = -1;
    for (int m = 0; m < 16; m++) mon_use[m] = 0;
    hreadyout = 1; hresp = 0; hrdata = 0; hexokay = 0;
  end

  function automatic logic [DATA_W-1:0] read_word(input int unsigned a);
    logic [DATA_W-1:0] w = 0;
    int unsigned word = (a / BUS) * BUS;
    for (int l = 0; l < BUS; l++) w[8*l +: 8] = mem[(word + l) % DEPTH];
    return w;
  endfunction

  // Write the lanes of one transfer: the lanes its address and size select, where the strobe is 1.
  task automatic write_bytes(input int unsigned a, input logic [2:0] size, input logic [DATA_W-1:0] data, input logic [BUS-1:0] strb);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    int unsigned word = (a / BUS) * BUS;
    int lo = ignore_size ? 0 : lanes[7:0];
    int hi = ignore_size ? BUS - 1 : lanes[15:8];
    for (int l = lo; l <= hi; l++)
      if (ignore_size || act_on_idle || strb[l]) begin
        mem[(word + l) % DEPTH] = data[8*l +: 8];
        if (trace) $display("mem e%0d:   byte 0x%0h <= %02h (lane %0d)", cycle, word + l, data[8*l +: 8], l);
      end
    // any write ends the exclusive monitors that cover one of its bytes
    for (int m = 0; m < 16; m++)
      if (mon_use[m] && a < mon_addr[m] + mon_bytes[m] && mon_addr[m] < a + (1 << size)) mon_use[m] = 0;
  endtask

  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) begin
      d_valid = 0; e_two = 0;
      hreadyout <= 1; hresp <= 0; hexokay <= 0;
    end else begin
      // 1. the data phase that ends or waits at this edge
      if (d_valid) begin
        if (hreadyout && hready) begin
          if (d_write && !d_err && !d_xfail)
            write_bytes(d_addr, d_size, wdata_in_addr ? d_wd : hwdata, hwstrb);
          d_valid = 0;
        end else if (!hreadyout) begin
          if (d_cnt > 0) d_cnt = d_cnt - 1;
          else if (d_err) e_two = 1;
        end
      end
      // 2. the address phase on the wires at this edge
      if (hsel && (htrans[1] || act_on_idle) && (hready || (ignore_hready && !d_valid))) begin
        d_valid = 1; d_addr = haddr; d_write = hwrite; d_size = hsize; d_trans = htrans; d_wd = hwdata;
        d_cnt = (taken < 64 && waits_at[taken] >= 0) ? waits_at[taken] : waits;
        taken = taken + 1;
        d_err = (haddr >= err_above); e_two = 0;
        d_exok = 0; d_xfail = 0;
        if (excl_on && hexcl && htrans[1] && !d_err) begin
          if (!hwrite) begin
            mon_use[hmaster] = 1; mon_addr[hmaster] = haddr; mon_bytes[hmaster] = 1 << hsize; d_exok = 1;
          end else if (mon_use[hmaster] && mon_addr[hmaster] == haddr && mon_bytes[hmaster] == (1 << hsize)) begin
            d_exok = 1; mon_use[hmaster] = 0;
          end else d_xfail = 1;
        end
        if (trace) $display("mem e%0d: %0s %0s 0x%0h size %0d%0s%0s%0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr, hsize,
                            opt(!hready, " (taken while HREADY is low)"), opt(d_err, " -> ERROR"),
                            opt(d_xfail, " -> exclusive failed"));
      end
      // 3. the response for the cycle that starts now
      if (d_valid && d_cnt > 0) begin
        hreadyout <= 0; hresp <= 0; hexokay <= 0;
      end else if (d_valid && d_err) begin
        hreadyout <= one_cycle_error ? 1'b1 : e_two; hresp <= 1; hexokay <= 0;
      end else begin
        hreadyout <= 1; hresp <= 0; hexokay <= d_valid && d_exok;
      end
      if (d_valid && !d_write) hrdata <= d_err ? '0 : read_word(d_addr);
    end
  end
endmodule
Read included file: ahb_checker.sv
`timescale 1ns/1ns
// ahb_checker.sv: the rules of an AHB interface, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
//   reset         HTRANS is not IDLE, or HREADY is low, while reset is active;
//   hold          the address or control changed during a wait state (HREADY low), outside the
//                 changes the protocol allows;
//   trans_change  HTRANS changed during a wait state other than IDLE to NONSEQ, BUSY to SEQ (fixed
//                 bursts) or BUSY to anything (INCR); or SEQ/BUSY with no burst in progress;
//   seq_addr      a SEQ or BUSY address that is not the previous beat's address plus the size
//                 (wrapped for a WRAP burst);
//   burst_ctrl    HSIZE, HBURST, HWRITE or HPROT changed inside a burst;
//   align         a transfer not aligned to its size;
//   size_bus      a transfer wider than the data bus;
//   kb1           an incrementing burst that crosses a 1 KB boundary;
//   burst_len     a fixed-length burst that ends early or runs long, BUSY after a SINGLE, or BUSY
//                 after the last beat of a fixed-length burst;
//   idle_okay     an IDLE or BUSY transfer that did not get a zero-wait OKAY;
//   err_shape     an ERROR that is not two cycles (HREADY low then high, HRESP high in both);
//   exokay        HEXOKAY with HREADY low, with an ERROR, or for a transfer that was not exclusive;
//   known         x on the address, control, or the active lanes of the data.
// A NOTE (not a broken rule) marks a transfer kept after the first cycle of an ERROR: the protocol
// allows it, most managers cancel.
module ahb_checker #(parameter int DATA_W = 32) (
  input logic              hclk,
  input logic              hresetn,
  input logic [31:0]       haddr,
  input logic [1:0]        htrans,
  input logic              hwrite,
  input logic [2:0]        hsize,
  input logic [2:0]        hburst,
  input logic [3:0]        hprot,
  input logic              hmastlock,
  input logic [DATA_W-1:0] hwdata,
  input logic              hexcl,
  input logic              hready,
  input logic              hresp,
  input logic [DATA_W-1:0] hrdata,
  input logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;
  int cycle = -1, transfers = 0, waits = 0, errors = 0, broken = 0, notes = 0;

  function void report(input string rule, input string what);
    broken++;
    $display("RULE %s broken at edge %0d: %s", rule, cycle, what);
  endfunction

  function automatic logic [DATA_W-1:0] lane_mask(input int unsigned a, input logic [2:0] size);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    logic [DATA_W-1:0] m = '0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) m[8*l +: 8] = 8'hff;
    return m;
  endfunction

  // The previous edge.
  bit          p_on = 0;
  logic        p_ready = 1, p_resp = 0, p_write = 0, p_lock = 0;
  logic [1:0]  p_trans = 0;
  logic [31:0] p_addr = 0;
  logic [2:0]  p_size = 0, p_burst = 0;
  logic [3:0]  p_prot = 0;
  bit          e_first = 0;           // the previous edge was the first cycle of an ERROR
  // The data phase in progress.
  bit          d_on = 0, d_idle = 0, d_write = 0, d_excl = 0;
  int unsigned d_addr = 0;
  logic [2:0]  d_size = 0;
  // The burst in progress.
  bit          b_on = 0, b_err = 0, b_write = 0, b_kb = 0;
  logic [2:0]  b_burst = 0, b_size = 0;
  logic [3:0]  b_prot = 0;
  int unsigned b_first = 0, b_last = 0;
  int          b_count = 0;
  int unsigned exp_a;

  task automatic end_burst();
    if (b_on && beats_of(b_burst) > 1 && b_count < beats_of(b_burst) && !b_err)
      report("burst_len", $sformatf("%0s burst at 0x%0h ended after %0d of %0d beats", burst_name(b_burst), b_first, b_count, beats_of(b_burst)));
    b_on = 0;
  endtask

  task automatic check_beat();
    if ((1 << hsize) > BUS) report("size_bus", $sformatf("a %0d-byte transfer on a %0d-byte bus", 1 << hsize, BUS));
    if (!aligned(haddr, hsize)) report("align", $sformatf("a %0d-byte transfer at 0x%0h is not aligned to its size", 1 << hsize, haddr));
  endtask

  task automatic check_ctrl(input string what);
    if (hsize !== b_size || hburst !== b_burst || hwrite !== b_write || hprot !== b_prot)
      report("burst_ctrl", $sformatf("%0s at 0x%0h changed HSIZE, HBURST, HWRITE or HPROT inside the burst", what, haddr));
  endtask

  always @(posedge hclk) begin
    cycle++;
    if (!hresetn) begin
      if (htrans !== IDLE) report("reset", "HTRANS is not IDLE during reset");
      if (hready !== 1) report("reset", "HREADY is low during reset");
      p_on = 0; d_on = 0; b_on = 0; e_first = 0;
    end else begin
      // ---- the data phase that ends or waits at this edge ----
      if (hready === 0) waits++;
      if (e_first) begin
        if (hresp !== 1 || hready !== 1) report("err_shape", "the second cycle of an ERROR must have HRESP high and HREADY high");
      end else if (hresp === 1 && hready === 1)
        report("err_shape", "ERROR with HREADY high in its first cycle: an ERROR takes two cycles");
      if (hresp === 1 && b_on) b_err = 1;
      if (d_on && d_idle) begin
        if (hready !== 1 || hresp !== 0) report("idle_okay", "an IDLE or BUSY transfer must get a zero-wait OKAY");
        d_on = 0;
      end
      if (hexokay === 1) begin
        if (hready !== 1) report("exokay", "HEXOKAY while HREADY is low");
        else if (hresp === 1) report("exokay", "HEXOKAY together with an ERROR");
        else if (!(d_on && d_excl)) report("exokay", "HEXOKAY for a transfer that was not exclusive");
      end
      if (hready === 1 && d_on && !d_idle) begin
        if (d_write && $isunknown(hwdata & lane_mask(d_addr, d_size))) report("known", "HWDATA is x on an active lane");
        if (!d_write && hresp === 0 && $isunknown(hrdata & lane_mask(d_addr, d_size))) report("known", "HRDATA is x on an active lane");
        transfers++;
        if (hresp === 1) errors++;
        d_on = 0;
      end
      if (e_first && hready === 1 && (htrans === NONSEQ || htrans === SEQ)) begin
        notes++;
        $display("NOTE cancel at edge %0d: the %0s at 0x%0h behind an ERROR was kept, not cancelled (allowed; most managers cancel)", cycle, trans_name(htrans), haddr);
      end
      // ---- the address phase on the wires at this edge ----
      if (htrans !== IDLE && ($isunknown(haddr) || $isunknown(hwrite) || $isunknown(hsize) || $isunknown(hburst)))
        report("known", "address or control is x while HTRANS is not IDLE");
      if ($isunknown(htrans)) report("known", "HTRANS is x");
      if (p_on && p_ready === 0 && !e_first) begin
        // a wait state at the previous edge: only the allowed changes
        if (p_trans == IDLE) begin
          if (htrans === BUSY || htrans === SEQ) report("trans_change", $sformatf("IDLE changed to %0s during a wait state (only NONSEQ may follow)", trans_name(htrans)));
        end else if (p_trans == BUSY) begin
          if (p_burst != INCR) begin
            if (htrans !== BUSY && htrans !== SEQ) report("trans_change", $sformatf("BUSY in a %0s burst changed to %0s during a wait state (only SEQ may follow)", burst_name(p_burst), trans_name(htrans)));
            else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          end
        end else begin
          if (htrans !== p_trans) report("trans_change", $sformatf("%0s changed to %0s during a wait state", trans_name(p_trans), trans_name(htrans)));
          else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          else if (hwrite !== p_write || hsize !== p_size || hburst !== p_burst || hprot !== p_prot || hmastlock !== p_lock)
            report("hold", "HWRITE, HSIZE, HBURST, HPROT or HMASTLOCK changed during a wait state");
        end
      end
      if (hready === 1) begin
        // the address phase is accepted at this edge
        if (htrans === IDLE) begin
          end_burst();
          d_on = 1; d_idle = 1;
        end else if (htrans === NONSEQ) begin
          end_burst();
          check_beat();
          b_on = 1; b_err = 0; b_kb = 0; b_first = haddr; b_last = haddr; b_count = 1;
          b_burst = hburst; b_size = hsize; b_write = hwrite; b_prot = hprot;
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === SEQ) begin
          if (!b_on) report("trans_change", $sformatf("SEQ at 0x%0h with no burst in progress", haddr));
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (haddr !== exp_a) report("seq_addr", $sformatf("SEQ at 0x%0h; the beat after 0x%0h in a %0s burst of %0d-byte transfers is 0x%0h", haddr, b_last, burst_name(b_burst), 1 << b_size, exp_a));
            check_ctrl("SEQ");
            if (!is_wrap(b_burst) && !b_kb && (haddr / 1024) != (b_first / 1024)) begin
              b_kb = 1;
              report("kb1", $sformatf("SEQ at 0x%0h crosses the 1 KB boundary of the burst that started at 0x%0h", haddr, b_first));
            end
            b_count++;
            if (beats_of(b_burst) > 0 && b_count > beats_of(b_burst))
              report("burst_len", $sformatf("beat %0d of a %0s burst, which has %0d beats", b_count, burst_name(b_burst), beats_of(b_burst)));
            b_last = haddr;
          end
          check_beat();
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === BUSY) begin
          if (!b_on) report("trans_change", $sformatf("BUSY at 0x%0h with no burst in progress", haddr));
          else if (b_burst == SINGLE) report("burst_len", "BUSY after a SINGLE transfer");
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (beats_of(b_burst) > 1 && b_count == beats_of(b_burst))
              report("burst_len", $sformatf("BUSY after the last beat of the %0s burst (a fixed-length burst ends with SEQ)", burst_name(b_burst)));
            else if (haddr !== exp_a) report("seq_addr", $sformatf("BUSY at 0x%0h must show the next beat's address 0x%0h", haddr, exp_a));
            check_ctrl("BUSY");
          end
          d_on = 1; d_idle = 1;
        end
      end
      e_first = (hresp === 1 && hready === 0);
    end
    p_on = hresetn; p_ready = hready; p_resp = hresp; p_trans = htrans; p_addr = haddr; p_write = hwrite;
    p_size = hsize; p_burst = hburst; p_prot = hprot; p_lock = hmastlock;
  end
  final $display("checker: %0d transfers, %0d wait edges, %0d errors, %0d rules broken", transfers, waits, errors, broken);
endmodule
Read included file: ahb_tasks.svh
// ahb_tasks.svh: the manager side of the bench, as tasks for the stimulus (read-only).
// ahb_write, ahb_read and ahb_burst queue transfers; go() puts the queue on the bus and returns when
// the last data phase has ended. The driver below issues the next address phase at the same edge where
// the previous one is accepted, so the address of transfer N+1 is on the wires during the data phase
// of transfer N. Write data for a burst goes in wd[0], wd[1], ... (one entry per beat, the lowest byte
// at the lowest address); the data of every read comes back in rd[0], rd[1], ... in issue order, with
// the responses in rr[].
// The beat log has one line per accepted address phase (A), per ended data phase (D) and per transfer
// the manager cancels (X), each with the edge where it happened; watch = 1 adds one line per edge.
int cycle = -1;
always @(posedge hclk) cycle = cycle + 1;

localparam int QN = 512;
int unsigned q_addr [0:QN-1];
logic        q_write [0:QN-1];
logic [2:0]  q_size [0:QN-1];
logic [2:0]  q_burst [0:QN-1];
logic [1:0]  q_trans [0:QN-1];
logic [63:0] q_data [0:QN-1];
logic        q_lock [0:QN-1], q_excl [0:QN-1], q_sforce [0:QN-1];
logic [3:0]  q_prot [0:QN-1], q_master [0:QN-1];
logic [7:0]  q_strb [0:QN-1];
int          q_bn [0:QN-1], q_beat [0:QN-1], q_state [0:QN-1];   // burst number, beat, 0 queued 1 done 2 cancelled
logic [63:0] q_rdata [0:QN-1];
logic        q_resp [0:QN-1], q_exok [0:QN-1];
int q_n = 0, q_next = 0, bn = 0, last_start = 0, last_bn = 0;

logic [63:0] wd [0:255];
logic [63:0] rd [0:255];
logic        rr [0:255];
int          n_rd = 0;
logic        last_resp = 0, last_exokay = 0;
// quiet = 1 silences the beat log; watch = 1 prints the bus at every edge.
bit quiet = 0, watch = 0, running = 0, drv_done = 0;
// Driver switches (broken twins, off by default):
//   move_in_wait  the address moves on by one transfer size during a wait state
//   no_cancel     the transfer behind an ERROR is not cancelled (allowed, but the checker notes it)
// After the first cycle of an ERROR the driver cancels the transfer already in its address phase: the
// rest of the same burst is dropped; a separate transfer is issued again afterwards (reissue = 0 drops it).
//   busy_end      a BUSY after the last beat of every fixed-length burst
//   cross_1k      incrementing bursts are issued across a 1 KB boundary instead of being split
//   unaligned     transfers not aligned to their size are issued instead of refused
//   wrap_bug      a WRAP8 burst's addresses wrap inside 16 bytes, as if it were WRAP4
bit reissue = 1;
bit move_in_wait = 0, no_cancel = 0, busy_end = 0, cross_1k = 0, unaligned = 0, wrap_bug = 0;
// Attributes of the transfers queued next.
logic       lock = 0, excl = 0;
logic [3:0] prot = 4'b0011, master = 0;
bit         strb_force = 0;
logic [7:0] strb_val = 8'hff;
// What the bus is carrying in the data phase, for the waveform scenes: the address and lanes of the beat.
int unsigned beat_a = 0;
logic [7:0]  beat_m = 0;

function automatic int unsigned m_next(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
  if (wrap_bug && b == WRAP8) return next_addr(a, size, WRAP4);
  return next_addr(a, size, b);
endfunction

task automatic q_push(input int unsigned a, input logic w, input logic [2:0] size, input logic [2:0] b, input logic [1:0] t,
                      input logic [63:0] data, input int burst_no, input int beat);
  q_addr[q_n] = a; q_write[q_n] = w; q_size[q_n] = size; q_burst[q_n] = b; q_trans[q_n] = t; q_data[q_n] = data;
  q_lock[q_n] = lock; q_excl[q_n] = excl; q_prot[q_n] = prot; q_master[q_n] = master;
  q_sforce[q_n] = strb_force; q_strb[q_n] = strb_val;
  q_bn[q_n] = burst_no; q_beat[q_n] = beat; q_state[q_n] = 0; q_resp[q_n] = 0; q_exok[q_n] = 0; q_rdata[q_n] = 0;
  q_n = q_n + 1;
endtask

task automatic q_copy(input int dst, input int src);
  q_addr[dst] = q_addr[src]; q_write[dst] = q_write[src]; q_size[dst] = q_size[src]; q_burst[dst] = q_burst[src];
  q_trans[dst] = q_trans[src]; q_data[dst] = q_data[src]; q_lock[dst] = q_lock[src]; q_excl[dst] = q_excl[src];
  q_prot[dst] = q_prot[src]; q_master[dst] = q_master[src]; q_sforce[dst] = q_sforce[src]; q_strb[dst] = q_strb[src];
  q_bn[dst] = q_bn[src]; q_beat[dst] = q_beat[src]; q_state[dst] = q_state[src];
endtask

// One write of size bytes (0 = byte, 1 = halfword, 2 = word) at addr; data holds the bytes, lowest first.
task automatic ahb_write(input int unsigned addr, input logic [2:0] size, input logic [63:0] data);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 1, size, SINGLE, NONSEQ, data, bn, 0);
  end
endtask

// One read of size bytes at addr; the bytes come back in rd[] in issue order.
task automatic ahb_read(input int unsigned addr, input logic [2:0] size);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 0, size, SINGLE, NONSEQ, 0, bn, 0);
  end
endtask

// A burst: hburst SINGLE, INCR (beats gives its length), WRAP4, INCR4, ... INCR16. Write data from wd[].
// An incrementing burst that would cross a 1 KB boundary is issued as two INCR bursts split at the
// boundary (unless cross_1k is set).
task automatic ahb_burst(input logic w, input logic [2:0] hb, input int unsigned addr, input logic [2:0] size, input int beats = 0);
  int n;
  int unsigned a;
  logic [2:0] b;
  logic [1:0] t;
  bit split;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr; b = hb; split = 0;
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte burst at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    if (!cross_1k && crosses_1k(addr, size, hb, n)) begin
      split = 1; b = INCR;
      $display("note: %0s at 0x%0h would cross a 1 KB boundary; issued as INCR bursts split at the boundary", burst_name(hb), addr);
    end
    bn = bn + 1; last_start = q_n; last_bn = bn;
    for (int k = 0; k < n; k++) begin
      t = (k == 0) ? NONSEQ : SEQ;
      if (k > 0 && split && (a % 1024) == 0) begin t = NONSEQ; bn = bn + 1; end
      q_push(a, w, size, b, t, w ? wd[k] : 64'h0, bn, k);
      a = m_next(a, size, b);
    end
    if (busy_end && beats_of(b) > 1) q_push(a, w, size, b, BUSY, 64'h0, bn, n);
  end
endtask

// n BUSY cycles before beat k of the burst queued last (k = its length: after the last beat).
task automatic busy_at(input int k, input int n = 1);
  int idx, src;
  idx = q_n;
  for (int j = q_n - 1; j >= last_start; j--)
    if (q_trans[j][1] && q_beat[j] == k) idx = j;
  src = (idx < q_n) ? idx : q_n - 1;           // the beat the BUSY announces, or the last beat
  for (int j = q_n - 1; j >= idx; j--) q_copy(j + n, j);
  for (int j = 0; j < n; j++) begin
    q_copy(idx + j, src >= idx ? src + n : src);
    q_trans[idx + j] = BUSY; q_data[idx + j] = 0; q_beat[idx + j] = k;
    if (src < idx) q_addr[idx + j] = m_next(q_addr[src], q_size[src], q_burst[src]);
  end
  q_n = q_n + n;
endtask

// n IDLE cycles in the queue (the address and control keep their last values).
task automatic idle_q(input int n);
  for (int j = 0; j < n; j++)
    if (q_n > 0) q_push(q_addr[q_n - 1], q_write[q_n - 1], q_size[q_n - 1], q_burst[q_n - 1], IDLE, 64'h0, 0, 0);
    else q_push(0, 0, 0, SINGLE, IDLE, 64'h0, 0, 0);
endtask

// The driver: pick, hold, cancel.
int a_idx = -1, d_idx = -1;       // queue entries in the address phase and in the data phase (-1: none)
bit moved = 0;
logic        s_ready, s_resp, s_exok, s_write;
logic [DATA_W-1:0] s_rdata;
logic [31:0] s_addr;
logic [1:0]  s_trans;
logic [2:0]  s_size, s_burst;
int          s_done, s_acc, s_cfrom, s_cto, s_cmode;

task automatic complete(input int i);
  logic [15:0] lanes;
  logic [63:0] bytes;
  q_state[i] = 1; q_resp[i] = s_resp; q_exok[i] = s_exok; last_resp = s_resp; last_exokay = s_exok;
  if (!q_write[i]) begin
    lanes = beat_lanes(q_addr[i], q_size[i], BUS_BYTES);
    bytes = (s_rdata >> (8 * lanes[7:0])) & ((64'h1 << (8 * (lanes[15:8] - lanes[7:0] + 1))) - 1);
    q_rdata[i] = bytes; rd[n_rd] = bytes; rr[n_rd] = s_resp; n_rd = n_rd + 1;
  end
  if (!quiet) $display("e%0d D  %0s addr=0x%0h data=0x%0h %0s%0s", cycle, q_write[i] ? "write" : "read ", q_addr[i],
                       q_write[i] ? q_data[i] : q_rdata[i], resp_name(s_resp),
                       opt(q_excl[i], s_exok ? " EXOKAY" : " (HEXOKAY low: exclusive failed)"));
endtask

task automatic drive();
  logic [15:0] lanes;
  logic [63:0] s;
  int e;
  if (a_idx >= 0) begin
    e = a_idx;
    htrans = q_trans[e]; haddr = q_addr[e] + (moved ? (1 << q_size[e]) : 0); hwrite = q_write[e]; hsize = q_size[e];
    hburst = q_burst[e]; hprot = q_prot[e]; hmastlock = q_lock[e]; hexcl = q_excl[e]; hmaster = q_master[e];
  end else begin
    htrans = IDLE; hmastlock = 0; hexcl = 0;      // address and control keep their last values
  end
  if (d_idx >= 0 && q_write[d_idx]) begin
    lanes = beat_lanes(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    s = beat_strb(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    hwdata = q_data[d_idx] << (8 * lanes[7:0]);
    hwstrb = q_sforce[d_idx] ? q_strb[d_idx][BUS_BYTES-1:0] : s[BUS_BYTES-1:0];
    beat_a = q_addr[d_idx]; beat_m = hwstrb;
  end else begin
    hwdata = '0; hwstrb = '0; beat_m = 0;
  end
endtask

always @(posedge hclk) begin
  s_ready = hready; s_resp = hresp; s_exok = hexokay; s_rdata = hrdata; s_addr = haddr; s_trans = htrans;
  s_write = hwrite; s_size = hsize; s_burst = hburst;
  s_done = -1; s_acc = -1; s_cmode = 0;
  if (hresetn) begin
    if (s_ready === 1'b1) begin
      s_done = d_idx; d_idx = -1;
      if (a_idx >= 0 && moved) q_addr[a_idx] = s_addr;   // the address the subordinate took
      if (a_idx >= 0 && q_trans[a_idx] != IDLE) s_acc = a_idx;
      if (a_idx >= 0 && q_trans[a_idx][1]) d_idx = a_idx;
      a_idx = -1; moved = 0;
      if (running && q_next < q_n) begin a_idx = q_next; q_next = q_next + 1; end
    end else if (s_resp === 1'b1 && !no_cancel && a_idx >= 0 && q_trans[a_idx] != IDLE) begin
      // the first cycle of an ERROR: the transfer already on the wires is cancelled (IHI 0033C 5.1.3)
      s_cfrom = a_idx;
      if (d_idx >= 0 && q_bn[a_idx] == q_bn[d_idx]) begin
        s_cto = a_idx;
        while (s_cto < q_n && q_bn[s_cto] == q_bn[d_idx]) begin q_state[s_cto] = 2; s_cto = s_cto + 1; end
        q_next = s_cto; s_cmode = 1;               // the rest of the burst is dropped
      end else if (reissue) begin
        q_next = a_idx; s_cmode = 2;               // a separate transfer: issued again after the ERROR
      end else begin
        q_state[a_idx] = 2; s_cto = a_idx + 1; s_cmode = 3;   // a separate transfer, dropped
      end
      a_idx = -1;
    end else if (move_in_wait && a_idx >= 0 && q_trans[a_idx][1]) moved = 1;
  end
  #1;
  if (s_done >= 0) complete(s_done);
  if (s_acc >= 0 && !quiet)
    $display("e%0d A  %0s %0s addr=0x%0h size=%0d %0s%0s%0s", cycle, trans_name(s_trans), s_write ? "WRITE" : "READ", s_addr, s_size,
             burst_name(s_burst), opt(q_lock[s_acc], " locked"), opt(q_excl[s_acc], " exclusive"));
  if (s_cmode == 1 && !quiet)
    for (int j = s_cfrom; j < s_cto; j++)
      if (q_trans[j][1]) $display("e%0d X  cancelled %0s %0s addr=0x%0h (the rest of the burst is dropped)", cycle, trans_name(q_trans[j]), q_write[j] ? "WRITE" : "READ", q_addr[j]);
  if (s_cmode == 3 && !quiet)
    $display("e%0d X  cancelled %0s %0s addr=0x%0h", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  if (s_cmode == 2 && !quiet)
    $display("e%0d X  withdrew %0s %0s addr=0x%0h (issued again after the ERROR)", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  drive();
  drv_done = running && q_next >= q_n && a_idx < 0 && d_idx < 0;   // go() returns after this edge's log
end

// A text waveform for the lesson's trace blocks: with watch = 1, one line per rising edge with the
// values sampled at that edge (cycle k of a trace is what edge k sees).
int wave_edge = -1;
always @(posedge hclk) begin
  wave_edge = wave_edge + 1;
  if (watch) $display("wave e%0d: htrans=%0s haddr=%h hwrite=%b hsize=%0d hburst=%0s hwdata=%h hready=%b hresp=%b hrdata=%h",
                      wave_edge, trans_name(htrans), haddr, hwrite, hsize, burst_name(hburst), hwdata, hready, hresp, hrdata);
end

// Put the queue on the bus and wait until every transfer in it has ended; then empty the queue.
task automatic go();
  n_rd = 0; drv_done = 0; running = 1;
  wait (drv_done);
  running = 0; drv_done = 0; q_n = 0; q_next = 0;
endtask

// The table of a burst before it runs: the address, lanes and strobe of every beat.
task automatic beat_table(input int unsigned addr, input logic [2:0] size, input logic [2:0] hb, input int beats = 0);
  int n;
  int unsigned a, window, base;
  logic [15:0] lanes;
  logic [63:0] s;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr;
  $display("%0s at 0x%0h: %0d beat%0s of %0d byte%0s, bus %0d bytes", burst_name(hb), addr, n, opt(n > 1, "s"), 1 << size, opt(size != 0, "s"), BUS_BYTES);
  if (is_wrap(hb)) begin
    window = (1 << size) * beats_of(hb); base = (addr / window) * window;
    $display("  wraps inside 0x%0h-0x%0h (%0d beats x %0d bytes)", base, base + window - 1, beats_of(hb), 1 << size);
  end
  $display("  beat  address  lanes  strobe");
  for (int k = 0; k < n; k++) begin
    lanes = beat_lanes(a, size, BUS_BYTES);
    s = beat_strb(a, size, BUS_BYTES);
    $display("  %4d  0x%05h  %0d-%0d    %b", k, a[19:0], lanes[7:0], lanes[15:8], s[BUS_BYTES-1:0]);
    a = m_next(a, size, hb);
  end
  if (crosses_1k(addr, size, hb, n)) $display("  this burst crosses a 1 KB boundary");
  if (!aligned(addr, size)) $display("  0x%0h is not aligned to %0d bytes: AHB does not allow it", addr, 1 << size);
endtask

// One byte of the memory that owns an address (the fabric's second memory from 0x1000).
function automatic logic [7:0] mem_byte(input int unsigned a);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) return `AHB_MEM1.mem[a % `AHB_DEPTH];
  if (a >= 32'h1000) return 8'h00;
`endif
  return `AHB_MEM.mem[a % `AHB_DEPTH];
endfunction
task automatic set_byte(input int unsigned a, input logic [7:0] v);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) `AHB_MEM1.mem[a % `AHB_DEPTH] = v;
  else if (a < 32'h1000) `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`else
  `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`endif
endtask

// The memory, n bytes from addr, one bus word per group.
task automatic dump(input int unsigned addr, input int n);
  string line;
  int unsigned a = (addr / 16) * 16;
  while (a < addr + n) begin
    line = $sformatf("0x%05h ", a[19:0]);
    for (int i = 0; i < 16; i++) begin
      if (i % BUS_BYTES == 0) line = {line, " "};
      line = {line, $sformatf(" %02h", mem_byte(a + i))};
    end
    $display("%s", line);
    a = a + 16;
  end
endtask

// Fill n bytes from addr with first, first+1, ... so untouched bytes stay recognisable.
task automatic mem_fill(input int unsigned addr, input int n, input logic [7:0] first = 8'h00);
  for (int i = 0; i < n; i++) set_byte(addr + i, first + i[7:0]);
endtask

// Drive nothing new for n cycles.
task automatic idle(input int n);
  repeat (n) @(posedge hclk);
  #1;
endtask
Read included file: ahb_fabric.sv
`timescale 1ns/1ns
// ahb_fabric.sv: one manager, an address decoder, two memories, the default subordinate and the
// multiplexor that returns HRDATA, HRESP, HREADY and HEXOKAY (read-only in this lesson).
// The address map (every region starts and ends on a 1 KB boundary):
//   0x0000-0x0fff  m0, a 4 KB memory
//   0x1000-0x1fff  m1, a 4 KB memory
//   0x2000-0x23ff  the AHB-to-APB bridge (32-bit bus) and behind it the APB completer p0 of P3
//                  (RAM at 0x2000-0x20ff, registers at 0x2100-0x2110; switches fab.p0.waits and the rest)
//   anything else  the default subordinate: OKAY to IDLE and BUSY, the two-cycle ERROR to NONSEQ and SEQ
// The decoder selects in the address phase; the multiplexor must follow the data phase, so its select
// is registered at every edge where HREADY is 1. Switches: trace (print the routing at each accepted
// address phase); broken twins mux_addr_phase (the multiplexor follows the address phase select) and
// mux_no_hready (the select is registered at every edge, even while HREADY is low).

// The default subordinate: it answers the addresses no other subordinate owns.
module ahb_default (
  input  logic hclk,
  input  logic hresetn,
  input  logic hsel,
  input  logic [1:0] htrans,
  input  logic hready,
  output logic hreadyout,
  output logic hresp
);
  bit d_err = 0, e_two = 0;
  initial begin hreadyout = 1; hresp = 0; end
  always @(posedge hclk) begin
    if (!hresetn) begin
      d_err = 0; e_two = 0; hreadyout <= 1; hresp <= 0;
    end else begin
      if (d_err) begin
        if (!hreadyout) e_two = 1;
        else d_err = 0;
      end
      if (hsel && hready) begin d_err = htrans[1]; e_two = 0; end
      hreadyout <= !(d_err && !e_two);
      hresp <= d_err;
    end
  end
endmodule

module ahb_fabric #(parameter int DATA_W = 32) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  output logic [DATA_W-1:0] hrdata,
  output logic              hready,
  output logic              hresp,
  output logic              hexokay
);
  import ahb_pkg::*;
  bit trace = 0, mux_addr_phase = 0, mux_no_hready = 0;
  localparam bit HAS_APB = (DATA_W == 32);    // the AHB-to-APB bridge serves a 32-bit bus
  int cycle = -1;

  // ---- the decoder: HSELx in the address phase ----
  function automatic logic [1:0] region(input logic [31:0] a);
    if (a < 32'h1000) return 2'd0;
    if (a < 32'h2000) return 2'd1;
    if (HAS_APB && a < 32'h2400) return 2'd2;
    return 2'd3;
  endfunction
  logic [1:0] asel;
  assign asel = region(haddr);
  logic hsel0, hsel1, hsel2, hseld;
  assign hsel0 = (asel == 2'd0);
  assign hsel1 = (asel == 2'd1);
  assign hsel2 = (asel == 2'd2);
  assign hseld = (asel == 2'd3);

  // ---- the subordinates ----
  logic [DATA_W-1:0] rdata0, rdata1, rdata2;
  logic ready0, ready1, ready2, readyd, resp0, resp1, resp2, respd, exok0, exok1, exok2;
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m0 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel0), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready0), .hresp(resp0), .hrdata(rdata0), .hexokay(exok0));
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m1 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel1), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready1), .hresp(resp1), .hrdata(rdata1), .hexokay(exok1));
  // ---- SLOT 2: the AHB-to-APB bridge ----
  logic        psel, penable, pwrite, pready, pslverr;
  logic [31:0] paddr, pwdata, prdata, br_rdata;
  logic [3:0]  pstrb;
  logic [2:0]  pprot;
  assign exok2 = 1'b0;
  generate if (DATA_W == 32) begin : apb
    ahb_apb_bridge br (.hclk(hclk), .hresetn(hresetn), .hsel(hsel2), .haddr(haddr), .htrans(htrans), .hwrite(hwrite),
      .hprot(hprot), .hwdata(hwdata), .hwstrb(hwstrb), .hready(hready), .hreadyout(ready2), .hresp(resp2), .hrdata(br_rdata),
      .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr), .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot),
      .pready(pready), .prdata(prdata), .pslverr(pslverr));
    assign rdata2 = br_rdata;
  end else begin : no_apb
    assign ready2 = 1'b1; assign resp2 = 1'b0; assign rdata2 = '0;
    assign psel = 0; assign penable = 0; assign pwrite = 0; assign paddr = 0; assign pwdata = 0; assign pstrb = 0; assign pprot = 0;
  end endgenerate
  apb_periph #(.NAME("p0")) p0 (.pclk(hclk), .presetn(hresetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
    .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr));
  ahb_default dflt (.hclk(hclk), .hresetn(hresetn), .hsel(hseld), .htrans(htrans), .hready(hready), .hreadyout(readyd), .hresp(respd));

  // ---- the multiplexor: keyed by the data phase ----
  logic [1:0] dsel = 2'd3;
  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) dsel <= 2'd3;
    else if (hready || mux_no_hready) dsel <= asel;
    if (trace && hresetn && hready && htrans[1])
      $display("fab e%0d: %0s %0s 0x%0h -> %0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr,
               asel == 0 ? "m0" : asel == 1 ? "m1" : asel == 2 ? "apb" : "default (no subordinate)");
  end
  logic [1:0] msel;
  assign msel = mux_addr_phase ? asel : dsel;
  always @* begin
    case (msel)
      2'd0: begin hrdata = rdata0; hready = ready0; hresp = resp0; hexokay = exok0; end
      2'd1: begin hrdata = rdata1; hready = ready1; hresp = resp1; hexokay = exok1; end
      2'd2: begin hrdata = rdata2; hready = ready2; hresp = resp2; hexokay = exok2; end
      default: begin hrdata = '0; hready = readyd; hresp = respd; hexokay = 1'b0; end
    endcase
  end
endmodule
Read included file: ahb_apb_bridge.sv
`timescale 1ns/1ns
// ahb_apb_bridge.sv: an AHB subordinate that turns each accepted transfer into one APB transfer
// (read-only). The address phase is registered when HSEL, HREADY and a NONSEQ or SEQ transfer are
// all present; the data phase then runs SETUP and ACCESS on APB with HREADYOUT low, and ends in the
// cycle PREADY is 1: OKAY with HRDATA = PRDATA, or, when PSLVERR is 1, the two-cycle ERROR (HREADYOUT
// low then high, HRESP high in both). HWDATA and HWSTRB are data-phase signals the manager holds
// through the wait states, so PWDATA and PSTRB are driven from them directly; PADDR is the aligned
// word address (IHI 0024E 2.1.1 leaves an unaligned PADDR unpredictable). HPROT maps onto PPROT:
// privileged from HPROT[1], instruction from HPROT[0] = 0, and the access is Secure.
// A write therefore costs one setup and one access cycle: two cycles of data phase with no APB wait.
module ahb_apb_bridge (
  input  logic        hclk,
  input  logic        hresetn,
  input  logic        hsel,
  input  logic [31:0] haddr,
  input  logic [1:0]  htrans,
  input  logic        hwrite,
  input  logic [3:0]  hprot,
  input  logic [31:0] hwdata,
  input  logic [3:0]  hwstrb,
  input  logic        hready,
  output logic        hreadyout,
  output logic        hresp,
  output logic [31:0] hrdata,
  output logic        psel,
  output logic        penable,
  output logic        pwrite,
  output logic [31:0] paddr,
  output logic [31:0] pwdata,
  output logic [3:0]  pstrb,
  output logic [2:0]  pprot,
  input  logic        pready,
  input  logic [31:0] prdata,
  input  logic        pslverr);
  localparam int IDLE = 0, SETUP = 1, ACCESS = 2, ERR1 = 3, ERR2 = 4;
  bit trace = 0;
  int st = IDLE;
  logic        wr = 0;
  logic [31:0] a = 0;
  logic [2:0]  prot = 0;
  wire take = hsel && hready && htrans[1];
  assign psel      = (st == SETUP || st == ACCESS);
  assign penable   = (st == ACCESS);
  assign pwrite    = wr;
  assign paddr     = {a[31:2], 2'b00};     // APB leaves an unaligned PADDR unpredictable: the word, and PSTRB picks the bytes
  assign pwdata    = wr ? hwdata : 32'h0;
  assign pstrb     = wr ? hwstrb : 4'h0;
  assign pprot     = prot;
  assign hreadyout = (st == IDLE || st == ERR2) ? 1'b1 : (st == ACCESS) ? (pready === 1'b1 && pslverr !== 1'b1) : 1'b0;
  assign hresp     = (st == ERR1 || st == ERR2);
  assign hrdata    = (st == ACCESS) ? prdata : 32'h0;
  always @(posedge hclk or negedge hresetn) begin
    if (!hresetn) st <= IDLE;
    else begin
      case (st)
        SETUP:  st <= ACCESS;
        ACCESS: if (pready === 1'b1) begin
                  if (trace) $display("apb e%0d: %0s 0x%03h%0s", ($time - 5) / 10, wr ? "write" : "read", a[11:0], pslverr === 1'b1 ? " PSLVERR" : "");
                  st <= (pslverr === 1'b1) ? ERR1 : IDLE;
                end
        ERR1:   st <= ERR2;
        default: st <= IDLE;
      endcase
      // A new address phase can be taken whenever HREADY is 1: in IDLE, in the last cycle of ERROR,
      // or at the edge an access completes with OKAY.
      if (take && (st == IDLE || st == ERR2 || (st == ACCESS && pready === 1'b1 && pslverr !== 1'b1))) begin
        wr <= hwrite; a <= haddr; prot <= {~hprot[0], 1'b0, hprot[1]}; st <= SETUP;
      end
    end
  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

Check yourself

Predict the outputm1 takes HWDATA in the address phase, and inserts one wait state in the second transfer it takes. Two writes and two reads back, the beat log silenced. What does the checker print?
`timescale 1ns/1ps
module tb;
  import ahb_pkg::*;
  localparam int DATA_W = 32, BUS_BYTES = DATA_W / 8;
  logic hclk = 0, hresetn = 0;
  always #5 hclk = ~hclk;                 // rising edges at 5, 15, 25, ...
  logic [31:0]       haddr = 0;
  logic [1:0]        htrans = 0;
  logic              hwrite = 0, hmastlock = 0, hexcl = 0;
  logic [2:0]        hsize = 0, hburst = 0;
  logic [3:0]        hprot = 4'b0011, hmaster = 0;
  logic [DATA_W-1:0] hwdata = 0;
  logic [DATA_W/8-1:0] hwstrb = 0;
  logic [DATA_W-1:0] hrdata;
  logic              hready, hresp, hexokay;
  ahb_fabric #(.DATA_W(DATA_W)) fab (.hclk(hclk), .hresetn(hresetn), .haddr(haddr), .htrans(htrans), .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb), .hexcl(hexcl), .hmaster(hmaster), .hrdata(hrdata), .hready(hready), .hresp(hresp), .hexokay(hexokay));
  `define AHB_MEM fab.m0
  `define AHB_MEM1 fab.m1
  `define AHB_DEPTH 4096
  ahb_checker #(.DATA_W(DATA_W)) chk (.hclk(hclk), .hresetn(hresetn), .haddr(haddr), .htrans(htrans), .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hexcl(hexcl), .hready(hready), .hresp(hresp), .hrdata(hrdata), .hexokay(hexokay));
  `include "ahb_tasks.svh"

  initial begin

    repeat (2) @(posedge hclk); #1 hresetn = 1;   // reset released after edge 1
    @(posedge hclk); #1;                          // the first transfer may start after edge 2
    quiet = 1;
    fab.m1.wdata_in_addr = 1;          // m1 takes HWDATA in the address phase
    fab.m1.waits_at[1] = 1;            // and inserts one wait state in the second transfer it takes
    ahb_write(32'h1200, 2, 32'h11111111); ahb_write(32'h1204, 2, 32'h22222222);
    ahb_read(32'h1200, 2); ahb_read(32'h1204, 2);
    go();
    repeat (3) @(posedge hclk);
    $finish;
  end
endmodule
Read included file: ahb_pkg.sv
`timescale 1ns/1ns
// ahb_pkg.sv: the encodings and the arithmetic of a burst (read-only).
// The memory, the driver, the checker and the lesson text all use these functions, so they never
// disagree about the address of a beat or the lanes it uses.
package ahb_pkg;
  localparam logic [1:0] IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
  localparam logic [2:0] SINGLE = 3'b000, INCR = 3'b001, WRAP4 = 3'b010, INCR4 = 3'b011,
                         WRAP8 = 3'b100, INCR8 = 3'b101, WRAP16 = 3'b110, INCR16 = 3'b111;
  localparam logic OKAY = 1'b0, ERROR = 1'b1;

  function automatic string trans_name(input logic [1:0] t);
    case (t) IDLE: return "IDLE"; BUSY: return "BUSY"; NONSEQ: return "NONSEQ"; default: return "SEQ"; endcase
  endfunction
  function automatic string burst_name(input logic [2:0] b);
    case (b)
      SINGLE: return "SINGLE"; INCR: return "INCR"; WRAP4: return "WRAP4"; INCR4: return "INCR4";
      WRAP8: return "WRAP8"; INCR8: return "INCR8"; WRAP16: return "WRAP16"; default: return "INCR16";
    endcase
  endfunction
  function automatic string resp_name(input logic r);
    return r ? "ERROR" : "OKAY";
  endfunction
  // s when c is 1, else the empty string (a string-typed result prints the same on every simulator).
  function automatic string opt(input bit c, input string s);
    if (c) return s;
    return "";
  endfunction

  // Beats in a burst: 1 for SINGLE, 4, 8 or 16 for the fixed bursts, 0 for INCR (undefined length).
  function automatic int beats_of(input logic [2:0] b);
    case (b) SINGLE: return 1; INCR: return 0; WRAP4, INCR4: return 4; WRAP8, INCR8: return 8; default: return 16; endcase
  endfunction
  function automatic bit is_wrap(input logic [2:0] b);
    return b == WRAP4 || b == WRAP8 || b == WRAP16;
  endfunction

  // The address of the beat after the one at a: the size in bytes further on; a wrapping burst
  // returns to the start of its window (beats x size bytes, aligned to that total) at the window's end.
  function automatic int unsigned next_addr(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
    int unsigned nb = 1 << size;
    int unsigned window = nb * beats_of(b);
    int unsigned base;
    if (!is_wrap(b)) return a + nb;
    base = (a / window) * window;
    if (a + nb >= base + window) return base;
    return a + nb;
  endfunction

  // The address of beat n of a burst that starts at start.
  function automatic int unsigned beat_addr(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int unsigned a = start;
    for (int k = 0; k < n; k++) a = next_addr(a, size, b);
    return a;
  endfunction

  // The lanes a transfer at address a uses on a bus of bus_bytes bytes, packed as {upper, lower}.
  // Every AHB transfer is aligned to its size, so it uses size bytes from its own lane upwards.
  function automatic logic [15:0] beat_lanes(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    int lower = a % bus_bytes;
    int upper = lower + (1 << size) - 1;
    if (upper > bus_bytes - 1) upper = bus_bytes - 1;
    return {upper[7:0], lower[7:0]};
  endfunction

  // The strobe of a transfer: a 1 for every lane it uses (bit l is lane l).
  function automatic logic [63:0] beat_strb(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    logic [15:0] lanes = beat_lanes(a, size, bus_bytes);
    logic [63:0] s = 0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) s[l] = 1'b1;
    return s;
  endfunction

  function automatic bit aligned(input int unsigned a, input logic [2:0] size);
    return (a % (1 << size)) == 0;
  endfunction

  // True when an incrementing burst of n beats (n is used only for INCR) crosses a 1 KB boundary.
  function automatic bit crosses_1k(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int beats = beats_of(b) ? beats_of(b) : n;
    if (is_wrap(b) || beats < 2) return 0;
    return (start / 1024) != ((start + beats * (1 << size) - 1) / 1024);
  endfunction
endpackage
Read included file: ahb_mem.sv
`timescale 1ns/1ns
// ahb_mem.sv: an AHB subordinate with a byte-addressed memory behind it (read-only in this lesson).
// It registers an address phase at an edge where HSEL is 1, HTRANS is NONSEQ or SEQ and HREADY is 1,
// then runs the data phase: wait states with HREADYOUT low, a write takes HWDATA (and HWSTRB) at the
// edge that ends the data phase, a read drives HRDATA for the cycle that ends it. IDLE and BUSY get a
// zero-wait OKAY and change nothing. The switches below are plain variables: the stimulus sets them by
// name (mem.waits = 2;).
//   waits          wait states inserted in every data phase
//   waits_at[k]    wait states for the k-th transfer this memory takes (from 0; -1 = use waits)
//   err_above      transfers at or above this address get the two-cycle ERROR (and write nothing)
//   excl_on        AHB5 exclusive access: one monitor per HMASTER, HEXOKAY on success
//   trace          print every transfer the memory takes and every byte it writes
// Broken twins, each off by default:
//   ignore_hready    registers an address phase while HREADY is low (another subordinate is stretching),
//                    so it acts on a transfer the manager may still change or cancel
//   wdata_in_addr    takes HWDATA in the address phase, so a write stores the previous transfer's data
//   act_on_idle      treats IDLE and BUSY as transfers (waits, reads, writes HWDATA); this twin is an
//                    AHB-Lite design without an HWSTRB input, so it writes the lanes HSIZE selects
//   one_cycle_error  answers ERROR in one cycle (HRESP high with HREADYOUT high)
//   ignore_size      writes every lane of the bus word, whatever HSIZE and HWSTRB say
module ahb_mem #(parameter int DATA_W = 32, parameter int DEPTH = 65536) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic              hsel,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  input  logic              hready,
  output logic              hreadyout,
  output logic              hresp,
  output logic [DATA_W-1:0] hrdata,
  output logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;

  // ---- switches ----
  int waits = 0;
  int waits_at [0:63];
  int unsigned err_above = 32'hffff_ffff;
  bit excl_on = 0, trace = 0;
  bit ignore_hready = 0, wdata_in_addr = 0, act_on_idle = 0, one_cycle_error = 0, ignore_size = 0;

  // ---- the memory ----
  logic [7:0] mem [0:DEPTH-1];
  int cycle = -1;
  int taken = 0;                              // transfers taken so far

  // ---- the data phase in progress ----
  bit              d_valid = 0, d_write = 0, d_err = 0, d_exok = 0, d_xfail = 0;
  int unsigned     d_addr = 0;
  logic [2:0]      d_size = 0;
  logic [1:0]      d_trans = 0;
  logic [DATA_W-1:0] d_wd = 0;                // HWDATA seen in the address phase (the wdata_in_addr twin)
  int              d_cnt = 0;                 // wait states still to insert
  bit              e_two = 0;                 // the ERROR response is in its second cycle

  // ---- exclusive monitors, one per HMASTER ----
  bit          mon_use [0:15];
  int unsigned mon_addr [0:15];
  int          mon_bytes [0:15];

  initial begin
    for (int i = 0; i < DEPTH; i++) mem[i] = 8'h00;
    for (int k = 0; k < 64; k++) waits_at[k] = -1;
    for (int m = 0; m < 16; m++) mon_use[m] = 0;
    hreadyout = 1; hresp = 0; hrdata = 0; hexokay = 0;
  end

  function automatic logic [DATA_W-1:0] read_word(input int unsigned a);
    logic [DATA_W-1:0] w = 0;
    int unsigned word = (a / BUS) * BUS;
    for (int l = 0; l < BUS; l++) w[8*l +: 8] = mem[(word + l) % DEPTH];
    return w;
  endfunction

  // Write the lanes of one transfer: the lanes its address and size select, where the strobe is 1.
  task automatic write_bytes(input int unsigned a, input logic [2:0] size, input logic [DATA_W-1:0] data, input logic [BUS-1:0] strb);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    int unsigned word = (a / BUS) * BUS;
    int lo = ignore_size ? 0 : lanes[7:0];
    int hi = ignore_size ? BUS - 1 : lanes[15:8];
    for (int l = lo; l <= hi; l++)
      if (ignore_size || act_on_idle || strb[l]) begin
        mem[(word + l) % DEPTH] = data[8*l +: 8];
        if (trace) $display("mem e%0d:   byte 0x%0h <= %02h (lane %0d)", cycle, word + l, data[8*l +: 8], l);
      end
    // any write ends the exclusive monitors that cover one of its bytes
    for (int m = 0; m < 16; m++)
      if (mon_use[m] && a < mon_addr[m] + mon_bytes[m] && mon_addr[m] < a + (1 << size)) mon_use[m] = 0;
  endtask

  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) begin
      d_valid = 0; e_two = 0;
      hreadyout <= 1; hresp <= 0; hexokay <= 0;
    end else begin
      // 1. the data phase that ends or waits at this edge
      if (d_valid) begin
        if (hreadyout && hready) begin
          if (d_write && !d_err && !d_xfail)
            write_bytes(d_addr, d_size, wdata_in_addr ? d_wd : hwdata, hwstrb);
          d_valid = 0;
        end else if (!hreadyout) begin
          if (d_cnt > 0) d_cnt = d_cnt - 1;
          else if (d_err) e_two = 1;
        end
      end
      // 2. the address phase on the wires at this edge
      if (hsel && (htrans[1] || act_on_idle) && (hready || (ignore_hready && !d_valid))) begin
        d_valid = 1; d_addr = haddr; d_write = hwrite; d_size = hsize; d_trans = htrans; d_wd = hwdata;
        d_cnt = (taken < 64 && waits_at[taken] >= 0) ? waits_at[taken] : waits;
        taken = taken + 1;
        d_err = (haddr >= err_above); e_two = 0;
        d_exok = 0; d_xfail = 0;
        if (excl_on && hexcl && htrans[1] && !d_err) begin
          if (!hwrite) begin
            mon_use[hmaster] = 1; mon_addr[hmaster] = haddr; mon_bytes[hmaster] = 1 << hsize; d_exok = 1;
          end else if (mon_use[hmaster] && mon_addr[hmaster] == haddr && mon_bytes[hmaster] == (1 << hsize)) begin
            d_exok = 1; mon_use[hmaster] = 0;
          end else d_xfail = 1;
        end
        if (trace) $display("mem e%0d: %0s %0s 0x%0h size %0d%0s%0s%0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr, hsize,
                            opt(!hready, " (taken while HREADY is low)"), opt(d_err, " -> ERROR"),
                            opt(d_xfail, " -> exclusive failed"));
      end
      // 3. the response for the cycle that starts now
      if (d_valid && d_cnt > 0) begin
        hreadyout <= 0; hresp <= 0; hexokay <= 0;
      end else if (d_valid && d_err) begin
        hreadyout <= one_cycle_error ? 1'b1 : e_two; hresp <= 1; hexokay <= 0;
      end else begin
        hreadyout <= 1; hresp <= 0; hexokay <= d_valid && d_exok;
      end
      if (d_valid && !d_write) hrdata <= d_err ? '0 : read_word(d_addr);
    end
  end
endmodule
Read included file: ahb_checker.sv
`timescale 1ns/1ns
// ahb_checker.sv: the rules of an AHB interface, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
//   reset         HTRANS is not IDLE, or HREADY is low, while reset is active;
//   hold          the address or control changed during a wait state (HREADY low), outside the
//                 changes the protocol allows;
//   trans_change  HTRANS changed during a wait state other than IDLE to NONSEQ, BUSY to SEQ (fixed
//                 bursts) or BUSY to anything (INCR); or SEQ/BUSY with no burst in progress;
//   seq_addr      a SEQ or BUSY address that is not the previous beat's address plus the size
//                 (wrapped for a WRAP burst);
//   burst_ctrl    HSIZE, HBURST, HWRITE or HPROT changed inside a burst;
//   align         a transfer not aligned to its size;
//   size_bus      a transfer wider than the data bus;
//   kb1           an incrementing burst that crosses a 1 KB boundary;
//   burst_len     a fixed-length burst that ends early or runs long, BUSY after a SINGLE, or BUSY
//                 after the last beat of a fixed-length burst;
//   idle_okay     an IDLE or BUSY transfer that did not get a zero-wait OKAY;
//   err_shape     an ERROR that is not two cycles (HREADY low then high, HRESP high in both);
//   exokay        HEXOKAY with HREADY low, with an ERROR, or for a transfer that was not exclusive;
//   known         x on the address, control, or the active lanes of the data.
// A NOTE (not a broken rule) marks a transfer kept after the first cycle of an ERROR: the protocol
// allows it, most managers cancel.
module ahb_checker #(parameter int DATA_W = 32) (
  input logic              hclk,
  input logic              hresetn,
  input logic [31:0]       haddr,
  input logic [1:0]        htrans,
  input logic              hwrite,
  input logic [2:0]        hsize,
  input logic [2:0]        hburst,
  input logic [3:0]        hprot,
  input logic              hmastlock,
  input logic [DATA_W-1:0] hwdata,
  input logic              hexcl,
  input logic              hready,
  input logic              hresp,
  input logic [DATA_W-1:0] hrdata,
  input logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;
  int cycle = -1, transfers = 0, waits = 0, errors = 0, broken = 0, notes = 0;

  function void report(input string rule, input string what);
    broken++;
    $display("RULE %s broken at edge %0d: %s", rule, cycle, what);
  endfunction

  function automatic logic [DATA_W-1:0] lane_mask(input int unsigned a, input logic [2:0] size);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    logic [DATA_W-1:0] m = '0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) m[8*l +: 8] = 8'hff;
    return m;
  endfunction

  // The previous edge.
  bit          p_on = 0;
  logic        p_ready = 1, p_resp = 0, p_write = 0, p_lock = 0;
  logic [1:0]  p_trans = 0;
  logic [31:0] p_addr = 0;
  logic [2:0]  p_size = 0, p_burst = 0;
  logic [3:0]  p_prot = 0;
  bit          e_first = 0;           // the previous edge was the first cycle of an ERROR
  // The data phase in progress.
  bit          d_on = 0, d_idle = 0, d_write = 0, d_excl = 0;
  int unsigned d_addr = 0;
  logic [2:0]  d_size = 0;
  // The burst in progress.
  bit          b_on = 0, b_err = 0, b_write = 0, b_kb = 0;
  logic [2:0]  b_burst = 0, b_size = 0;
  logic [3:0]  b_prot = 0;
  int unsigned b_first = 0, b_last = 0;
  int          b_count = 0;
  int unsigned exp_a;

  task automatic end_burst();
    if (b_on && beats_of(b_burst) > 1 && b_count < beats_of(b_burst) && !b_err)
      report("burst_len", $sformatf("%0s burst at 0x%0h ended after %0d of %0d beats", burst_name(b_burst), b_first, b_count, beats_of(b_burst)));
    b_on = 0;
  endtask

  task automatic check_beat();
    if ((1 << hsize) > BUS) report("size_bus", $sformatf("a %0d-byte transfer on a %0d-byte bus", 1 << hsize, BUS));
    if (!aligned(haddr, hsize)) report("align", $sformatf("a %0d-byte transfer at 0x%0h is not aligned to its size", 1 << hsize, haddr));
  endtask

  task automatic check_ctrl(input string what);
    if (hsize !== b_size || hburst !== b_burst || hwrite !== b_write || hprot !== b_prot)
      report("burst_ctrl", $sformatf("%0s at 0x%0h changed HSIZE, HBURST, HWRITE or HPROT inside the burst", what, haddr));
  endtask

  always @(posedge hclk) begin
    cycle++;
    if (!hresetn) begin
      if (htrans !== IDLE) report("reset", "HTRANS is not IDLE during reset");
      if (hready !== 1) report("reset", "HREADY is low during reset");
      p_on = 0; d_on = 0; b_on = 0; e_first = 0;
    end else begin
      // ---- the data phase that ends or waits at this edge ----
      if (hready === 0) waits++;
      if (e_first) begin
        if (hresp !== 1 || hready !== 1) report("err_shape", "the second cycle of an ERROR must have HRESP high and HREADY high");
      end else if (hresp === 1 && hready === 1)
        report("err_shape", "ERROR with HREADY high in its first cycle: an ERROR takes two cycles");
      if (hresp === 1 && b_on) b_err = 1;
      if (d_on && d_idle) begin
        if (hready !== 1 || hresp !== 0) report("idle_okay", "an IDLE or BUSY transfer must get a zero-wait OKAY");
        d_on = 0;
      end
      if (hexokay === 1) begin
        if (hready !== 1) report("exokay", "HEXOKAY while HREADY is low");
        else if (hresp === 1) report("exokay", "HEXOKAY together with an ERROR");
        else if (!(d_on && d_excl)) report("exokay", "HEXOKAY for a transfer that was not exclusive");
      end
      if (hready === 1 && d_on && !d_idle) begin
        if (d_write && $isunknown(hwdata & lane_mask(d_addr, d_size))) report("known", "HWDATA is x on an active lane");
        if (!d_write && hresp === 0 && $isunknown(hrdata & lane_mask(d_addr, d_size))) report("known", "HRDATA is x on an active lane");
        transfers++;
        if (hresp === 1) errors++;
        d_on = 0;
      end
      if (e_first && hready === 1 && (htrans === NONSEQ || htrans === SEQ)) begin
        notes++;
        $display("NOTE cancel at edge %0d: the %0s at 0x%0h behind an ERROR was kept, not cancelled (allowed; most managers cancel)", cycle, trans_name(htrans), haddr);
      end
      // ---- the address phase on the wires at this edge ----
      if (htrans !== IDLE && ($isunknown(haddr) || $isunknown(hwrite) || $isunknown(hsize) || $isunknown(hburst)))
        report("known", "address or control is x while HTRANS is not IDLE");
      if ($isunknown(htrans)) report("known", "HTRANS is x");
      if (p_on && p_ready === 0 && !e_first) begin
        // a wait state at the previous edge: only the allowed changes
        if (p_trans == IDLE) begin
          if (htrans === BUSY || htrans === SEQ) report("trans_change", $sformatf("IDLE changed to %0s during a wait state (only NONSEQ may follow)", trans_name(htrans)));
        end else if (p_trans == BUSY) begin
          if (p_burst != INCR) begin
            if (htrans !== BUSY && htrans !== SEQ) report("trans_change", $sformatf("BUSY in a %0s burst changed to %0s during a wait state (only SEQ may follow)", burst_name(p_burst), trans_name(htrans)));
            else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          end
        end else begin
          if (htrans !== p_trans) report("trans_change", $sformatf("%0s changed to %0s during a wait state", trans_name(p_trans), trans_name(htrans)));
          else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          else if (hwrite !== p_write || hsize !== p_size || hburst !== p_burst || hprot !== p_prot || hmastlock !== p_lock)
            report("hold", "HWRITE, HSIZE, HBURST, HPROT or HMASTLOCK changed during a wait state");
        end
      end
      if (hready === 1) begin
        // the address phase is accepted at this edge
        if (htrans === IDLE) begin
          end_burst();
          d_on = 1; d_idle = 1;
        end else if (htrans === NONSEQ) begin
          end_burst();
          check_beat();
          b_on = 1; b_err = 0; b_kb = 0; b_first = haddr; b_last = haddr; b_count = 1;
          b_burst = hburst; b_size = hsize; b_write = hwrite; b_prot = hprot;
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === SEQ) begin
          if (!b_on) report("trans_change", $sformatf("SEQ at 0x%0h with no burst in progress", haddr));
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (haddr !== exp_a) report("seq_addr", $sformatf("SEQ at 0x%0h; the beat after 0x%0h in a %0s burst of %0d-byte transfers is 0x%0h", haddr, b_last, burst_name(b_burst), 1 << b_size, exp_a));
            check_ctrl("SEQ");
            if (!is_wrap(b_burst) && !b_kb && (haddr / 1024) != (b_first / 1024)) begin
              b_kb = 1;
              report("kb1", $sformatf("SEQ at 0x%0h crosses the 1 KB boundary of the burst that started at 0x%0h", haddr, b_first));
            end
            b_count++;
            if (beats_of(b_burst) > 0 && b_count > beats_of(b_burst))
              report("burst_len", $sformatf("beat %0d of a %0s burst, which has %0d beats", b_count, burst_name(b_burst), beats_of(b_burst)));
            b_last = haddr;
          end
          check_beat();
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === BUSY) begin
          if (!b_on) report("trans_change", $sformatf("BUSY at 0x%0h with no burst in progress", haddr));
          else if (b_burst == SINGLE) report("burst_len", "BUSY after a SINGLE transfer");
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (beats_of(b_burst) > 1 && b_count == beats_of(b_burst))
              report("burst_len", $sformatf("BUSY after the last beat of the %0s burst (a fixed-length burst ends with SEQ)", burst_name(b_burst)));
            else if (haddr !== exp_a) report("seq_addr", $sformatf("BUSY at 0x%0h must show the next beat's address 0x%0h", haddr, exp_a));
            check_ctrl("BUSY");
          end
          d_on = 1; d_idle = 1;
        end
      end
      e_first = (hresp === 1 && hready === 0);
    end
    p_on = hresetn; p_ready = hready; p_resp = hresp; p_trans = htrans; p_addr = haddr; p_write = hwrite;
    p_size = hsize; p_burst = hburst; p_prot = hprot; p_lock = hmastlock;
  end
  final $display("checker: %0d transfers, %0d wait edges, %0d errors, %0d rules broken", transfers, waits, errors, broken);
endmodule
Read included file: ahb_tasks.svh
// ahb_tasks.svh: the manager side of the bench, as tasks for the stimulus (read-only).
// ahb_write, ahb_read and ahb_burst queue transfers; go() puts the queue on the bus and returns when
// the last data phase has ended. The driver below issues the next address phase at the same edge where
// the previous one is accepted, so the address of transfer N+1 is on the wires during the data phase
// of transfer N. Write data for a burst goes in wd[0], wd[1], ... (one entry per beat, the lowest byte
// at the lowest address); the data of every read comes back in rd[0], rd[1], ... in issue order, with
// the responses in rr[].
// The beat log has one line per accepted address phase (A), per ended data phase (D) and per transfer
// the manager cancels (X), each with the edge where it happened; watch = 1 adds one line per edge.
int cycle = -1;
always @(posedge hclk) cycle = cycle + 1;

localparam int QN = 512;
int unsigned q_addr [0:QN-1];
logic        q_write [0:QN-1];
logic [2:0]  q_size [0:QN-1];
logic [2:0]  q_burst [0:QN-1];
logic [1:0]  q_trans [0:QN-1];
logic [63:0] q_data [0:QN-1];
logic        q_lock [0:QN-1], q_excl [0:QN-1], q_sforce [0:QN-1];
logic [3:0]  q_prot [0:QN-1], q_master [0:QN-1];
logic [7:0]  q_strb [0:QN-1];
int          q_bn [0:QN-1], q_beat [0:QN-1], q_state [0:QN-1];   // burst number, beat, 0 queued 1 done 2 cancelled
logic [63:0] q_rdata [0:QN-1];
logic        q_resp [0:QN-1], q_exok [0:QN-1];
int q_n = 0, q_next = 0, bn = 0, last_start = 0, last_bn = 0;

logic [63:0] wd [0:255];
logic [63:0] rd [0:255];
logic        rr [0:255];
int          n_rd = 0;
logic        last_resp = 0, last_exokay = 0;
// quiet = 1 silences the beat log; watch = 1 prints the bus at every edge.
bit quiet = 0, watch = 0, running = 0, drv_done = 0;
// Driver switches (broken twins, off by default):
//   move_in_wait  the address moves on by one transfer size during a wait state
//   no_cancel     the transfer behind an ERROR is not cancelled (allowed, but the checker notes it)
// After the first cycle of an ERROR the driver cancels the transfer already in its address phase: the
// rest of the same burst is dropped; a separate transfer is issued again afterwards (reissue = 0 drops it).
//   busy_end      a BUSY after the last beat of every fixed-length burst
//   cross_1k      incrementing bursts are issued across a 1 KB boundary instead of being split
//   unaligned     transfers not aligned to their size are issued instead of refused
//   wrap_bug      a WRAP8 burst's addresses wrap inside 16 bytes, as if it were WRAP4
bit reissue = 1;
bit move_in_wait = 0, no_cancel = 0, busy_end = 0, cross_1k = 0, unaligned = 0, wrap_bug = 0;
// Attributes of the transfers queued next.
logic       lock = 0, excl = 0;
logic [3:0] prot = 4'b0011, master = 0;
bit         strb_force = 0;
logic [7:0] strb_val = 8'hff;
// What the bus is carrying in the data phase, for the waveform scenes: the address and lanes of the beat.
int unsigned beat_a = 0;
logic [7:0]  beat_m = 0;

function automatic int unsigned m_next(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
  if (wrap_bug && b == WRAP8) return next_addr(a, size, WRAP4);
  return next_addr(a, size, b);
endfunction

task automatic q_push(input int unsigned a, input logic w, input logic [2:0] size, input logic [2:0] b, input logic [1:0] t,
                      input logic [63:0] data, input int burst_no, input int beat);
  q_addr[q_n] = a; q_write[q_n] = w; q_size[q_n] = size; q_burst[q_n] = b; q_trans[q_n] = t; q_data[q_n] = data;
  q_lock[q_n] = lock; q_excl[q_n] = excl; q_prot[q_n] = prot; q_master[q_n] = master;
  q_sforce[q_n] = strb_force; q_strb[q_n] = strb_val;
  q_bn[q_n] = burst_no; q_beat[q_n] = beat; q_state[q_n] = 0; q_resp[q_n] = 0; q_exok[q_n] = 0; q_rdata[q_n] = 0;
  q_n = q_n + 1;
endtask

task automatic q_copy(input int dst, input int src);
  q_addr[dst] = q_addr[src]; q_write[dst] = q_write[src]; q_size[dst] = q_size[src]; q_burst[dst] = q_burst[src];
  q_trans[dst] = q_trans[src]; q_data[dst] = q_data[src]; q_lock[dst] = q_lock[src]; q_excl[dst] = q_excl[src];
  q_prot[dst] = q_prot[src]; q_master[dst] = q_master[src]; q_sforce[dst] = q_sforce[src]; q_strb[dst] = q_strb[src];
  q_bn[dst] = q_bn[src]; q_beat[dst] = q_beat[src]; q_state[dst] = q_state[src];
endtask

// One write of size bytes (0 = byte, 1 = halfword, 2 = word) at addr; data holds the bytes, lowest first.
task automatic ahb_write(input int unsigned addr, input logic [2:0] size, input logic [63:0] data);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 1, size, SINGLE, NONSEQ, data, bn, 0);
  end
endtask

// One read of size bytes at addr; the bytes come back in rd[] in issue order.
task automatic ahb_read(input int unsigned addr, input logic [2:0] size);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 0, size, SINGLE, NONSEQ, 0, bn, 0);
  end
endtask

// A burst: hburst SINGLE, INCR (beats gives its length), WRAP4, INCR4, ... INCR16. Write data from wd[].
// An incrementing burst that would cross a 1 KB boundary is issued as two INCR bursts split at the
// boundary (unless cross_1k is set).
task automatic ahb_burst(input logic w, input logic [2:0] hb, input int unsigned addr, input logic [2:0] size, input int beats = 0);
  int n;
  int unsigned a;
  logic [2:0] b;
  logic [1:0] t;
  bit split;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr; b = hb; split = 0;
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte burst at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    if (!cross_1k && crosses_1k(addr, size, hb, n)) begin
      split = 1; b = INCR;
      $display("note: %0s at 0x%0h would cross a 1 KB boundary; issued as INCR bursts split at the boundary", burst_name(hb), addr);
    end
    bn = bn + 1; last_start = q_n; last_bn = bn;
    for (int k = 0; k < n; k++) begin
      t = (k == 0) ? NONSEQ : SEQ;
      if (k > 0 && split && (a % 1024) == 0) begin t = NONSEQ; bn = bn + 1; end
      q_push(a, w, size, b, t, w ? wd[k] : 64'h0, bn, k);
      a = m_next(a, size, b);
    end
    if (busy_end && beats_of(b) > 1) q_push(a, w, size, b, BUSY, 64'h0, bn, n);
  end
endtask

// n BUSY cycles before beat k of the burst queued last (k = its length: after the last beat).
task automatic busy_at(input int k, input int n = 1);
  int idx, src;
  idx = q_n;
  for (int j = q_n - 1; j >= last_start; j--)
    if (q_trans[j][1] && q_beat[j] == k) idx = j;
  src = (idx < q_n) ? idx : q_n - 1;           // the beat the BUSY announces, or the last beat
  for (int j = q_n - 1; j >= idx; j--) q_copy(j + n, j);
  for (int j = 0; j < n; j++) begin
    q_copy(idx + j, src >= idx ? src + n : src);
    q_trans[idx + j] = BUSY; q_data[idx + j] = 0; q_beat[idx + j] = k;
    if (src < idx) q_addr[idx + j] = m_next(q_addr[src], q_size[src], q_burst[src]);
  end
  q_n = q_n + n;
endtask

// n IDLE cycles in the queue (the address and control keep their last values).
task automatic idle_q(input int n);
  for (int j = 0; j < n; j++)
    if (q_n > 0) q_push(q_addr[q_n - 1], q_write[q_n - 1], q_size[q_n - 1], q_burst[q_n - 1], IDLE, 64'h0, 0, 0);
    else q_push(0, 0, 0, SINGLE, IDLE, 64'h0, 0, 0);
endtask

// The driver: pick, hold, cancel.
int a_idx = -1, d_idx = -1;       // queue entries in the address phase and in the data phase (-1: none)
bit moved = 0;
logic        s_ready, s_resp, s_exok, s_write;
logic [DATA_W-1:0] s_rdata;
logic [31:0] s_addr;
logic [1:0]  s_trans;
logic [2:0]  s_size, s_burst;
int          s_done, s_acc, s_cfrom, s_cto, s_cmode;

task automatic complete(input int i);
  logic [15:0] lanes;
  logic [63:0] bytes;
  q_state[i] = 1; q_resp[i] = s_resp; q_exok[i] = s_exok; last_resp = s_resp; last_exokay = s_exok;
  if (!q_write[i]) begin
    lanes = beat_lanes(q_addr[i], q_size[i], BUS_BYTES);
    bytes = (s_rdata >> (8 * lanes[7:0])) & ((64'h1 << (8 * (lanes[15:8] - lanes[7:0] + 1))) - 1);
    q_rdata[i] = bytes; rd[n_rd] = bytes; rr[n_rd] = s_resp; n_rd = n_rd + 1;
  end
  if (!quiet) $display("e%0d D  %0s addr=0x%0h data=0x%0h %0s%0s", cycle, q_write[i] ? "write" : "read ", q_addr[i],
                       q_write[i] ? q_data[i] : q_rdata[i], resp_name(s_resp),
                       opt(q_excl[i], s_exok ? " EXOKAY" : " (HEXOKAY low: exclusive failed)"));
endtask

task automatic drive();
  logic [15:0] lanes;
  logic [63:0] s;
  int e;
  if (a_idx >= 0) begin
    e = a_idx;
    htrans = q_trans[e]; haddr = q_addr[e] + (moved ? (1 << q_size[e]) : 0); hwrite = q_write[e]; hsize = q_size[e];
    hburst = q_burst[e]; hprot = q_prot[e]; hmastlock = q_lock[e]; hexcl = q_excl[e]; hmaster = q_master[e];
  end else begin
    htrans = IDLE; hmastlock = 0; hexcl = 0;      // address and control keep their last values
  end
  if (d_idx >= 0 && q_write[d_idx]) begin
    lanes = beat_lanes(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    s = beat_strb(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    hwdata = q_data[d_idx] << (8 * lanes[7:0]);
    hwstrb = q_sforce[d_idx] ? q_strb[d_idx][BUS_BYTES-1:0] : s[BUS_BYTES-1:0];
    beat_a = q_addr[d_idx]; beat_m = hwstrb;
  end else begin
    hwdata = '0; hwstrb = '0; beat_m = 0;
  end
endtask

always @(posedge hclk) begin
  s_ready = hready; s_resp = hresp; s_exok = hexokay; s_rdata = hrdata; s_addr = haddr; s_trans = htrans;
  s_write = hwrite; s_size = hsize; s_burst = hburst;
  s_done = -1; s_acc = -1; s_cmode = 0;
  if (hresetn) begin
    if (s_ready === 1'b1) begin
      s_done = d_idx; d_idx = -1;
      if (a_idx >= 0 && moved) q_addr[a_idx] = s_addr;   // the address the subordinate took
      if (a_idx >= 0 && q_trans[a_idx] != IDLE) s_acc = a_idx;
      if (a_idx >= 0 && q_trans[a_idx][1]) d_idx = a_idx;
      a_idx = -1; moved = 0;
      if (running && q_next < q_n) begin a_idx = q_next; q_next = q_next + 1; end
    end else if (s_resp === 1'b1 && !no_cancel && a_idx >= 0 && q_trans[a_idx] != IDLE) begin
      // the first cycle of an ERROR: the transfer already on the wires is cancelled (IHI 0033C 5.1.3)
      s_cfrom = a_idx;
      if (d_idx >= 0 && q_bn[a_idx] == q_bn[d_idx]) begin
        s_cto = a_idx;
        while (s_cto < q_n && q_bn[s_cto] == q_bn[d_idx]) begin q_state[s_cto] = 2; s_cto = s_cto + 1; end
        q_next = s_cto; s_cmode = 1;               // the rest of the burst is dropped
      end else if (reissue) begin
        q_next = a_idx; s_cmode = 2;               // a separate transfer: issued again after the ERROR
      end else begin
        q_state[a_idx] = 2; s_cto = a_idx + 1; s_cmode = 3;   // a separate transfer, dropped
      end
      a_idx = -1;
    end else if (move_in_wait && a_idx >= 0 && q_trans[a_idx][1]) moved = 1;
  end
  #1;
  if (s_done >= 0) complete(s_done);
  if (s_acc >= 0 && !quiet)
    $display("e%0d A  %0s %0s addr=0x%0h size=%0d %0s%0s%0s", cycle, trans_name(s_trans), s_write ? "WRITE" : "READ", s_addr, s_size,
             burst_name(s_burst), opt(q_lock[s_acc], " locked"), opt(q_excl[s_acc], " exclusive"));
  if (s_cmode == 1 && !quiet)
    for (int j = s_cfrom; j < s_cto; j++)
      if (q_trans[j][1]) $display("e%0d X  cancelled %0s %0s addr=0x%0h (the rest of the burst is dropped)", cycle, trans_name(q_trans[j]), q_write[j] ? "WRITE" : "READ", q_addr[j]);
  if (s_cmode == 3 && !quiet)
    $display("e%0d X  cancelled %0s %0s addr=0x%0h", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  if (s_cmode == 2 && !quiet)
    $display("e%0d X  withdrew %0s %0s addr=0x%0h (issued again after the ERROR)", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  drive();
  drv_done = running && q_next >= q_n && a_idx < 0 && d_idx < 0;   // go() returns after this edge's log
end

// A text waveform for the lesson's trace blocks: with watch = 1, one line per rising edge with the
// values sampled at that edge (cycle k of a trace is what edge k sees).
int wave_edge = -1;
always @(posedge hclk) begin
  wave_edge = wave_edge + 1;
  if (watch) $display("wave e%0d: htrans=%0s haddr=%h hwrite=%b hsize=%0d hburst=%0s hwdata=%h hready=%b hresp=%b hrdata=%h",
                      wave_edge, trans_name(htrans), haddr, hwrite, hsize, burst_name(hburst), hwdata, hready, hresp, hrdata);
end

// Put the queue on the bus and wait until every transfer in it has ended; then empty the queue.
task automatic go();
  n_rd = 0; drv_done = 0; running = 1;
  wait (drv_done);
  running = 0; drv_done = 0; q_n = 0; q_next = 0;
endtask

// The table of a burst before it runs: the address, lanes and strobe of every beat.
task automatic beat_table(input int unsigned addr, input logic [2:0] size, input logic [2:0] hb, input int beats = 0);
  int n;
  int unsigned a, window, base;
  logic [15:0] lanes;
  logic [63:0] s;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr;
  $display("%0s at 0x%0h: %0d beat%0s of %0d byte%0s, bus %0d bytes", burst_name(hb), addr, n, opt(n > 1, "s"), 1 << size, opt(size != 0, "s"), BUS_BYTES);
  if (is_wrap(hb)) begin
    window = (1 << size) * beats_of(hb); base = (addr / window) * window;
    $display("  wraps inside 0x%0h-0x%0h (%0d beats x %0d bytes)", base, base + window - 1, beats_of(hb), 1 << size);
  end
  $display("  beat  address  lanes  strobe");
  for (int k = 0; k < n; k++) begin
    lanes = beat_lanes(a, size, BUS_BYTES);
    s = beat_strb(a, size, BUS_BYTES);
    $display("  %4d  0x%05h  %0d-%0d    %b", k, a[19:0], lanes[7:0], lanes[15:8], s[BUS_BYTES-1:0]);
    a = m_next(a, size, hb);
  end
  if (crosses_1k(addr, size, hb, n)) $display("  this burst crosses a 1 KB boundary");
  if (!aligned(addr, size)) $display("  0x%0h is not aligned to %0d bytes: AHB does not allow it", addr, 1 << size);
endtask

// One byte of the memory that owns an address (the fabric's second memory from 0x1000).
function automatic logic [7:0] mem_byte(input int unsigned a);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) return `AHB_MEM1.mem[a % `AHB_DEPTH];
  if (a >= 32'h1000) return 8'h00;
`endif
  return `AHB_MEM.mem[a % `AHB_DEPTH];
endfunction
task automatic set_byte(input int unsigned a, input logic [7:0] v);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) `AHB_MEM1.mem[a % `AHB_DEPTH] = v;
  else if (a < 32'h1000) `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`else
  `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`endif
endtask

// The memory, n bytes from addr, one bus word per group.
task automatic dump(input int unsigned addr, input int n);
  string line;
  int unsigned a = (addr / 16) * 16;
  while (a < addr + n) begin
    line = $sformatf("0x%05h ", a[19:0]);
    for (int i = 0; i < 16; i++) begin
      if (i % BUS_BYTES == 0) line = {line, " "};
      line = {line, $sformatf(" %02h", mem_byte(a + i))};
    end
    $display("%s", line);
    a = a + 16;
  end
endtask

// Fill n bytes from addr with first, first+1, ... so untouched bytes stay recognisable.
task automatic mem_fill(input int unsigned addr, input int n, input logic [7:0] first = 8'h00);
  for (int i = 0; i < n; i++) set_byte(addr + i, first + i[7:0]);
endtask

// Drive nothing new for n cycles.
task automatic idle(input int n);
  repeat (n) @(posedge hclk);
  #1;
endtask
Read included file: ahb_fabric.sv
`timescale 1ns/1ns
// ahb_fabric.sv: one manager, an address decoder, two memories, the default subordinate and the
// multiplexor that returns HRDATA, HRESP, HREADY and HEXOKAY (read-only in this lesson).
// The address map (every region starts and ends on a 1 KB boundary):
//   0x0000-0x0fff  m0, a 4 KB memory
//   0x1000-0x1fff  m1, a 4 KB memory
//   0x2000-0x23ff  the AHB-to-APB bridge (32-bit bus) and behind it the APB completer p0 of P3
//                  (RAM at 0x2000-0x20ff, registers at 0x2100-0x2110; switches fab.p0.waits and the rest)
//   anything else  the default subordinate: OKAY to IDLE and BUSY, the two-cycle ERROR to NONSEQ and SEQ
// The decoder selects in the address phase; the multiplexor must follow the data phase, so its select
// is registered at every edge where HREADY is 1. Switches: trace (print the routing at each accepted
// address phase); broken twins mux_addr_phase (the multiplexor follows the address phase select) and
// mux_no_hready (the select is registered at every edge, even while HREADY is low).

// The default subordinate: it answers the addresses no other subordinate owns.
module ahb_default (
  input  logic hclk,
  input  logic hresetn,
  input  logic hsel,
  input  logic [1:0] htrans,
  input  logic hready,
  output logic hreadyout,
  output logic hresp
);
  bit d_err = 0, e_two = 0;
  initial begin hreadyout = 1; hresp = 0; end
  always @(posedge hclk) begin
    if (!hresetn) begin
      d_err = 0; e_two = 0; hreadyout <= 1; hresp <= 0;
    end else begin
      if (d_err) begin
        if (!hreadyout) e_two = 1;
        else d_err = 0;
      end
      if (hsel && hready) begin d_err = htrans[1]; e_two = 0; end
      hreadyout <= !(d_err && !e_two);
      hresp <= d_err;
    end
  end
endmodule

module ahb_fabric #(parameter int DATA_W = 32) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  output logic [DATA_W-1:0] hrdata,
  output logic              hready,
  output logic              hresp,
  output logic              hexokay
);
  import ahb_pkg::*;
  bit trace = 0, mux_addr_phase = 0, mux_no_hready = 0;
  localparam bit HAS_APB = (DATA_W == 32);    // the AHB-to-APB bridge serves a 32-bit bus
  int cycle = -1;

  // ---- the decoder: HSELx in the address phase ----
  function automatic logic [1:0] region(input logic [31:0] a);
    if (a < 32'h1000) return 2'd0;
    if (a < 32'h2000) return 2'd1;
    if (HAS_APB && a < 32'h2400) return 2'd2;
    return 2'd3;
  endfunction
  logic [1:0] asel;
  assign asel = region(haddr);
  logic hsel0, hsel1, hsel2, hseld;
  assign hsel0 = (asel == 2'd0);
  assign hsel1 = (asel == 2'd1);
  assign hsel2 = (asel == 2'd2);
  assign hseld = (asel == 2'd3);

  // ---- the subordinates ----
  logic [DATA_W-1:0] rdata0, rdata1, rdata2;
  logic ready0, ready1, ready2, readyd, resp0, resp1, resp2, respd, exok0, exok1, exok2;
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m0 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel0), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready0), .hresp(resp0), .hrdata(rdata0), .hexokay(exok0));
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m1 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel1), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready1), .hresp(resp1), .hrdata(rdata1), .hexokay(exok1));
  // ---- SLOT 2: the AHB-to-APB bridge ----
  logic        psel, penable, pwrite, pready, pslverr;
  logic [31:0] paddr, pwdata, prdata, br_rdata;
  logic [3:0]  pstrb;
  logic [2:0]  pprot;
  assign exok2 = 1'b0;
  generate if (DATA_W == 32) begin : apb
    ahb_apb_bridge br (.hclk(hclk), .hresetn(hresetn), .hsel(hsel2), .haddr(haddr), .htrans(htrans), .hwrite(hwrite),
      .hprot(hprot), .hwdata(hwdata), .hwstrb(hwstrb), .hready(hready), .hreadyout(ready2), .hresp(resp2), .hrdata(br_rdata),
      .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr), .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot),
      .pready(pready), .prdata(prdata), .pslverr(pslverr));
    assign rdata2 = br_rdata;
  end else begin : no_apb
    assign ready2 = 1'b1; assign resp2 = 1'b0; assign rdata2 = '0;
    assign psel = 0; assign penable = 0; assign pwrite = 0; assign paddr = 0; assign pwdata = 0; assign pstrb = 0; assign pprot = 0;
  end endgenerate
  apb_periph #(.NAME("p0")) p0 (.pclk(hclk), .presetn(hresetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
    .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr));
  ahb_default dflt (.hclk(hclk), .hresetn(hresetn), .hsel(hseld), .htrans(htrans), .hready(hready), .hreadyout(readyd), .hresp(respd));

  // ---- the multiplexor: keyed by the data phase ----
  logic [1:0] dsel = 2'd3;
  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) dsel <= 2'd3;
    else if (hready || mux_no_hready) dsel <= asel;
    if (trace && hresetn && hready && htrans[1])
      $display("fab e%0d: %0s %0s 0x%0h -> %0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr,
               asel == 0 ? "m0" : asel == 1 ? "m1" : asel == 2 ? "apb" : "default (no subordinate)");
  end
  logic [1:0] msel;
  assign msel = mux_addr_phase ? asel : dsel;
  always @* begin
    case (msel)
      2'd0: begin hrdata = rdata0; hready = ready0; hresp = resp0; hexokay = exok0; end
      2'd1: begin hrdata = rdata1; hready = ready1; hresp = resp1; hexokay = exok1; end
      2'd2: begin hrdata = rdata2; hready = ready2; hresp = resp2; hexokay = exok2; end
      default: begin hrdata = '0; hready = readyd; hresp = respd; hexokay = 1'b0; end
    endcase
  end
endmodule
Read included file: ahb_apb_bridge.sv
`timescale 1ns/1ns
// ahb_apb_bridge.sv: an AHB subordinate that turns each accepted transfer into one APB transfer
// (read-only). The address phase is registered when HSEL, HREADY and a NONSEQ or SEQ transfer are
// all present; the data phase then runs SETUP and ACCESS on APB with HREADYOUT low, and ends in the
// cycle PREADY is 1: OKAY with HRDATA = PRDATA, or, when PSLVERR is 1, the two-cycle ERROR (HREADYOUT
// low then high, HRESP high in both). HWDATA and HWSTRB are data-phase signals the manager holds
// through the wait states, so PWDATA and PSTRB are driven from them directly; PADDR is the aligned
// word address (IHI 0024E 2.1.1 leaves an unaligned PADDR unpredictable). HPROT maps onto PPROT:
// privileged from HPROT[1], instruction from HPROT[0] = 0, and the access is Secure.
// A write therefore costs one setup and one access cycle: two cycles of data phase with no APB wait.
module ahb_apb_bridge (
  input  logic        hclk,
  input  logic        hresetn,
  input  logic        hsel,
  input  logic [31:0] haddr,
  input  logic [1:0]  htrans,
  input  logic        hwrite,
  input  logic [3:0]  hprot,
  input  logic [31:0] hwdata,
  input  logic [3:0]  hwstrb,
  input  logic        hready,
  output logic        hreadyout,
  output logic        hresp,
  output logic [31:0] hrdata,
  output logic        psel,
  output logic        penable,
  output logic        pwrite,
  output logic [31:0] paddr,
  output logic [31:0] pwdata,
  output logic [3:0]  pstrb,
  output logic [2:0]  pprot,
  input  logic        pready,
  input  logic [31:0] prdata,
  input  logic        pslverr);
  localparam int IDLE = 0, SETUP = 1, ACCESS = 2, ERR1 = 3, ERR2 = 4;
  bit trace = 0;
  int st = IDLE;
  logic        wr = 0;
  logic [31:0] a = 0;
  logic [2:0]  prot = 0;
  wire take = hsel && hready && htrans[1];
  assign psel      = (st == SETUP || st == ACCESS);
  assign penable   = (st == ACCESS);
  assign pwrite    = wr;
  assign paddr     = {a[31:2], 2'b00};     // APB leaves an unaligned PADDR unpredictable: the word, and PSTRB picks the bytes
  assign pwdata    = wr ? hwdata : 32'h0;
  assign pstrb     = wr ? hwstrb : 4'h0;
  assign pprot     = prot;
  assign hreadyout = (st == IDLE || st == ERR2) ? 1'b1 : (st == ACCESS) ? (pready === 1'b1 && pslverr !== 1'b1) : 1'b0;
  assign hresp     = (st == ERR1 || st == ERR2);
  assign hrdata    = (st == ACCESS) ? prdata : 32'h0;
  always @(posedge hclk or negedge hresetn) begin
    if (!hresetn) st <= IDLE;
    else begin
      case (st)
        SETUP:  st <= ACCESS;
        ACCESS: if (pready === 1'b1) begin
                  if (trace) $display("apb e%0d: %0s 0x%03h%0s", ($time - 5) / 10, wr ? "write" : "read", a[11:0], pslverr === 1'b1 ? " PSLVERR" : "");
                  st <= (pslverr === 1'b1) ? ERR1 : IDLE;
                end
        ERR1:   st <= ERR2;
        default: st <= IDLE;
      endcase
      // A new address phase can be taken whenever HREADY is 1: in IDLE, in the last cycle of ERROR,
      // or at the edge an access completes with OKAY.
      if (take && (st == IDLE || st == ERR2 || (st == ACCESS && pready === 1'b1 && pslverr !== 1'b1))) begin
        wr <= hwrite; a <= haddr; prot <= {~hprot[0], 1'b0, hprot[1]}; st <= SETUP;
      end
    end
  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
Spot the bugEach option turns on one fault and runs the same traffic: three writes, two to m0 and one to m1 between them, then two reads. The harness fails when the rule checker reports. Which fault does the checker catch?
Read included file: ahb_pkg.sv
`timescale 1ns/1ns
// ahb_pkg.sv: the encodings and the arithmetic of a burst (read-only).
// The memory, the driver, the checker and the lesson text all use these functions, so they never
// disagree about the address of a beat or the lanes it uses.
package ahb_pkg;
  localparam logic [1:0] IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
  localparam logic [2:0] SINGLE = 3'b000, INCR = 3'b001, WRAP4 = 3'b010, INCR4 = 3'b011,
                         WRAP8 = 3'b100, INCR8 = 3'b101, WRAP16 = 3'b110, INCR16 = 3'b111;
  localparam logic OKAY = 1'b0, ERROR = 1'b1;

  function automatic string trans_name(input logic [1:0] t);
    case (t) IDLE: return "IDLE"; BUSY: return "BUSY"; NONSEQ: return "NONSEQ"; default: return "SEQ"; endcase
  endfunction
  function automatic string burst_name(input logic [2:0] b);
    case (b)
      SINGLE: return "SINGLE"; INCR: return "INCR"; WRAP4: return "WRAP4"; INCR4: return "INCR4";
      WRAP8: return "WRAP8"; INCR8: return "INCR8"; WRAP16: return "WRAP16"; default: return "INCR16";
    endcase
  endfunction
  function automatic string resp_name(input logic r);
    return r ? "ERROR" : "OKAY";
  endfunction
  // s when c is 1, else the empty string (a string-typed result prints the same on every simulator).
  function automatic string opt(input bit c, input string s);
    if (c) return s;
    return "";
  endfunction

  // Beats in a burst: 1 for SINGLE, 4, 8 or 16 for the fixed bursts, 0 for INCR (undefined length).
  function automatic int beats_of(input logic [2:0] b);
    case (b) SINGLE: return 1; INCR: return 0; WRAP4, INCR4: return 4; WRAP8, INCR8: return 8; default: return 16; endcase
  endfunction
  function automatic bit is_wrap(input logic [2:0] b);
    return b == WRAP4 || b == WRAP8 || b == WRAP16;
  endfunction

  // The address of the beat after the one at a: the size in bytes further on; a wrapping burst
  // returns to the start of its window (beats x size bytes, aligned to that total) at the window's end.
  function automatic int unsigned next_addr(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
    int unsigned nb = 1 << size;
    int unsigned window = nb * beats_of(b);
    int unsigned base;
    if (!is_wrap(b)) return a + nb;
    base = (a / window) * window;
    if (a + nb >= base + window) return base;
    return a + nb;
  endfunction

  // The address of beat n of a burst that starts at start.
  function automatic int unsigned beat_addr(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int unsigned a = start;
    for (int k = 0; k < n; k++) a = next_addr(a, size, b);
    return a;
  endfunction

  // The lanes a transfer at address a uses on a bus of bus_bytes bytes, packed as {upper, lower}.
  // Every AHB transfer is aligned to its size, so it uses size bytes from its own lane upwards.
  function automatic logic [15:0] beat_lanes(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    int lower = a % bus_bytes;
    int upper = lower + (1 << size) - 1;
    if (upper > bus_bytes - 1) upper = bus_bytes - 1;
    return {upper[7:0], lower[7:0]};
  endfunction

  // The strobe of a transfer: a 1 for every lane it uses (bit l is lane l).
  function automatic logic [63:0] beat_strb(input int unsigned a, input logic [2:0] size, input int bus_bytes);
    logic [15:0] lanes = beat_lanes(a, size, bus_bytes);
    logic [63:0] s = 0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) s[l] = 1'b1;
    return s;
  endfunction

  function automatic bit aligned(input int unsigned a, input logic [2:0] size);
    return (a % (1 << size)) == 0;
  endfunction

  // True when an incrementing burst of n beats (n is used only for INCR) crosses a 1 KB boundary.
  function automatic bit crosses_1k(input int unsigned start, input logic [2:0] size, input logic [2:0] b, input int n);
    int beats = beats_of(b) ? beats_of(b) : n;
    if (is_wrap(b) || beats < 2) return 0;
    return (start / 1024) != ((start + beats * (1 << size) - 1) / 1024);
  endfunction
endpackage
Read included file: ahb_mem.sv
`timescale 1ns/1ns
// ahb_mem.sv: an AHB subordinate with a byte-addressed memory behind it (read-only in this lesson).
// It registers an address phase at an edge where HSEL is 1, HTRANS is NONSEQ or SEQ and HREADY is 1,
// then runs the data phase: wait states with HREADYOUT low, a write takes HWDATA (and HWSTRB) at the
// edge that ends the data phase, a read drives HRDATA for the cycle that ends it. IDLE and BUSY get a
// zero-wait OKAY and change nothing. The switches below are plain variables: the stimulus sets them by
// name (mem.waits = 2;).
//   waits          wait states inserted in every data phase
//   waits_at[k]    wait states for the k-th transfer this memory takes (from 0; -1 = use waits)
//   err_above      transfers at or above this address get the two-cycle ERROR (and write nothing)
//   excl_on        AHB5 exclusive access: one monitor per HMASTER, HEXOKAY on success
//   trace          print every transfer the memory takes and every byte it writes
// Broken twins, each off by default:
//   ignore_hready    registers an address phase while HREADY is low (another subordinate is stretching),
//                    so it acts on a transfer the manager may still change or cancel
//   wdata_in_addr    takes HWDATA in the address phase, so a write stores the previous transfer's data
//   act_on_idle      treats IDLE and BUSY as transfers (waits, reads, writes HWDATA); this twin is an
//                    AHB-Lite design without an HWSTRB input, so it writes the lanes HSIZE selects
//   one_cycle_error  answers ERROR in one cycle (HRESP high with HREADYOUT high)
//   ignore_size      writes every lane of the bus word, whatever HSIZE and HWSTRB say
module ahb_mem #(parameter int DATA_W = 32, parameter int DEPTH = 65536) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic              hsel,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  input  logic              hready,
  output logic              hreadyout,
  output logic              hresp,
  output logic [DATA_W-1:0] hrdata,
  output logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;

  // ---- switches ----
  int waits = 0;
  int waits_at [0:63];
  int unsigned err_above = 32'hffff_ffff;
  bit excl_on = 0, trace = 0;
  bit ignore_hready = 0, wdata_in_addr = 0, act_on_idle = 0, one_cycle_error = 0, ignore_size = 0;

  // ---- the memory ----
  logic [7:0] mem [0:DEPTH-1];
  int cycle = -1;
  int taken = 0;                              // transfers taken so far

  // ---- the data phase in progress ----
  bit              d_valid = 0, d_write = 0, d_err = 0, d_exok = 0, d_xfail = 0;
  int unsigned     d_addr = 0;
  logic [2:0]      d_size = 0;
  logic [1:0]      d_trans = 0;
  logic [DATA_W-1:0] d_wd = 0;                // HWDATA seen in the address phase (the wdata_in_addr twin)
  int              d_cnt = 0;                 // wait states still to insert
  bit              e_two = 0;                 // the ERROR response is in its second cycle

  // ---- exclusive monitors, one per HMASTER ----
  bit          mon_use [0:15];
  int unsigned mon_addr [0:15];
  int          mon_bytes [0:15];

  initial begin
    for (int i = 0; i < DEPTH; i++) mem[i] = 8'h00;
    for (int k = 0; k < 64; k++) waits_at[k] = -1;
    for (int m = 0; m < 16; m++) mon_use[m] = 0;
    hreadyout = 1; hresp = 0; hrdata = 0; hexokay = 0;
  end

  function automatic logic [DATA_W-1:0] read_word(input int unsigned a);
    logic [DATA_W-1:0] w = 0;
    int unsigned word = (a / BUS) * BUS;
    for (int l = 0; l < BUS; l++) w[8*l +: 8] = mem[(word + l) % DEPTH];
    return w;
  endfunction

  // Write the lanes of one transfer: the lanes its address and size select, where the strobe is 1.
  task automatic write_bytes(input int unsigned a, input logic [2:0] size, input logic [DATA_W-1:0] data, input logic [BUS-1:0] strb);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    int unsigned word = (a / BUS) * BUS;
    int lo = ignore_size ? 0 : lanes[7:0];
    int hi = ignore_size ? BUS - 1 : lanes[15:8];
    for (int l = lo; l <= hi; l++)
      if (ignore_size || act_on_idle || strb[l]) begin
        mem[(word + l) % DEPTH] = data[8*l +: 8];
        if (trace) $display("mem e%0d:   byte 0x%0h <= %02h (lane %0d)", cycle, word + l, data[8*l +: 8], l);
      end
    // any write ends the exclusive monitors that cover one of its bytes
    for (int m = 0; m < 16; m++)
      if (mon_use[m] && a < mon_addr[m] + mon_bytes[m] && mon_addr[m] < a + (1 << size)) mon_use[m] = 0;
  endtask

  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) begin
      d_valid = 0; e_two = 0;
      hreadyout <= 1; hresp <= 0; hexokay <= 0;
    end else begin
      // 1. the data phase that ends or waits at this edge
      if (d_valid) begin
        if (hreadyout && hready) begin
          if (d_write && !d_err && !d_xfail)
            write_bytes(d_addr, d_size, wdata_in_addr ? d_wd : hwdata, hwstrb);
          d_valid = 0;
        end else if (!hreadyout) begin
          if (d_cnt > 0) d_cnt = d_cnt - 1;
          else if (d_err) e_two = 1;
        end
      end
      // 2. the address phase on the wires at this edge
      if (hsel && (htrans[1] || act_on_idle) && (hready || (ignore_hready && !d_valid))) begin
        d_valid = 1; d_addr = haddr; d_write = hwrite; d_size = hsize; d_trans = htrans; d_wd = hwdata;
        d_cnt = (taken < 64 && waits_at[taken] >= 0) ? waits_at[taken] : waits;
        taken = taken + 1;
        d_err = (haddr >= err_above); e_two = 0;
        d_exok = 0; d_xfail = 0;
        if (excl_on && hexcl && htrans[1] && !d_err) begin
          if (!hwrite) begin
            mon_use[hmaster] = 1; mon_addr[hmaster] = haddr; mon_bytes[hmaster] = 1 << hsize; d_exok = 1;
          end else if (mon_use[hmaster] && mon_addr[hmaster] == haddr && mon_bytes[hmaster] == (1 << hsize)) begin
            d_exok = 1; mon_use[hmaster] = 0;
          end else d_xfail = 1;
        end
        if (trace) $display("mem e%0d: %0s %0s 0x%0h size %0d%0s%0s%0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr, hsize,
                            opt(!hready, " (taken while HREADY is low)"), opt(d_err, " -> ERROR"),
                            opt(d_xfail, " -> exclusive failed"));
      end
      // 3. the response for the cycle that starts now
      if (d_valid && d_cnt > 0) begin
        hreadyout <= 0; hresp <= 0; hexokay <= 0;
      end else if (d_valid && d_err) begin
        hreadyout <= one_cycle_error ? 1'b1 : e_two; hresp <= 1; hexokay <= 0;
      end else begin
        hreadyout <= 1; hresp <= 0; hexokay <= d_valid && d_exok;
      end
      if (d_valid && !d_write) hrdata <= d_err ? '0 : read_word(d_addr);
    end
  end
endmodule
Read included file: ahb_checker.sv
`timescale 1ns/1ns
// ahb_checker.sv: the rules of an AHB interface, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
//   reset         HTRANS is not IDLE, or HREADY is low, while reset is active;
//   hold          the address or control changed during a wait state (HREADY low), outside the
//                 changes the protocol allows;
//   trans_change  HTRANS changed during a wait state other than IDLE to NONSEQ, BUSY to SEQ (fixed
//                 bursts) or BUSY to anything (INCR); or SEQ/BUSY with no burst in progress;
//   seq_addr      a SEQ or BUSY address that is not the previous beat's address plus the size
//                 (wrapped for a WRAP burst);
//   burst_ctrl    HSIZE, HBURST, HWRITE or HPROT changed inside a burst;
//   align         a transfer not aligned to its size;
//   size_bus      a transfer wider than the data bus;
//   kb1           an incrementing burst that crosses a 1 KB boundary;
//   burst_len     a fixed-length burst that ends early or runs long, BUSY after a SINGLE, or BUSY
//                 after the last beat of a fixed-length burst;
//   idle_okay     an IDLE or BUSY transfer that did not get a zero-wait OKAY;
//   err_shape     an ERROR that is not two cycles (HREADY low then high, HRESP high in both);
//   exokay        HEXOKAY with HREADY low, with an ERROR, or for a transfer that was not exclusive;
//   known         x on the address, control, or the active lanes of the data.
// A NOTE (not a broken rule) marks a transfer kept after the first cycle of an ERROR: the protocol
// allows it, most managers cancel.
module ahb_checker #(parameter int DATA_W = 32) (
  input logic              hclk,
  input logic              hresetn,
  input logic [31:0]       haddr,
  input logic [1:0]        htrans,
  input logic              hwrite,
  input logic [2:0]        hsize,
  input logic [2:0]        hburst,
  input logic [3:0]        hprot,
  input logic              hmastlock,
  input logic [DATA_W-1:0] hwdata,
  input logic              hexcl,
  input logic              hready,
  input logic              hresp,
  input logic [DATA_W-1:0] hrdata,
  input logic              hexokay
);
  import ahb_pkg::*;
  localparam int BUS = DATA_W / 8;
  int cycle = -1, transfers = 0, waits = 0, errors = 0, broken = 0, notes = 0;

  function void report(input string rule, input string what);
    broken++;
    $display("RULE %s broken at edge %0d: %s", rule, cycle, what);
  endfunction

  function automatic logic [DATA_W-1:0] lane_mask(input int unsigned a, input logic [2:0] size);
    logic [15:0] lanes = beat_lanes(a, size, BUS);
    logic [DATA_W-1:0] m = '0;
    for (int l = lanes[7:0]; l <= lanes[15:8]; l++) m[8*l +: 8] = 8'hff;
    return m;
  endfunction

  // The previous edge.
  bit          p_on = 0;
  logic        p_ready = 1, p_resp = 0, p_write = 0, p_lock = 0;
  logic [1:0]  p_trans = 0;
  logic [31:0] p_addr = 0;
  logic [2:0]  p_size = 0, p_burst = 0;
  logic [3:0]  p_prot = 0;
  bit          e_first = 0;           // the previous edge was the first cycle of an ERROR
  // The data phase in progress.
  bit          d_on = 0, d_idle = 0, d_write = 0, d_excl = 0;
  int unsigned d_addr = 0;
  logic [2:0]  d_size = 0;
  // The burst in progress.
  bit          b_on = 0, b_err = 0, b_write = 0, b_kb = 0;
  logic [2:0]  b_burst = 0, b_size = 0;
  logic [3:0]  b_prot = 0;
  int unsigned b_first = 0, b_last = 0;
  int          b_count = 0;
  int unsigned exp_a;

  task automatic end_burst();
    if (b_on && beats_of(b_burst) > 1 && b_count < beats_of(b_burst) && !b_err)
      report("burst_len", $sformatf("%0s burst at 0x%0h ended after %0d of %0d beats", burst_name(b_burst), b_first, b_count, beats_of(b_burst)));
    b_on = 0;
  endtask

  task automatic check_beat();
    if ((1 << hsize) > BUS) report("size_bus", $sformatf("a %0d-byte transfer on a %0d-byte bus", 1 << hsize, BUS));
    if (!aligned(haddr, hsize)) report("align", $sformatf("a %0d-byte transfer at 0x%0h is not aligned to its size", 1 << hsize, haddr));
  endtask

  task automatic check_ctrl(input string what);
    if (hsize !== b_size || hburst !== b_burst || hwrite !== b_write || hprot !== b_prot)
      report("burst_ctrl", $sformatf("%0s at 0x%0h changed HSIZE, HBURST, HWRITE or HPROT inside the burst", what, haddr));
  endtask

  always @(posedge hclk) begin
    cycle++;
    if (!hresetn) begin
      if (htrans !== IDLE) report("reset", "HTRANS is not IDLE during reset");
      if (hready !== 1) report("reset", "HREADY is low during reset");
      p_on = 0; d_on = 0; b_on = 0; e_first = 0;
    end else begin
      // ---- the data phase that ends or waits at this edge ----
      if (hready === 0) waits++;
      if (e_first) begin
        if (hresp !== 1 || hready !== 1) report("err_shape", "the second cycle of an ERROR must have HRESP high and HREADY high");
      end else if (hresp === 1 && hready === 1)
        report("err_shape", "ERROR with HREADY high in its first cycle: an ERROR takes two cycles");
      if (hresp === 1 && b_on) b_err = 1;
      if (d_on && d_idle) begin
        if (hready !== 1 || hresp !== 0) report("idle_okay", "an IDLE or BUSY transfer must get a zero-wait OKAY");
        d_on = 0;
      end
      if (hexokay === 1) begin
        if (hready !== 1) report("exokay", "HEXOKAY while HREADY is low");
        else if (hresp === 1) report("exokay", "HEXOKAY together with an ERROR");
        else if (!(d_on && d_excl)) report("exokay", "HEXOKAY for a transfer that was not exclusive");
      end
      if (hready === 1 && d_on && !d_idle) begin
        if (d_write && $isunknown(hwdata & lane_mask(d_addr, d_size))) report("known", "HWDATA is x on an active lane");
        if (!d_write && hresp === 0 && $isunknown(hrdata & lane_mask(d_addr, d_size))) report("known", "HRDATA is x on an active lane");
        transfers++;
        if (hresp === 1) errors++;
        d_on = 0;
      end
      if (e_first && hready === 1 && (htrans === NONSEQ || htrans === SEQ)) begin
        notes++;
        $display("NOTE cancel at edge %0d: the %0s at 0x%0h behind an ERROR was kept, not cancelled (allowed; most managers cancel)", cycle, trans_name(htrans), haddr);
      end
      // ---- the address phase on the wires at this edge ----
      if (htrans !== IDLE && ($isunknown(haddr) || $isunknown(hwrite) || $isunknown(hsize) || $isunknown(hburst)))
        report("known", "address or control is x while HTRANS is not IDLE");
      if ($isunknown(htrans)) report("known", "HTRANS is x");
      if (p_on && p_ready === 0 && !e_first) begin
        // a wait state at the previous edge: only the allowed changes
        if (p_trans == IDLE) begin
          if (htrans === BUSY || htrans === SEQ) report("trans_change", $sformatf("IDLE changed to %0s during a wait state (only NONSEQ may follow)", trans_name(htrans)));
        end else if (p_trans == BUSY) begin
          if (p_burst != INCR) begin
            if (htrans !== BUSY && htrans !== SEQ) report("trans_change", $sformatf("BUSY in a %0s burst changed to %0s during a wait state (only SEQ may follow)", burst_name(p_burst), trans_name(htrans)));
            else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          end
        end else begin
          if (htrans !== p_trans) report("trans_change", $sformatf("%0s changed to %0s during a wait state", trans_name(p_trans), trans_name(htrans)));
          else if (haddr !== p_addr) report("hold", $sformatf("HADDR changed from 0x%0h to 0x%0h during a wait state", p_addr, haddr));
          else if (hwrite !== p_write || hsize !== p_size || hburst !== p_burst || hprot !== p_prot || hmastlock !== p_lock)
            report("hold", "HWRITE, HSIZE, HBURST, HPROT or HMASTLOCK changed during a wait state");
        end
      end
      if (hready === 1) begin
        // the address phase is accepted at this edge
        if (htrans === IDLE) begin
          end_burst();
          d_on = 1; d_idle = 1;
        end else if (htrans === NONSEQ) begin
          end_burst();
          check_beat();
          b_on = 1; b_err = 0; b_kb = 0; b_first = haddr; b_last = haddr; b_count = 1;
          b_burst = hburst; b_size = hsize; b_write = hwrite; b_prot = hprot;
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === SEQ) begin
          if (!b_on) report("trans_change", $sformatf("SEQ at 0x%0h with no burst in progress", haddr));
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (haddr !== exp_a) report("seq_addr", $sformatf("SEQ at 0x%0h; the beat after 0x%0h in a %0s burst of %0d-byte transfers is 0x%0h", haddr, b_last, burst_name(b_burst), 1 << b_size, exp_a));
            check_ctrl("SEQ");
            if (!is_wrap(b_burst) && !b_kb && (haddr / 1024) != (b_first / 1024)) begin
              b_kb = 1;
              report("kb1", $sformatf("SEQ at 0x%0h crosses the 1 KB boundary of the burst that started at 0x%0h", haddr, b_first));
            end
            b_count++;
            if (beats_of(b_burst) > 0 && b_count > beats_of(b_burst))
              report("burst_len", $sformatf("beat %0d of a %0s burst, which has %0d beats", b_count, burst_name(b_burst), beats_of(b_burst)));
            b_last = haddr;
          end
          check_beat();
          d_on = 1; d_idle = 0; d_write = hwrite; d_addr = haddr; d_size = hsize; d_excl = hexcl;
        end else if (htrans === BUSY) begin
          if (!b_on) report("trans_change", $sformatf("BUSY at 0x%0h with no burst in progress", haddr));
          else if (b_burst == SINGLE) report("burst_len", "BUSY after a SINGLE transfer");
          else begin
            exp_a = next_addr(b_last, b_size, b_burst);
            if (beats_of(b_burst) > 1 && b_count == beats_of(b_burst))
              report("burst_len", $sformatf("BUSY after the last beat of the %0s burst (a fixed-length burst ends with SEQ)", burst_name(b_burst)));
            else if (haddr !== exp_a) report("seq_addr", $sformatf("BUSY at 0x%0h must show the next beat's address 0x%0h", haddr, exp_a));
            check_ctrl("BUSY");
          end
          d_on = 1; d_idle = 1;
        end
      end
      e_first = (hresp === 1 && hready === 0);
    end
    p_on = hresetn; p_ready = hready; p_resp = hresp; p_trans = htrans; p_addr = haddr; p_write = hwrite;
    p_size = hsize; p_burst = hburst; p_prot = hprot; p_lock = hmastlock;
  end
  final $display("checker: %0d transfers, %0d wait edges, %0d errors, %0d rules broken", transfers, waits, errors, broken);
endmodule
Read included file: ahb_tasks.svh
// ahb_tasks.svh: the manager side of the bench, as tasks for the stimulus (read-only).
// ahb_write, ahb_read and ahb_burst queue transfers; go() puts the queue on the bus and returns when
// the last data phase has ended. The driver below issues the next address phase at the same edge where
// the previous one is accepted, so the address of transfer N+1 is on the wires during the data phase
// of transfer N. Write data for a burst goes in wd[0], wd[1], ... (one entry per beat, the lowest byte
// at the lowest address); the data of every read comes back in rd[0], rd[1], ... in issue order, with
// the responses in rr[].
// The beat log has one line per accepted address phase (A), per ended data phase (D) and per transfer
// the manager cancels (X), each with the edge where it happened; watch = 1 adds one line per edge.
int cycle = -1;
always @(posedge hclk) cycle = cycle + 1;

localparam int QN = 512;
int unsigned q_addr [0:QN-1];
logic        q_write [0:QN-1];
logic [2:0]  q_size [0:QN-1];
logic [2:0]  q_burst [0:QN-1];
logic [1:0]  q_trans [0:QN-1];
logic [63:0] q_data [0:QN-1];
logic        q_lock [0:QN-1], q_excl [0:QN-1], q_sforce [0:QN-1];
logic [3:0]  q_prot [0:QN-1], q_master [0:QN-1];
logic [7:0]  q_strb [0:QN-1];
int          q_bn [0:QN-1], q_beat [0:QN-1], q_state [0:QN-1];   // burst number, beat, 0 queued 1 done 2 cancelled
logic [63:0] q_rdata [0:QN-1];
logic        q_resp [0:QN-1], q_exok [0:QN-1];
int q_n = 0, q_next = 0, bn = 0, last_start = 0, last_bn = 0;

logic [63:0] wd [0:255];
logic [63:0] rd [0:255];
logic        rr [0:255];
int          n_rd = 0;
logic        last_resp = 0, last_exokay = 0;
// quiet = 1 silences the beat log; watch = 1 prints the bus at every edge.
bit quiet = 0, watch = 0, running = 0, drv_done = 0;
// Driver switches (broken twins, off by default):
//   move_in_wait  the address moves on by one transfer size during a wait state
//   no_cancel     the transfer behind an ERROR is not cancelled (allowed, but the checker notes it)
// After the first cycle of an ERROR the driver cancels the transfer already in its address phase: the
// rest of the same burst is dropped; a separate transfer is issued again afterwards (reissue = 0 drops it).
//   busy_end      a BUSY after the last beat of every fixed-length burst
//   cross_1k      incrementing bursts are issued across a 1 KB boundary instead of being split
//   unaligned     transfers not aligned to their size are issued instead of refused
//   wrap_bug      a WRAP8 burst's addresses wrap inside 16 bytes, as if it were WRAP4
bit reissue = 1;
bit move_in_wait = 0, no_cancel = 0, busy_end = 0, cross_1k = 0, unaligned = 0, wrap_bug = 0;
// Attributes of the transfers queued next.
logic       lock = 0, excl = 0;
logic [3:0] prot = 4'b0011, master = 0;
bit         strb_force = 0;
logic [7:0] strb_val = 8'hff;
// What the bus is carrying in the data phase, for the waveform scenes: the address and lanes of the beat.
int unsigned beat_a = 0;
logic [7:0]  beat_m = 0;

function automatic int unsigned m_next(input int unsigned a, input logic [2:0] size, input logic [2:0] b);
  if (wrap_bug && b == WRAP8) return next_addr(a, size, WRAP4);
  return next_addr(a, size, b);
endfunction

task automatic q_push(input int unsigned a, input logic w, input logic [2:0] size, input logic [2:0] b, input logic [1:0] t,
                      input logic [63:0] data, input int burst_no, input int beat);
  q_addr[q_n] = a; q_write[q_n] = w; q_size[q_n] = size; q_burst[q_n] = b; q_trans[q_n] = t; q_data[q_n] = data;
  q_lock[q_n] = lock; q_excl[q_n] = excl; q_prot[q_n] = prot; q_master[q_n] = master;
  q_sforce[q_n] = strb_force; q_strb[q_n] = strb_val;
  q_bn[q_n] = burst_no; q_beat[q_n] = beat; q_state[q_n] = 0; q_resp[q_n] = 0; q_exok[q_n] = 0; q_rdata[q_n] = 0;
  q_n = q_n + 1;
endtask

task automatic q_copy(input int dst, input int src);
  q_addr[dst] = q_addr[src]; q_write[dst] = q_write[src]; q_size[dst] = q_size[src]; q_burst[dst] = q_burst[src];
  q_trans[dst] = q_trans[src]; q_data[dst] = q_data[src]; q_lock[dst] = q_lock[src]; q_excl[dst] = q_excl[src];
  q_prot[dst] = q_prot[src]; q_master[dst] = q_master[src]; q_sforce[dst] = q_sforce[src]; q_strb[dst] = q_strb[src];
  q_bn[dst] = q_bn[src]; q_beat[dst] = q_beat[src]; q_state[dst] = q_state[src];
endtask

// One write of size bytes (0 = byte, 1 = halfword, 2 = word) at addr; data holds the bytes, lowest first.
task automatic ahb_write(input int unsigned addr, input logic [2:0] size, input logic [63:0] data);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 1, size, SINGLE, NONSEQ, data, bn, 0);
  end
endtask

// One read of size bytes at addr; the bytes come back in rd[] in issue order.
task automatic ahb_read(input int unsigned addr, input logic [2:0] size);
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte transfer at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    bn = bn + 1; last_start = q_n; last_bn = bn;
    q_push(addr, 0, size, SINGLE, NONSEQ, 0, bn, 0);
  end
endtask

// A burst: hburst SINGLE, INCR (beats gives its length), WRAP4, INCR4, ... INCR16. Write data from wd[].
// An incrementing burst that would cross a 1 KB boundary is issued as two INCR bursts split at the
// boundary (unless cross_1k is set).
task automatic ahb_burst(input logic w, input logic [2:0] hb, input int unsigned addr, input logic [2:0] size, input int beats = 0);
  int n;
  int unsigned a;
  logic [2:0] b;
  logic [1:0] t;
  bit split;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr; b = hb; split = 0;
  if (!unaligned && !aligned(addr, size))
    $display("refused: a %0d-byte burst at 0x%0h is not aligned (every AHB transfer is aligned to its size)", 1 << size, addr);
  else begin
    if (!cross_1k && crosses_1k(addr, size, hb, n)) begin
      split = 1; b = INCR;
      $display("note: %0s at 0x%0h would cross a 1 KB boundary; issued as INCR bursts split at the boundary", burst_name(hb), addr);
    end
    bn = bn + 1; last_start = q_n; last_bn = bn;
    for (int k = 0; k < n; k++) begin
      t = (k == 0) ? NONSEQ : SEQ;
      if (k > 0 && split && (a % 1024) == 0) begin t = NONSEQ; bn = bn + 1; end
      q_push(a, w, size, b, t, w ? wd[k] : 64'h0, bn, k);
      a = m_next(a, size, b);
    end
    if (busy_end && beats_of(b) > 1) q_push(a, w, size, b, BUSY, 64'h0, bn, n);
  end
endtask

// n BUSY cycles before beat k of the burst queued last (k = its length: after the last beat).
task automatic busy_at(input int k, input int n = 1);
  int idx, src;
  idx = q_n;
  for (int j = q_n - 1; j >= last_start; j--)
    if (q_trans[j][1] && q_beat[j] == k) idx = j;
  src = (idx < q_n) ? idx : q_n - 1;           // the beat the BUSY announces, or the last beat
  for (int j = q_n - 1; j >= idx; j--) q_copy(j + n, j);
  for (int j = 0; j < n; j++) begin
    q_copy(idx + j, src >= idx ? src + n : src);
    q_trans[idx + j] = BUSY; q_data[idx + j] = 0; q_beat[idx + j] = k;
    if (src < idx) q_addr[idx + j] = m_next(q_addr[src], q_size[src], q_burst[src]);
  end
  q_n = q_n + n;
endtask

// n IDLE cycles in the queue (the address and control keep their last values).
task automatic idle_q(input int n);
  for (int j = 0; j < n; j++)
    if (q_n > 0) q_push(q_addr[q_n - 1], q_write[q_n - 1], q_size[q_n - 1], q_burst[q_n - 1], IDLE, 64'h0, 0, 0);
    else q_push(0, 0, 0, SINGLE, IDLE, 64'h0, 0, 0);
endtask

// The driver: pick, hold, cancel.
int a_idx = -1, d_idx = -1;       // queue entries in the address phase and in the data phase (-1: none)
bit moved = 0;
logic        s_ready, s_resp, s_exok, s_write;
logic [DATA_W-1:0] s_rdata;
logic [31:0] s_addr;
logic [1:0]  s_trans;
logic [2:0]  s_size, s_burst;
int          s_done, s_acc, s_cfrom, s_cto, s_cmode;

task automatic complete(input int i);
  logic [15:0] lanes;
  logic [63:0] bytes;
  q_state[i] = 1; q_resp[i] = s_resp; q_exok[i] = s_exok; last_resp = s_resp; last_exokay = s_exok;
  if (!q_write[i]) begin
    lanes = beat_lanes(q_addr[i], q_size[i], BUS_BYTES);
    bytes = (s_rdata >> (8 * lanes[7:0])) & ((64'h1 << (8 * (lanes[15:8] - lanes[7:0] + 1))) - 1);
    q_rdata[i] = bytes; rd[n_rd] = bytes; rr[n_rd] = s_resp; n_rd = n_rd + 1;
  end
  if (!quiet) $display("e%0d D  %0s addr=0x%0h data=0x%0h %0s%0s", cycle, q_write[i] ? "write" : "read ", q_addr[i],
                       q_write[i] ? q_data[i] : q_rdata[i], resp_name(s_resp),
                       opt(q_excl[i], s_exok ? " EXOKAY" : " (HEXOKAY low: exclusive failed)"));
endtask

task automatic drive();
  logic [15:0] lanes;
  logic [63:0] s;
  int e;
  if (a_idx >= 0) begin
    e = a_idx;
    htrans = q_trans[e]; haddr = q_addr[e] + (moved ? (1 << q_size[e]) : 0); hwrite = q_write[e]; hsize = q_size[e];
    hburst = q_burst[e]; hprot = q_prot[e]; hmastlock = q_lock[e]; hexcl = q_excl[e]; hmaster = q_master[e];
  end else begin
    htrans = IDLE; hmastlock = 0; hexcl = 0;      // address and control keep their last values
  end
  if (d_idx >= 0 && q_write[d_idx]) begin
    lanes = beat_lanes(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    s = beat_strb(q_addr[d_idx], q_size[d_idx], BUS_BYTES);
    hwdata = q_data[d_idx] << (8 * lanes[7:0]);
    hwstrb = q_sforce[d_idx] ? q_strb[d_idx][BUS_BYTES-1:0] : s[BUS_BYTES-1:0];
    beat_a = q_addr[d_idx]; beat_m = hwstrb;
  end else begin
    hwdata = '0; hwstrb = '0; beat_m = 0;
  end
endtask

always @(posedge hclk) begin
  s_ready = hready; s_resp = hresp; s_exok = hexokay; s_rdata = hrdata; s_addr = haddr; s_trans = htrans;
  s_write = hwrite; s_size = hsize; s_burst = hburst;
  s_done = -1; s_acc = -1; s_cmode = 0;
  if (hresetn) begin
    if (s_ready === 1'b1) begin
      s_done = d_idx; d_idx = -1;
      if (a_idx >= 0 && moved) q_addr[a_idx] = s_addr;   // the address the subordinate took
      if (a_idx >= 0 && q_trans[a_idx] != IDLE) s_acc = a_idx;
      if (a_idx >= 0 && q_trans[a_idx][1]) d_idx = a_idx;
      a_idx = -1; moved = 0;
      if (running && q_next < q_n) begin a_idx = q_next; q_next = q_next + 1; end
    end else if (s_resp === 1'b1 && !no_cancel && a_idx >= 0 && q_trans[a_idx] != IDLE) begin
      // the first cycle of an ERROR: the transfer already on the wires is cancelled (IHI 0033C 5.1.3)
      s_cfrom = a_idx;
      if (d_idx >= 0 && q_bn[a_idx] == q_bn[d_idx]) begin
        s_cto = a_idx;
        while (s_cto < q_n && q_bn[s_cto] == q_bn[d_idx]) begin q_state[s_cto] = 2; s_cto = s_cto + 1; end
        q_next = s_cto; s_cmode = 1;               // the rest of the burst is dropped
      end else if (reissue) begin
        q_next = a_idx; s_cmode = 2;               // a separate transfer: issued again after the ERROR
      end else begin
        q_state[a_idx] = 2; s_cto = a_idx + 1; s_cmode = 3;   // a separate transfer, dropped
      end
      a_idx = -1;
    end else if (move_in_wait && a_idx >= 0 && q_trans[a_idx][1]) moved = 1;
  end
  #1;
  if (s_done >= 0) complete(s_done);
  if (s_acc >= 0 && !quiet)
    $display("e%0d A  %0s %0s addr=0x%0h size=%0d %0s%0s%0s", cycle, trans_name(s_trans), s_write ? "WRITE" : "READ", s_addr, s_size,
             burst_name(s_burst), opt(q_lock[s_acc], " locked"), opt(q_excl[s_acc], " exclusive"));
  if (s_cmode == 1 && !quiet)
    for (int j = s_cfrom; j < s_cto; j++)
      if (q_trans[j][1]) $display("e%0d X  cancelled %0s %0s addr=0x%0h (the rest of the burst is dropped)", cycle, trans_name(q_trans[j]), q_write[j] ? "WRITE" : "READ", q_addr[j]);
  if (s_cmode == 3 && !quiet)
    $display("e%0d X  cancelled %0s %0s addr=0x%0h", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  if (s_cmode == 2 && !quiet)
    $display("e%0d X  withdrew %0s %0s addr=0x%0h (issued again after the ERROR)", cycle, trans_name(q_trans[s_cfrom]), q_write[s_cfrom] ? "WRITE" : "READ", q_addr[s_cfrom]);
  drive();
  drv_done = running && q_next >= q_n && a_idx < 0 && d_idx < 0;   // go() returns after this edge's log
end

// A text waveform for the lesson's trace blocks: with watch = 1, one line per rising edge with the
// values sampled at that edge (cycle k of a trace is what edge k sees).
int wave_edge = -1;
always @(posedge hclk) begin
  wave_edge = wave_edge + 1;
  if (watch) $display("wave e%0d: htrans=%0s haddr=%h hwrite=%b hsize=%0d hburst=%0s hwdata=%h hready=%b hresp=%b hrdata=%h",
                      wave_edge, trans_name(htrans), haddr, hwrite, hsize, burst_name(hburst), hwdata, hready, hresp, hrdata);
end

// Put the queue on the bus and wait until every transfer in it has ended; then empty the queue.
task automatic go();
  n_rd = 0; drv_done = 0; running = 1;
  wait (drv_done);
  running = 0; drv_done = 0; q_n = 0; q_next = 0;
endtask

// The table of a burst before it runs: the address, lanes and strobe of every beat.
task automatic beat_table(input int unsigned addr, input logic [2:0] size, input logic [2:0] hb, input int beats = 0);
  int n;
  int unsigned a, window, base;
  logic [15:0] lanes;
  logic [63:0] s;
  n = beats_of(hb) ? beats_of(hb) : (beats > 0 ? beats : 1);
  a = addr;
  $display("%0s at 0x%0h: %0d beat%0s of %0d byte%0s, bus %0d bytes", burst_name(hb), addr, n, opt(n > 1, "s"), 1 << size, opt(size != 0, "s"), BUS_BYTES);
  if (is_wrap(hb)) begin
    window = (1 << size) * beats_of(hb); base = (addr / window) * window;
    $display("  wraps inside 0x%0h-0x%0h (%0d beats x %0d bytes)", base, base + window - 1, beats_of(hb), 1 << size);
  end
  $display("  beat  address  lanes  strobe");
  for (int k = 0; k < n; k++) begin
    lanes = beat_lanes(a, size, BUS_BYTES);
    s = beat_strb(a, size, BUS_BYTES);
    $display("  %4d  0x%05h  %0d-%0d    %b", k, a[19:0], lanes[7:0], lanes[15:8], s[BUS_BYTES-1:0]);
    a = m_next(a, size, hb);
  end
  if (crosses_1k(addr, size, hb, n)) $display("  this burst crosses a 1 KB boundary");
  if (!aligned(addr, size)) $display("  0x%0h is not aligned to %0d bytes: AHB does not allow it", addr, 1 << size);
endtask

// One byte of the memory that owns an address (the fabric's second memory from 0x1000).
function automatic logic [7:0] mem_byte(input int unsigned a);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) return `AHB_MEM1.mem[a % `AHB_DEPTH];
  if (a >= 32'h1000) return 8'h00;
`endif
  return `AHB_MEM.mem[a % `AHB_DEPTH];
endfunction
task automatic set_byte(input int unsigned a, input logic [7:0] v);
`ifdef AHB_MEM1
  if (a >= 32'h1000 && a < 32'h2000) `AHB_MEM1.mem[a % `AHB_DEPTH] = v;
  else if (a < 32'h1000) `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`else
  `AHB_MEM.mem[a % `AHB_DEPTH] = v;
`endif
endtask

// The memory, n bytes from addr, one bus word per group.
task automatic dump(input int unsigned addr, input int n);
  string line;
  int unsigned a = (addr / 16) * 16;
  while (a < addr + n) begin
    line = $sformatf("0x%05h ", a[19:0]);
    for (int i = 0; i < 16; i++) begin
      if (i % BUS_BYTES == 0) line = {line, " "};
      line = {line, $sformatf(" %02h", mem_byte(a + i))};
    end
    $display("%s", line);
    a = a + 16;
  end
endtask

// Fill n bytes from addr with first, first+1, ... so untouched bytes stay recognisable.
task automatic mem_fill(input int unsigned addr, input int n, input logic [7:0] first = 8'h00);
  for (int i = 0; i < n; i++) set_byte(addr + i, first + i[7:0]);
endtask

// Drive nothing new for n cycles.
task automatic idle(input int n);
  repeat (n) @(posedge hclk);
  #1;
endtask
Read included file: ahb_fabric.sv
`timescale 1ns/1ns
// ahb_fabric.sv: one manager, an address decoder, two memories, the default subordinate and the
// multiplexor that returns HRDATA, HRESP, HREADY and HEXOKAY (read-only in this lesson).
// The address map (every region starts and ends on a 1 KB boundary):
//   0x0000-0x0fff  m0, a 4 KB memory
//   0x1000-0x1fff  m1, a 4 KB memory
//   0x2000-0x23ff  the AHB-to-APB bridge (32-bit bus) and behind it the APB completer p0 of P3
//                  (RAM at 0x2000-0x20ff, registers at 0x2100-0x2110; switches fab.p0.waits and the rest)
//   anything else  the default subordinate: OKAY to IDLE and BUSY, the two-cycle ERROR to NONSEQ and SEQ
// The decoder selects in the address phase; the multiplexor must follow the data phase, so its select
// is registered at every edge where HREADY is 1. Switches: trace (print the routing at each accepted
// address phase); broken twins mux_addr_phase (the multiplexor follows the address phase select) and
// mux_no_hready (the select is registered at every edge, even while HREADY is low).

// The default subordinate: it answers the addresses no other subordinate owns.
module ahb_default (
  input  logic hclk,
  input  logic hresetn,
  input  logic hsel,
  input  logic [1:0] htrans,
  input  logic hready,
  output logic hreadyout,
  output logic hresp
);
  bit d_err = 0, e_two = 0;
  initial begin hreadyout = 1; hresp = 0; end
  always @(posedge hclk) begin
    if (!hresetn) begin
      d_err = 0; e_two = 0; hreadyout <= 1; hresp <= 0;
    end else begin
      if (d_err) begin
        if (!hreadyout) e_two = 1;
        else d_err = 0;
      end
      if (hsel && hready) begin d_err = htrans[1]; e_two = 0; end
      hreadyout <= !(d_err && !e_two);
      hresp <= d_err;
    end
  end
endmodule

module ahb_fabric #(parameter int DATA_W = 32) (
  input  logic              hclk,
  input  logic              hresetn,
  input  logic [31:0]       haddr,
  input  logic [1:0]        htrans,
  input  logic              hwrite,
  input  logic [2:0]        hsize,
  input  logic [2:0]        hburst,
  input  logic [3:0]        hprot,
  input  logic              hmastlock,
  input  logic [DATA_W-1:0] hwdata,
  input  logic [DATA_W/8-1:0] hwstrb,
  input  logic              hexcl,
  input  logic [3:0]        hmaster,
  output logic [DATA_W-1:0] hrdata,
  output logic              hready,
  output logic              hresp,
  output logic              hexokay
);
  import ahb_pkg::*;
  bit trace = 0, mux_addr_phase = 0, mux_no_hready = 0;
  localparam bit HAS_APB = (DATA_W == 32);    // the AHB-to-APB bridge serves a 32-bit bus
  int cycle = -1;

  // ---- the decoder: HSELx in the address phase ----
  function automatic logic [1:0] region(input logic [31:0] a);
    if (a < 32'h1000) return 2'd0;
    if (a < 32'h2000) return 2'd1;
    if (HAS_APB && a < 32'h2400) return 2'd2;
    return 2'd3;
  endfunction
  logic [1:0] asel;
  assign asel = region(haddr);
  logic hsel0, hsel1, hsel2, hseld;
  assign hsel0 = (asel == 2'd0);
  assign hsel1 = (asel == 2'd1);
  assign hsel2 = (asel == 2'd2);
  assign hseld = (asel == 2'd3);

  // ---- the subordinates ----
  logic [DATA_W-1:0] rdata0, rdata1, rdata2;
  logic ready0, ready1, ready2, readyd, resp0, resp1, resp2, respd, exok0, exok1, exok2;
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m0 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel0), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready0), .hresp(resp0), .hrdata(rdata0), .hexokay(exok0));
  ahb_mem #(.DATA_W(DATA_W), .DEPTH(4096)) m1 (.hclk(hclk), .hresetn(hresetn), .hsel(hsel1), .haddr(haddr), .htrans(htrans),
    .hwrite(hwrite), .hsize(hsize), .hburst(hburst), .hprot(hprot), .hmastlock(hmastlock), .hwdata(hwdata), .hwstrb(hwstrb),
    .hexcl(hexcl), .hmaster(hmaster), .hready(hready), .hreadyout(ready1), .hresp(resp1), .hrdata(rdata1), .hexokay(exok1));
  // ---- SLOT 2: the AHB-to-APB bridge ----
  logic        psel, penable, pwrite, pready, pslverr;
  logic [31:0] paddr, pwdata, prdata, br_rdata;
  logic [3:0]  pstrb;
  logic [2:0]  pprot;
  assign exok2 = 1'b0;
  generate if (DATA_W == 32) begin : apb
    ahb_apb_bridge br (.hclk(hclk), .hresetn(hresetn), .hsel(hsel2), .haddr(haddr), .htrans(htrans), .hwrite(hwrite),
      .hprot(hprot), .hwdata(hwdata), .hwstrb(hwstrb), .hready(hready), .hreadyout(ready2), .hresp(resp2), .hrdata(br_rdata),
      .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr), .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot),
      .pready(pready), .prdata(prdata), .pslverr(pslverr));
    assign rdata2 = br_rdata;
  end else begin : no_apb
    assign ready2 = 1'b1; assign resp2 = 1'b0; assign rdata2 = '0;
    assign psel = 0; assign penable = 0; assign pwrite = 0; assign paddr = 0; assign pwdata = 0; assign pstrb = 0; assign pprot = 0;
  end endgenerate
  apb_periph #(.NAME("p0")) p0 (.pclk(hclk), .presetn(hresetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
    .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr));
  ahb_default dflt (.hclk(hclk), .hresetn(hresetn), .hsel(hseld), .htrans(htrans), .hready(hready), .hreadyout(readyd), .hresp(respd));

  // ---- the multiplexor: keyed by the data phase ----
  logic [1:0] dsel = 2'd3;
  always @(posedge hclk) begin
    cycle = cycle + 1;
    if (!hresetn) dsel <= 2'd3;
    else if (hready || mux_no_hready) dsel <= asel;
    if (trace && hresetn && hready && htrans[1])
      $display("fab e%0d: %0s %0s 0x%0h -> %0s", cycle, trans_name(htrans), hwrite ? "write" : "read", haddr,
               asel == 0 ? "m0" : asel == 1 ? "m1" : asel == 2 ? "apb" : "default (no subordinate)");
  end
  logic [1:0] msel;
  assign msel = mux_addr_phase ? asel : dsel;
  always @* begin
    case (msel)
      2'd0: begin hrdata = rdata0; hready = ready0; hresp = resp0; hexokay = exok0; end
      2'd1: begin hrdata = rdata1; hready = ready1; hresp = resp1; hexokay = exok1; end
      2'd2: begin hrdata = rdata2; hready = ready2; hresp = resp2; hexokay = exok2; end
      default: begin hrdata = '0; hready = readyd; hresp = respd; hexokay = 1'b0; end
    endcase
  end
endmodule
Read included file: ahb_apb_bridge.sv
`timescale 1ns/1ns
// ahb_apb_bridge.sv: an AHB subordinate that turns each accepted transfer into one APB transfer
// (read-only). The address phase is registered when HSEL, HREADY and a NONSEQ or SEQ transfer are
// all present; the data phase then runs SETUP and ACCESS on APB with HREADYOUT low, and ends in the
// cycle PREADY is 1: OKAY with HRDATA = PRDATA, or, when PSLVERR is 1, the two-cycle ERROR (HREADYOUT
// low then high, HRESP high in both). HWDATA and HWSTRB are data-phase signals the manager holds
// through the wait states, so PWDATA and PSTRB are driven from them directly; PADDR is the aligned
// word address (IHI 0024E 2.1.1 leaves an unaligned PADDR unpredictable). HPROT maps onto PPROT:
// privileged from HPROT[1], instruction from HPROT[0] = 0, and the access is Secure.
// A write therefore costs one setup and one access cycle: two cycles of data phase with no APB wait.
module ahb_apb_bridge (
  input  logic        hclk,
  input  logic        hresetn,
  input  logic        hsel,
  input  logic [31:0] haddr,
  input  logic [1:0]  htrans,
  input  logic        hwrite,
  input  logic [3:0]  hprot,
  input  logic [31:0] hwdata,
  input  logic [3:0]  hwstrb,
  input  logic        hready,
  output logic        hreadyout,
  output logic        hresp,
  output logic [31:0] hrdata,
  output logic        psel,
  output logic        penable,
  output logic        pwrite,
  output logic [31:0] paddr,
  output logic [31:0] pwdata,
  output logic [3:0]  pstrb,
  output logic [2:0]  pprot,
  input  logic        pready,
  input  logic [31:0] prdata,
  input  logic        pslverr);
  localparam int IDLE = 0, SETUP = 1, ACCESS = 2, ERR1 = 3, ERR2 = 4;
  bit trace = 0;
  int st = IDLE;
  logic        wr = 0;
  logic [31:0] a = 0;
  logic [2:0]  prot = 0;
  wire take = hsel && hready && htrans[1];
  assign psel      = (st == SETUP || st == ACCESS);
  assign penable   = (st == ACCESS);
  assign pwrite    = wr;
  assign paddr     = {a[31:2], 2'b00};     // APB leaves an unaligned PADDR unpredictable: the word, and PSTRB picks the bytes
  assign pwdata    = wr ? hwdata : 32'h0;
  assign pstrb     = wr ? hwstrb : 4'h0;
  assign pprot     = prot;
  assign hreadyout = (st == IDLE || st == ERR2) ? 1'b1 : (st == ACCESS) ? (pready === 1'b1 && pslverr !== 1'b1) : 1'b0;
  assign hresp     = (st == ERR1 || st == ERR2);
  assign hrdata    = (st == ACCESS) ? prdata : 32'h0;
  always @(posedge hclk or negedge hresetn) begin
    if (!hresetn) st <= IDLE;
    else begin
      case (st)
        SETUP:  st <= ACCESS;
        ACCESS: if (pready === 1'b1) begin
                  if (trace) $display("apb e%0d: %0s 0x%03h%0s", ($time - 5) / 10, wr ? "write" : "read", a[11:0], pslverr === 1'b1 ? " PSLVERR" : "");
                  st <= (pslverr === 1'b1) ? ERR1 : IDLE;
                end
        ERR1:   st <= ERR2;
        default: st <= IDLE;
      endcase
      // A new address phase can be taken whenever HREADY is 1: in IDLE, in the last cycle of ERROR,
      // or at the edge an access completes with OKAY.
      if (take && (st == IDLE || st == ERR2 || (st == ACCESS && pready === 1'b1 && pslverr !== 1'b1))) begin
        wr <= hwrite; a <= haddr; prot <= {~hprot[0], 1'b0, hprot[1]}; st <= SETUP;
      end
    end
  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
ConceptIn fault 10 an INCR4 crosses from m0 into m1 and every byte lands at its address. Why is it still a broken rule?
ConceptIn fault 11 one APB peripheral never raises PREADY. Why does a read of an AHB memory issued afterwards never complete either?
Read the waveformFault 6: m0 has two wait states to give the read of 0x100. At which cycle does that read end, and whose data does it carry?
The read of 0x100 under fault 6
01234htransIDLENONSEQIDLEhaddr11001001100hreadyhrdatab1b1b1b1a0a0a0a0b1b1b1b1

The read is taken at cycle 1. a0a0a0a0 is the content of 0x100; b1b1b1b1 that of 0x1100.

Remember

  • A rule checker answers "did every phase follow the rules?" from the wires; a scoreboard answers "did every byte arrive where it was sent?" from the data. Seven of the eleven faults here need the scoreboard.
  • The read multiplexor follows the data phase: its select is registered at edges where HREADY is high, never in the address phase and never during a wait.
  • A subordinate samples an address phase only when HREADY is high; one that does otherwise acts on transfers the manager may still cancel.
  • ERROR takes two cycles so the manager can cancel the transfer behind it; a one-cycle ERROR leaves it no edge to do so.
  • HREADY is shared: one subordinate that never ends its data phase, even an APB peripheral behind a bridge, stops the whole bus, and only a watchdog sees it.