Skip to content

AMBA APB · Module 11

Write Logic

The slave's write logic — qualifying PSEL, PENABLE, PWRITE, PREADY, and the register select into a per-register write-enable that commits exactly once on the completing edge, merging PSTRB byte lanes and preserving unstrobed bytes. The single most corruption-prone piece of the slave if the completion condition is wrong.

The write logic is the slave's single most corruption-prone piece of combinational glue. Everything before it has been structural — you have the register bank that holds state (11.1) and the decoder that selects a register (11.2). This chapter builds the thing that connects them on the write path: the logic that decides when to commit, and which bytes to commit. It is tiny — a handful of AND gates and a byte-lane mux — but it sits exactly where a wrong condition silently writes the wrong data to the right register, or the right data twice, or once too early. The single idea to carry: the write logic commits the right bytes to the right register exactly once, on the cycle the access completes — and "completes" means PREADY is high, not "PENABLE rose."

1. Problem statement

The problem is generating, from the raw APB write phase, a per-register write-enable that pulses exactly once at the moment the access commits, and a write-data word in which only the strobed byte lanes are updated.

The bank from 11.1 does not want bus signals. It wants two clean products: a decoded wr_en[reg] that is high for exactly one clock edge, and a wr_data in which each byte lane is either the new PWDATA byte (strobed) or the register's current byte (unstrobed). Producing those two products correctly forces three decisions that have no margin for error:

  • The commit condition. A write must commit when, and only when, all of these hold at the same sampled edge: the slave is selected (PSEL), the access phase is active (PENABLE), it is a write (PWRITE), the slave has completed the access (PREADY high), and this register is the one addressed (reg_sel from the decoder). Drop any one term and you commit at the wrong time or to the wrong place. The most dangerous omission is PREADY — without it, a slave that inserts a wait commits during the wait.
  • Exactly once. Across a multi-cycle access (one or more wait states), the write-enable for a register must be high on exactly one edge — the completing one. Commit on every ACCESS cycle and a wait state becomes two or three writes of (possibly changing) data.
  • The byte merge. When PSTRB is not all-ones, only the strobed lanes change. The write logic must form wr_data lane-by-lane: strobed lane takes PWDATA[lane], unstrobed lane takes the register's current byte. Write the full word regardless of PSTRB and every sub-word write corrupts the bytes the master deliberately left alone.

So the job is not "latch PWDATA when a write happens." It is to manufacture a single-pulse, per-register, byte-accurate commit from a bus phase whose completion timing the slave itself controls.

2. Why previous knowledge is insufficient

You arrive here with four pieces that are each necessary and none sufficient:

  • From 11.1, register-bank design, the bank takes a decoded wr_en and an already-merged wr_data and applies each register's kind rule. The bank deliberately knows nothing about when the write is valid or which bytes to keep — it trusts the glue. This chapter is that glue: it is the module that owes the bank a correct wr_en and a correct wr_data.
  • From 11.2, the address decoder, you have reg_sel — a one-hot (or per-register) signal that says which register PADDR points at. But reg_sel is purely positional; it is high during SETUP and ACCESS alike and says nothing about whether a write should commit or when. It is one input to the commit condition, not the commit itself.
  • From Module 7, write completion, you know the protocol contract: a write commits on the edge where PSEL & PENABLE & PREADY are all high, and a wait state (PREADY low) means "not yet — hold." That chapter proved the rule from the manager's point of view. This chapter is the slave RTL that implements it — we do not re-derive the contract, we build the gate that obeys it.
  • From Module 10, the PSTRB deep dive, you know what byte strobes mean and why partial-word writes need them. Here we apply that on the slave's write path — the byte-lane merge — rather than re-explaining the feature.

What no prior chapter gives you is the combination: the single combinational expression that ANDs the protocol-completion qualifier with the positional reg_sel to make a per-register enable, plus the lane-wise merge that respects PSTRB. That synthesis — and its forward link to 11.5, PREADY generation, which decides when the access completes and therefore when this logic fires — is exactly the gap this chapter closes.

3. Mental model

The model: the write logic is a turnstile with a strict admission rule. A passenger (the write data) only goes through — commits to the register — when every condition on the gate is satisfied at the same instant: the right ticket (PSEL), the doors actually open (PENABLE), it's the right direction (PWRITE), the gate is clear to release (PREADY high), and the passenger is standing at this turnstile (reg_sel). Miss any one and the turnstile stays locked. And it admits exactly one passenger per access — it does not let the same passenger through twice while the queue (the wait state) backs up.

Three refinements make it precise:

  • The commit is a single AND. wr_en = PSEL & PENABLE & PWRITE & PREADY & reg_sel. Read it as five guarantees: selected, in ACCESS, writing, completing now, and this register. The whole subtlety of the chapter lives in the PREADY term — it is what makes the pulse land on the completing edge and what suppresses commits during a wait.
  • One pulse, by construction. Because the access stays in ACCESS (with PENABLE high and PSEL high) for the entire wait, the only thing keeping wr_en low through the wait is PREADY. The instant PREADY rises, wr_en goes high for that one cycle, the register clocks, and the next edge the access tears down (PENABLE drops). So a single completing edge ⇒ a single commit, automatically — provided PREADY is in the condition.
  • The data is merged before it reaches the flop. In parallel with the enable, the logic forms wr_data lane-by-lane: wr_data[lane] = PSTRB[lane] ? PWDATA[lane] : reg_q[lane]. The strobed lanes carry the new bytes; the unstrobed lanes carry the register's current bytes so they are written back unchanged. The bank then clocks the whole 32-bit wr_data under the single wr_en — one enable, byte-accurate data.
A timing diagram of an APB write with one wait state: PSEL, PENABLE, PWRITE and reg_sel are high through the ACCESS phase, but PREADY is low during the wait so the wr_en AND stays low; when PREADY goes high on the completing edge wr_en pulses exactly once and the register latches PWDATA; PSTRB 0b0110 means only the two strobed byte lanes are merged while the unstrobed lanes are preserved.
Figure 1 — the slave's write-commit condition across a write access with one inserted wait state. PSEL asserts and the access enters ACCESS when PENABLE rises, but PREADY is held low for one cycle (the amber wait band), so although PSEL, PENABLE, PWRITE and reg_sel are all high, the AND wr_en stays low — no commit happens during the wait. On the completing edge (the green band, where PREADY is sampled high), wr_en pulses exactly once and the bank latches PWDATA. The lower rows show PSTRB = 0b0110 gating the byte lanes: only lanes 1 and 2 (bytes BB and CC) are merged into the register, while the unstrobed lanes are preserved unchanged. The figure teaches the throughline: commit on the PREADY-high edge, exactly once, with only the strobed bytes.

4. Real SoC implementation

In RTL, the write logic is a thin combinational block that produces the bank's two inputs. It does not contain flops for the register state — that lives in the bank — it only manufactures the wr_en and the merged wr_data. Here is the idiomatic implementation.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Slave write logic: turns a qualified APB write into the bank's two inputs:
//   (1) a per-register write-enable that pulses EXACTLY ONCE, at completion
//   (2) a byte-merged write-data word (PSTRB-gated)
// Storage lives in the register bank (chapter 11.1); this block is pure glue.
module apb_write_logic #(
  parameter int unsigned NUM_REGS = 4
)(
  input  logic              psel, penable, pwrite, pready,
  input  logic [31:0]       pwdata,
  input  logic [3:0]        pstrb,
  input  logic [NUM_REGS-1:0] reg_sel,   // one-hot decode from chapter 11.2
  input  logic [31:0]       reg_q [NUM_REGS], // current bank values (read-back)
  output logic [NUM_REGS-1:0] wr_en,     // single-cycle per-register commit
  output logic [31:0]       wr_data [NUM_REGS] // byte-merged data per register
);
  // --- The commit qualifier: true ONLY on the completing edge of a write. ---
  // PREADY is the load-bearing term: low during a wait, so no early commit;
  // high exactly on the cycle the access completes, so the pulse lands once.
  logic write_commit;
  assign write_commit = psel & penable & pwrite & pready;
 
  for (genvar r = 0; r < NUM_REGS; r++) begin : g_reg
    // Per-register enable: the global commit qualifier ANDed with THIS select.
    // reg_sel[r] is positional (from the decoder); write_commit adds the
    // "now, and only now" timing. Together => one pulse, to the right register.
    assign wr_en[r] = write_commit & reg_sel[r];
 
    // PSTRB byte-merge: strobed lanes take PWDATA, unstrobed lanes hold the
    // register's CURRENT byte so they are written back unchanged. Forming the
    // full 32-bit word here lets the bank clock it under one enable.
    for (genvar b = 0; b < 4; b++) begin : g_byte
      assign wr_data[r][b*8 +: 8] =
          pstrb[b] ? pwdata[b*8 +: 8]   // strobed: new byte
                   : reg_q[r][b*8 +: 8]; // unstrobed: preserve current byte
    end
  end
endmodule

Two facts make this correct and minimal. First, the commit is one AND and the pulse is automatic. write_commit is psel & penable & pwrite & pready; because the access holds PENABLE/PSEL high for the whole wait, the only term that suppresses the commit during the wait is pready, and the instant it rises the access also tears down on the next edge — so the enable is high for exactly one cycle without any extra "have I already written?" state. Second, the byte-merge happens here, not in the bank — the bank receives a full 32-bit wr_data in which the unstrobed lanes already equal the register's current bytes, so its if (wr_en) reg <= wr_data; clocks all 32 bits yet only the strobed bytes actually change. The decoder (11.2) supplied reg_sel; the completion timing is set by PREADY generation (11.5); this block is the small synthesis of the two onto the bank's write port.

5. Engineering tradeoffs

The central design choice is which terms qualify the write-enable — and each choice commits something different, with a sharply different corruption risk.

Write-enable expressionWhen it pulsesWhat it commitsCorruption risk
PENABLE & reg_sel onlyEvery ACCESS cycle, including waitsThe current PWDATA on every wait cycle, then againSevere — multi-commit; if PWDATA shifts across a wait, latches a stale/intermediate word; also fires even on reads
PSEL & PENABLE & reg_selEvery ACCESS cycle (no read filter still missing PREADY)Commits during a wait, then again at completionSevere — double-commit; back-to-back writes can bleed into a neighbour; ignores wait states entirely
PSEL & PENABLE & PWRITE & reg_selEvery ACCESS cycle of a writeWrites are isolated from reads, but still commits in the waitHigh — no read corruption, but still early/double commit when the slave inserts waits
PSEL & PENABLE & PWRITE & PREADY & reg_sel (correct)The single completing edge of a write to this registerThe final PWDATA, onceNone — exactly-once, byte-accurate (with PSTRB merge), on completion
Full condition without PSTRB mergeCompleting edge (timing correct)The full 32-bit word regardless of strobesHigh — sub-word writes overwrite the bytes the master left unstrobed; corrupts neighbouring fields in the same word

The throughline: the timing terms (PSEL & PENABLE & PWRITE & PREADY) decide when and once, and the positional term (reg_sel) plus the PSTRB merge decide where and which bytes. Dropping PREADY is the classic and most damaging cut — it converts a clean single commit into an early/double commit the moment the slave is not zero-wait. Dropping the PSTRB merge is the second — it silently widens every sub-word write to the full word. Neither costs you any meaningful area to do right; both cost you a debugging week to get wrong.

6. Common RTL mistakes

7. Debugging scenario

The signature write-logic bug is an early / double commit caused by a write-enable that omits PREADY — invisible on a zero-wait slave, catastrophic the moment a wait state appears.

  • Observed symptom: a peripheral that worked in early bring-up starts corrupting register writes once a real (waited) slave is integrated. A driver writes a 32-bit configuration word and reads back a different value — sometimes the value it intended, sometimes a stale or half-formed one. With a "start" command register, the action occasionally fires twice per write. The corruption correlates with the slave inserting wait states (e.g. when a clock-crossing or a busy flag is active).
  • Waveform clue: the internal wr_en[reg] is high for more than one cycle during the ACCESS phase — it asserts the moment PENABLE rises and stays high across the wait cycle and the completing cycle. PREADY is low for the first ACCESS cycle (the wait) yet the register changes on that edge, then changes again on the PREADY-high edge. If PWDATA shifted between the two edges, the first (stale) value is what survives the intermediate cycle.
  • Root cause: the write-enable was coded as wr_en = psel & penable & pwrite & reg_sel — the PREADY term was omitted. Because the access holds PENABLE/PSEL high for the entire wait, the enable is high on every ACCESS cycle, not just the completing one. The slave commits during its own wait state and again at completion: a double-commit (and, for changing data, a stale-commit).
  • Correct RTL: add PREADY to the commit qualifier so the enable can only fire on the completing edge: assign wr_en[r] = psel & penable & pwrite & pready & reg_sel[r];. No extra flop or "already wrote" state is needed — the single AND is self-limiting because the access tears down the cycle after PREADY rises.
  • Verification assertion: assert that the enable implies the full completion condition, and that it pulses at most once per access — assert property (@(posedge pclk) disable iff(!presetn) wr_en[r] |-> (psel & penable & pwrite & pready & reg_sel[r])); (the enable never asserts without completion) plus assert property (@(posedge pclk) disable iff(!presetn) (psel & penable & pwrite & reg_sel[r] & !pready) |-> !wr_en[r]); (no commit during a wait). A coverage-grade check also asserts the register is $stable across the wait cycle.
  • Debug habit: when a register write is corrupted or an action fires twice, do not start in the driver or the bank — put wr_en[reg] on the wave and count how many edges it is high during one access. More than one ⇒ your commit condition is missing a term, and the term is almost always PREADY. The completion-qualified single-AND is the first thing to check on any slave that inserts waits; a huge fraction of "register reads back wrong" bugs are exactly this missing term.
Two stacked APB write timing diagrams with one wait state. Top correct case in green: PREADY low through the wait keeps wr_en low, so wr_en pulses once on the completing edge and the register commits once. Bottom buggy case in red: with PREADY omitted from the condition, wr_en asserts when PENABLE rises and stays high across the wait, so the register commits twice — once on stale data during the wait and again at completion.
Figure 2 — a write-enable missing PREADY, contrasted correct (top, green) versus bug (bottom, red), across a write with one wait state. Top: wr_en = PSEL & PENABLE & PWRITE & PREADY & reg_sel; PREADY is low through the wait, so wr_en stays low and pulses exactly once on the completing edge — the register latches the final data once. Bottom: wr_en = PSEL & PENABLE & reg_sel with PREADY omitted; the enable asserts as soon as PENABLE rises and stays high across the wait, so the register commits during the wait (latching the stale data D0) and again at completion — a double-commit that corrupts the value or fires a strobe action twice. The figure shows the fix is simply adding PREADY to the commit condition so the write fires once, on the cycle the access actually completes.

8. Verification perspective

The write logic is verified as a timing-and-data correctness property: the enable must fire exactly once, at completion, and the merged data must respect PSTRB. The highest-value checks are the ones that introduce a wait and a partial strobe — the cases zero-wait, full-word tests never exercise.

  • Commit exactly once, at completion. The core assertion: wr_en[r] is high if and only if the full completion condition holds — wr_en[r] |-> (psel & penable & pwrite & pready & reg_sel[r]) — and across any single access (including waits) the enable is high on at most one edge. Drive a slave that inserts 0, 1, and several wait states and assert the register changes only on the PREADY-high edge. This single property catches the entire early/double-commit family.
  • No commit during a wait. Explicitly assert that while the access is in ACCESS but not yet ready, the enable stays low and the register is stable: (psel & penable & pwrite & reg_sel[r] & !pready) |-> (!wr_en[r] && $stable(reg_q[r])). This is the targeted form of the rule and the one that fails loudly on a PREADY-less design.
  • Byte-merge correctness. For every PSTRB pattern (all 16 for a 32-bit word, or directed: all-ones, single-byte, two-lane, zero), assert that after the commit, strobed lanes equal the written byte and unstrobed lanes are unchanged from before the access: strobed ⇒ reg_q[r][lane] == pwdata_at_commit[lane], unstrobed ⇒ $stable. A simple full-word test with PSTRB = 4'hF passes a broken merge — you must drive partial strobes to exercise the lane logic.
  • Back-to-back writes. Two writes to the same register on consecutive accesses, and two writes to adjacent registers, must each produce exactly one commit to the intended register with no bleed — covering the case where a held (buggy) enable would corrupt a neighbour. Add functional coverage that a write completed after a wait, and that every PSTRB pattern was seen with a commit, so the verification actually visited the corners the bug lives in.

The point: an address-only or zero-wait write test is nearly blind to this logic's real risks. Verify the single-pulse-at-completion property with inserted waits, sweep PSTRB to check the merge, and run back-to-back writes — that is where the early-commit, stale-data, and lane-corruption bugs surface.

9. Interview discussion

"Write the slave logic that commits an APB write" is a precise RTL question, and the depth is entirely in the commit condition and the byte merge.

Lead with the expression: wr_en = PSEL & PENABLE & PWRITE & PREADY & reg_sel, and explain each term as a guarantee — selected, in ACCESS, a write, completing now, and this register. Then deliver the term that separates strong answers: PREADY is the load-bearing term. A write does not commit when PENABLE rises; it commits on the edge the access completes, which is when PREADY is high. If the slave inserts a wait, a condition without PREADY commits during the wait and again at completion — a double-commit that latches stale data or fires a strobe action twice. Stress that no extra "already wrote" state is needed — the single AND is self-limiting because the access tears down the cycle after PREADY rises. Then close on the data path: the byte-merge respects PSTRB — strobed lanes take PWDATA, unstrobed lanes preserve the register's current byte — so a sub-word write does not clobber neighbouring bytes. Mentioning that this is the slave-side implementation of the Module 7 write-completion contract, and that the wait timing comes from PREADY generation, signals you see the whole slave, not just one gate. If you can name the failure mode — "register reads back wrong / action fires twice once the slave isn't zero-wait" — you've shown you've debugged it.

10. Practice

  1. Write the commit AND. From memory, write the one-line expression for a per-register write-enable, name each term, and state which term suppresses a commit during a wait state.
  2. Add the merge. Extend it to produce a 32-bit wr_data that respects PSTRB for register r; write the per-lane ternary and explain what the unstrobed lanes carry and why.
  3. Trace a waited write. Given an access with one wait state, draw PSEL/PENABLE/PWRITE/PREADY/reg_sel and the resulting wr_en; mark the exact edge the register clocks and prove the enable is high for only one cycle.
  4. Spot the bug. You are handed wr_en = psel & penable & pwrite & reg_sel. State what goes wrong on a slave that inserts a wait, give the symptom a verification engineer would see, and write the one-term fix.
  5. Sweep the strobes. For PSTRB = 4'hF, 4'b0001, 4'b0110, and 4'h0, state which register bytes change after a write of 0xAABBCCDD and which are preserved.

11. Q&A

12. Key takeaways

  • The write logic commits the right bytes to the right register exactly once, at completion. It produces the bank's two inputs — a single-cycle per-register wr_en and a byte-merged wr_data — and holds no state itself.
  • wr_en = PSEL & PENABLE & PWRITE & PREADY & reg_sel. Each term is a guarantee: selected, in ACCESS, a write, completing now, this register. The whole subtlety is the PREADY term.
  • PREADY is the load-bearing term. The access holds PSEL/PENABLE high through any wait, so PREADY is the only thing that pins the commit to the completing edge. Omit it and the slave double-commits during its own wait, latching stale data or firing strobes twice — invisible on a zero-wait slave.
  • The single AND is self-limiting — no "already wrote" flag. Because the access tears down the cycle after PREADY rises, the completion-qualified enable is high for exactly one edge by construction.
  • Merge bytes with PSTRB before the flop. Strobed lanes take PWDATA, unstrobed lanes preserve the register's current byte, so sub-word writes never clobber neighbouring bytes.
  • Verify with waits and partial strobes. Assert commit-exactly-once-at-completion, no-commit-during-wait, byte-merge correctness, and back-to-back writes — corners a zero-wait, full-word test is blind to. This is the slave-side implementation of the write-completion contract, with completion timing set by PREADY generation.