The last lesson populated a die with a
One block wants to hand a value to another. The producer drives the data lines and raises VALID: "this is real, take it." The consumer raises READY: "I can accept." The transfer — a beat — happens on the first rising clock edge where both are high. That's the whole protocol. Its consequences are wonderful:
There is one golden rule, and it exists to prevent deadlock: VALID must not wait for READY. A producer with data asserts VALID unconditionally — it must never sit back thinking "I'll raise VALID once I see READY", because a consumer is fully entitled to think "I'll raise READY once I see VALID" (that direction is allowed). If both waited politely for the other, the chip would stand in the doorway forever. And once VALID is up, it stays up, with the data held steady, until the beat completes — no changing your mind mid-offer.
Here is the handshake as designers actually see it — a timing diagram, one row per signal, with a stalled beat followed by an instant one:
Count what the consumer's low READY cost: two cycles of waiting, zero bytes lost, zero extra control logic. That is why this handshake conquered the industry.
A full AXI connection between a master and a slave is just five of these handshakes running side by side, one per channel, each with its own VALID/READY pair:
| Channel | Direction | Carries |
|---|---|---|
| AW — write address | master → slave | where to write, burst length, ID |
| W — write data | master → slave | the data beats, WLAST on the final one |
| B — write response | slave → master | one "it worked / it failed" per whole write |
| AR — read address | master → slave | where to read, burst length, ID |
| R — read data | slave → master | the data beats coming back, with the ID |
The channels are deliberately decoupled: a master can fire off a write address on AW before the data is ready on W, stream read data on R while a new address goes out on AR, and in general keep every lane of the conversation moving independently. Addresses, data and responses are separate flows that only meet again at the endpoints.
Two more ingredients complete the picture. Bursts: one address transfer can request N data beats ("start at address A, give me 8 words"), amortising the address handshake across a whole cache line — one AW, eight W beats, one B. And IDs: every transaction carries a tag, so a master can have multiple transactions outstanding and a slave can answer them out of order — a fast SRAM answering while a slow DRAM row opens. Matching responses to requests by ID is what unlocks memory-level parallelism, and it is the thread we will pick up again in the memory-controller lesson.
Not every block needs the full orchestra. AXI-Lite is the subset for humble register access — the same five channels, but single-beat only, no bursts, no reordering. Your UART speaks AXI-Lite; your DRAM controller speaks the whole language.
Four beats, five handshake edges, one address, one response. Notice beat 3 politely waiting out the WREADY dip: every beat of a burst is still an individual VALID/READY handshake. The atom never goes away — AXI is just the atom, arranged.
Let's run the atom. A producer always has data; a consumer is ready only about 60% of cycles (seeded, so your run matches mine). Watch backpressure regulate the flow with no losses and no duplicates:
Change the 0.6 to 0.2 and rerun: the consumer gets slower, the stall
count climbs, and still nothing is lost. Now change it to 1.0: a beat every
cycle — the handshake costs nothing when nobody is waiting.
Older on-chip buses (and many off-chip ones) bundled address, data and response into one shared conversation: request, wait, reply, next. That serialises everything — a slow response blocks the next request even if it's headed somewhere fast. AXI's designers split the conversation into five independently flowing lanes precisely so that nothing waits on anything it doesn't have to: a read to fast SRAM overtakes a write still trickling into a slow flash controller; addresses queue ahead of data; responses stream back whenever they're ready. It is the same insight that gave us pipelining inside the CPU — decouple the stages and let each run at its own pace — applied to the wires between blocks. When you meet the memory controller reordering requests for DRAM-friendliness, remember: the five-channel split is what makes that legal at all.
Two classics. The combinational loop: a producer computes
valid = ready && … while the consumer computes
ready = valid && …. Each output now depends combinationally on the other —
wired together, they form a loop with no register in it: at best a deadlock where both sit low
forever, at worst an oscillating, unsynthesisable mess. One direction of dependence is legal
(READY may look at VALID); both directions is a design bug. The retracted
offer: a producer raises VALID, gets bored two cycles later, and drops it (or swaps the
data) before READY ever came. The consumer may have already committed to accepting that
beat — now the stream is corrupted in a way that only shows up under exactly the right timing, the
nastiest kind of bug to chase. Protocol checkers (assertion IP that watches every AXI rule, from
the verification module's toolbox) exist mostly because of these two families. Run them always.