AMBA APB · Module 7
The APB Write Flow
An APB write end to end as a one-way push — the manager presents an address and data and holds them, and the subordinate commits the value into its register on the single completion edge. Data flows manager→subordinate; there is no data return leg.
A read asks a question and waits for the answer to come back. A write is the opposite shape — it pushes a value out and waits only for acknowledgement. This chapter opens Module 7 by walking an APB write as one continuous push: the manager presents an address and data and holds them stable, and the subordinate captures that data into its register on the completion edge — the commit. The single idea to carry: a write is a one-way push in which data flows manager→subordinate, there is no data return leg, and the register changes on exactly one cycle — the completion edge where PSEL, PENABLE, PWRITE, and PREADY are all high. Hold that and every later write-timing chapter is just a closer look at the commit or the acknowledgement.
1. Problem statement
The problem is delivering a value from the manager into a specific register inside a subordinate, with one unambiguous cycle on which that register actually changes — and a way for the subordinate to say "done" or "failed."
A write is not symmetric with a read. On a read the subordinate produces data and the manager consumes it, so the data makes a round trip. On a write the manager produces the data and the subordinate consumes it — a one-way push, with no value travelling back. What does travel back is only acknowledgement: PREADY ("I have taken it / I am ready") and, on APB3+, PSLVERR ("the access failed"). So the write flow has three jobs:
- Present (outbound): the manager drives
PADDR,PWRITEhigh, andPWDATAto say "store this value at this address," and holds them stable. - Commit (inside the subordinate): the addressed register captures
PWDATA— a single write-enable pulse — on the completion edge. - Acknowledge (inbound): the subordinate drives
PREADYto complete andPSLVERRto flag failure. No data returns.
The job APB has to do is make the commit land on a cycle both sides agree on — not when the address appears, not while the subordinate is still busy, but on the one edge the handshake declares the transfer complete.
2. Why previous knowledge is insufficient
You have met the pieces: Module 4 walked a write cycle by cycle, the PWDATA signal gave you its capture-at-completion rule, and Module 6 framed a read as a round trip. Each is correct, but a write engineer needs the connected push, because that is what reveals the three things a per-piece or read-centric view hides:
- A write is a push, not a round trip. Reusing the read mental model is the classic error: a write has no data return leg. The manager never waits for data to come back — it waits only for
PREADY. The asymmetry changes who drives what and where the latency lives. - The data source is the manager, so the manager owns stability. On a read the subordinate owns
PRDATAand its validity; on a write the manager ownsPWDATAand must hold it stable from setup through the commit. The single most common write bug is a manager that stops drivingPWDATAtoo early. - The "result" is a state change, not a returned value. A read's result is a value on
PRDATA; a write's result is a register that changed inside the subordinate. You cannot see it on the bus — which is exactly why writes are verified with a scoreboard and debugged by reading the register back, not by watching the data lines.
So the model to add is not another signal rule — it is the push: present and hold, commit once, acknowledge with no data back.
3. Mental model
The model: a write is dropping a letter into a mailbox — you hold the letter at the slot, the slot accepts it with a click, and the only thing you get back is the click, not the letter. You never expect the letter to travel back to you; you only need to know it was taken.
You write the address on the envelope and hold the letter at the slot (PADDR, PWRITE high, PWDATA, presented in setup and held). The mailbox is the subordinate's register; the click — the instant the letter is actually accepted — is the commit, on the completion edge. The postal worker behind the slot may be slow (a wait state), so you keep holding the letter at the slot until the click. What comes back to you is only the click (PREADY) and, if the slot is jammed, a "rejected" light (PSLVERR) — never the letter.
Three refinements make the push precise:
- You hold the letter until the click.
PADDR,PWRITE, andPWDATAare held byte-for-byte from setup through the completion edge. Let go early — stop drivingPWDATAbefore the commit — and the slot accepts whatever is there now, which may be garbage. - The click is one event. The register's write-enable pulses on exactly one cycle: completion, where
PSEL,PENABLE,PWRITE, andPREADYare all high. Before it, nothing is stored; the data is merely offered. - Nothing comes back but the acknowledgement. There is no data return leg. The subordinate raises
PREADY(taken / ready) and may raisePSLVERR(failed). The manager reads those, not a value.
4. Real SoC implementation
In silicon the write flow is a registered capture inside the subordinate, driven by held signals from the manager. The manager registers the request (drives PADDR/PWRITE/PWDATA and sequences PSEL/PENABLE from its FSM) and holds them; the subordinate gates a single write-enable on the completion edge and clocks PWDATA into the addressed flop.
// Subordinate write commit: the register changes on EXACTLY one edge.
// write_commit is high only on the completion cycle of a write — PSEL, PENABLE,
// PWRITE, and PREADY all high. PADDR is held stable across access, so the decode
// is settled by then; PWDATA is held stable, so the captured value is correct.
wire write_commit = psel & penable & pwrite & pready; // the one commit edge
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
ctrl_q <= '0;
baud_q <= '0;
end else if (write_commit) begin
case (paddr)
ADDR_CTRL: ctrl_q <= pwdata; // commit here, and only here
ADDR_BAUD: baud_q <= pwdata;
default: /* unmapped write: no side effect */ ;
endcase
end
// NOTE: there is no other path that writes ctrl_q/baud_q. The register can
// ONLY change on write_commit — that absence of other write paths IS the
// guarantee that nothing commits early, late, or twice.
endTwo facts make the push robust. First, because PADDR and PWDATA are held stable from setup through completion, the address decode (case (paddr)) is settled and the captured value is the intended one — the subordinate just pulses the write-enable on the completion edge. Second, the write-enable is the only path that updates the register, so the register changes exactly once per write and never on a wait cycle. (The detailed timing of each piece is the rest of this module: PWDATA timing, the write completion contract, and write wait states.)
5. Engineering tradeoffs
Laying a write out as a held one-way push with a single commit edge is deliberate and minimal. Each property trades a capability APB does not need for the clarity it does.
| Write-flow property | What it gives up | What it buys | Why it is correct for APB |
|---|---|---|---|
| One-way push, no data return | Read-after-write in one transfer | A trivially simple write path | A write only needs to deliver and be acknowledged |
Manager holds PWDATA stable | Just-in-time data | A value guaranteed good on whatever cycle commits | One capture rule works at any subordinate speed |
| Commit on one edge only | A spread-out write | An unambiguous "the register changed here" | A single write-enable pulse, easy to build and verify |
Acknowledge via PREADY/PSLVERR | A rich response payload | A two-signal ack with no return bus | Sparse control writes need no return data path |
PWRITE high set in setup, held | Mid-transfer direction change | Direction fixed before any commit | One decode of write-vs-read per transfer |
The throughline: an APB write spends a held push and one commit edge and gains a flow identical whether the subordinate is instant or slow — only the acknowledgement is delayed, never the data, because there is no data to wait for. The commit edge is the single state-change landmark, which is why an APB write is small to build and easy to verify.
6. Common RTL mistakes
7. Debugging scenario
A write that "completes" on the bus but never changes the register is the canonical APB write bug — and it is invisible on the data lines, because a write has no return leg.
- Observed symptom: firmware writes a configuration register (say, a baud divisor), but the peripheral behaves as if unconfigured, and a read-back of the same register returns its old value. The write "doesn't stick."
- Waveform clue: the write transfer looks complete —
PSEL,PENABLE,PWRITE, andPREADYall high on the completion edge (Figure 2's dashed line) — but the register's internal write-enable never pulses there, or pulses a cycle early during a wait whenPREADYwas still low. - Root cause: the subordinate gated its register update on a subset of the commit —
PSELalone, orPENABLEalone — instead of the fullwrite_commit. With a wait state, the misgated enable fires on the wrong cycle or, with a wrong address-decode comparison, never fires for that register at all. - Correct RTL: gate every register update on the full commit term:
wire write_commit = psel & penable & pwrite & pready;thenif (write_commit) case (paddr) ... endcase— the register changes only on that edge, only for the matched address. - Verification assertion: bind a check that the register only changes on a commit, and that a write is observed:
assert property (@(posedge pclk) $changed(ctrl_q) |-> (write_commit && paddr == ADDR_CTRL));plus a scoreboard write-then-read-back that fails if the read does not return the written value. - Debug habit: when a write "doesn't stick," do not stare at
PWDATA— check the register's write-enable pulse against the completion edge first: is it firing at all, and on the exact cyclePSEL & PENABLE & PWRITE & PREADYis high? Nine times in ten the enable is misgated or the address decode is off by a bit.
8. Verification perspective
A write's result is a hidden state change, so verification cannot watch it on the bus — it must predict it. The standard structure is a reference model plus a scoreboard: a model of the register file that the testbench updates on every write commit, and a comparison on every read.
- Model the commit, not the access. On each
write_commit(the monitor seesPSEL & PENABLE & PWRITE & PREADY), the reference model updatesmodel_reg[paddr] = pwdata. On each read completion, the scoreboard comparesPRDATAtomodel_reg[paddr]. A misgated or mistimed commit shows up as a read mismatch — the very symptom of the debug scenario above, now caught automatically. - Assert the commit is exact. Two properties cover most write bugs: the register changes only on a commit (
$changed(reg) |-> write_commit), and a write commits exactly once per transfer (no double-pulse across a wait). Bind them per register or generate them from the register map. - Cover the corners that hide write bugs. Coverage should hit: a write to every register, a write-then-immediate-read-back, a write with wait states, a write with
PSLVERR, and a write whosePWDATAchanges value between back-to-back transfers (to catch a stale-data capture). Functional coverage onwrite_commit && paddrper register closes the "did we ever write this one" gap.
The point: because the effect is invisible on the bus, the scoreboard is the test. A write monitor that only checks PREADY rose has verified nothing about whether the right value reached the right register.
9. Interview discussion
"Walk me through an APB write" is a staple, and the strong answer leads with the push, not a signal list. A weak answer recites PWDATA and PREADY; a strong one shows you understand one-way data flow and where the state change is.
Lead with the shape: a write is a one-way push where data flows manager→subordinate, committed on a single edge, with no data return. Then walk it: setup presents PADDR, PWRITE high, and PWDATA and holds them; access raises PENABLE; the commit is the cycle PSEL & PENABLE & PWRITE & PREADY are all high, where the subordinate's write-enable pulses and the register captures PWDATA; the only thing that returns is PREADY (and PSLVERR). Then deliver the two depth points that separate levels: the manager owns PWDATA and must hold it stable until the commit (a read's data is subordinate-owned, the mirror image), and a write's result is a hidden state change, so you verify it with a scoreboard and debug it by reading the register back — you cannot see the commit on the data bus. Volunteering "for a write, PWRITE is high, PWDATA carries the value out, and PRDATA is unused" signals you think in data-flow direction.
10. Practice
- Trace the push. From IDLE, narrate an APB write's present, commit, and acknowledge legs, naming the signal active in each and the exact cycle the register changes.
- Name the owner. State which side drives
PADDR,PWRITE,PWDATA,PREADY, andPSLVERRon a write, and which signal (PRDATA) is unused and why. - Place the commit. On a zero-wait write waveform, mark the one cycle the register changes and state what is stored on every earlier cycle.
- Write the gate. From memory, write
write_commitand thealways_ffthat commitsPWDATAinto one register, and explain why gating onPENABLEalone is wrong under a wait state. - Catch the lost write. Describe the assertion and the scoreboard check that would have caught a "write doesn't stick" bug before silicon, and what each compares.
11. Q&A
12. Key takeaways
- An APB write is a one-way push — the manager presents an address and data and the subordinate commits the value into its register. Data flows manager→subordinate; there is no data return leg.
- The manager owns
PWDATAand must hold it (withPADDRandPWRITE) stable from setup through the commit. The subordinate captures;PRDATAis unused. - The register changes on exactly one edge — the commit, where
PSEL,PENABLE,PWRITE, andPREADYare all high — via a single write-enable pulse. Nothing commits earlier or twice. - The only thing that returns is acknowledgement —
PREADY(taken / ready) andPSLVERR(failed). A write waits for the ack, never for data. - A write's result is invisible on the bus, so verify it with a scoreboard (model on commit, compare on read) and debug it by checking the write-enable pulse against the completion edge — not by staring at
PWDATA. - Gate every commit on the full term
psel & penable & pwrite & pready; gating on a subset is the "write doesn't stick" bug, and it only shows up under a wait state.