Two teams build the two sides of an interface from the same two-page document. Each side passes its own tests. On the first integrated run the system hangs within a microsecond. The sending team assumed the receiver could always accept a word; the receiving team assumed the sender would wait until asked. Both read the document carefully, and both were right about what it said. It never said who waits for whom.
What exactly must a protocol pin down so that two blocks, built separately, work the first time they meet?
The first answer most people give is a port list: signal names, widths and directions. It is necessary, but it promises almost nothing. Take the smallest possible interface: a valid signal and an 8-bit data bus from a sender to a receiver. The port list does not tell you:
whether data is meaningful in the same cycle valid is high, or one cycle later;
whether valid may stay high for two cycles to send the same word twice, or two different words;
whether the receiver can refuse a word, and how the sender would find out;
what valid is while the sender is in reset.
Two careful engineers can answer each question differently, and both designs will match the port list exactly. The failure only appears when the two sides meet.
A protocol is a contract between two parties that cannot see inside each other. Each side can observe only the wires. So everything one side relies on must be something the other side promises, and every promise must be checkable on the wires: if you cannot tell from the signals whether a promise was kept, it is not a promise, it is a hope.
Think of two companies that ship goods to each other without ever visiting each other's factories. The delivery contract says what arrives, in what packaging, at which dock, and what happens when a delivery is refused. Neither cares how the other runs its factory, and the contract must not depend on it.
Most promises fall into four kinds:
Kind of promise | What it says | Example |
|---|---|---|
Meaning | What each value stands for |
|
Timing | When a value counts, and how long it must stay | data is captured on the rising edge where |
Sequence | What may follow what | a new word may not replace one that has not been taken |
Exceptional cases | Reset, errors, power-down | during reset the sender drives |
Here is a complete contract for the valid/data interface above, extended with a ready signal so the receiver can refuse. Read each row as "who promises what, and how would you check it on a waveform".
# | Promise | Made by | How to check it on the wires |
|---|---|---|---|
1 |
| Sender | Ignore |
2 | A word moves on a rising edge where | Both | Count edges with both high; that is the number of words |
3 | Once | Sender | If |
4 |
| Sender | Not checkable on one waveform; checked by design review or formal proof (lesson 3.4) |
5 | While | Sender |
|
6 | The receiver eventually raises | Receiver | A liveness promise: checked with a bound ("within 100 cycles") or by a formal tool |
Notice what the contract does not say: how deep the receiver's buffer is, how many cycles it takes to process a word, how the sender generates data. Those are left open on purpose, so either side can be redesigned without touching the other. A good contract is as small as it can be while still letting the two sides work together.
Promises like row 3 translate directly into assertions, the executable form of a contract. Part 15 builds a full set; here is the shape of one:
// snippet: promise 3 as a SystemVerilog assertion
assert property (@(posedge clk) disable iff (!rst_n)
valid && !ready |=> valid && $stable(data));The assertion reads almost like the promise: if valid is high and ready is low at an edge, then at the next edge valid is still high and data has not changed.
Decision | Promise it | Leave it open |
|---|---|---|
Latency | When the other side must plan around it (fixed-latency memories) | Usually: let each implementation take as long as it needs, and use a handshake |
Buffering | Only when the other side's correctness depends on it (credits, Part 4) | Otherwise: it is an internal choice |
Order of results | Always state it, even if the answer is "no order" (Part 7) | Never leave it implicit |
Reset and error behaviour | Always | Never: it is where most integration bugs live |
Optional features | As explicit, negotiable options (Part 11) | Silently optional features become incompatibilities |
Protocol | A promise it makes on the wires |
|---|---|
APB | A transfer has a one-cycle setup phase and then an access phase that lasts until the completer drives PREADY high; address, control and write data stay constant through the access phase. |
AXI | A source must not wait for READY before asserting VALID, and once VALID is high it stays high, with the payload stable, until the transfer happens. |
I2C | Data on SDA is stable while SCL is high; a change of SDA while SCL is high is not data but a START or a STOP condition. |
Each of these is a promise about meaning and timing that you can check by looking at a waveform. None of them says anything about how the blocks are built inside.
Back to the opening hang (illustrative). The document described valid, data and ready, but not reset. The sender came out of reset one cycle later than the receiver and, during that cycle, its valid output was not yet driven to a known value. In simulation it was X; in the receiver's model, X was treated as "maybe", and the receiver's state machine took a branch that waited forever for a word that never existed. Each team's tests started both sides out of reset at the same time, so neither saw it.
The fix was one row in the contract, promise 5 above, plus an assertion for it. The lesson is general: integration bugs live in the cases the contract forgot, most often reset, errors, back-pressure and the first and last transfer.
Only if the sender genuinely needs it, for example a sender with no back-pressure input that must know the receiver keeps up. Otherwise it constrains every future receiver for no benefit. The usual better choice is a handshake (ready), which lets the receiver take as long as it needs.
(a) can: look at every stalled cycle. (b) cannot on a finite trace, because "eventually" has no deadline; you check it with a bound ("within N cycles") in simulation or with a formal tool.
Reset is the case contracts most often forget, and the two sides of an interface rarely leave reset at exactly the same time. Whatever a side drives in the gap is unspecified unless the contract says so.
A protocol is a contract: promises about meaning, timing, sequence and exceptional cases that two hidden implementations rely on.
Every promise must be checkable on the wires; if you cannot see it on a waveform or prove it, it is not a promise.
Good contracts state reset and error behaviour explicitly and leave internal choices, such as latency and buffering, open.