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
PREADYextends 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)
PADDRis 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.
PSTRBis 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 own PADDR, 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
PREADYcan 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 thepreadyterm, 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
PSTRBselects byte lanes. That chapter is the deep treatment of the feature; here, ignoringPSTRBis 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
preadyfires on every ACCESS cycle of a wait-extended write (writes N times); an enable that ignorespenablecan fire in SETUP (writes early); a one-cycle enable pulse can be missed whenPREADYstretches the access and the pulse and the completing edge never coincide (writes zero times). On the bus you seePSEL/PENABLEhigh 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 registered
PADDR— 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
PSTRBenables. It breaks when the register's write takes the fullPWDATAword and ignoresPSTRB, 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. Droppready→ multi-write on wait states. Droppenable→ write in SETUP. Droppwrite→ 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.
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.
// ============================================================
// 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 && pready
always_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 write
always_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 once
end
// ============================================================
// 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 address
always_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 live PADDR (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 registered PADDR 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 —
PSELandPENABLEare high for three ACCESS cycles whilePREADYis 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 plainCTRLregister this is invisible (it stores the samePWDATAthree times). For the W1C register, the clear maskw1c_reg & ~pwdatais 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— thepreadyterm was omitted. On a zero-wait access this is harmless (ACCESS is one cycle, so it fires once). On a wait-extended access,PENABLEstays 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:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
wire wr_en = psel && penable && pwrite && pready; // one pulse, completing edge always_ff @(posedge pclk) if (wr_en && (paddr == ADDR_W1C)) w1c_reg <= w1c_reg & ~pwdata; // cleared exactly once - Verification assertion: prove the write fires exactly once per access — no write while waiting:
Azvya Education Pvt. Ltd.VLSI MentorSnippet
// 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
preadyterm 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 forgotpreadyand you are multi-writing. The "only the slow peripherals, only the stateful registers" signature almost always means a missingpreadyin the enable.
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 —Azvya Education Pvt. Ltd.VLSI MentorSnippet
// COUNT: no write commit while PREADY is low (catches multi-write & SETUP-write) property p_no_write_while_waiting; @(posedge pclk) disable iff (!presetn) (psel && pwrite && !(penable && pready)) |-> !wr_en; endproperty a_no_write_while_waiting: assert property (p_no_write_while_waiting);$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 missingpenable(fires in SETUP) orpwrite(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
PSTRBenabled, at the address the access presented — then on read-back compare lane by lane. A stale-PADDRwrite lands at the wrong address and the scoreboard's address mismatch flags it; aPSTRB-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
PREADYwas 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, thenpready). - Catch the multi-write. Write the SVA that proves a write never commits while
PREADYis 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 honourPSTRBper byte lane, and describe the symptom and the affected bytes if you leave it unmasked on aPSTRB=4'b0001write. - Reason about RMW. Explain why a multi-write is invisible on a plain
CTRLregister 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. Droppready→ multi-write on wait states; droppenable→ write in SETUP; droppwrite→ 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
PADDRinto the decode — the right data in the wrong register. Decode the live access address the manager holds stable through the transfer; never registerPADDRinto the write decode. - The lane bug is an ignored
PSTRB— a sub-word write clobbers the neighbouring bytes. Mask each lane: updatedata_reg[8i+:8]only whenpstrb[i], per AMBA APB (IHI 0024C) §2.1 PWDATA/PSTRB write timing. - 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-address scoreboard for target and lane, and a wait-extended-write cover bin — carried into the full APB assertion plan.
