Skip to content

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 PSEL all 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 to ACCESS, 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 IDLE when something goes wrong; it owes the upstream master a response (OKAY or ERROR) 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 → ACCESS as the spine of the bridge. But that core was drawn for a peripheral that returns PREADY and 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=0 becomes upstream back-pressure. In the FSM that is a self-loop on ACCESS — and a self-loop with no escape is exactly how a bridge deadlocks. Turning wait propagation into a bounded ACCESS state 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 to IDLE.
  • You have address decode — but not driving the live PSEL fan-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 which PSEL line goes high during SETUP/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 N PSEL lines asserts — so a single FSM fans out to many peripherals; the decode, not the FSM, picks the target (bridge address mapping).
  • ACCESS is a bounded wait, not an open one. The FSM holds PSEL & PENABLE and samples PREADY each cycle. While PREADY is low it loops on ACCESS and increments a counter — but the loop is guarded by !timeout. The moment the counter hits its limit, the FSM leaves whether or not PREADY ever arrived. That single !timeout guard is the difference between a robust bridge and a bus that hangs.
  • Every exit completes the upstream response and returns to IDLE. Clean completion drives OKAY upstream; PSLVERR or a timeout drives ERROR upstream (the timeout synthesises an error so the master is never left hanging). All three paths land in IDLE, where the FSM can immediately accept the next transfer — enabling back-to-back transfers with no bubble where the upstream protocol allows it.
A four-state FSM diagram for a production APB bridge: IDLE captures and decodes the request and on accept goes to SETUP; SETUP asserts PSEL and after one cycle goes unconditionally to ACCESS; ACCESS asserts PSEL and PENABLE and waits on PREADY with a self-loop guarded by not-PREADY-and-not-timeout, exiting to IDLE on a clean PREADY-and-not-PSLVERR completion (returning OKAY) and exiting through a RESP state on PREADY-and-PSLVERR or on timeout (returning ERROR); RESP always returns to IDLE; reset forces all states to IDLE.
Figure 1 — the full production bridge FSM. From reset the machine sits in IDLE (PSEL/PENABLE low), capturing the address, write flag, write data, and decoded PSEL when it accepts an upstream transfer. On accept it advances to SETUP (PSEL high, PENABLE low) for exactly one cycle, then unconditionally to ACCESS (PSEL and PENABLE high), where it waits on PREADY. ACCESS has a wait self-loop guarded by !PREADY && !timeout that increments a counter each cycle, and three defined exits, all returning to IDLE: a clean completion (PREADY && !PSLVERR) returns OKAY upstream directly; an error completion (PREADY && PSLVERR) and a timeout both route through the RESP state, which drives an ERROR response upstream and returns to IDLE in bounded time. Asynchronous reset forces every state back to IDLE. The figure's whole point is that no state can hang: every wait has a timeout, every path returns to IDLE.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
// 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)
 
endmodule

Three 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:

StateWhat it doesExit conditionModule facet it handles
IDLEIdle; on req_valid with a valid decode, captures addr/write/wdata and the decoded PSEL; PSEL/PENABLE held lowaccept (valid request + non-zero decode) → SETUPReset/safe initial state + capture (13.1) + decode (13.6)
SETUPAsserts the decoded PSEL with PENABLE low for exactly one cycleunconditional (always) → ACCESSCore two-phase APB sequence (13.1)
ACCESSAsserts PSEL & PENABLE, samples PREADY, increments the timeout counter while waitingpready or timeoutRESP; else self-loop (wait)Wait propagation (13.4) + the bounded-timeout robustness facet
RESPDrives resp_valid for one cycle with OKAY (clean) or ERROR (PSLVERR or timeout)unconditional (always) → IDLEError propagation (13.5) + guaranteed upstream completion
(IDLE again)Returns immediately, ready to accept the next request with no bubblen/a — enables back-to-backClean 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 (RESPIDLE) 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). PSEL and PENABLE are held high, PREADY is flat low, the upstream HREADY/resp_valid never asserts, and the wait counter is either saturated or — the tell — was never incrementing because, in this build, a PSLVERR had 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 ACCESS next-state logic handled pready and handled timeout, but the corner where pready && pslverr coincided with timeout — 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 causepslverr vs timeout — 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 clean IDLE.
  • Verification assertion: prove liveness — the FSM always returns to IDLE within 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 to IDLE within the bounded window) plus assert 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.
Two FSM fragments side by side. Left, in red: IDLE to ACCESS, ACCESS with a not-PREADY self-loop and a single clean-PREADY exit, with a dashed red callout marking ACCESS as a dead state that hangs the bus when PREADY never arrives. Right, in green and amber: IDLE to ACCESS, ACCESS with a not-PREADY-and-not-timeout self-loop, a clean exit to IDLE, and a timeout-or-PSLVERR exit to a RESP state that returns to IDLE, with reset returning all states to IDLE — every wait has a bounded exit.
Figure 2 — the deadlock bug versus the fix. Left (bug, red): the ACCESS wait state has a self-loop for !PREADY and an exit only on the clean PREADY case, but the corner where PREADY never arrives (a hung peripheral, or a PSLVERR colliding with a timeout the logic didn't branch on) has no transition out — the FSM stays in ACCESS forever, holding PSEL/PENABLE high and the upstream handshake low, deadlocking the whole bus. Right (fix, green/amber): the same ACCESS state carries a bounded timeout counter and leaves on the disjunction pready || timeout to a RESP state that drives a synthetic ERROR upstream and returns to IDLE; clean and error completions also return to IDLE, and reset returns every state to IDLE. The lesson: every wait state needs a total, bounded exit so no peripheral or input corner can leave the FSM with no way back to IDLE.

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 → RESP causes (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) and accept |-> ##[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 one resp_valid per accept" so the FSM neither drops nor duplicates a response.
  • Drive the timeout on the only waiting state. A peripheral model that never returns PREADY must make the FSM fire the timeout, route through RESP, drive an upstream ERROR, and return to IDLE — 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 the PSLVERR-coincident-with-timeout corner, are mandatory coverage points.
  • Stress reset-mid-transfer and back-to-back. Assert reset from each non-IDLE state lands the FSM in IDLE with 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

  1. 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.
  2. Trace a timeout. For a peripheral that never returns PREADY, walk the FSM from accept through the ACCESS wait, the timeout fire, the RESP state, and back to IDLE, marking when the upstream ERROR is driven.
  3. Justify the bounded wait. Explain, in terms of the bus, what happens if ACCESS has a !PREADY self-loop with no timeout, and why "this peripheral always responds" is not a safe assumption.
  4. Fan out to N slaves. Describe how a single FSM drives the correct one of four PSEL lines, and where in the FSM the decode is captured versus used.
  5. 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) → SETUPACCESS (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 condition pready || 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 RESP to drive OKAY or a synthesised ERROR (on PSLVERR or timeout) before returning to IDLE — 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 IDLE and IDLE can 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 IDLE in 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.