AMBA AHB · Module 16
Write FSM
Designing the slave write-side FSM — IDLE to WRITE-DATA (the data phase, where HWDATA is valid) to optional WRITE-WAIT (HREADYOUT low until storage is ready) to COMMIT or ERROR. The crucial rule is the commit condition: the write updates storage only on the completing cycle (sel_q && write_q && HREADYOUT && !error) — exactly once, at the captured address, never during a wait state and never on an illegal access. HWDATA is a data-phase signal, so the address is captured in the address phase but the data arrives in the data phase, combined only at commit.
The capture chapter (16.7) gave the slave its view of the transfer; this chapter builds the slave's write-side control — the FSM that commits a write correctly. A write has a subtlety the read doesn't: its two halves arrive in different phases. The address and control (which location, that it's a write) are captured in the address phase (16.7); but the write data — HWDATA — is driven by the master in the data phase, one cycle later. So the slave has the address and the data together only in the data phase — and that's when it can commit the write (update the register/memory). The write-side logic, expressed as an FSM, is: IDLE → on a captured real write, WRITE-DATA (the data phase, HWDATA valid) → if the storage needs time, WRITE-WAIT (HREADYOUT low) → COMMIT (store HWDATA at the captured address) or, if the access is illegal, ERROR (run the two-cycle ERROR — chapter 16.6 — and do not commit). The critical rule is the commit condition: the write lands only on the completing cycle — sel_q && write_q && HREADYOUT && !error — exactly once, never during a wait, never on an illegal access. This chapter builds the write FSM and that commit discipline.
1. What Is It?
The write FSM is the slave's write-side control — the states and the commit condition that update storage correctly. Its parts:
- IDLE — waiting; no write in progress (
HREADYOUThigh, ready). - WRITE-DATA — entered on a captured real write; the data phase where
HWDATAis valid (address already captured in 16.7). - WRITE-WAIT (optional) — if the storage needs time, hold here with
HREADYOUTlow until ready. - COMMIT / ERROR — on the completing cycle, store
HWDATAat the captured address (if legal), or run the two-cycle ERROR and suppress the write (if illegal); then back to IDLE.
So the write FSM is the slave's correct write committer. The challenge it solves is the split timing: the write's destination (the captured address) is known a cycle before its payload (HWDATA, data-phase). So the FSM waits for the data phase (WRITE-DATA), optionally stalls for slow storage (WRITE-WAIT), and commits the write — store HWDATA at addr_q — exactly on the completing cycle. The commit condition is the heart: sel_q && write_q && HREADYOUT && !error — a real, selected write, on the completing cycle (HREADYOUT high — not a wait), that's legal (!error). This ensures the write lands once, at the right address, with the right data, only for a valid completing transfer. For a simple zero-wait, all-legal slave, the FSM collapses to "capture → store on the completing cycle" (no explicit states) — but the full FSM is what handles waits and errors correctly. So the write FSM is the write-commit discipline. So it's how a write lands correctly.
2. Why Does It Exist?
The write FSM exists because a write's address and data arrive in different phases (address-phase control, data-phase HWDATA), so the slave must wait until the data phase to commit; because slow storage needs wait states; and because illegal writes must be suppressed — all of which require sequencing the commit correctly.
The split-phase timing is the root: a write's two pieces arrive one cycle apart. The address/control (where to write, that it's a write) is address-phase — captured into addr_q/write_q (16.7). The write data (HWDATA) is data-phase — the master drives it in the data phase, after the address phase. So the slave has the destination before the payload. It can't commit in the address phase (no data yet); it must commit in the data phase (when HWDATA is present and it still has the captured address). So the write FSM exists to sequence the commit to the data phase — combining the captured address with the data-phase data. So it bridges the split. So the timing forces sequencing.
The wait states need a hold state drives WRITE-WAIT: if the storage is slow (a multi-cycle memory — 16.3) or busy, the write can't commit immediately — the slave must insert wait states (HREADYOUT low) and hold until the storage is ready. So the FSM needs a WRITE-WAIT state — a place to stay while HREADYOUT is low, not yet committing, until the storage accepts the write. So the FSM exists to handle slow writes — sequencing through waits. So it's a hold mechanism. So slow storage needs the state.
The illegal writes must be suppressed drives the ERROR path: some writes are invalid — to a read-only register, a reserved address, a protection-violating access (16.6). These must not commit (corrupting state) — the slave must detect the illegality and suppress the write while signaling ERROR. So the FSM needs an ERROR path that runs the two-cycle ERROR response and gates off the commit. So the FSM exists to handle illegal writes — erroring without committing. So it's error handling. So illegal writes need suppression. So the write FSM exists because: a write's address and data are split across phases (commit must wait for the data phase — the why); slow storage needs wait states (a WRITE-WAIT hold state — the waits); and illegal writes must be suppressed (an ERROR path that doesn't commit — the errors). So the write FSM is the slave's write-commit sequencer — combining the captured address with the data-phase HWDATA and committing exactly once, on the completing cycle, only for a legal write — handling the split timing, the waits, and the errors correctly. So this chapter builds the write side's control. So writes land correctly.
3. Mental Model
Model the write FSM as a warehouse loading dock. A truck driver first hands the clerk a delivery slip with the bay number (the address phase — the destination), then, a moment later, backs the truck up and the actual pallet arrives (the data phase — HWDATA). The clerk can only store the pallet in the bay once both the slip and the pallet are in hand — so they wait for the pallet, hold if the bay's forklift is busy (a wait state), and place it exactly once in the slip's bay. If the slip names a sealed, no-deliveries bay (a read-only/illegal target), the clerk refuses — stamps "REJECTED" and does not store the pallet.
A warehouse loading dock where a delivery happens in two steps, a moment apart (the pipeline). First, the driver hands the clerk a delivery slip naming the bay number — where this goes (the address phase: addr_q, and "it's a delivery" = write_q). The clerk writes the bay number on a clipboard (captures it). Then, a moment later, the truck backs up and the actual pallet arrives at the dock (the data phase: HWDATA). Only now does the clerk have both the bay number (on the clipboard) and the pallet — so only now can they store the pallet in the bay. They couldn't have stored it when only the slip arrived (no pallet yet); they must do it when the pallet arrives (and they still know the bay, from the clipboard). If the bay's forklift is busy (slow storage), the clerk holds the pallet on the dock (WRITE-WAIT, HREADYOUT low) until the forklift is free, then stores it — they don't abandon it on the dock or store it twice. And they place it exactly once, in the slip's bay. But suppose the slip names a sealed "NO DELIVERIES" bay — a read-only location, or a bay the driver isn't authorized for (a protection violation). The clerk refuses: stamps the slip "REJECTED" (runs the two-cycle ERROR) and does not store the pallet (suppresses the write) — the sealed bay stays untouched. So the clerk combines slip and pallet, holds for a busy forklift, stores exactly once in the right bay, and refuses illegal bays without storing.
This captures the write FSM: the delivery slip with the bay number = the captured address/control (addr_q, write_q); writing it on the clipboard = capturing into the _q registers (16.7); the pallet arriving a moment later = HWDATA arriving in the data phase; needing both to store = committing only when address and data are both present (the data phase); holding for a busy forklift = WRITE-WAIT (HREADYOUT low for slow storage); placing it exactly once in the slip's bay = committing once, at addr_q, on the completing cycle; refusing a sealed bay without storing = the ERROR path suppressing the write on an illegal access. Combine slip and pallet, hold if busy, store once in the right bay, refuse illegal bays.
Watch a write commit on the completing cycle, after a wait:
The write commits on the completing cycle
4 cyclesThe model's lesson: combine slip and pallet, hold if busy, store once in the right bay. In the waveform, HWDATA = D is present from cycle 1, but the commit waits for the completing cycle (cycle 2, HREADYOUT high) and fires exactly once — storing D at A.
4. Real Hardware Perspective
In hardware, the write FSM is often not an explicit multi-state machine but a commit condition on the storage's write-enable; the WRITE-WAIT is the HREADYOUT logic (16.4); and the ERROR suppression gates the write-enable (16.6).
The commit as a gated write-enable: for a zero-wait slave, there's no explicit FSM — the write is a gated register/memory write-enable: we = sel_q && write_q && HREADYOUT && !error; and if (we) storage[addr_q] <= HWDATA;. The commit condition is the write-enable. The HREADYOUT term ensures the write commits only on the completing cycle (not during a wait); the !error term suppresses illegal writes; the sel_q && write_q ensures a real, selected write. So in hardware, the "FSM" is frequently just this write-enable expression. So it's a condition, not states. So simple slaves need no explicit FSM.
The WRITE-WAIT is the HREADYOUT logic: for a slave with slow storage, the "WRITE-WAIT state" is the HREADYOUT-low logic (16.4) — the slave drives HREADYOUT low while the storage isn't ready, and the commit condition's HREADYOUT term naturally prevents the commit during the wait (since HREADYOUT is low). When the storage becomes ready, HREADYOUT goes high and the same commit condition fires. So in hardware, the wait handling falls out of the HREADYOUT logic + the commit condition — no separate state needed for simple cases. So it's integrated. So the wait is the HREADYOUT term.
The explicit FSM for complex writes: for richer write behavior — multi-beat commits, write buffering, read-modify-write, byte-lane handling (using size_q and the byte strobes), or a bridge (16.10) — an explicit FSM is warranted (states for the phases of the complex write). But the core invariant is unchanged: commit once, on the completing legal cycle, at the captured address. So in hardware, the write side is a commit condition (sel_q && write_q && HREADYOUT && !error) gating the storage write-enable — trivially for simple slaves, or wrapped in an explicit FSM for complex ones — with the WRITE-WAIT handled by the HREADYOUT logic and the ERROR by the !error gate. The commit condition is the essential piece. So in hardware, get the commit condition right. So that's the crux.
5. System Architecture Perspective
At the system level, the write FSM (and its commit condition) is where the master's write becomes a committed state change in the slave — the write-completion point — and its correctness (commit once, legally, on completion) is what makes the system's writes reliable and atomic.
The write-completion point: the commit is the moment a write takes effect in the system — before it, the target holds the old value; after it, the new. So the commit is the write-completion point — where the master's intent (write D to A) becomes reality (A holds D). So at the system level, the write FSM's commit defines when a write happens. So it's the write's effect point. So commits are state changes.
The atomicity and exactly-once: the commit condition's exactly once property is an atomicity guarantee — a write commits one time, cleanly, on the completing cycle. Not multiple times (which a wait-state-committing bug could cause — corrupting via repeated writes, problematic for side-effecting writes), not zero times (a missed commit — the write lost), not early (before the data is final). So the commit's exactly-once-on-completion is what makes writes atomic and reliable at the system level. So it's write atomicity. So exactly-once matters.
The safety via suppression: the !error gate is a safety property — illegal writes (to read-only, protected, reserved targets) don't commit, so the system's protected state is safe from invalid writes. This matters for security (a forbidden write changes nothing) and robustness (a buggy write to a read-only register doesn't corrupt it). So at the system level, the write FSM is the write-completion point (where the master's intent becomes committed state — defining when a write takes effect), its exactly-once-on-completion commit is an atomicity guarantee (writes happen once, cleanly — not repeated, lost, or early), and its error-suppression is a safety property (illegal writes are inert — protecting state, critical for security). So the write FSM is where the bus's write semantics are enforced — making reliable, atomic, safe writes a property the slave's commit condition guarantees. So commit once, on completion, legally — the system's writes depend on it.
6. Engineering Tradeoffs
The write FSM embodies the commit-on-completion, hold-for-waits, suppress-on-error design.
- Commit condition (gated write-enable) vs explicit FSM. For simple slaves, a commit condition (gated write-enable) is minimal and correct; for complex writes (multi-beat, RMW, bridge), an explicit FSM is clearer. Use the simplest that captures the behavior.
- Commit on completion (HREADYOUT) vs commit eagerly. Committing only on the completing cycle (
HREADYOUTterm) is correct (once, with final data); committing eagerly (during a wait) risks early/repeated writes. Always gate the commit onHREADYOUT. - Suppress illegal writes vs commit-then-check. Suppressing via
!errorkeeps state safe (illegal writes inert); committing then checking corrupts state. Always suppress (gate on!error). - Byte-lane (size_q) handling vs word-only. Honoring
size_q/byte strobes supports sub-word writes (general) at the cost of lane logic; word-only is simpler but limited. Implement the granularity the slave needs.
The throughline: the write FSM is the slave's write-commit sequencer — IDLE → WRITE-DATA (the data phase, where HWDATA is valid) → optional WRITE-WAIT (HREADYOUT low for slow storage) → COMMIT or ERROR. Because a write's address is address-phase (captured — 16.7) but HWDATA is data-phase, the slave commits only in the data phase, when it has both. The commit condition — sel_q && write_q && HREADYOUT && !error — ensures the write lands exactly once, at the captured address, on the completing cycle, only for a legal write: never during a wait (early/repeated) and never on an illegal access (which is suppressed while ERROR is signaled — chapter 16.6). For simple slaves this collapses to a gated write-enable; complex writes warrant an explicit FSM. It's the write-completion point — guaranteeing atomic, reliable, safe writes.
7. Industry Example
Build the write side for a peripheral with a plain register, a side-effecting register, and a read-only register — across wait states.
A peripheral has CTRL (RW), a TX-FIFO push register (write pushes a FIFO — side-effecting), and STATUS (RO), with the FIFO sometimes needing a wait.
- CTRL (RW), zero-wait. A write to CTRL: capture
addr_q = CTRL,write_q = 1(address phase). In the data phase,HWDATA = config. Commit conditionsel_q && write_q && HREADYOUT && !erroris true (zero-wait, legal) →CTRL <= config, once. Simple. - TX-FIFO push (side-effecting), with a wait. A write to the TX-FIFO push register pushes
HWDATAonto the FIFO (a side effect). If the FIFO is near full, the slave inserts a wait (HREADYOUTlow) until there's room. Critically, the push commits only on the completing cycle —HREADYOUThigh. If the write-enable omitted theHREADYOUTterm, the FIFO would be pushed every wait cycle — multiple pushes of the same data — corrupting the FIFO. TheHREADYOUTterm ensures exactly one push, when the FIFO is ready. - STATUS (RO), illegal write. A write to STATUS (read-only) sets the error condition (
write_q && is_RO(addr_q)). The commit condition's!errorterm is false → STATUS is not written (suppressed). The slave runs the two-cycle ERROR (16.6); the master faults. STATUS stays uncorrupted. - The commit discipline. Every write commits exactly once, on the completing legal cycle, at the captured address. The side-effecting FIFO push especially depends on the
HREADYOUTterm (no multiple pushes); the read-only STATUS depends on the!errorterm (no illegal write). - Byte lanes. If a write is sub-word (
size_q< word), the slave uses the byte strobes to update only the addressed bytes — committed once, on completion.
The example shows the write FSM's commit condition protecting both a side-effecting write (the HREADYOUT term → exactly one FIFO push, despite waits) and a read-only register (the !error term → no illegal write). The commit-once-on-completing-legal-cycle discipline is what makes every write — plain, side-effecting, illegal — behave correctly. This is the write side done right. This is reliable, atomic writes.
8. Common Mistakes
9. Interview Insight
The write FSM is a practical RTL interview topic — the split-phase timing, the commit condition (with HREADYOUT and !error), and the multiple-commit bug are the signals.
The answer that lands gives the split timing and the commit condition: "The slave's write side has to deal with the write's address and data arriving in different phases. The address and control — which location, that it's a write — are captured in the address phase. But the write data, HWDATA, arrives in the data phase, one cycle later. So the slave has the address and the data together only in the data phase, and that's when it can commit the write. As an FSM: from idle, a captured real write moves to a write-data state in the data phase where HWDATA is valid; if the storage needs time, it stays in a write-wait state driving HREADYOUT low; then it commits — stores HWDATA at the captured address — or, if the access is illegal, it takes an error path, runs the two-cycle ERROR, and does not commit. The crucial thing is the commit condition: the write updates storage only when the captured select and write flag are true, and HREADYOUT is high, and there's no error — sel_q and write_q and HREADYOUT and not-error. The HREADYOUT term is critical: it means the write commits only on the completing cycle, exactly once. A classic bug is committing on just sel_q and write_q, omitting HREADYOUT — then on a waited write, the write commits every wait cycle, multiple times. For a plain register that's idempotent so it hides, but for a side-effecting write like a FIFO push or a counter increment, it fires the side effect multiple times and corrupts state. And the not-error term suppresses illegal writes so a write to a read-only register doesn't corrupt it. For a simple zero-wait slave this collapses to just a gated write-enable; the explicit states matter for waits and errors." The split-phase timing, the commit condition with HREADYOUT and !error, and the multiple-commit bug are the senior signals.
10. Practice Challenge
Build and reason from the write FSM.
- The states. Describe the write FSM states (IDLE, WRITE-DATA, WRITE-WAIT, COMMIT/ERROR) and the transitions.
- Split timing. Explain why the write commits in the data phase (address captured in the address phase,
HWDATAdata-phase). - Read the waveform. From Figure 2, explain how the write commits once, on the completing cycle, after a wait — and why not during the wait.
- The commit condition. State the commit condition (
sel_q && write_q && HREADYOUT && !error) and explain the role of each term, especiallyHREADYOUT. - The multiple-commit bug. Explain the bug of omitting
HREADYOUT, why it hides for plain registers, and why it corrupts side-effecting writes.
11. Key Takeaways
- The write FSM sequences a write: IDLE → WRITE-DATA (the data phase,
HWDATAvalid) → optional WRITE-WAIT (HREADYOUTlow for slow storage) → COMMIT or ERROR → IDLE. - A write commits in the data phase — the address is captured in the address phase (16.7), but
HWDATAis data-phase, so the slave combines them at commit. - The commit condition is
sel_q && write_q && HREADYOUT && !error— the write lands exactly once, at the captured address, on the completing legal cycle. - The
HREADYOUTterm is essential — without it, a waited write commits repeatedly (every wait cycle): idempotent for plain registers (so it hides), corrupting for side-effecting writes (FIFO push, counter). - The
!errorterm suppresses illegal writes — a write to a read-only/protected/reserved target doesn't commit (state stays safe — a security property), whileERRORis signaled (16.6). - It's the write-completion point — where the master's intent becomes committed state; the exactly-once-on-completion discipline makes writes atomic, reliable, safe. Zero-wait slaves collapse it to a gated write-enable.
12. What Comes Next
You now can build the write side. The next chapter builds the read side:
- Read FSM (next) — design the slave read-side FSM (returning data, aligned to the data phase).
- Bridge FSM (RTL) and Reusable AHB RTL Templates — the bridge control and drop-in building blocks.
To revisit the capture the commit relies on, see Address / Control Capture; for the pace and response the FSM drives, see HREADYOUT Generation and HRESP Generation.