AMBA APB · Module 13
Bridge FSM Design
The production bridge state machine that assembles the whole module — capture, SETUP/ACCESS sequencing, PREADY wait with a timeout, PSLVERR-to-error, and decode fan-out — into one deadlock-free controller where every state has a defined exit and every transfer returns to IDLE.
This is where the module comes together. Across this module you have built every piece of a bridge in isolation — the core IDLE/SETUP/ACCESS sequence, the AXI and AHB upstream faces, the protocol-conversion mechanics, wait-state propagation, error propagation, and address decode. This closing chapter is the assembly: the single finite state machine that sequences all of it into one robust, deadlock-free controller. The throughline to carry: a production bridge is one well-structured FSM that sequences capture → APB transfer → response, with a timeout on every wait, a decoded PSEL fan-out to N slaves, and a clean return to IDLE for the next transfer — and its defining property is that every state has a defined exit, so no peripheral or corner case can ever leave the bus hung.
1. Problem statement
The problem is turning the six separate behaviours you have studied — capture, conversion, wait propagation, error propagation, decode, and the core transfer sequence — into one controller that is correct under every input, every peripheral, and every corner case, and that can never deadlock.
Each prior chapter solved one facet against a clean backdrop. The production bridge has to run all of them at once, on the same clock, with shared state, and survive the inputs you do not want to think about: a peripheral that never returns PREADY, a PSLVERR arriving in the same cycle a timeout fires, a reset dropping mid-transfer, back-to-back upstream transfers with no idle cycle between them. A bridge that works on the happy path but hangs on one of these is not a bridge — it is a latent bus lockup.
Three things make this genuinely hard, and they are integration problems, not facet problems:
- State has to be shared and consistent. The captured request, the FSM state, the wait counter, and the decoded
PSELall advance together off one clock. A facet implemented correctly in isolation can still be wrong when it has to coexist — for example, a timeout counter that is not reset on entry toACCESS, or a decode that is sampled a cycle too late. - Every state must have a defined exit. The unpipelined APB transfer means the FSM waits — and any state that waits can hang if its exit condition never occurs. A production bridge needs a bounded escape from every wait, or one stuck peripheral takes down the whole subsystem behind the bridge.
- The upstream response must always be completed. The FSM cannot simply return to
IDLEwhen something goes wrong; it owes the upstream master a response (OKAYorERROR) for every accepted transfer. Skipping the response on an error or timeout leaves the master hung waiting for a completion that never comes — a deadlock that looks like the bridge "worked."
So the job is to design the one FSM whose structure makes correct-under-all-inputs and deadlock-free true by construction, then layer the module's facets onto its states.
2. Why previous knowledge is insufficient
You now have every piece, but you have only ever seen each piece alone. The production FSM is the assembly, and assembly introduces failure modes that no single-facet chapter could expose.
- You have the core three-state sequence — but only on the happy path. The AHB-to-APB bridge and AXI-to-APB bridge chapters established
IDLE → SETUP → ACCESSas the spine of the bridge. But that core was drawn for a peripheral that returnsPREADYand never errors. Production needs that spine extended with the wait, timeout, and error states folded in without breaking back-to-back behaviour. - You have wait propagation — but not as a state that can hang. Wait-state propagation showed how
PREADY=0becomes upstream back-pressure. In the FSM that is a self-loop onACCESS— and a self-loop with no escape is exactly how a bridge deadlocks. Turning wait propagation into a boundedACCESSstate with a timeout is new. - You have error propagation — but not as a mandatory FSM exit. Error propagation showed
PSLVERR → upstream ERROR. In the assembled FSM that becomes a transition you are obligated to take and to complete; the FSM must route the error to a response, not silently fall back toIDLE. - You have address decode — but not driving the live
PSELfan-out. Bridge address mapping produced the decode that selects one of N slaves. In the FSM that decode must be captured with the request and used to drive whichPSELline goes high duringSETUP/ACCESS— one FSM, N peripherals, the decode picks the target.
The model to add is the integrated controller: a single FSM whose states are the assembly of all six facets, with the new properties — bounded waits, mandatory responses, deadlock-freedom — that only exist once the pieces share one machine.
3. Mental model
The model: the production bridge is one disciplined sequencer with a stopwatch and a fire exit on every room. It captures the request and the decoded target, walks the APB transfer room by room (SETUP, then ACCESS), and in the ACCESS room it waits at the counter with a stopwatch running — but it can always leave: through the front door when the peripheral finishes cleanly, through the side door when the peripheral signals an error, and through the fire exit when the stopwatch runs out. Whatever happens, it ends up back in IDLE, having told the upstream master how the transfer went, ready to start the next one immediately.
Three refinements make it precise:
- Capture and decode on accept, then sequence. When the FSM accepts an upstream transfer in
IDLE, it registers the address, write flag, and write data and the decoded target select. The captured decode drives which one of the NPSELlines asserts — so a single FSM fans out to many peripherals; the decode, not the FSM, picks the target (bridge address mapping). ACCESSis a bounded wait, not an open one. The FSM holdsPSEL & PENABLEand samplesPREADYeach cycle. WhilePREADYis low it loops onACCESSand increments a counter — but the loop is guarded by!timeout. The moment the counter hits its limit, the FSM leaves whether or notPREADYever arrived. That single!timeoutguard is the difference between a robust bridge and a bus that hangs.- Every exit completes the upstream response and returns to
IDLE. Clean completion drivesOKAYupstream;PSLVERRor a timeout drivesERRORupstream (the timeout synthesises an error so the master is never left hanging). All three paths land inIDLE, where the FSM can immediately accept the next transfer — enabling back-to-back transfers with no bubble where the upstream protocol allows it.
4. Real SoC implementation
In RTL the production bridge is an enumerated FSM with a captured request, a captured decode, a bounded wait counter, and a registered upstream response. The block below is the assembled controller — every facet of the module folded into one lint-clean machine, with comments mapping each part back to the chapter that introduced it.
// =====================================================================
// Production APB bridge FSM — the assembly of Module 13 into one controller.
// * IDLE/SETUP/ACCESS core ........... ahb-to-apb-bridge / axi-to-apb-bridge
// * capture of the upstream request .. ahb-to-apb-bridge (capture)
// * decode -> PSEL fan-out (N slaves). bridge-address-mapping
// * PREADY wait (self-loop) .......... wait-state-propagation
// * bounded timeout on the wait ...... (the production-robustness facet)
// * PSLVERR -> upstream ERROR ........ error-propagation
// * clean return to IDLE (back-to-back)
// Lint-conscious: enumerated states, full case, no latches (every output
// assigned every comb pass), defined async reset, a defined exit on every
// state including the timeout fire-exit on the only waiting state.
// =====================================================================
module apb_bridge_fsm #(
parameter int N_SLAVES = 4, // bridge-address-mapping: N peripherals
parameter int TIMEOUT = 1024, // bounded PREADY wait (cycles)
parameter int TW = $clog2(TIMEOUT)
)(
input logic pclk,
input logic presetn, // active-low async reset
// --- upstream face (generic: 'req' is one accepted AHB/AXI transfer) ---
input logic req_valid, // an upstream transfer is offered
input logic [31:0] req_addr,
input logic req_write,
input logic [31:0] req_wdata,
output logic resp_valid, // 1-cycle: upstream response ready
output logic [31:0] resp_rdata,
output logic resp_error, // OKAY=0 / ERROR=1 to the upstream master
// --- downstream APB manager face (N peripherals share addr/data) ---
output logic [N_SLAVES-1:0] psel, // one-hot decoded select (fan-out)
output logic penable,
output logic [31:0] paddr,
output logic pwrite,
output logic [31:0] pwdata,
input logic [31:0] prdata,
input logic pready,
input logic pslverr
);
typedef enum logic [1:0] {IDLE, SETUP, ACCESS, RESP} state_t;
state_t state, nstate;
// ---- captured request + captured decode (frozen for the whole transfer) ----
logic [31:0] addr_q, wdata_q;
logic write_q;
logic [N_SLAVES-1:0] sel_q; // captured one-hot PSEL (bridge-address-mapping)
logic [TW-1:0] to_cnt; // bounded wait counter
logic timeout;
logic err_q; // latched error cause (PSLVERR or timeout)
logic [31:0] rdata_q;
// address decode: which peripheral does req_addr target? (one-hot, default 0)
// (real decode lives in bridge-address-mapping; shown inline for the fan-out.)
function automatic logic [N_SLAVES-1:0] decode(input logic [31:0] a);
decode = '0;
// bits [13:12] pick a 4KB-windowed peripheral; replace with your map.
if (a[31:14] == 18'h0_0000) decode[a[13:12]] = 1'b1;
endfunction
wire accept = (state == IDLE) && req_valid && (decode(req_addr) != '0);
assign timeout = (to_cnt == TW'(TIMEOUT-1));
// ---------------- next-state: every state has a defined exit ----------------
always_comb begin
nstate = state; // default: hold (no latch on state)
unique case (state)
IDLE: if (accept) nstate = SETUP; // capture+decode done
SETUP: nstate = ACCESS; // SETUP is always 1 cycle
ACCESS: if (pready) nstate = RESP; // clean OR error completion
else if (timeout) nstate = RESP; // <-- the fire exit (no hang)
// else: stay in ACCESS (the PREADY wait self-loop)
RESP: nstate = IDLE; // always: response done, ready
default: nstate = IDLE; // unreachable -> safe IDLE
endcase
end
always_ff @(posedge pclk or negedge presetn)
if (!presetn) state <= IDLE; // defined reset -> safe initial state
else state <= nstate;
// ---------------- datapath registers (capture, counter, response) ----------------
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
addr_q <= '0; wdata_q <= '0; write_q <= 1'b0; sel_q <= '0;
to_cnt <= '0; err_q <= 1'b0; rdata_q <= '0;
end else begin
if (accept) begin // CAPTURE the request + the decoded select
addr_q <= req_addr;
write_q <= req_write;
wdata_q <= req_wdata;
sel_q <= decode(req_addr); // freeze which PSEL fans out
end
if (state == ACCESS && !pready) to_cnt <= to_cnt + 1'b1; // bounded wait
else if (state != ACCESS) to_cnt <= '0; // reset counter off the wait
if (state == ACCESS && (pready || timeout)) begin
rdata_q <= prdata; // sample read data
err_q <= timeout ? 1'b1 : pslverr; // PSLVERR or timeout => ERROR
end
end
end
// ---------------- APB manager outputs (decode-driven fan-out) ----------------
assign psel = ((state == SETUP) || (state == ACCESS)) ? sel_q : '0; // one FSM, N slaves
assign penable = (state == ACCESS);
assign paddr = addr_q;
assign pwrite = write_q;
assign pwdata = wdata_q;
// ---------------- upstream response (always completed, then IDLE) ----------------
assign resp_valid = (state == RESP); // exactly one response per accepted transfer
assign resp_rdata = rdata_q;
assign resp_error = err_q; // OKAY / ERROR (error-propagation)
endmoduleThree facts make this the production pattern. First, the capture freezes both the request and the decode (addr_q, write_q, wdata_q, and sel_q) on accept — so the one FSM drives whichever PSEL the decode chose, and a single machine fans out to N peripherals (bridge address mapping). Second, the only waiting state has a bounded escape: ACCESS loops while !pready, but the else if (timeout) branch is a defined exit, so a hung peripheral cannot deadlock the bridge — this is the production-robustness facet that the happy-path bridge omits. Third, every accepted transfer produces exactly one response: the FSM always passes through RESP (driving resp_valid for one cycle with OKAY or ERROR) before returning to IDLE, so the upstream master is never left hanging, and the immediate return to IDLE lets the next transfer start back-to-back.
5. Engineering tradeoffs
The FSM is best understood state by state — what each state does, when it leaves, and which chapter's concern it discharges. This is the assembly made literal:
| State | What it does | Exit condition | Module facet it handles |
|---|---|---|---|
| IDLE | Idle; on req_valid with a valid decode, captures addr/write/wdata and the decoded PSEL; PSEL/PENABLE held low | accept (valid request + non-zero decode) → SETUP | Reset/safe initial state + capture (13.1) + decode (13.6) |
| SETUP | Asserts the decoded PSEL with PENABLE low for exactly one cycle | unconditional (always) → ACCESS | Core two-phase APB sequence (13.1) |
| ACCESS | Asserts PSEL & PENABLE, samples PREADY, increments the timeout counter while waiting | pready or timeout → RESP; else self-loop (wait) | Wait propagation (13.4) + the bounded-timeout robustness facet |
| RESP | Drives resp_valid for one cycle with OKAY (clean) or ERROR (PSLVERR or timeout) | unconditional (always) → IDLE | Error propagation (13.5) + guaranteed upstream completion |
| (IDLE again) | Returns immediately, ready to accept the next request with no bubble | n/a — enables back-to-back | Clean back-to-back transfer handling |
A note on the structural choices behind that table. Whether RESP is a separate state or folded into ACCESS is a real trade: a dedicated RESP state (as above) makes "exactly one response per transfer" obvious and easy to assert, at the cost of one extra cycle of latency; folding the response into the ACCESS-completion cycle saves that cycle but makes the back-to-back and error paths harder to keep correct. For a peripheral bus the extra cycle is invisible and the clarity is worth it. Upstream arbitration is the other structural call: when multiple masters or a multi-layer interconnect funnel into one bridge, the interconnect almost always arbitrates and the bridge sees a single serialised request stream — so the FSM stays single-threaded and the ordering guarantee is "one transfer fully completes (RESP → IDLE) before the next is accepted." If you ever do put arbitration inside the bridge, it belongs in front of IDLE (pick a winner, present it as the single req), never inside SETUP/ACCESS — the transfer sequencing must stay one stream.
6. Common RTL mistakes
7. Debugging scenario
The signature production-bridge bug is a deadlock in a wait/error state that has no exit on a corner case — the FSM is correct on every path you tested, but one untested input combination has no defined transition, so the machine stops moving and the whole peripheral bus behind the bridge goes dark.
- Observed symptom: the SoC runs fine for hours, then the entire APB peripheral subsystem freezes — every CPU access to any peripheral behind the bridge hangs, the upstream master never gets a completion, and a watchdog eventually resets the chip. It is not reproducible on demand; it correlates loosely with one flaky peripheral and with heavy back-to-back traffic.
- Waveform clue: the bridge FSM is stuck in
ACCESS(or a wait sub-state).PSELandPENABLEare held high,PREADYis flat low, the upstreamHREADY/resp_validnever asserts, and the wait counter is either saturated or — the tell — was never incrementing because, in this build, aPSLVERRhad arrived in the same cycle the timeout was about to fire and the next-state logic had no branch for that combination, so the FSM held its state. - Root cause: the
ACCESSnext-state logic handledpreadyand handledtimeout, but the corner wherepready && pslverrcoincided withtimeout— or, in a variant, a reset deasserting one cycle into a transfer leaving a datapath register stale — fell through to the implicit "hold" with no transition out. The state had an exit for each input alone but a dead spot where two exit conditions collided, and the FSM parked there forever. - Correct RTL: make the wait state's exit total — it must leave on the disjunction, not on mutually-exclusive special cases.
ACCESS: if (pready || timeout) nstate = RESP;covers clean, error, and timeout in one transition (the cause —pslverrvstimeout— is latched separately for the response), so no combination of inputs can leave the state with nowhere to go. Pair it with a complete, defined reset on every register so a mid-transfer reset lands in a cleanIDLE. - Verification assertion: prove liveness — the FSM always returns to
IDLEwithin a bounded time, and no state is a sink. For example:assert property (@(posedge pclk) disable iff(!presetn) (state != IDLE) |-> ##[1:TIMEOUT+4] (state == IDLE));(every transfer makes it back toIDLEwithin the bounded window) plusassert property (@(posedge pclk) disable iff(!presetn) accept |-> ##[1:TIMEOUT+4] resp_valid);(every accepted transfer eventually produces exactly one response). A cover on each state and each transition flags any unreachable or dead state at coverage-closure time. - Debug habit: when a bridge hangs intermittently under stress, suspect a missing exit on a state-input corner first — enumerate, for the waiting state, every combination of its inputs (
pready,pslverr,timeout, reset) and confirm the next-state logic has a defined transition for all of them, not just each one in isolation. A wait state is robust only if its exit condition is total; a dead spot where two conditions collide is invisible on the happy path and lethal in the field.
8. Verification perspective
A production bridge FSM is verified as a state machine — its job is no longer just protocol compliance on each face but the structural guarantees of the controller: reachability, liveness, bounded waits, and clean recovery. The highest-value checks are the ones that prove the FSM can never hang.
- Cover every state and every transition. Functional coverage must hit all four states and every legal transition arc — including the three distinct
ACCESS → RESPcauses (clean,PSLVERR, timeout) and the wait self-loop with zero, one, and many iterations. Any state or arc that never colours is either dead code (a lint/reachability problem) or an untested path — both are exactly where field deadlocks hide. Add a check that asserts no unreachable state and no sink state exists. - Assert liveness: every transfer returns to IDLE in bounded time. The headline property is deadlock-freedom:
(state != IDLE) |-> ##[1:TIMEOUT+4] (state == IDLE)andaccept |-> ##[1:TIMEOUT+4] resp_valid— proven, ideally, with a formal liveness/bounded-model check, not just simulation. This is the one assertion that catches the missing-exit corner before silicon. Pair it with "exactly oneresp_validperaccept" so the FSM neither drops nor duplicates a response. - Drive the timeout on the only waiting state. A peripheral model that never returns
PREADYmust make the FSM fire the timeout, route throughRESP, drive an upstreamERROR, and return toIDLE— proving the wait is bounded. Sweep the wait length: zero wait states, one, many short of the limit, exactly at the limit, and never (forces timeout). The transition right at the timeout boundary, and thePSLVERR-coincident-with-timeout corner, are mandatory coverage points. - Stress reset-mid-transfer and back-to-back. Assert reset from each non-
IDLEstate lands the FSM inIDLEwith all datapath registers cleared and no phantom APB activity afterward (!presetn |=> psel == '0). Separately, drive back-to-back accepts with no idle gap and confirm the FSM sequences each transfer fully and cleanly with no bubble required and no state leakage between transfers (the second transfer's decode/capture must not be contaminated by the first).
The point: verify the FSM as a controller whose structure is the guarantee — full state/transition coverage, a formal liveness proof that every transfer returns to IDLE, a forced timeout on the only wait, and clean reset-mid-transfer and back-to-back behaviour. A bridge that passes per-protocol monitors but has never had its liveness proven still carries a latent bus-lockup.
9. Interview discussion
"Design the state machine for an APB bridge" — or the harder follow-up, "how do you guarantee it never deadlocks?" — is a senior SoC-integration question, and the strong answer leads with structure and deadlock-freedom, not a state list.
Frame it as one FSM that sequences capture → APB transfer → response, with a bounded exit on every state. Name the four states and their reason for existing: IDLE (capture the request and the decoded PSEL, safe reset state), SETUP/ACCESS (the core two-phase APB transfer, the spine from the AHB/AXI bridge chapters), and the response/RESP handling. Then deliver the depth that separates a senior answer: the only waiting state, ACCESS, must have a bounded timeout so a hung peripheral cannot deadlock the bus; every accepted transfer must complete exactly one upstream response (OKAY or a synthesised ERROR on timeout), or the master hangs; and the decode drives a one-hot PSEL fan-out, so one FSM serves N peripherals. Mentioning that the interconnect arbitrates upstream (the bridge sees a single serialised stream, and the ordering guarantee is one-transfer-completes-before-the-next) shows you understand where the bridge sits. Close with the verification angle — "I'd prove liveness formally: every transfer returns to IDLE within a bounded window, and full state/transition coverage flags any dead state" — which signals you think about the bug that actually ships: the untested corner with no exit. The throughline to land: a production bridge is deadlock-free by construction because every state has a defined, bounded exit and every transfer is answered.
10. Practice
- Name the states and exits. List the four FSM states, state what each drives on the APB face, and give the exit condition for each — including the total exit on the waiting state.
- Trace a timeout. For a peripheral that never returns
PREADY, walk the FSM from accept through theACCESSwait, the timeout fire, theRESPstate, and back toIDLE, marking when the upstreamERRORis driven. - Justify the bounded wait. Explain, in terms of the bus, what happens if
ACCESShas a!PREADYself-loop with no timeout, and why "this peripheral always responds" is not a safe assumption. - Fan out to N slaves. Describe how a single FSM drives the correct one of four
PSELlines, and where in the FSM the decode is captured versus used. - Cost a back-to-back pair. For two back-to-back single transfers (zero wait states) through the four-state FSM, count the cycles and identify where, if anywhere, a bubble appears.
11. Q&A
12. Key takeaways
- The production bridge is one well-structured FSM that sequences capture → APB transfer → response:
IDLE(capture + decode) →SETUP→ACCESS(wait + timeout) →RESP(respond) →IDLE. It is the assembly of the whole module — capture, decode fan-out, wait propagation, error propagation — into one controller. - Every state must have a defined exit, and every wait must be bounded. The only waiting state,
ACCESS, leaves on the total conditionpready || timeout; a wait with no timeout is how a single hung peripheral deadlocks the entire bus behind the bridge. - Every accepted transfer owes exactly one response. The FSM always passes through
RESPto driveOKAYor a synthesisedERROR(onPSLVERRor timeout) before returning toIDLE— skipping the response hangs the upstream master. - One FSM drives N peripherals via the decoded one-hot
PSEL. The decode (captured with the request) picks the target; you never need one FSM per slave. Upstream arbitration lives in the interconnect, so the FSM sees a single serialised stream. - Back-to-back is free where the upstream allows it. Because every path returns to
IDLEandIDLEcan accept the next request immediately, the FSM needs no mandatory bubble — a forced gap is a throughput bug. - Verify it as a controller, structurally. Full state/transition coverage, a formal liveness proof that every transfer returns to
IDLEin bounded time, a forced timeout on the wait, and clean reset-mid-transfer and back-to-back behaviour — these prove the property that matters: the bridge can never deadlock.