Almost every corrupted APB write — a value that never lands, lands twice, lands in the wrong register, or clobbers a neighbouring byte — traces back to a single mistake: the write-enable was not the one golden term wr_en = psel && penable && pwrite && pready. You have learned how an APB write is supposed to commit — PWDATA and PSTRB are valid through the ACCESS phase, and the data is captured on the edge the access completes. This chapter inverts that into a debugging instrument: a catalog of the specific ways real RTL miscommits a write, each as a named bug with a symptom, the wrong RTL that produces it, the correct RTL, the waveform signature, and the fix. The idea to carry: a write must land exactly once, in the register the access addresses, in only the byte lanes PSTRB enables — and the moment the enable term drops one of psel, penable, pwrite, or pready, one of those three guarantees breaks.
1. Problem statement
The problem is that the entire correctness of an APB write rests on one combinational enable, and a handful of recurring RTL mistakes break it in ways that look like different failures but trace back to the same term.
A write commits when the subordinate latches PWDATA into a register. For that commit to be correct it must satisfy three promises at once: it happens exactly once (not zero times, not N times), it targets the register the access addresses (not a stale one), and it touches only the byte lanes PSTRB enables (not the neighbours). Each classic write-corruption bug breaks one of those promises:
Count bugs — the write fires the wrong number of times. The enable pulses in the SETUP phase, or it fires on every ACCESS cycle of a wait-extended access (a multi-write), or a one-cycle enable pulse is missed entirely when PREADY extends the access. The register ends up written zero, two, or N times instead of once.
Target bugs — the write lands in the wrong register. A registered (stale) PADDR is fed into the write decode, so the enable is correct but it points at last access's address. The right data lands in the wrong place.
Lane bugs — the write clobbers bytes it should not.PSTRB is ignored and the register takes a full-word write, so a sub-word write that should touch one byte overwrites all four — destroying the neighbouring bytes.
The engineering problem of this chapter is therefore not "how does an APB write commit" — you know that — but "what are the specific ways real RTL miscommits a write, what does each look like on the bus and in the register, and what is the correct fix for each." And the unifying answer is one line of RTL: wr_en = psel && penable && pwrite && pready, decoded from the access's ownPADDR, masked per lane by PSTRB.
2. Why previous knowledge is insufficient
This module has built the model of an APB write from the data phase outward, and each prior chapter is now a reference this catalog points back to — but none of them is the catalog itself.
The write transfer chapter and the write flow taught the correct SETUP→ACCESS→complete sequence and where in it the data is captured. That is the centre of the state space; this chapter is about every way the capture itself goes wrong even when the sequence looks right.
Write logic and write completion taught how the enable and the completion handshake are built. They assumed the enable was gated correctly. This chapter is precisely about the cases where one term of that gate is missing — and how each omission corrupts the commit in a different, recognisable way.
Write wait-state behaviour taught that PREADY can hold a write in ACCESS for many cycles. That is the mechanism the multi-write bug exploits: a write held in ACCESS for three cycles, with an enable that forgot the pready term, commits three times. Knowing the wait extends the access does not, by itself, tell you that an ungated enable will fire on every cycle of it.
PSTRB write strobes taught that PSTRB selects byte lanes. That chapter is the deep treatment of the feature; here, ignoringPSTRB is one entry in the corruption catalog — the lane-clobber bug — summarised and linked, not re-taught.
The gap is this: prior chapters taught the correct write and a few individual mechanisms. They did not assemble the failure taxonomy — the named set of write-corruption bugs with wrong-versus-right RTL — that lets you debug a real write by recognition instead of derivation. Building that taxonomy, anchored on the single golden enable term, is this chapter.
3. Mental model
The model: a correct write makes three promises — once, right register, right lanes — and every write-corruption bug breaks exactly one of them, so sort the symptom into one of three bins first, then pick the bug.
The three bins are the three promises a write commit must keep:
Count (the "once" promise). The write must fire exactly one time per access. It breaks when the enable is not gated on the completing edge: an enable that ignores pready fires on every ACCESS cycle of a wait-extended write (writes N times); an enable that ignores penable can fire in SETUP (writes early); a one-cycle enable pulse can be missed when PREADY stretches the access and the pulse and the completing edge never coincide (writes zero times). On the bus you see PSEL/PENABLE high across several cycles; in the register you see a value that was written the wrong number of times.
Target (the "right register" promise). The write must land in the register the current access addresses. It breaks when a registeredPADDR — a flop holding the previous access's address — drives the write decode, so the enable fires correctly but the one-hot select points at the wrong register. The right data, the right number of times, in the wrong place.
Lane (the "right lanes" promise). The write must update only the byte lanes PSTRB enables. It breaks when the register's write takes the fullPWDATA word and ignores PSTRB, so a sub-word write that intended to touch one byte clobbers all four — corrupting the neighbouring bytes that were never addressed.
The discipline this model buys you: bin before you debug. "The register was written twice" instantly points at the count family and the missing pready term; "the right value but in the wrong register" points at the target family and a stale PADDR; "writing one byte corrupted the other three" points at the lane family and an ignored PSTRB. Three refinements sharpen it:
The golden enable is one line, and most count bugs are a missing term in it.wr_en = psel && penable && pwrite && pready. Drop pready → multi-write on wait states. Drop penable → write in SETUP. Drop pwrite → a read corrupts a register. The single term, fully assembled, makes the write fire exactly once on the completing edge of its own access.
A missed write is the inverse of a multi-write, and it has the opposite cause. Multi-write is an enable that is too permissive (fires every cycle); a missed write is an enable pulse that is too narrow — a one-cycle strobe that does not survive a wait-extended access. Both are count bugs; one writes too often, the other not at all.
Read-modify-write hazards on W1C / clear-on-write registers turn a count bug into silent state corruption. For a plain data register, writing twice is harmless — the second write stores the same value. But for a write-1-to-clear or increment-on-write register, each extra write changes state: a W1C bit clears events that the second write should never have seen, a counter increments N times. The count bug is invisible on a plain register and catastrophic on a stateful one.
Figure 1 — the APB write path, with the single golden write-enable term and the two structural points it fails. PADDR feeds a write-address decoder that selects one register in a byte-laned bank; PWDATA and PSTRB feed per-byte write muxes so PSTRB chooses which lanes update; the central gating block forms wr_en = psel && penable && pwrite && pready, producing exactly one pulse per access on the completing edge. Failure point one (amber, the gating block) is the count family: dropping pready or penable from the term lets the write fire every ACCESS cycle during wait states, or fire in SETUP — the multi-write. Failure point two (red, the decode and the lane mux) is the target-and-lane family: a stale registered PADDR into the decode writes the wrong register, and a PSTRB-ignored full-word write clobbers the neighbouring byte lanes on a sub-word write. The figure is the chapter's spine: one correctly-gated wr_en, decoded from the access's own PADDR, masked per lane by PSTRB, is what makes a write land once, in the right register, in the right lanes.
4. Real SoC implementation
In real RTL these bugs are a missing term or a missing mask — one or two characters of wrong code — and the fixes are equally short, which is exactly why they ship. Here are the wrong-versus-right pairs for the two highest-impact bugs: the multi-write-during-wait (the missing pready term) and the PSTRB-ignored lane clobber, plus the stale-PADDR target bug.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ============================================================// BUG 1 — MULTI-WRITE DURING WAIT: enable not gated on PREADY// ============================================================// WRONG: the write enable is asserted for the WHOLE ACCESS phase. When the// slave inserts wait states (PREADY low for a few cycles), PENABLE stays high// the entire time, so this enable pulses on EVERY ACCESS edge -> the register// is written N times for one access. Harmless on a plain reg; catastrophic on// a W1C / increment-on-write reg, which clears/increments N times.assign wr_en_bug = psel && penable && pwrite; // <-- missing && preadyalways_ff @(posedge pclk) begin if (wr_en_bug && (paddr == ADDR_CTRL)) ctrl_reg <= pwdata; // committed on c3, c4, c5... if (wr_en_bug && (paddr == ADDR_W1C)) w1c_reg <= w1c_reg & ~pwdata; // W1C cleared multiple times!end// CORRECT: the SINGLE golden enable term — one pulse, on the completing edge.// PREADY is what collapses a multi-cycle ACCESS into one write.wire wr_en = psel && penable && pwrite && pready; // exactly one pulse per writealways_ff @(posedge pclk) begin if (wr_en && (paddr == ADDR_CTRL)) ctrl_reg <= pwdata; // committed once, on c5 only if (wr_en && (paddr == ADDR_W1C)) w1c_reg <= w1c_reg & ~pwdata; // W1C cleared exactly onceend// ============================================================// BUG 2 — PSTRB IGNORED: sub-word write clobbers neighbour bytes// ============================================================// WRONG: the register takes the full 32-bit PWDATA regardless of PSTRB, so a// byte/half-word write (e.g. PSTRB=4'b0001) overwrites all four lanes -> the// three bytes the access never addressed are destroyed.always_ff @(posedge pclk) if (wr_en && (paddr == ADDR_DATA)) data_reg <= pwdata; // <-- ignores PSTRB, clobbers all 4 bytes// CORRECT: honour PSTRB per byte lane. Lane i is updated only when PSTRB[i]=1;// otherwise it keeps its old value. (AMBA APB IHI 0024C section 2.1: PSTRB[n]// qualifies PWDATA[8n+7:8n]; a strobe bit low means that byte must NOT change.)always_ff @(posedge pclk) if (wr_en && (paddr == ADDR_DATA)) for (int i = 0; i < 4; i++) if (pstrb[i]) data_reg[8*i +: 8] <= pwdata[8*i +: 8]; // only the enabled lanes change// ============================================================// BUG 3 — STALE PADDR INTO DECODE: write hits the WRONG register// ============================================================// WRONG: PADDR is registered for "timing", and the STALE flop drives the write// decode, so wr_en fires correctly but selects the PREVIOUS access's register.// Right data, right count, WRONG target.logic [31:0] paddr_q;always_ff @(posedge pclk) paddr_q <= paddr; // last access's addressalways_ff @(posedge pclk) if (wr_en && (paddr_q == ADDR_CTRL)) // <-- decodes the STALE address ctrl_reg <= pwdata;// CORRECT: decode the SAME PADDR the access is presenting. PADDR is held stable// by the manager for the whole transfer, so the live access address is correct.always_ff @(posedge pclk) if (wr_en && (paddr == ADDR_CTRL)) // decode the access's own address ctrl_reg <= pwdata;
Two facts drive these fixes. First, the count fix is the single term: wr_en = psel && penable && pwrite && pready. Every term earns its place — psel (this slave is addressed), penable (we are in ACCESS, not SETUP), pwrite (a write, not a read), and pready (the completing edge, so we fire once no matter how many wait states the access took). Drop any one and a specific corruption appears: drop pready and you multi-write on waits; drop penable and you write in SETUP; drop pwrite and a read corrupts a register. Second, the target and lane fixes are about what the correct enable writes: decode the livePADDR (not a stale flop) so the right register is selected, and mask each byte lane with PSTRB[i] so a sub-word write touches only the lanes it addresses. One enable, the right target, the right lanes — and the W1C / clear-on-write hazard disappears the moment the enable is a single pulse, because a stateful register can only be safely written once per access.
5. Engineering tradeoffs
The catalog itself is the deliverable: one row per bug, the symptom you will actually see, the root cause in the RTL, and the fix. Memorise the shape of each row — bin (count / target / lane), with the missing term or mask it restores — and you can debug most APB write failures by recognition.
Bug name
Bin
Symptom on the bus / in the register
Root cause
Fix
Multi-write on wait states
Count
PSEL/PENABLE high across several wait cycles; register written N times — W1C cleared / counter incremented N×
Enable is psel && penable && pwrite, missing pready, so it fires every ACCESS edge
Add the pready term: wr_en = psel && penable && pwrite && pready — one pulse on the completing edge
Write in SETUP
Count
Register updates a cycle early, before the access can be aborted; intermittent with back-to-back traffic
Enable missing penable, so it asserts in the SETUP phase (PSEL high, PENABLE low)
Gate on penable so the write only commits in ACCESS, never SETUP
Missed write (narrow pulse)
Count
A wait-extended write never lands; the register keeps its old value
A one-cycle enable pulse doesn't coincide with the completing edge when PREADY stretches the access
Derive the enable from the level psel && penable && pwrite && pready, not a pre-timed one-cycle strobe
Stale PADDR decode
Target
Right value lands in the wrong register; off-by-one-access addressing
A registeredPADDR drives the write decode instead of the live access address
Decode the live PADDR the manager holds stable through the transfer; do not register it into the decode
PSTRB ignored
Lane
A byte/half-word write clobbers the neighbouring bytes; full-word reads back wrong
Register takes full PWDATA, ignoring PSTRB byte qualification
Mask per lane: update data_reg[8i+:8] only when pstrb[i] — keep the other lanes
Read corrupts a register
Count
A read to a register's address mutates it; reads have side effects they shouldn't
Enable missing pwrite, so it fires on reads too
Add pwrite to the enable so only writes commit
W1C / RMW hazard
Count
A clear-on-write or counter register lands in the wrong state even when the value was right
A multi-write (missing pready) applied to a stateful register clears/increments N times
Same single-pulse enable; a stateful register must be written exactly once per access
The throughline: every row sorts into count, target, or lane, and every fix restores one missing piece of the same canonical write path — the pready term, the penable term, the pwrite term, the live PADDR decode, or the PSTRB mask. The tradeoff is almost always spurious "optimisation" against correctness: registering PADDR "for timing" causes the stale-decode bug; taking the full word "to keep it simple" causes the lane clobber; pre-computing a one-cycle enable "to save a gate" causes the missed write. The correct write path costs a few extra terms and a per-lane mask — and that is the entire price of a write that lands once, in the right place, in the right lanes.
6. Common RTL mistakes
7. Debugging scenario
Pick the multi-write-during-wait bug, because it is the most deceptive of the catalog: it passes every directed test on plain registers, ships, and then surfaces as a clear-on-write register that drops interrupts — a field bug no one can reproduce on the bench.
Observed symptom: firmware writes a "1" to clear a pending-interrupt bit in a W1C status register, and more events than expected disappear — interrupts that arrived after the clear are silently lost. It only happens on the peripherals whose register accesses take wait states; the fast (zero-wait) peripherals are fine. On the bench, with single-stepped writes, it never reproduces; under interrupt load it does.
Waveform clue: in the capture (Figure 2), the addressed write takes two wait states — PSEL and PENABLE are high for three ACCESS cycles while PREADY is low for the first two and rises on the third. The subordinate's internal write strobe pulses on all three ACCESS edges, not just the completing one. For the plain CTRL register this is invisible (it stores the same PWDATA three times). For the W1C register, the clear mask w1c_reg & ~pwdata is applied three times — and any event that set a bit between the first and last pulse is cleared by a later pulse it should never have seen.
Root cause: the write enable was assign wr_en = psel && penable && pwrite — the pready term was omitted. On a zero-wait access this is harmless (ACCESS is one cycle, so it fires once). On a wait-extended access, PENABLE stays high for the whole stretch, so the enable fires on every ACCESS edge — a multi-write. The plain registers hid it; the W1C register, being a stateful clear-on-write, turned the repeated writes into lost events.
Correct RTL: add the missing term so the enable is a single pulse on the completing edge:
Verification assertion: prove the write fires exactly once per access — no write while waiting:
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// no register write may commit while the access is still waiting (PREADY low)assert property (@(posedge pclk) disable iff (!presetn) (psel && penable && pwrite && !pready) |-> !wr_en);
Debug habit: when a stateful register (W1C, counter, clear-on-write) misbehaves but plain registers are fine, do not chase the register's own logic first — chase the write enable's pready term on a wait-extended access. Capture a write to that register on a peripheral that inserts wait states and count $rose(wr_en) across the access. If it is more than one, the enable forgot pready and you are multi-writing. The "only the slow peripherals, only the stateful registers" signature almost always means a missing pready in the enable.
Figure 2 — the multi-write bug versus the single correct pulse, on the same write with two wait states. Both panels show PCLK, PENABLE, PREADY, PWDATA (holding a constant value D), the register write-enable, and the committed register. The access is held in ACCESS for three cycles: PREADY is low for c3 and c4 and rises on c5. Top (correct, green): wr_en = psel && penable && pwrite && pready pulses exactly once — on c5, the completing edge — committing D one time. Bottom (bug, red): wr_en = psel && penable && pwrite omits pready, so it fires on all three ACCESS edges (c3, c4, c5) — three back-to-back write pulses. For a plain register the value is simply written three times (invisible), but for the W1C / increment-on-write register shown, the repeated writes clear or increment the register three times instead of once, corrupting its state. The figure teaches that the pready term is precisely what collapses a multi-cycle wait-extended ACCESS into a single write pulse.
8. Verification perspective
Because the catalog splits cleanly into count, target, and lane failures, the verification plan needs one assertion class per bin plus a scoreboard that models the byte-laned commit — a value check alone will pass a multi-write on a plain register and a lane clobber it never strobed.
Exactly-one-pulse catches the whole count family. The single most valuable property is that a write fires once per access. Express it as a no-write-while-waiting guard — a write may not commit while the access is still extended:
Pair it with an explicit pulse-count check across an access — $rose(wr_en) must equal 1 between SETUP and completion — to catch the missed write (zero pulses) as well as the multi-write (N pulses). Together these prove the "once" promise for every access regardless of how many wait states it took.
Phase-and-direction gating catches write-in-SETUP and read-corrupts-register. Assert that the enable is only ever high inside ACCESS on a write — wr_en |-> (psel && penable && pwrite && pready) — which fails the moment the enable is missing penable (fires in SETUP) or pwrite (fires on a read). This is the assertion form of the golden term itself: the enable must imply the full condition.
A byte-laned scoreboard catches the target and lane bugs. Value-checking the word is not enough; the scoreboard must model the commit per byte lane and per address. Mirror the register bank, apply each observed write only to the lanes PSTRB enabled, at the address the access presented — then on read-back compare lane by lane. A stale-PADDR write lands at the wrong address and the scoreboard's address mismatch flags it; a PSTRB-ignored write clobbers lanes the scoreboard left untouched and the lane-level compare flags it. A whole-word compare would miss both.
Cover the wait-extended write explicitly. The multi-write only exists on a wait-extended access, so a named bin — a write completing after PREADY was low for [*2:$] cycles — proves the directed stimulus actually drove the corner. Without it, a regression of zero-wait writes reports closure while the bug ships un-exercised. Carry these into the full APB assertion plan: one count property, one phase-gate, one byte-laned scoreboard, and a wait-extended-write cover bin.
The point: each bin has a named check — exactly-one-pulse for count, enable-implies-golden-term for phase/direction, a per-lane per-address scoreboard for target and lane — and the wait-extended cover bin is what makes the multi-write corner non-vacuous.
9. Interview discussion
"Tell me every way an APB write can land wrong" is a senior screening question because a weak answer says "the data could be wrong" and stops, while a strong answer reveals an organised catalog anchored on a single enable term — exactly the recognition speed that separates someone who has debugged APB writes from someone who has only read the spec.
Frame it as three promises, one golden term: a write must land once, in the right register, in the right byte lanes — and the term that guarantees all three is wr_en = psel && penable && pwrite && pready. Then enumerate by bin. In the count family: multi-write on wait states (the enable forgot pready, so it fires on every ACCESS edge of a wait-extended access — invisible on plain registers, lethal on W1C / counters), write-in-SETUP (forgot penable), missed write (a one-cycle strobe that doesn't survive a stretched access), and read-corrupts-register (forgot pwrite). In the target family: stale-PADDR decode (a registered address into the decode lands the right data in the wrong register). In the lane family: PSTRB-ignored (a sub-word write clobbers the neighbouring bytes). Land the depth points: most count bugs are one missing term in a single line of RTL, the cure is to assemble the full enable; the W1C / RMW hazard is why a multi-write that's harmless on a plain register is catastrophic on a stateful one — which is the field bug that survives directed tests; and the verification is one exactly-one-pulse property, one phase-and-direction gate, and a byte-laned scoreboard, because a whole-word value check passes both the lane clobber and the multi-write. Closing with "and the multi-write taught me to always test writes on the slow peripherals and the stateful registers, not just the fast plain ones" signals real silicon debugging, not spec reading.
10. Practice
Bin the symptom. Given three field reports — "a clear-on-write register drops interrupts under load," "the right value shows up in the wrong register," and "a byte write corrupted the other three bytes" — assign each to the count, target, or lane family and name the specific bug.
Assemble the golden term. Starting from wr_en = psel && penable, add the terms one at a time and state, for each addition, exactly which corruption it prevents (pwrite, then pready).
Catch the multi-write. Write the SVA that proves a write never commits while PREADY is low, and the companion $rose(wr_en)-count check, and state which catches the multi-write and which the missed write.
Mask the lanes. Given data_reg <= pwdata, rewrite it to honour PSTRB per byte lane, and describe the symptom and the affected bytes if you leave it unmasked on a PSTRB=4'b0001 write.
Reason about RMW. Explain why a multi-write is invisible on a plain CTRL register but corrupting on a W1C status register, and why this is exactly why the bug survives directed tests and surfaces in the field.
11. Q&A
12. Key takeaways
A correct APB write makes three promises — land once, in the right register, in the right byte lanes — and every write-corruption bug breaks exactly one of them. Bin the symptom into count, target, or lane before debugging.
The single golden enable is wr_en = psel && penable && pwrite && pready, and most count bugs are one missing term. Drop pready → multi-write on wait states; drop penable → write in SETUP; drop pwrite → a read corrupts a register; a too-narrow pulse → a missed write. Assemble the full term and the write fires exactly once on the completing edge.
The target bug is a stale PADDR into the decode — the right data in the wrong register. Decode the live access address the manager holds stable through the transfer; never register PADDR into the write decode.
The W1C / RMW hazard is why a multi-write is invisible on a plain register and catastrophic on a stateful one — a clear-on-write or counter register lands in the wrong state even when the value was right. This is the field bug that survives directed tests; always test writes on the slow peripherals and the stateful registers.
The verification reduces to a few named checks: one exactly-one-pulse / no-write-while-waiting property for count, one enable-implies-golden-term gate for phase and direction, a byte-laned per-addressscoreboard for target and lane, and a wait-extended-write cover bin — carried into the full APB assertion plan.