Skip to content

AMBA CHI · Module 7 · CHI Transaction Model

Transaction Lifecycle

You have traced all four transaction kinds as flows of packets; this chapter lifts to the transaction as an object with a lifetime. Every outstanding transaction is owned by a tracker slot, and passes through four phases: dispatch allocates the slot and sends the request; in-flight covers the wait while snoops, data, and grants are outstanding; response is when the completion arrives; and retirement frees the slot and its transaction ID. The tracker pool size is how many transactions can be in flight at once — a flow-control limit: issue more than the pool holds and a completion arrives with no slot to match it. Learn the four phases and why the pool bounds concurrency. Representative model, not the specification.

Intermediate16 min readAMBA CHITransaction LifecycleTrackerOutstandingFlow Control

Module 7 · Chapter 7.5 · CHI Transaction Model

Project thread — 7.1–7.4 traced read, write, atomic, and snoop as packet flows. This chapter treats any transaction as an object with a lifetime, owned by a tracker. 7.6 opens the tracker's own state machine.

1. Learning Outcomes

By the end of this chapter you should be able to:

  • Describe a transaction as an object with a lifetime, owned by a tracker entry.
  • Name the four phases — dispatch, in-flight, response, retirement.
  • Explain why the tracker pool size is the limit on outstanding transactions.
  • State how retry (RetryAck / credit) fits when the home cannot accept a transaction yet.
  • Diagnose why over-issuing past the tracker pool drops completions.
  • Implement a representative tracker-slot lifecycle in SystemVerilog, Verilog-2001, and VHDL.

2. Why Should I Learn This?

A real system has many transactions in flight at once — dozens of reads and writes outstanding, overlapping, completing out of order. What keeps them from being confused for one another is that each is an object with an owner: a tracker entry that holds its TxnID, its address, its state, and its progress. Every requester and every home has a finite pool of these entries, and that pool governs how much work can be in flight.

This is the level at which throughput and correctness meet. Too few trackers and the pipe stalls; mismanage them and a returning completion matches the wrong transaction or none at all. Understanding the lifecycle — when a slot is allocated, when it is truly free — is what lets you reason about concurrency without losing track of a single transaction. It is the bookkeeping the whole model rests on.

3. Key Terms

4. Previous Chapter Connection

Chapters 7.1–7.4 each showed a transaction as a sequence of packets — request, snoop, data, completion, acknowledgement. Each of those chapters quietly mentioned a tracker: the read allocated one at the request and freed it at CompAck; the write held one until its data was sent; the snoop fed the transaction it nested inside.

This chapter makes the tracker the subject. Whatever the transaction does, its lifetime is the same shape: allocate, run, complete, retire. And because trackers are a finite resource, that lifetime is also a story about flow control — how many transactions a node can have in flight, and what happens when it wants more than it has slots for. The packets were the actions; the lifecycle is the accounting.

5. Core Concept — a transaction is an object with a lifetime

Every outstanding transaction is owned by a tracker entry that lives from the moment the request is issued to the moment the transaction retires. Its life has four phases.

  • Dispatch. The requester allocates a tracker slot, assigns it a TxnID, records the address and opcode, and sends the REQ. If no slot is free, it cannot dispatch — the pool is the gate.
  • In-flight. The transaction is outstanding. The home may be snooping holders, fetching memory, or granting a DBID; responses trickle back, each correlated to this slot by (SrcID, TxnID) (Chapter 6.6). The slot holds the transaction's state while it waits.
  • Response. The completion arrives — CompData for a read, Comp/CompDBIDResp for a write, the result for an atomic. The slot records that the transaction is answered, but it is not yet free.
  • Retirement. The closing CompAck is sent (Chapter 7.1); now the slot and its TxnID are freed and returned to the pool for the next transaction. Only here does the lifetime end.

The synthesis:

A transaction is not just a flow of packets — it is an object with a lifetime, owned by a tracker slot from dispatch to retirement. The slot is allocated with a TxnID, holds the transaction's state while it is in-flight, records its response, and is freed at retirement. Because slots are finite, the pool size is the number of transactions a node can have outstanding — the lifecycle and flow control are the same story.

6. Engineering Mental Model — a service ticket at a help desk

A support desk with a fixed number of ticket slots on its board.

  • A new issue comes in: the desk opens a ticket (dispatch), stamps it with a number, and starts work. If every slot on the board is full, the next issue must wait — there is no room to track it.
  • While the work proceeds — waiting on another team, gathering data — the ticket sits open on the board (in-flight), holding everything known about the issue.
  • A resolution comes back (response), and the desk notes it on the ticket.
  • Only when the customer confirms the fix does the desk close the ticket (retirement) and clear the slot for a new one.

The board has a fixed size on purpose: it is how many issues the desk can genuinely keep track of. Open more tickets than slots — scribble a new issue over an open one — and a resolution comes back for an issue no ticket remembers.

7. Engineering Diagram — the lifecycle state machine

A transaction lifecycle state machine with four states. Free, the start state, means the tracker slot is available. From Free, allocating a slot and sending the request moves to Dispatch. From Dispatch, being accepted moves to In-flight; a RetryAck keeps it in Dispatch until a credit lets it re-send. From In-flight, the completion and data arriving move to Respond. From Respond, sending CompAck retires the transaction and returns to Free, freeing the slot and its transaction ID.FreeDispatchIn-flightRespondallocate, send REQallocate, send REQacceptedacceptedComp + DataComp + DataCompAck: retireCompAck: retireRetryAck: await creditRetryAck: await creditRetryAck:await…
Figure 1 — the lifecycle of one tracker slot. From Free it Dispatches (allocate a TxnID, send the REQ), moves In-flight once accepted, reaches Respond when the completion arrives, and returns to Free at retirement when CompAck is sent. If the home cannot accept the request, a RetryAck holds the transaction in Dispatch until a credit lets it re-send. The slot is occupied — its TxnID reserved — for the entire loop.

Read it as a loop: a slot leaves Free only to run one transaction and returns only when that transaction retires. The self-loop is retry — the transaction is held, not lost, until a credit lets it proceed.

8. The Four Phases

Each phase, what owns the transaction, and what frees it to the next.

PhaseWhat happensSlot state
Dispatchallocate slot + TxnID, send REQnewly occupied
In-flightsnoops / data / DBID outstanding, correlated by (SrcID, TxnID)occupied, waiting
Responsecompletion arrives (CompData / Comp / result)occupied, answered
RetirementCompAck closes it; slot + TxnID freedreturns to free

The rule to carry: the slot is occupied for the whole lifetime — from dispatch through retirement — and the TxnID is reserved that entire time (Chapter 7.1). The transaction is "done" only at retirement, not at response. A slot that looks free before retirement is a bug waiting to happen.

9. Retry and Credits — when the home cannot accept yet

A dispatched request is not guaranteed to be accepted — the home has finite resources too.

  • RetryAck. If the home has no tracker slot for the incoming request, it returns a RetryAck instead of accepting. The transaction stays in the requester's tracker, in the dispatch phase — held, not failed.
  • Credit (PCrdGrant). When the home frees a slot, it issues a protocol credit (PCrdGrant). The requester, holding a credit, re-sends the request, which is now accepted.
  • Why it exists. Retry is back-pressure without dropping: the requester never loses the transaction, and the home never overflows. Both sides bound their outstanding work by their tracker pools.

The point to carry:

The tracker pool is a hard limit on concurrency, enforced at both ends. A requester cannot dispatch without a free slot of its own; a home that is full says RetryAck and grants a credit later. This is why the lifecycle is inseparable from flow control: a transaction's very existence consumes a slot, so the number of slots is the number of transactions that can live at once. Retry is how the system stays within that limit without ever losing a transaction.

10. Transaction Walkthrough — one read through its life

RN0 has a 4-entry tracker pool; three are busy. It issues a read.

  1. Dispatch. One slot is free — RN0 allocates it, assigns TxnID 12, records the address, and sends ReadShared. The pool is now full (4/4).
  2. Accepted or retried. If the home has room, the request is accepted → in-flight. If not, the home returns RetryAck; TxnID 12 stays in its slot, waiting for a PCrdGrant to re-send.
  3. In-flight. The home snoops a holder and fetches data; RN0's slot holds the transaction's state, matching returning packets by (SrcID 0, TxnID 12).
  4. Response. CompData arrives. The slot marks the transaction answered — but the slot is still occupied.
  5. Retirement. RN0 sends CompAck; slot 12 is freed, TxnID 12 returns to the pool, and the pool drops to 3/4 — room for the next dispatch.

The slot was owned for the whole journey. Note step 1's gate: RN0 could dispatch only because a slot was free. Had all four been busy, it would have had to wait — the subject of the DebugLab.

11. RTL / Hardware View — a tracker-slot lifecycle

One tracker slot is a small state machine: Free until dispatched, occupied through the response, freed at retirement. Crucially, busy marks the whole lifetime, and a busy slot cannot be re-allocated. Representative and sequential.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative single tracker-slot lifecycle (educational).
// FREE -> DISPATCH (allocate + send REQ) -> INFLIGHT (accepted) -> RESPOND
// (completion in) -> retire on CompAck back to FREE. A RetryAck holds DISPATCH.
// busy marks the ENTIRE lifetime: the slot (and its TxnID) is reserved until retire.
module chi_txn_slot (
  input  logic clk, rst_n,
  input  logic dispatch,     // request to allocate this slot and send REQ
  input  logic accepted,     // home accepted (not RetryAck)
  input  logic retry,        // home returned RetryAck
  input  logic comp,         // completion arrived
  input  logic ack,          // CompAck sent -> retire
  output logic busy,         // slot occupied (TxnID reserved) for the whole life
  output logic can_alloc     // slot is free to take a new transaction
);
  typedef enum logic [1:0] { FREE, DISPATCH, INFLIGHT, RESPOND } state_t;
  state_t state, next;
 
  always_comb begin
    next = state;
    case (state)
      FREE:     if (dispatch) next = DISPATCH;
      DISPATCH: if (accepted) next = INFLIGHT;   // retry: stay in DISPATCH
                else if (retry) next = DISPATCH;
      INFLIGHT: if (comp)     next = RESPOND;
      RESPOND:  if (ack)      next = FREE;        // retire only on CompAck
    endcase
  end
 
  assign busy      = (state != FREE);
  assign can_alloc = (state == FREE);            // dispatch is gated on this
 
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) state <= FREE;
    else        state <= next;
endmodule

The same behavior in Verilog-2001:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Representative single tracker-slot lifecycle (Verilog-2001).
module chi_txn_slot (
  input clk, rst_n, dispatch, accepted, retry, comp, ack,
  output busy, can_alloc
);
  localparam FREE = 2'd0, DISPATCH = 2'd1, INFLIGHT = 2'd2, RESPOND = 2'd3;
  reg [1:0] state, next;
 
  always @* begin
    next = state;
    case (state)
      FREE:     if (dispatch) next = DISPATCH;
      DISPATCH: if (accepted) next = INFLIGHT;
                else if (retry) next = DISPATCH;
      INFLIGHT: if (comp)     next = RESPOND;
      RESPOND:  if (ack)      next = FREE;
      default:                next = FREE;
    endcase
  end
 
  assign busy      = (state != FREE);
  assign can_alloc = (state == FREE);
 
  always @(posedge clk or negedge rst_n)
    if (!rst_n) state <= FREE;
    else        state <= next;
endmodule

And in VHDL:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Representative single tracker-slot lifecycle (VHDL).
library ieee;
use ieee.std_logic_1164.all;
 
entity chi_txn_slot is
  port (
    clk, rst_n : in  std_logic;
    dispatch   : in  std_logic;
    accepted   : in  std_logic;
    retry      : in  std_logic;
    comp       : in  std_logic;
    ack        : in  std_logic;
    busy       : out std_logic;
    can_alloc  : out std_logic
  );
end entity;
 
architecture rtl of chi_txn_slot is
  type state_t is (FREE, DISPATCH, INFLIGHT, RESPOND);
  signal state : state_t := FREE;
begin
  process (clk, rst_n)
  begin
    if rst_n = '0' then
      state <= FREE;
    elsif rising_edge(clk) then
      case state is
        when FREE     => if dispatch = '1' then state <= DISPATCH; end if;
        when DISPATCH => if accepted = '1' then state <= INFLIGHT;
                         elsif retry = '1' then state <= DISPATCH; end if;
        when INFLIGHT => if comp = '1'     then state <= RESPOND;  end if;
        when RESPOND  => if ack = '1'      then state <= FREE;     end if;
      end case;
    end if;
  end process;
 
  busy      <= '0' when state = FREE else '1';
  can_alloc <= '1' when state = FREE else '0';
end architecture;

All three keep busy asserted from dispatch to retirement and expose can_alloc only in FREE — so a slot is never re-allocated while it owns a live transaction. The DebugLab shows what happens when dispatch ignores that gate.

12. Verification View — a slot is busy for its whole lifetime

The properties that keep the lifecycle sound: a slot cannot be reallocated while busy, and it retires only on CompAck.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind to chi_txn_slot.
// 1. can_alloc (slot free) and busy are mutually exclusive — a busy slot is
//    never offered for a new transaction.
property p_no_alloc_while_busy;
  @(posedge clk) disable iff (!rst_n)
    busy |-> !can_alloc;
endproperty
 
// 2. The slot returns to free only via RESPOND on an ack — never skips retirement.
property p_retire_needs_ack;
  @(posedge clk) disable iff (!rst_n)
    ($fell(busy)) |-> $past(ack);
endproperty

The system point, beyond the checks:

The tracker pool turns concurrency into a countable resource. Each live transaction holds exactly one slot for its whole lifetime, so at any instant the number of busy slots is the number of outstanding transactions — and that number cannot exceed the pool. This is what makes the design bounded: buffers, correlation tables, and retry credits can all be sized to the pool, because nothing can be in flight without a slot. Free a slot early — before retirement — and you break the count: a transaction still live in the fabric no longer has a slot to receive its response.

  • What it proves: a busy slot is never re-allocated, and retirement requires an ack.
  • What it does not prove: the pool is sized correctly for the workload — that is a system parameter.
  • Bug signature: a dispatch that proceeds with no free slot — a completion returning to an overwritten or absent tracker.

13. Testbench — a slot through its life, and no early free

Runs one transaction through dispatch, in-flight, response, and retirement, checking busy holds throughout and can_alloc returns only after retirement.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_chi_txn_slot;
  logic clk = 0, rst_n = 0;
  logic dispatch = 0, accepted = 0, retry = 0, comp = 0, ack = 0;
  logic busy, can_alloc;
  int errors = 0;
 
  chi_txn_slot dut (.*);
  always #5 clk = ~clk;
 
  initial begin
    @(posedge clk) rst_n = 1;
    if (!can_alloc) begin errors++; $display("FAIL slot not free at reset"); end
    else $display("PASS slot free at reset");
 
    // Dispatch.
    @(posedge clk) dispatch = 1;
    @(posedge clk) dispatch = 0;
    if (!busy || can_alloc) begin errors++; $display("FAIL not busy after dispatch"); end
    else $display("PASS busy, not allocatable after dispatch");
 
    // Accept -> in-flight.
    @(posedge clk) accepted = 1;
    @(posedge clk) accepted = 0;
 
    // Completion -> respond (still busy, not yet retired).
    @(posedge clk) comp = 1;
    @(posedge clk) comp = 0;
    if (!busy) begin errors++; $display("FAIL freed at response, before CompAck"); end
    else $display("PASS still busy at response (not retired)");
 
    // Retire on ack.
    @(posedge clk) ack = 1;
    @(posedge clk) ack = 0;
    if (busy || !can_alloc) begin errors++; $display("FAIL not retired after CompAck"); end
    else $display("PASS retired and allocatable after CompAck");
 
    if (errors == 0) $display("ALL TESTS PASSED");
    else             $display("%0d FAILURE(S)", errors);
    $finish;
  end
endmodule

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PASS slot free at reset
PASS busy, not allocatable after dispatch
PASS still busy at response (not retired)
PASS retired and allocatable after CompAck
ALL TESTS PASSED

14. DebugLab — over-issuing past the tracker pool

1

Over-issuing past the tracker pool

DISPATCH NOT GATED ON A FREE SLOT -> COMPLETION LOST
Symptom

Under bursts of requests, occasional transactions never complete — a read's data never lands, or a completion is applied to the wrong read. It only happens when many transactions are outstanding; light traffic is flawless.

Evidence

A slot is reused before its first transaction retired:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pool size = 4, all 4 busy
new request arrives -> dispatch WITHOUT a free slot
  slot 2 (TxnID 2, still in-flight) reused for the new txn, TxnID 2 rewritten
later: completion for the ORIGINAL TxnID 2 returns
  slot 2 now holds the NEW transaction's state
  -> completion matched to the new txn (wrong) or dropped (no match)

Two transactions shared slot 2 and TxnID 2 while both were live — the older one's completion had nowhere valid to go.

First Divergence

Dispatch was gated on having work, not on having a free slot. From that point the requester could issue more outstanding transactions than its pool could track, forcing a live slot to be overwritten.

Root Cause

The tracker pool size is the hard limit on outstanding transactions, because each live transaction owns a slot for its whole lifetime (from dispatch to retirement). Dispatching without a free slot violates that invariant: two transactions share one slot and TxnID while both are in flight, and correlation by (SrcID, TxnID) collapses — exactly the aliasing the lifecycle exists to prevent, now caused by over-subscription rather than early retirement.

Fix

Gate every dispatch on can_alloc — a genuinely free slot — and treat the pool size as the ceiling on outstanding transactions. When there is work but no slot, wait; when the home is full, use the RetryAck / credit mechanism rather than forcing the request. The slot's busy covers the whole lifetime, so a slot frees only at retirement — never issue against a slot that has not retired.

15. Common Mistakes

  • Dispatching without a free slot. Assumption: issue whenever there is work. Bug: completion lost (the DebugLab). Prevention: gate dispatch on a free tracker entry.
  • Treating response as retirement. Assumption: completion means done. Bug: early free / aliasing (Chapter 7.1). Prevention: retire at CompAck.
  • Sizing the pool blindly. Assumption: any pool size works. Bug: throughput stall or wasted area. Prevention: size the pool to the target outstanding count and latency.
  • Ignoring RetryAck. Assumption: a request is always accepted. Bug: dropped request when the home is full. Prevention: hold the transaction and re-send on a credit.
  • Losing state across retry. Assumption: retry re-creates the request. Bug: mismatched re-send. Prevention: the slot keeps the transaction's state while it waits.
  • Sharing a TxnID across live transactions. Assumption: TxnIDs are plentiful. Bug: correlation collapse. Prevention: one TxnID per live slot, freed only at retirement.

16. Engineering Checklist

  • Model every transaction as a slot owned from dispatch to retirement.
  • Gate dispatch on a free tracker entry — the pool is the outstanding limit.
  • Keep the slot busy (TxnID reserved) through the whole lifetime.
  • Retire — free the slot and TxnID — only at CompAck.
  • Handle RetryAck: hold the transaction, re-send on a credit.
  • Size the tracker pool to the target concurrency and latency.

17. Key Takeaways

  • A transaction is an object with a lifetime, owned by a tracker slot from dispatch to retirement.
  • Its four phases are dispatch → in-flight → response → retirement.
  • The tracker pool size is the number of outstanding transactions allowed — the lifecycle is flow control.
  • Retry (RetryAck / credit) holds a transaction when the home is full, without ever losing it.
  • The slot is occupied for the whole lifetime; it frees only at retirement, not at response.
  • Gate dispatch on a free slot, retire on CompAck, size the pool deliberately; the model here is representative.

18. Quick Revision

Transaction lifecycle. Every outstanding transaction is an object owned by a tracker slot, alive through four phases: dispatch (allocate a slot + TxnID, send the REQ), in-flight (snoops, data, and grants outstanding, correlated by (SrcID, TxnID)), response (the completion arrives), and retirement (CompAck frees the slot and its TxnID). The slot is occupied for the whole lifetime — the transaction is done only at retirement, not at response. Because slots are finite, the tracker pool size is the hard limit on outstanding transactions: dispatch must be gated on a free slot, and when the home is full it returns RetryAck and later a credit (PCrdGrant) so the requester re-sends without losing the transaction. Over-issue past the pool and a live slot is overwritten, so an older transaction's completion is dropped or mismatched. Gate dispatch on a free slot, retire only at CompAck, size the pool to your concurrency. Representative model; 7.6 opens the tracker's own RN/HN state machine.

Coming Next

Chapter 7.6 — The Transaction State Machine. This chapter treated the lifecycle as one clean loop; the real trackers are richer. Chapter 7.6 opens the requester-side and home-side state machines that own an in-flight transaction — the concrete states each keeps, how they advance on requests, snoops, data, and acknowledgements, and how the two sides stay consistent. It turns the four-phase lifecycle into the actual FSMs an RN and an HN implement to track every transaction from dispatch to retirement.