Wishbone · Module 2
Bus Transactions
A transaction is the unit of bus work: one beginning, one ending, and an interval in between during which the request must not move. Making waiting expressible is what lets a slow target share a bus with a fast one, and it is what turns an initiator from a wire into a state machine with real failure modes.
Everything so far has completed in the cycle it was requested. Every target in Chapter 2.4 was never busy, ready was a combinational function of the inputs, and the initiator never had to remember anything.
That simplification is now removed, and removing it is what makes a bus a bus.
What turns a collection of address, data and control signals into one coherent operation — and what must remain true for as long as that operation is in flight?
1. The Lifecycle
Stated as states, from the initiator's point of view:
IDLE ──issue──▶ OUTSTANDING ──accepted──▶ IDLE (or straight to the next)
│
└── while waiting: request must not moveOnly two states are needed, and it is worth knowing why the second one exists. It is not there to do work; it is there to remember. Its entire content is a transaction is in flight, and this is what it was.
Three properties of the interval are the chapter's substance.
It has no upper bound in this model. One cycle, fifty, or — if a target is broken — never. Chapter 2.7 is where the never case gets a system-level answer.
The request must not move during it. Change the address while outstanding and the target may have latched the old one while the fabric is now routing by the new one. Both blocks are individually reasonable and the pair is broken — the disagreement class Chapter 1.3 §1 identified.
It ends exactly once. A completion that asserts for two cycles is two acceptances of one transaction, which on a write means the write happened twice.
2. RTL 1 — The Initiator's Transaction Controller
// ─────────────────────────────────────────────────────────────────────────
// txn_ctrl — the initiator side of one outstanding transaction.
//
// PURPOSE. Turn a fire-and-forget request from surrounding logic into a bus
// transaction that is held stable until the target accepts it, and hand the
// result back. This is the state that a fixed-latency bus does not need and
// a completion-signalled bus does.
//
// ONE transaction outstanding at a time — deliberately. Pipelining is a
// later subject and it changes this design substantially.
//
// Generic educational interface — NOT Wishbone signal names.
// ─────────────────────────────────────────────────────────────────────────
module txn_ctrl #(
parameter int unsigned AW = 32,
parameter int unsigned DW = 32
) (
input logic clk,
input logic rst_n,
// ── Client side: surrounding logic asks for one access ───────────────
input logic req, // pulse or level: please do an access
input logic req_write,
input logic [AW-1:0] req_addr,
input logic [DW-1:0] req_wdata,
input logic [3:0] req_byte_en,
output logic busy, // 1 = cannot accept a new request
output logic done, // 1-cycle pulse: the access finished
output logic [DW-1:0] done_rdata,
output logic done_error,
// ── Bus side ─────────────────────────────────────────────────────────
output logic valid,
output logic write,
output logic [AW-1:0] addr,
output logic [DW-1:0] wdata,
output logic [3:0] byte_en,
input logic ready,
input logic [DW-1:0] rdata,
input logic error
);
typedef enum logic [0:0] { S_IDLE = 1'b0, S_OUTSTANDING = 1'b1 } state_e;
state_e state_q;
logic write_q;
logic [AW-1:0] addr_q;
logic [DW-1:0] wdata_q;
logic [3:0] byte_en_q;
// ── The acceptance term. EVERY state change and EVERY side effect in
// this design hangs off this one expression, which is the rule
// Chapter 2.4 derived: a transfer takes effect on exactly one edge.
logic accepted;
assign accepted = (state_q == S_OUTSTANDING) & valid & ready;
// ── Combinational outputs ────────────────────────────────────────────
// The request presented on the bus comes from the LATCHED copy, never
// from the client's inputs. That is what guarantees stability even if the
// client changes its mind mid-transaction — the single most important
// design decision in this module.
assign valid = (state_q == S_OUTSTANDING);
assign write = write_q;
assign addr = addr_q;
assign wdata = wdata_q;
assign byte_en = byte_en_q;
assign busy = (state_q == S_OUTSTANDING);
// ── Sequential ───────────────────────────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
write_q <= 1'b0;
addr_q <= '0;
wdata_q <= '0;
byte_en_q <= '0;
end else begin
unique case (state_q)
S_IDLE: begin
if (req) begin
// Latch the whole request once. From here the client may do
// whatever it likes with its own signals.
write_q <= req_write;
addr_q <= req_addr;
wdata_q <= req_wdata;
byte_en_q <= req_byte_en;
state_q <= S_OUTSTANDING;
end
end
S_OUTSTANDING: begin
// The ONLY exit. Note what is absent: no timeout, no abort, no
// path that withdraws the request. Section 6 is what that costs.
if (accepted) state_q <= S_IDLE;
end
endcase
end
end
// ── Result capture ───────────────────────────────────────────────────
// rdata is valid only in the accepting cycle (Chapter 2.3), so it is
// registered on exactly that edge and presented with a one-cycle `done`.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
done <= 1'b0;
done_rdata <= '0;
done_error <= 1'b0;
end else begin
done <= accepted; // one cycle, always
if (accepted) begin
done_rdata <= rdata;
done_error <= error;
end
end
end
endmoduleReading this module
Purpose. Hold one transaction stable for as long as the target needs, and deliver the result once.
Interface contract. On the client side, req is honoured only while busy is low. On the bus side, valid is asserted for the whole outstanding interval and the payload never moves.
Combinational decisions. Only one, really: valid is the state. Everything else on the bus side is a register output. That is the design — by driving the bus from latched copies rather than from the client's live signals, stability is structural rather than a rule the client must obey.
Sequential behaviour. Two always_ff blocks with distinct jobs. The first owns the state and the latched request; the second owns result capture. Splitting them keeps each readable and makes a failure message point at one or the other.
Cycle by cycle, for a read that waits two cycles:
| Cycle | State | valid | ready | What happens |
|---|---|---|---|---|
| 0 | IDLE | 0 | – | req high; request latched at the edge |
| 1 | OUTSTANDING | 1 | 0 | Target busy. Nothing moves. |
| 2 | OUTSTANDING | 1 | 0 | Still busy. Payload identical to cycle 1. |
| 3 | OUTSTANDING | 1 | 1 | accepted is high; rdata sampled at this edge |
| 4 | IDLE | 0 | – | done pulses for exactly one cycle |
Assumptions and simplifications. One outstanding transaction; no pipelining; no timeout; req is assumed stable for the cycle it is asserted; and valid does not depend combinationally on ready, which is what keeps the pair out of a loop.
How it could fail.
- Drive the bus from
req_addrinstead ofaddr_qand the request moves whenever the client's signals move. Works perfectly against a never-busy target and breaks the first time anything waits. - Make
donea level instead of a pulse and the client sees one access as many. - Capture
rdataunconditionally rather than onacceptedand the captured value is whatever was on the bus at an arbitrary edge. - Add a path that leaves
S_OUTSTANDINGwithoutaccepted— an abort, say — and the target, which never saw the withdrawal, completes into a fabric that has moved on.
Scaling. One outstanding transaction means the bus is idle during every wait. Allowing several in flight raises questions this design cannot answer — how responses are matched to requests, whether they may return out of order, how much buffering is needed — and that is a substantially different controller.
3. Waveform — Immediate and Delayed
One transaction, two targets
10 cyclesWhat the two halves are meant to show together. The initiator issued the same kind of operation both times and its logic did not change. The fast target contributed zero wait cycles, the slow one contributed two, and nothing in the initiator knows or cares which.
That is the property the whole module has been building toward. Chapter 1.5 argued for it structurally — a fabric that can absorb a register slice, a slower peripheral revision, a bridge. This is the mechanism underneath that argument.
Read cycle 5 specifically. addr shows a continuation, not a new value, and that is not a drawing convention — it is the contract. The moment the address moves while valid is high and ready is low, the transaction has two identities and the system has a bug that no single block is responsible for.
4. Verification — Properties of an In-Flight Transaction
// ─────────────────────────────────────────────────────────────────────────
// Transaction properties for the Module 2 teaching interface.
//
// These encode the two rules from Section 1: the request does not move
// while in flight, and the transaction ends exactly once. They are
// architectural properties of THIS chapter's contract, not any bus's
// protocol rules.
// ─────────────────────────────────────────────────────────────────────────
module txn_checker #(
parameter int unsigned AW = 32,
parameter int unsigned DW = 32
) (
input logic clk,
input logic rst_n,
input logic valid,
input logic ready,
input logic write,
input logic [AW-1:0] addr,
input logic [DW-1:0] wdata,
input logic [3:0] byte_en,
input logic busy,
input logic done
);
default disable iff (!rst_n);
logic accepted;
assign accepted = valid && ready;
// P1 — the request does not move while in flight. Split across two
// assertions so a failure names the field that moved rather than
// reporting that something did.
property p_addr_stable;
@(posedge clk) (valid && !ready) |=> ($stable(addr) && $stable(write) && $stable(byte_en));
endproperty
a_addr_stable : assert property (p_addr_stable)
else $error("addr/write/byte_en changed while the transaction was in flight");
property p_wdata_stable;
@(posedge clk) (valid && !ready && write) |=> $stable(wdata);
endproperty
a_wdata_stable : assert property (p_wdata_stable)
else $error("wdata changed during an outstanding write");
// P2 — a request is never withdrawn before it is accepted. A target that
// completes into a withdrawn request has done work nobody will
// collect, and on a write that work already happened.
property p_no_withdrawal;
@(posedge clk) (valid && !ready) |=> valid;
endproperty
a_no_withdrawal : assert property (p_no_withdrawal)
else $error("valid dropped before the transaction was accepted");
// P3 — a transaction ends exactly once. Two acceptances of one request
// is a double write. Expressed as: after an acceptance, `valid` must
// fall before it can be accepted again.
property p_single_acceptance;
@(posedge clk) accepted |=> (!valid || !$stable(addr) || !$stable(write));
endproperty
a_single_acceptance : assert property (p_single_acceptance)
else $error("the same request appears to have been accepted twice");
// P4 — `done` is a one-cycle pulse following acceptance, and occurs only
// then. Catches both a level-instead-of-pulse bug and a spurious
// completion with no transaction behind it.
property p_done_follows_accept;
@(posedge clk) done |-> $past(accepted);
endproperty
a_done_follows_accept : assert property (p_done_follows_accept)
else $error("done asserted without a preceding acceptance");
// P5 — the controller cannot accept new work while outstanding. This is
// the client-facing half of the contract.
property p_busy_while_valid;
@(posedge clk) valid |-> busy;
endproperty
a_busy_while_valid : assert property (p_busy_while_valid)
else $error("controller presented a request without reporting busy");
endmoduleWhy these five, and what each one catches. P1 and P2 are the in-flight rules, and they catch the disagreement class where both blocks are individually correct. P3 catches the double-acceptance that turns one write into two. P4 catches the level-versus-pulse confusion at the client boundary. P5 catches a controller that would accept a second request while the first is outstanding — the bug that corrupts the latched copy mid-transaction.
The asymmetry worth noticing: P1, P2, P3 and P5 constrain the initiator; only P4 concerns the result path. That reflects where the obligations actually sit in this contract — the initiator does most of the promising, because it is the side that must not move.
What is deliberately absent. No timeout property, because this contract has no timeout. No property about how long a target may take, because unbounded is the rule. Both of those are policy that a real specification must supply, and Section 6 is why.
5. Failure Modes and Discriminating Evidence
Symptom: reads return the previous transaction's data.
Candidates. The initiator captures rdata on the wrong edge. Or it captures unconditionally rather than on acceptance.
Discriminating evidence. Put valid, ready and the capture strobe on one waveform. If the capture is one cycle after the acceptance edge, the initiator is late and the target is blameless. If it fires on cycles where ready is low, the capture is ungated.
Likely RTL location. The result-capture always_ff, not the state machine.
Property that catches it. P4.
Symptom: a write happens twice for one software store.
Candidates. Two acceptances of one request — valid stayed high through a second ready. Or the target treats the held request as level-sensitive.
Discriminating evidence. Count valid && ready edges per software access. Two is conclusive, and then the question is which side: if valid never fell, the initiator failed to leave S_OUTSTANDING; if it did fall and the target still applied twice, the target is not gating on the acceptance term.
Property that catches it. P3 on the initiator; Chapter 2.4's P4 on the target.
Symptom: works against fast targets, corrupts against slow ones.
Candidates. Almost always the request moving during a wait — the bus driven from live client signals rather than latched copies.
Discriminating evidence. Trigger on valid && !ready and watch addr. Any change is conclusive, and it is the single highest-yield trigger in this chapter.
Likely RTL location. The initiator's output assignments — assign addr = req_addr instead of addr_q.
Property that catches it. P1 — and note it can only ever fire when a target actually waits, which is why this bug ships.
Symptom: the whole system stops on one access.
Candidates. A target that never completes. An unmapped address with no default target. An initiator waiting on a completion it already missed.
Discriminating evidence. Look at target_sel first, per Chapter 2.2. All-zero is decode. A valid select with ready never rising is the target. A valid select with ready having pulsed is the initiator missing it.
Property that catches it. None of these. This contract has no property that can catch a target which never completes, because unbounded is legal here — which is exactly the gap Section 6 is about.
6. What This Contract Deliberately Leaves Open
Being explicit about the holes is what separates a teaching model from a specification.
There is no timeout. A target that never completes hangs the initiator forever, and nothing in the contract forbids it. Real systems add a watchdog — a counter in the fabric that terminates the transaction with an error after N cycles — and that is a system decision with real consequences: too short and a legitimately slow target is aborted; too long and a hang takes the product down anyway.
There is no abort. Once valid is asserted, the initiator is committed. A transaction cannot be cancelled, which matters for a CPU that takes an interrupt mid-access.
There is no pipelining. One transaction at a time means the bus is idle during every wait cycle. Allowing several in flight is where most of the performance in a modern interconnect comes from, and it introduces response-to-request matching, ordering rules and buffering — a substantially larger design.
There is no ordering rule across targets. With one outstanding transaction the question does not arise. It arises immediately with two.
And there is no rule about combinational dependency. This module has assumed valid does not depend on ready. A real specification must say so explicitly, or two reasonable implementations form a loop.
Every one of those gaps is a thing a published protocol has to decide. That is not a criticism of the teaching contract — it is the argument for why a real specification is longer than one paragraph, and it is Chapter 1.3's point arriving with concrete content.
7. Common Mistakes
"The request is just the values I put on the bus."
Wrong mental model: a transaction is a moment, so the signals only matter when it starts.
Concrete failure: the initiator drives the bus from live client signals. Against a never-busy target every transfer lasts one cycle and nothing moves. Against a waiting target the address changes mid-flight; the target latched one address, the fabric now routes by another.
Observable evidence: correct behaviour with fast peripherals, corruption with slow ones — and the corruption follows the peripheral's speed rather than any pattern in the data.
Correct model: a transaction is an interval. Drive the bus from a latched copy so stability is structural.
"Completion means the initiator can move on immediately."
Wrong mental model: ready is a permission slip for the next cycle.
Concrete failure: the initiator deasserts valid and captures rdata in the next cycle, by which time the target is no longer driving it.
Observable evidence: every read returns the following access's data, or zero.
Correct model: the acceptance edge is where everything happens — the transfer takes effect, read data is valid, and the capture must occur. Not before, not after.
"A one-cycle pulse on the request is enough."
Wrong mental model: asserting the request is how you ask, so one cycle asks once.
Concrete failure: the target was busy that cycle and never saw a request it could accept. The initiator believes it issued an access; nothing happened.
Observable evidence: accesses that silently do not occur, at a rate that tracks how busy the target is.
Correct model: the request must be held until accepted. valid is a level for the duration of the transaction, not a pulse at its start.
"An unbounded wait is fine because targets are well behaved."
Wrong mental model: a target that never completes is a bug that will be found in testing.
Concrete failure: a clock-domain issue, a reset released in the wrong order, or an unmapped access reaches a fabric with no default target — and the system stops, with no diagnostic beyond it hung.
Observable evidence: a lock-up reproducible on one access, with valid high forever.
Correct model: unbounded is what the interface permits; the system must still decide what happens. A timeout is a design element, and its absence is a decision rather than an omission.
8. Interview Reasoning
A transaction is defined by the request that started it — address, direction, write data and byte enables — plus the fact that it has begun and not yet ended.
Two rules govern the interval:
- The initiator must hold the request stable. Every field. If the address moves while the transaction is outstanding, the target may have latched one value while the fabric is routing by another, and both blocks are individually correct.
- The target must eventually end it. Successfully or with an error, but it must end. An ending is not optional, which is why an errored access still asserts completion.
And it ends exactly once. Two acceptances of one request is a double write — a real and quiet failure.
The design consequence that shows understanding: stability is best made structural rather than contractual. Drive the bus from a latched copy of the request, so it cannot move even if the client changes its mind. A design that relies on the client behaving correctly has converted a hardware guarantee into a convention.
9. Understanding Check
10. What's Next
A transaction now has a lifecycle, the initiator has the state to hold one in flight, and the properties that make it correct are written down — along with the five things this contract deliberately does not decide.
Everything so far has assumed one initiator. That assumption has been doing quiet work: with one initiator, whoever is asking is never a question, and nothing can be taken away mid-transaction.
What happens when a second initiator wants the same target — and what breaks if ownership can change while a transaction is still in flight?
Chapter 2.6 — Shared Resources adds a DMA engine beside the CPU, shows why wiring two initiators to one target is not a wiring problem, and builds enough arbitration to expose the ownership rules that matter. The full path is on the Wishbone curriculum index.
Continue learning
Related tutorials
- Related topic
The Master Interface
A Wishbone master owns the address, the direction, the write data, the byte selects and both qualifiers, and must hold them until a termination arrives. What it is deliberately not allowed to know matters as much as what it drives — a master that knows the address map or a slave's latency has been coupled to one system.
- Related topic
FPGA Design Challenges
Six peripherals and two masters on one FPGA is where on-chip integration stops being theory. The decode that must be exhaustive and one-hot, the read multiplexer that grows with every target and carries the critical path, the latency and reset conventions that refuse to agree, and the point at which the fabric rather than the peripherals starts failing timing.
- Related topic
Address Space
An address is a number until a decoder turns it into a target selection and a local offset. Range comparison and mask comparison are different engineering choices with different costs; exhaustive, mutually exclusive decode is a property to be proved rather than assumed; and a flat decoder's critical path is what eventually forces a hierarchy.
- Related topic
Control Signals
Address and data are payload; they say what values are involved and nothing about what should happen to them. Control information is what makes a bus interpretable: a qualifier that says a request is real, a direction, lane enables, a completion and an error — each derived from a failure that occurs without it.
Standards & specifications
- Governing standard
- Wishbone SoC Interconnection Architecture (OpenCores)(opens OpenCores in a new tab)
Defines the Wishbone signal set, the bus cycles built from it and the interface rules a portable IP core must follow. It deliberately leaves interconnect topology, address map and arbitration policy to the integrator, so those are system decisions rather than requirements of the specification.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Wishbone curriculum.
