Skip to content

AMBA APB · Module 11

Slave RTL Templates

Production-grade APB slave RTL templates that assemble the whole module — bank, decoder, read mux, write logic, PREADY and PSLVERR generation, and reset — into reusable, parameterised, lint-clean blocks, and the trade-offs between a minimal always-ready, a wait-capable registered, and a fully-registered variant.

You did not build seven things in this module — you built one thing, seven times over. The register bank, the decoder, the read mux, the write logic, PREADY, PSLVERR, and reset are not independent peripherals; they are the seven sections of a single circuit: an APB slave. This closing chapter does the assembly. It takes those pieces and welds them into reusable, production-grade slave templates — RTL you can instantiate today, change one register map, and ship as a real peripheral. The single idea to carry: a correct APB slave is the composition of this module's seven pieces, and a good template makes that composition correct-by-construction and reusable across every peripheral in the SoC. The danger this chapter retires is the worst habit in RTL design — hand-copying a slave per peripheral and mis-wiring one of the seven pieces during the copy. A template replaces copy-paste with parameterisation: the structure is fixed and proven once; only the map changes.

1. Problem statement

The problem is turning seven verified sub-modules into one reusable peripheral skeleton — a template that integrates the bank, decoder, read mux, write logic, PREADY, PSLVERR, and reset so cleanly that instantiating a new peripheral is a matter of editing a register map, not re-deriving a slave.

Every SoC has dozens of APB peripherals — a UART, a timer, a GPIO block, a watchdog, a PWM, a mailbox — and every one of them is an APB slave with the same seven-part structure. The naive approach builds each from scratch (or copies the last one and edits it), which means the seven pieces get re-wired by hand dozens of times, and each copy is a fresh opportunity to mis-align PREADY with PRDATA, forget a reset value, or break the one-hot decode. The template's job is to make that impossible by construction. That forces three decisions:

  • What is fixed, and what is parameterised. The structure — how the seven pieces wire together, the handshake timing, the no-latch read default, the reset discipline — must be fixed and proven once. The map — how many registers, at what offsets, of what kind, with what reset values — must be a parameter, because that is the only thing that legitimately differs between a UART and a timer.
  • How many template variants you need. One template cannot serve every timing context. A slow, low-fanout APB segment is happy with a minimal always-ready slave; a fast bus near timing closure needs fully-registered outputs; a slave fronting a slow data source needs a wait-capable variant. The chapter must define a small set of variants and the rule for choosing among them.
  • What makes a template production-grade rather than a sketch. Lint-clean (no latches, no inferred logic), defined defaults on every path, one-hot decode, a defined reset on every flop, registered or combinationally-defined outputs by policy, and a clear separation between the register bank and the bus glue — so the template survives synthesis, CDC sign-off, and reuse without surprises.

So the job is not "write one more slave" — it is to capture the invariant structure of an APB slave as a parameterised, lint-clean template, define the handful of variants that cover real timing contexts, and make every per-peripheral change a map edit.

2. Why previous knowledge is insufficient

You have built all seven pieces, and each is correct on its own. But a pile of correct sub-modules is not a slave — the value, and the remaining risk, is entirely in the composition:

  • 11.1 Register-bank design gave you the typed storage — RW, RO, W1C, WO — and the discipline of separating storage from bus glue. But a bank is not a slave; it has no bus interface, no handshake, no error response.
  • 11.2 Address decoder gave you the one-hot per-register select and the decode-error flag. But a decoder produces selects, not a working transfer.
  • 11.3 Read-data mux gave you the combinational select onto PRDATA with a defined default. But a mux without a phase-aligned PREADY is exactly the stale-read trap this chapter's debug beat exposes.
  • 11.4 Write logic gave you PSTRB byte-merge and the access-phase commit. But write-enable timing only works relative to the handshake — it cannot be reasoned about in isolation from PREADY.
  • 11.5 PREADY generation gave you the ready handshake and wait-state insertion. But ready timing is meaningful only when it is aligned with when the read mux and write commit actually happen.
  • 11.6 PSLVERR generation gave you the error response for decode misses and illegal accesses. But the error flag must be wired to the decoder's miss output and qualified by the same handshake — composition again.
  • 11.7 Slave reset behaviour gave you the defined power-on state. But reset is a property of every flop in the whole slave, which only exists once the pieces are assembled.

Notice the pattern: every piece's correctness depends on its phase relationship to the others. The decode feeds both the read mux and the write logic and PSLVERR. PREADY must agree with when PRDATA is valid and when the write commits. Reset must cover every flop across all seven pieces. None of that can be reasoned about one chapter at a time — it is an integration property. So the knowledge to add is not a new piece; it is the assembly rule that makes the seven pieces a single correct circuit, captured as a template so the assembly is done right once and reused everywhere.

3. Mental model

The model: an APB slave template is a printed circuit board with seven fixed footprints and one swappable component. The board layout — where the bank sits, how the decode traces run to the mux and the write logic and the error generator, how PREADY lines up with the data, how reset reaches every flop — is fixed, proven, and never re-drawn. The one swappable component is the register map: a parameter that says "this peripheral has these registers at these offsets, of these kinds, with these reset values." Building a new peripheral is populating the swappable slot, not re-laying the board. And because the board is proven, you cannot accidentally mis-route a trace during the swap — the very class of bug that hand-copying invites.

Three refinements make it precise:

  • The structure is the invariant; the map is the variable. Everything that is the same across every APB slave — the seven-piece wiring, the handshake phase relationship, the no-latch read default, the reset-every-flop rule, the one-hot decode — lives in fixed template code. Everything that differs between peripherals — register count, offsets, kinds, reset constants, which bits are hardware-owned — lives in a parameter or a localised map block. If a per-peripheral change forces you to edit the structure, the template is leaking, and that is a smell to fix, not work around.
  • Variants are about timing context, not about features. There is a small, fixed set of variants — a minimal always-ready slave (combinational PREADY/PRDATA, lowest latency, for slow segments), a wait-capable slave (a tiny FSM and registered ready for a slow source), and a fully-registered slave (all outputs flopped, for timing closure on a fast bus). They differ only in where the flops sit and how ready is generated — the seven pieces and the map are identical. You pick a variant by the bus's timing situation, then drop in the same map.
  • Correct-by-construction beats correct-by-inspection. A template's whole point is that the dangerous integration choices — PREADY/PRDATA phase alignment, the no-latch default on the read mux, the decode feeding both the mux and the error path, reset on every flop — are baked into the fixed structure, so a new instance is correct because it reused the proven assembly, not because a reviewer re-checked all seven phase relationships by eye. That is the difference between a template and a copy.
A block diagram of an APB slave assembled from seven labelled pieces: the address decoder and write logic on the left feeding a central typed register bank, a read-data mux on the right selecting PRDATA, PREADY and PSLVERR generators along the bottom, and a reset wrapper enclosing all of it, each block annotated with the chapter that built it.
Figure 1 — a production APB slave as the composition of the module's seven pieces. The APB interface enters on the left and is recognised in the access phase; the address decoder (11.2) produces a one-hot per-register select and a decode-error flag. That select drives the write logic (11.4), which byte-merges PWDATA via PSTRB into a per-register decoded write-enable feeding the central register bank (11.1) — the only state in the slave. The same one-hot decode feeds the read-data mux (11.3), which selects one register onto PRDATA with a defined zero default. Across the bottom, PREADY generation (11.5) drives the ready handshake and PSLVERR generation (11.6) flags decode errors and illegal accesses, both qualified by the same handshake. Wrapping the whole circuit, the reset block (11.7) defines every flop out of PRESETn. The figure reads as the assembly of chapters 11.1 through 11.7 into one parameterised, correct-by-construction template whose only per-peripheral change is the register map.

4. Real SoC implementation

Here is a complete, production-grade, parameterised APB slave template — the minimal always-ready variant — assembling all seven pieces. It is lint-conscious: no latches, defined defaults on every path, one-hot decode, registered storage with a defined reset, and a combinational but fully-defined PRDATA/PREADY/PSLVERR. Each section is commented with the chapter it comes from. To re-use it for another peripheral you change only the address map and the register declarations — nothing structural.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  apb_slave_template  —  minimal always-ready variant (APB4)
//  Assembles the seven pieces of Module 11 into one reusable slave.
//  Per-peripheral, you change ONLY the address map + register block.
// =====================================================================
module apb_slave_template #(
  parameter int ADDR_W = 12,        // PADDR bits this slave decodes
  parameter int DATA_W = 32         // APB data width (APB is 32-bit)
)(
  input  logic                 pclk,
  input  logic                 presetn,
  // ---------------- APB4 subordinate interface ----------------
  input  logic                 psel,
  input  logic                 penable,
  input  logic                 pwrite,
  input  logic [ADDR_W-1:0]    paddr,
  input  logic [DATA_W-1:0]    pwdata,
  input  logic [DATA_W/8-1:0]  pstrb,
  output logic [DATA_W-1:0]    prdata,
  output logic                 pready,
  output logic                 pslverr,
  // ---------------- hardware-side ports (peripheral logic) ----
  input  logic [DATA_W-1:0]    hw_status, // RO value owned by hardware
  input  logic                 hw_event,  // sets the W1C interrupt flag
  output logic [DATA_W-1:0]    ctrl_o,    // RW control, consumed by hw
  output logic                 cmd_start  // WO strobe pulse to hardware
);
  // ------------------------------------------------------------------
  //  Address map  —  THE ONLY THING THAT CHANGES PER PERIPHERAL
  //  (word-aligned offsets; one-hot decode below, 11.2)
  // ------------------------------------------------------------------
  localparam logic [ADDR_W-1:0] OFF_CTRL   = 'h00; // RW   control
  localparam logic [ADDR_W-1:0] OFF_STATUS = 'h04; // RO   status (hw)
  localparam logic [ADDR_W-1:0] OFF_IFLAG  = 'h08; // W1C  interrupt flags
  localparam logic [ADDR_W-1:0] OFF_CMD    = 'h0C; // WO   command strobe
 
  // ------------------------------------------------------------------
  //  Access-phase qualifiers (11.5 PREADY / 11.4 write logic)
  //  Always-ready: the access phase is psel & penable. A committing
  //  read or write happens in exactly that cycle.
  // ------------------------------------------------------------------
  logic access  = psel &  penable;          // APB access phase
  logic do_write = access &  pwrite;        // committing write
  logic do_read  = access & ~pwrite;        // committing read
 
  // ------------------------------------------------------------------
  //  Address decoder (11.2)  —  one-hot register select + decode error.
  //  Word offset is paddr with the byte-lane bits masked off.
  // ------------------------------------------------------------------
  logic sel_ctrl, sel_status, sel_iflag, sel_cmd, decode_err;
  always_comb begin
    sel_ctrl   = 1'b0; sel_status = 1'b0;
    sel_iflag  = 1'b0; sel_cmd    = 1'b0;
    decode_err = 1'b0;
    if (psel) begin                          // only decode when selected
      unique case (paddr & ~('h3))           // word-align (drop [1:0])
        OFF_CTRL  : sel_ctrl   = 1'b1;
        OFF_STATUS: sel_status = 1'b1;
        OFF_IFLAG : sel_iflag  = 1'b1;
        OFF_CMD   : sel_cmd    = 1'b1;
        default   : decode_err = 1'b1;        // unmapped → PSLVERR (11.6)
      endcase
    end
  end
 
  // ------------------------------------------------------------------
  //  Write logic (11.4)  —  PSTRB byte-merge into a 32-bit write word.
  //  Only lanes whose strobe is set are updated; the rest hold.
  // ------------------------------------------------------------------
  function automatic logic [DATA_W-1:0] byte_merge
      (logic [DATA_W-1:0] old_v, logic [DATA_W-1:0] new_v,
       logic [DATA_W/8-1:0] strb);
    byte_merge = old_v;
    for (int b = 0; b < DATA_W/8; b++)
      if (strb[b]) byte_merge[b*8 +: 8] = new_v[b*8 +: 8];
  endfunction
 
  // ------------------------------------------------------------------
  //  Register bank (11.1)  —  the only state; reset on every flop (11.7)
  // ------------------------------------------------------------------
  logic [DATA_W-1:0] ctrl_q, status_q, iflag_q;
 
  // CTRL : RW, software-owned. byte-merged write commits in access phase.
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn)                     ctrl_q <= '0;            // reset (11.7)
    else if (do_write && sel_ctrl)    ctrl_q <= byte_merge(ctrl_q, pwdata, pstrb);
 
  // STATUS : RO, hardware-owned. Software writes can never reach it.
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) status_q <= '0;
    else          status_q <= hw_status;                       // hw only
 
  // IFLAG : W1C, joint ownership — hardware-set beats software-clear.
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) iflag_q <= '0;
    else begin
      if (do_write && sel_iflag)
        iflag_q <= iflag_q & ~byte_merge('0, pwdata, pstrb);   // write-1-clear
      if (hw_event) iflag_q[0] <= 1'b1;                        // hw set wins
    end
 
  // CMD : WO strobe — a one-cycle pulse to hardware on a write; reads 0.
  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) cmd_start <= 1'b0;
    else          cmd_start <= (do_write && sel_cmd);          // 1-cycle pulse
 
  assign ctrl_o = ctrl_q;
 
  // ------------------------------------------------------------------
  //  Read-data mux (11.3)  —  combinational, DEFINED DEFAULT (no latch).
  //  WO register reads back 0; everything else returns its stored value.
  // ------------------------------------------------------------------
  always_comb begin
    prdata = '0;                              // default 0 → no inferred latch
    unique case (1'b1)
      sel_ctrl  : prdata = ctrl_q;
      sel_status: prdata = status_q;
      sel_iflag : prdata = iflag_q;
      sel_cmd   : prdata = '0;                // WO reads as 0
      default   : prdata = '0;                // unmapped read → 0
    endcase
  end
 
  // ------------------------------------------------------------------
  //  PREADY generation (11.5)  —  always-ready: complete every access in
  //  one cycle. Tied to the access phase so the master never stalls.
  // ------------------------------------------------------------------
  assign pready = 1'b1;                        // single-cycle slave
 
  // ------------------------------------------------------------------
  //  PSLVERR generation (11.6)  —  flagged ONLY in the access phase and
  //  only for an unmapped offset (decode miss). Defined low otherwise.
  // ------------------------------------------------------------------
  assign pslverr = access & decode_err;       // qualified by the handshake
endmodule

Three facts make this a template and not just a slave. First, the only per-peripheral edit is the map — the four localparam offsets and the four register declarations. The decode, the byte-merge, the handshake, the read default, and the reset discipline are structural and never touched, which is the whole point: a UART instance and a timer instance differ by their map and nothing else. Second, PREADY and PRDATA are phase-aligned by constructionPREADY is tied high because PRDATA is combinational from the selected register in the same access phase; there is no flop between them to fall out of sync (the exact bug the copy-paste workflow invites, dissected in beat 7). Third, every output is defined on every pathprdata defaults to '0, pslverr is qualified by access, the read mux uses unique case with a default, and every flop has a reset value, so the design is lint-clean with no latches and no Xs. The wait-capable and fully-registered variants change only PREADY/PRDATA timing (a small FSM and a registered ready, or a flopped prdata/pready stage) — the seven pieces and the map are unchanged.

5. Engineering tradeoffs

The template is not one design — it is a small family, and you pick the variant by the bus's timing context. All three integrate the same seven pieces and the same map; they differ only in where the flops sit and how PREADY is generated.

VariantRead latencyTiming closureAreaComplexityWhen to use
Minimal always-ready (combinational PREADY/PRDATA, PREADY=1)Lowest — 0 wait states; access completes in the access phaseHardest — PRDATA and PSLVERR are combinational from the bank through the read mux; long path on a fast busSmallest — no extra flopsLowest — one mux, no FSMSlow/low-fanout APB segment, small register count, relaxed timing — the default for most peripherals
Wait-capable registered (small FSM, registered PREADY, inserts wait states)Higher — one or more wait cycles for a slow sourceModerate — registered ready breaks the combinational ready path; data may still be combinationalModerate — a few FSM flops + ready registerModerate — a 2–3 state FSM and its assertionsSlave fronting a slow data source (a shared RAM, an external bridge, a clock-crossing FIFO) that genuinely cannot answer in one cycle
Fully-registered (all outputs flopped: PRDATA, PREADY, PSLVERR)Higher — read returns one cycle later; PREADY registered to matchEasiest — every output leaves a flop, so the slave-to-master path is a register-to-register hopLargest — output register stage on every signalHighest — must keep registered PREADY and registered PRDATA phase-aligned, the very thing that breaks if mis-wiredFast APB bus or a slave far from the master where the combinational output path fails timing; timing-closure-driven

The throughline: you do not choose a variant on preference — you choose it on the bus's timing situation, and you keep the map identical across the choice. Start with the minimal always-ready variant (lowest latency, smallest, simplest) and escalate only when forced: to wait-capable when a source is genuinely slow, to fully-registered when synthesis fails the combinational output path. The cost of escalating is always latency (and, for fully-registered, the discipline of keeping a registered PREADY aligned with a registered PRDATA) — which is exactly why "more registered" is not "more correct," only "easier to close." The template family makes the escalation a swap of the ready/data stage, not a rewrite of the slave.

6. Common RTL mistakes

7. Debugging scenario

The signature template bug is a slave that returns stale read data, caused not by any one of the seven pieces being wrong but by two of them being mis-aligned during a hand-copyPRDATA was registered an extra stage when the design was copied from a pipelined peripheral, but PREADY was left tied high as in the always-ready original, so ready asserts a cycle before the data is valid. It is precisely the class of bug a correct-by-construction template prevents.

  • Observed symptom: software reads a register immediately after writing it and gets the old value; or a status register read returns the value of the previous read's address. The slave passes a single isolated read in directed simulation but fails back-to-back reads and fails on hardware intermittently — the data is always exactly one cycle late.
  • Waveform clue: on the APB, PREADY asserts in the access phase (PSEL & PENABLE) as it should, but PRDATA at that sampling edge still holds the previous register's value; the correctly-addressed value appears on PRDATA one cycle after the master has already latched the read. PREADY and PRDATA are one cycle out of phase.
  • Root cause: during a copy-paste from a pipelined slave, an extra output flop was left on the PRDATA path (adding a cycle of latency) while PREADY was kept tied high from the always-ready source. The two pieces — read-data path (11.3) and ready generation (11.5) — disagree on which cycle the read completes. Neither piece is individually wrong; their composition is. This is the integration property that no single-chapter review catches, and the exact failure a fixed template structure makes impossible.
  • Correct RTL: make PREADY and PRDATA agree on the completion cycle. In the always-ready variant, keep PRDATA combinational so it is valid the same cycle PREADY is high: assign pready = 1'b1; always_comb prdata = sel_ctrl ? ctrl_q : '0; /* etc */. In the fully-registered variant, register PREADY to match the registered PRDATA so both advance together — never one flopped and the other not. The template enforces this by tying the two generations to the same phase by construction, so an instance cannot split them.
  • Verification assertion: assert that whenever the slave completes a read, PRDATA equals the addressed register — assert property (@(posedge pclk) disable iff(!presetn) (psel && penable && pready && !pwrite && sel_ctrl) |-> (prdata == ctrl_q)); (and one per mapped register). A stale-read bug fails this on the completing edge because prdata still holds the previous value. A protocol-monitor variant asserts PRDATA is stable while PREADY is low and valid the cycle PREADY rises.
  • Debug habit: when reads are exactly one cycle stale, do not hunt inside the read mux — check the phase relationship between PREADY and PRDATA first. Count the flops on the PRDATA path and confirm PREADY was registered the same number of times. The moment you find a registered data path with an unregistered (or differently-registered) ready, you have it — and the fix is to re-align the two, which is exactly what instantiating the template instead of copying would have guaranteed.
Two stacked timing-and-block cases: the top buggy case shows PREADY tied high while PRDATA is registered an extra cycle, so the master samples the old register value marked stale in red; the bottom correct case shows PREADY and the read mux phase-aligned so PRDATA holds the addressed value when PREADY asserts, marked correct in green.
Figure 2 — a mis-wired hand-copied slave versus a correct-by-construction template instance. Top (bug, red): the read path was registered an extra stage during a copy from a pipelined peripheral, but PREADY was left tied high as in the always-ready original; PREADY asserts in the access phase while PRDATA still holds the previous register's value, so the master samples a stale read one cycle early. Bottom (correct, green): the template keeps the read mux and PREADY generation phase-aligned by construction, so when PREADY asserts in the access phase, PRDATA already holds the freshly addressed value and the master samples the correct read. The figure shows the failure is a phase mismatch between two of the seven pieces introduced by copy-paste, and that a template enforces the alignment so the class of bug cannot occur.

8. Verification perspective

A template's verification is itself reusable — you verify the structure once against the seven-piece contract, then verify each instance against its map. The leverage of a template is that the testbench, the protocol monitor, and the assertion set are written once and ride along with every peripheral.

  • Build one reusable slave testbench, parameterise it by the map. The same environment — APB driver, protocol monitor, RAL model, scoreboard — serves every instance; only the RAL register model (offsets, kinds, reset values, access policy) changes per peripheral, mirroring the RTL's "only the map changes." A UVM RAL built-in sequence (reset-value, bit-bash, access) then exercises every register's kind contract automatically, so per-instance verification is configuration, not new code.
  • A protocol monitor enforces the integration properties the template guarantees. Bind an APB protocol checker that asserts PREADY/PRDATA phase alignment, PRDATA stability while PREADY is low, PSLVERR only in the access phase, no X on PRDATA when selected, and PENABLE/PSEL legality. These are exactly the cross-piece properties a hand-copy breaks — so the monitor is the automated guardian of correct-by-construction, run on every instance.
  • Coverage is per-variant, not one-size-fits-all. The always-ready variant covers single-cycle read/write of every register, the decode-error access, and reset values. The wait-capable variant additionally covers every wait-state count and the FSM's states/transitions. The fully-registered variant covers the back-to-back read pipeline where the registered PREADY/PRDATA alignment is stressed. Reuse the base coverage and add only the variant-specific bins.
  • Lint and CDC sign-off are part of "done," and reusable. Lint the template once for no latches, no inferred logic, full-case/parallel-case on the decode and read mux, and defined resets — properties that hold for every instance because the structure is shared. If the slave bridges clock domains (a fast core to a slow APB, or a wait-capable slave fronting a CDC FIFO), the reset and the data-path synchronisers get a CDC review; a registered-output variant on a crossing simplifies that sign-off.

The point: verify the template's structure once and reuse the whole environment per instance. The reusable testbench, protocol monitor, RAL model, per-variant coverage, and lint/CDC sign-off are the verification analogue of the template itself — the assembly is proven once, and every peripheral inherits it by changing only its map.

9. Interview discussion

"Walk me through how you would structure a reusable APB slave you could drop into any peripheral" is a strong senior-level question because a weak answer describes one slave while a strong one shows you think in invariant structure plus a parameterised map, with variants for timing context.

Lead with the architecture: an APB slave is the composition of seven pieces — register bank, address decoder, read mux, write logic, PREADY, PSLVERR, and reset — and a template fixes that composition once so the only per-peripheral change is the register map. Then show the integration insight that separates senior answers: the pieces' correctness is a phase relationship, not a per-piece property — the decode feeds the mux, the write logic, and the error path; PREADY must agree with when PRDATA is valid and when the write commits; reset must cover every flop. That is why you template the structure rather than copy a slave — copying re-wires those relationships by hand and invites the stale-read bug where a registered PRDATA and a tied-high PREADY fall out of phase. Deliver the variant judgment: a minimal always-ready slave for slow segments (lowest latency, hardest to close), a wait-capable variant for a genuinely slow source, and a fully-registered variant for timing closure on a fast bus — same seven pieces, same map, different ready/data staging — and you escalate only when forced, because more registering costs latency, not correctness. Close with production discipline — lint-clean, no latches, one-hot decode, defined defaults, reset on every flop, and a reusable testbench/RAL/monitor that verifies the structure once and every instance by its map. Mentioning that "the bug a template prevents is the one where two correct pieces are mis-aligned during a copy" signals you have actually shipped peripherals this way.

10. Practice

  1. Spot the variable. Given the template in beat 4, list every line you would edit to retarget it from this generic peripheral to a 4-register GPIO block — and confirm none of them are structural.
  2. Choose the variant. For (a) a watchdog on a slow APB segment, (b) a slave fronting a shared dual-port RAM, and (c) a high-fanout slave on a fast APB bus failing the PRDATA output path, pick the template variant and justify it on latency vs timing closure.
  3. Add a wait state. Convert the always-ready variant into the wait-capable variant: write the 2–3 state FSM and the registered PREADY that inserts exactly one wait cycle, keeping PRDATA valid the cycle PREADY rises.
  4. Register the outputs. Convert the always-ready variant into the fully-registered variant — flop PRDATA, PREADY, and PSLVERR — and state the new read latency and the one alignment invariant you must preserve.
  5. Break it on purpose. Introduce the beat-7 bug (extra PRDATA flop, PREADY still tied high), predict the exact waveform on a back-to-back read, and write the assertion that catches it.

11. Q&A

12. Key takeaways

  • A production APB slave is the composition of this module's seven pieces — register bank, address decoder, read mux, write logic, PREADY, PSLVERR, and reset — and a template captures that composition once so it is correct-by-construction and reusable across every peripheral.
  • Fix the structure; parameterise the map. The seven-piece wiring, handshake phase alignment, no-latch read default, one-hot decode, and reset-every-flop rule are invariant; register count, offsets, kinds, and reset values are the only legitimate per-peripheral change. If an instance forces a structural edit, the template is leaking.
  • The danger a template retires is hand-copying. Copying re-wires the pieces and invites the stale-read bug where two individually-correct pieces — a registered PRDATA and a tied-high PREADY — fall out of phase. A template makes that mis-alignment impossible in the swap.
  • There is a small variant family chosen by timing context, not preference — minimal always-ready (lowest latency), wait-capable (slow source), fully-registered (timing closure) — same seven pieces and same map, different ready/data staging. Escalate only when forced, because more registering costs latency, not correctness.
  • A template does not skip verification — it makes verification reusable. Verify the structure once with a parameterised testbench, protocol monitor, RAL model, per-variant coverage, and lint/CDC sign-off; verify each instance against its map (offsets, kinds, reset values, propagated widths), which the structural proof never saw.
  • Every output defined on every pathPRDATA zero default, PSLVERR qualified by the access phase, unique case with a default, reset on every flop — is what makes the template lint-clean, latch-free, and safe to drop into any peripheral.