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 (PREADYhigh), and this register is the one addressed (reg_selfrom the decoder). Drop any one term and you commit at the wrong time or to the wrong place. The most dangerous omission isPREADY— 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
PSTRBis not all-ones, only the strobed lanes change. The write logic must formwr_datalane-by-lane: strobed lane takesPWDATA[lane], unstrobed lane takes the register's current byte. Write the full word regardless ofPSTRBand 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_enand an already-mergedwr_dataand 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 correctwr_enand a correctwr_data. - From 11.2, the address decoder, you have
reg_sel— a one-hot (or per-register) signal that says which registerPADDRpoints at. Butreg_selis 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 & PREADYare all high, and a wait state (PREADYlow) 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 thePREADYterm — 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
PENABLEhigh andPSELhigh) for the entire wait, the only thing keepingwr_enlow through the wait isPREADY. The instantPREADYrises,wr_engoes high for that one cycle, the register clocks, and the next edge the access tears down (PENABLEdrops). So a single completing edge ⇒ a single commit, automatically — providedPREADYis in the condition. - The data is merged before it reaches the flop. In parallel with the enable, the logic forms
wr_datalane-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-bitwr_dataunder the singlewr_en— one enable, byte-accurate data.
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.
// 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
endmoduleTwo 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 expression | When it pulses | What it commits | Corruption risk |
|---|---|---|---|
PENABLE & reg_sel only | Every ACCESS cycle, including waits | The current PWDATA on every wait cycle, then again | Severe — multi-commit; if PWDATA shifts across a wait, latches a stale/intermediate word; also fires even on reads |
PSEL & PENABLE & reg_sel | Every ACCESS cycle (no read filter still missing PREADY) | Commits during a wait, then again at completion | Severe — double-commit; back-to-back writes can bleed into a neighbour; ignores wait states entirely |
PSEL & PENABLE & PWRITE & reg_sel | Every ACCESS cycle of a write | Writes are isolated from reads, but still commits in the wait | High — 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 register | The final PWDATA, once | None — exactly-once, byte-accurate (with PSTRB merge), on completion |
| Full condition without PSTRB merge | Completing edge (timing correct) | The full 32-bit word regardless of strobes | High — 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 momentPENABLErises and stays high across the wait cycle and the completing cycle.PREADYis low for the first ACCESS cycle (the wait) yet the register changes on that edge, then changes again on thePREADY-high edge. IfPWDATAshifted 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— thePREADYterm was omitted. Because the access holdsPENABLE/PSELhigh 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
PREADYto 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 afterPREADYrises. - 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) plusassert 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$stableacross 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 alwaysPREADY. 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.
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 thePREADY-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 aPREADY-less design. - Byte-merge correctness. For every
PSTRBpattern (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 withPSTRB = 4'hFpasses 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
PSTRBpattern 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
- 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.
- Add the merge. Extend it to produce a 32-bit
wr_datathat respectsPSTRBfor registerr; write the per-lane ternary and explain what the unstrobed lanes carry and why. - Trace a waited write. Given an access with one wait state, draw
PSEL/PENABLE/PWRITE/PREADY/reg_seland the resultingwr_en; mark the exact edge the register clocks and prove the enable is high for only one cycle. - 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. - Sweep the strobes. For
PSTRB=4'hF,4'b0001,4'b0110, and4'h0, state which register bytes change after a write of0xAABBCCDDand 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_enand a byte-mergedwr_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 thePREADYterm.PREADYis the load-bearing term. The access holdsPSEL/PENABLEhigh through any wait, soPREADYis 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
PREADYrises, the completion-qualified enable is high for exactly one edge by construction. - Merge bytes with
PSTRBbefore the flop. Strobed lanes takePWDATA, 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.