Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • How to Read the Diagrams
  • Part 0: What Is a Protocol?10
    • A Protocol Is a Contract
    • Roles and Agents
    • Signals, Meaning and Timing
    • Reading Timing Diagrams
    • Protocols as State Machines
    • Topologies and Duplex
    • Layering and the End-to-End Argument
    • Memory, Message and Stream Semantics
    • Protocols Compared: What Is a Protocol?
    • Drills: What Is a Protocol?
  • Part 1: Timing and Clocking11
  • Part 2: Encoding and Framing12
  • Part 3: Handshakes13
  • Part 4: Flow Control17
  • Part 5: Transactions19
  • Part 6: Concurrency and Performance17
  • Part 7: Ordering13
  • Part 8: Sharing and Arbitration12
  • Part 9: Deadlock and Liveness13
  • Part 10: Reliability and Recovery20
  • Part 11: Bring-Up and Link Management10
  • Part 12: Power8
  • Part 13: Shared State and Coherence9
  • Part 14: Security and Isolation7
  • Part 15: Specifying and Verifying11
  • Part 16: Protocols Dissected17
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 0: What Is a Protocol?
  1. Topics
  2. First Principles of Hardware Protocols
  3. Part 0: What Is a Protocol?
  4. Protocols as State Machines

Part 0 · What Is a Protocol? · Working (W)

Protocols as State Machines

Each side of an interface is a state machine; the protocol is the two running together. Exploring every combination finds bugs that no single waveform shows.

On this page

    Note

    Level: Working (W) · Answers: What is a protocol, really?

    Before this: A Protocol Is a Contract, Reading Timing Diagrams

    A request/acknowledge interface between two blocks passes thousands of simulation runs. In the lab, once every few hours, a word goes missing. The waveform captured around the failure looks almost normal: a request rises and falls, the next request rises, and the receiver never captured the first one. No single test had produced that ordering of events, so no test had seen it.

    How can you find the one ordering of events that breaks a protocol, without waiting for the lab to find it?

    The obvious answer, and why it breaks

    The obvious answer is more simulation: more seeds, longer runs, more random delays. Simulation explores the orderings your stimulus happens to create. Two independent sides, each with a few states and variable timing, produce many interleavings, and the dangerous ones are often rare, because they need one side to be unusually fast at exactly the moment the other is unusually slow. Random testing finds them eventually; the lab finds them sooner, at a much higher price.

    The idea: a protocol is two state machines running together

    Each side of an interface is a state machine: it is in one state, and the signals it sees move it to the next. The protocol is not either machine alone. It is the pair running side by side, called the product machine, whose state is (requester state, completer state, the signals between them). Every rule in the contract is an invariant on that product state: a condition that must hold in every state the pair can ever reach.

    The product has a useful property: it is finite. For a small protocol you can list every reachable state by trying every move each side could make from every state, which is what a model checker does. If no reachable state breaks an invariant, the protocol is correct for every ordering of events, not only the ones a test produced.

    The figure shows the two machines of a four-phase request/acknowledge protocol. The requester raises req with the data held stable, waits for ack, drops req, and waits for ack to fall before the next word. The completer captures the data when it sees req, raises ack, and drops ack when req falls.

    Top row: the requester (R). Bottom row: the completer (C). The protocol is the two running together; its state is the pair plus the req and ack wires.

    The product, state by state

    Ignoring the word counters, each side has a handful of control states, so the product has only six combinations. Walking through them shows which can actually occur and what each means:

    Requester

    Completer

    Reachable?

    Meaning

    IDLE

    WAIT

    Yes

    Idle; both wires low

    REQ

    WAIT

    Yes

    Request raised, not yet seen; data must be stable

    REQ

    ACKED

    Yes

    Data captured; requester has not yet seen ack

    WAIT LOW

    ACKED

    Yes

    Requester dropped req; completer has not yet seen it

    WAIT LOW

    WAIT

    Yes

    Completer dropped ack; requester has not yet seen it

    IDLE

    ACKED

    No

    Would mean the requester started over while ack is still high

    The last row is the point of the exercise. The requester leaves WAIT LOW only after seeing ack low, so it can never be idle while the completer still holds ack high. That one unreachable state is what makes the protocol safe: a new request can never be mistaken for the old one.

    Build it: explore every interleaving

    The model below is a tiny model checker. From each state it tries every move either side could make, one at a time, in any order, and records every new state it reaches. It checks three promises: the completer captures only while the requester holds the data; no word is ever lost; and the pair never stops before all words are delivered. Then it repeats the search with a buggy requester that drops req without waiting for ack.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    """Explore every interleaving of a requester and a completer joined by a
    four-phase request/acknowledge handshake, and check three promises:
      1. the completer captures data only while the requester is holding it,
      2. no word is ever lost (every word sent is captured exactly once),
      3. the pair never deadlocks before all words are delivered.
    Then repeat with a requester that drops req without waiting for ack.
    """
    from collections import deque
    
    N = 2  # words to send
    
    
    def requester_moves(st, correct):
        r, c, req, ack, sent, cap = st
        if r == "IDLE" and sent < N:
            yield ("R raises req", ("REQ", c, 1, ack, sent + 1, cap))
        if r == "REQ" and (ack == 1 or not correct):
            yield ("R drops req", ("WAITLOW" if correct else "IDLE", c, 0, ack, sent, cap))
        if r == "WAITLOW" and ack == 0:
            yield ("R sees ack low", ("IDLE", c, req, ack, sent, cap))
    
    
    def completer_moves(st):
        r, c, req, ack, sent, cap = st
        if c == "WAIT" and req == 1:
            yield ("C captures, raises ack", (r, "ACKED", req, 1, sent, cap + 1))
        if c == "ACKED" and req == 0:
            yield ("C drops ack", (r, "WAIT", req, 0, sent, cap))
    
    
    def explore(correct):
        start = ("IDLE", "WAIT", 0, 0, 0, 0)
        parent = {start: None}
        queue = deque([start])
        violations = []
        while queue:
            st = queue.popleft()
            moves = list(requester_moves(st, correct)) + list(completer_moves(st))
            for label, nxt in moves:
                if label.startswith("C captures") and st[0] != "REQ":
                    violations.append(("captured data the requester was not holding", st))
                if nxt not in parent:
                    parent[nxt] = (st, label)
                    queue.append(nxt)
            if not moves:  # nobody can move: the run has ended here
                r, c, req, ack, sent, cap = st
                if cap != sent:
                    violations.append((f"lost word: sent {sent}, captured {cap}", st))
                elif sent != N:
                    violations.append(("deadlock before all words were sent", st))
        return len(parent), violations, parent
    
    
    def trace(parent, st):
        steps = []
        while parent[st] is not None:
            st, label = parent[st]
            steps.append(label)
        return list(reversed(steps))
    
    
    n_ok, bad_ok, _ = explore(correct=True)
    print(f"correct requester: {n_ok} reachable states, {len(bad_ok)} violations")
    n_bug, bad_bug, parent = explore(correct=False)
    print(f"buggy requester:   {n_bug} reachable states, {len(bad_bug)} violations")
    what, st = bad_bug[0]
    print(f"first violation: {what}")
    print("  shortest path: " + " -> ".join(trace(parent, st)))
    assert not bad_ok and bad_bug
    print("PASS: the four-phase rules keep every promise; dropping req early loses a word")

    It prints:

    Diagram
    correct requester: 11 reachable states, 0 violations
    buggy requester:   15 reachable states, 2 violations
    first violation: lost word: sent 2, captured 0
      shortest path: R raises req -> R drops req -> R raises req -> R drops req
    PASS: the four-phase rules keep every promise; dropping req early loses a word

    The correct protocol has 11 reachable states (control states plus word counts) and no violations: every ordering of events, however unlikely, keeps every promise. The buggy requester reaches 15 states, two of which break a promise, and the search hands you the shortest path to the failure: the requester raises and drops req twice before the completer moves at all, so both words are sent and none is captured. That is the lab failure from the opening, found in milliseconds.

    The design space: how to check a protocol

    Method

    What it explores

    When it wins

    Directed tests

    The cases you thought of

    Bring-up and documenting intended behaviour

    Constrained-random simulation

    Many orderings, weighted by your stimulus

    Large designs, real data paths

    Exhaustive exploration of a protocol model (as above)

    Every ordering of an abstract model

    Small protocols and the control part of larger ones; finding races

    Formal property checking on RTL

    Every ordering of the real design, within limits

    Interfaces and control logic where a missed race is expensive (Part 15)

    How real protocols use state machines

    Protocol

    Where its specification uses state machines

    APB

    A transfer moves through idle, setup and access phases; the access phase repeats until PREADY is high.

    PCI Express

    Link training and link power states are defined as a state machine (the LTSSM, Part 11); the data link layer's replay mechanism is defined by counters and timers in each direction (lesson 10.11).

    I2C

    Each transfer is a sequence of conditions on the wires: START, address, acknowledge, data bytes each with an acknowledge, and STOP.

    What breaks

    The buggy requester in the model is not contrived (illustrative story). A designer optimised the requester to drop req one cycle after raising it, reasoning that the completer always responded within one cycle in the current system. The completer was later moved behind a synchroniser into a slower clock domain, where it sometimes needed several cycles to see req. Short requests then vanished entirely, exactly as in the model's counterexample.

    The fix was to restore the rule "hold req until ack is seen". The deeper lesson: the optimisation was correct only for one timing of the other side. A protocol rule exists precisely so that each side stays correct for every timing of the other. Exploring the product machine makes that visible, because it does not assume any timing at all.

    Why this matters

    Senior lens. Treat every handshake you design as a product machine and ask two questions: which combinations must be unreachable for the protocol to be safe, and what in each side's rules makes them unreachable? If you cannot name the rule that excludes a dangerous state, you are relying on timing, and timing changes when blocks move to new clocks, new processes or new systems. For anything with more than a few states, write the invariants as assertions and let a formal tool explore the real RTL (Part 15).

    Interview

    Asked: "How would you show that two communicating state machines can never both be waiting for each other?"

    Weak answer: Run a lot of random simulation and check it never hangs.

    Strong answer: Model the pair as one product machine: state = (state of A, state of B, the signals between them). Enumerate every reachable state by trying every move each side can make, in any order, and check there is no reachable state, other than the intended end, where neither can move. For small protocols that's a quick exhaustive search; for real RTL I'd write the condition as an assertion and use a formal tool. Simulation only covers the orderings it happens to produce.

    Follow-ups: (1) What makes the product machine finite? (2) Which state in a four-phase handshake must be unreachable, and why?

    Check yourself Why is the product state (requester IDLE, completer ACKED) unreachable in the correct protocol?

    The requester returns to IDLE only from WAIT LOW, and only after seeing ack low. While the completer is in ACKED, ack is high, so the requester cannot be idle at the same time.

    Check yourself The buggy requester works perfectly when the completer always reacts within one cycle. Why is it still a bug?

    It is correct only for one timing of the other side. The contract must hold for every timing; when the completer slows down (another clock domain, a busy pipeline), requests vanish. Exhaustive exploration assumes no timing, so it exposes the dependency.

    Check yourself What makes exhaustive exploration practical for this protocol but not for a whole SoC?

    This protocol has a few control states per side and a small bound on words, so the product has only tens of states. A whole SoC has astronomically many states, so you explore abstract models of its protocols or apply formal tools to bounded parts.

    Key takeaways

    • Each side of an interface is a state machine; the protocol is the product of the two, including the wires between them.

    • Every rule of the contract is an invariant on the product; safety comes from dangerous states being unreachable.

    • Exploring every interleaving finds races that random simulation reaches only by luck; a rule that holds only for one timing of the other side is a bug.

    Keep going

    Next: Lesson 0.6, Topologies and Duplex. Point-to-point, shared bus or switch; one direction or both; one wire or many: the shape of the channel before any signal is defined.

    Related topics

    • Part 0 overview: What Is a Protocol?

    • CDC & RDC: Handshake Protocols Across Domains

    Previous

    9 / 234

    Next: Topologies and Duplex

    Up next

    Up nextTopologies and Duplex

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • A Protocol Is a Contract
    • Roles and Agents
    • Signals, Meaning and Timing