PCIe · Module 3
Transaction Layer — Expressing the Operation
What the Transaction Layer owns: turning a local function's requests into transactions the fabric can carry, interpreting arriving transactions for local consumers, and why operation meaning is deliberately separated from delivery and transport.
Modules 1 and 2 built the structure: why PCIe is shaped this way, and what it is made of. Both stayed at the level of components and connections, and both leaned on a word neither examined. Every chapter that needed to describe what crosses a Link said "a transaction" and moved on.
Module 3 opens that word. PCIe divides the work of moving a transaction into three layers with distinct responsibilities, and this chapter takes the topmost:
What does the Transaction Layer own, and what information must it create and consume so PCIe components can perform operations?
1. Why an Operation Needs a Representation
Start with the problem, because the layer is otherwise easy to mistake for bureaucracy.
Some logic inside a device wants something to happen somewhere else. A processor wants to read a register in an Endpoint. A storage controller wants to write a block it has assembled into host memory. In both cases there is an intent that exists locally and a target that is elsewhere in the hierarchy.
Chapter 1.8 established why that intent cannot simply be asserted on wires. On the shared bus of Chapter 1.3, every device observed the same address and control lines, so an operation could be expressed as coordinated activity on a medium everybody could see. Dedicated Links destroyed that universality: a component on one Link cannot observe another, so intent must travel as something self-contained.
That is what a transaction is. It carries enough information to be understood at its destination without the destination having watched it being created — what operation is wanted, where it is directed, how much data is involved, and enough context for any response to find its way back.
The Transaction Layer is the logic that performs that conversion in both directions:
- Outbound — take what the local function wants and produce a transaction the fabric can carry.
- Inbound — take an arriving transaction and turn it into something the local destination can act on.
2. What This Layer Owns — and What It Does Not
Being explicit about the boundary is more useful than any definition, because most confusion about the Transaction Layer is really confusion about where it stops.
It owns operation semantics. What kind of operation this is. Where it is directed. How much data is involved. Whether a response is expected. Which local consumer an arriving transaction belongs to. Whether an arriving transaction makes sense for this device at all.
It does not own delivery. Whether the packet crossed a Link intact, whether it needs sending again, and what bookkeeping that requires belong to the Data Link Layer. The Transaction Layer hands a packet down and does not track its journey across the wire.
It does not own transport. How information is put onto the connection and recovered at the far end belongs to the Physical Layer.
That separation is a deliberate design decision with a practical payoff. Because delivery is somebody else's problem, the Transaction Layer can be written as though the packet simply arrives. Because operation meaning is somebody else's problem, the layers below can move packets without understanding them — which is precisely why a Switch can forward a transaction it has no interest in interpreting.
3. Requesters, Completers, and Responses
Two roles describe how a transaction relates to its counterpart, and they should feel familiar — Chapter 1.3 introduced the same idea for PCI, and PCIe carried the model forward.
A requester originates a transaction. A completer is the component that ultimately acts on it.
Roles are per transaction, not per device. An Endpoint is a requester when it writes to host memory and a completer when the host reads its registers. Nothing about a component fixes it in one role.
Some operations need something back. A read is the obvious case: the requester wants data, so the completer must return it, and the returned information has to find its way back to the requester and be matched with the request that asked for it. Other operations do not require a response of that kind — a write can, in principle, be considered handed off once it has been accepted for delivery.
That distinction has significant consequences for how much state a requester must hold, how long resources stay committed, and how failures surface. Module 10 develops it properly, along with the precise terminology PCIe uses. What matters here is the architectural fact: the Transaction Layer is where the need for a response is decided and where a returning response is matched to what asked for it.
4. Following One Operation Through
Take a concrete case and carry it through the layer. This same operation is used as a thread in the two chapters that follow, so the responsibility boundaries become visible by comparison.
An Endpoint has assembled data and needs it written into host memory.
Inside the Endpoint, some engine — a data mover, an accumulator, whatever the device does — has a buffer and a destination address. That is intent, expressed in whatever internal form the device's designers chose. It is not yet a transaction.
The Transaction Layer takes it and determines what the operation actually is: that it is a write, where in the system address map it is directed, how much data is involved, and whether anything needs to come back. It assembles a transaction carrying that information together with the payload, and hands it down.
Everything after that is not this layer's concern. The packet crosses a Link — perhaps several, if a Switch is in the path — and lower layers deal with whatever that involves.
At the destination, the process runs in reverse. An arriving transaction is examined: what operation is this, is it directed here, is it something this component can act on? If so, it is delivered to the local logic that should handle it — memory, in this case.
Now run it the other way. The host wants to read an Endpoint's status register. The Root Complex's Transaction Layer produces a read transaction directed at that address. It travels the hierarchy. The Endpoint's Transaction Layer receives it, recognises it as a read directed at its own resources, and routes it to the logic that owns that register. That logic supplies the value, and the Transaction Layer produces a response carrying the data back toward the requester — which must then match it against the read that is still outstanding.
Notice what appeared only in the second case: outstanding state. The requester of a read has something pending until the response arrives. That is Transaction Layer state, it is finite, and it is one of the more common sources of real bugs.
5. An Illustrative Microarchitecture
Protocol responsibility does not dictate hardware structure, but responsibility does suggest what work exists. The following is one reasonable decomposition — useful for reasoning about where things happen, not a structure PCIe requires.
Three observations are worth drawing out, because they generalise well beyond PCIe.
Classification happens early. Deciding what an operation is, before assembling anything, keeps the assembly stage simple and gives one obvious place to reject malformed requests.
Metadata and payload are separable. The description of an operation and the data it carries have different sizes, different timing, and often different storage. Treating them as one blob tends to produce awkward hardware.
Outstanding tracking spans both directions. This is the structural reason the receive path cannot be designed in isolation from the transmit path: a response arriving must be reconciled with a request issued, and that shared state is where a surprising number of real bugs live.
6. Representing a Request in RTL
Now make one part of this concrete. The interface between a device's internal logic and its Transaction Layer is a place a real RTL engineer works, and it illustrates the layer's responsibility better than more prose would.
// Conceptual SystemVerilog model — internal request metadata handed from a
// device's own logic to its Transaction Layer. NOT the PCIe TLP header format.
// Names and widths are implementation-defined teaching choices.
typedef struct packed {
logic is_write; // write (no response expected) vs read (response expected)
logic [63:0] addr; // destination in the system address map
logic [12:0] byte_count; // payload size in bytes for this request
logic [7:0] local_tag; // requester-local handle used to match a response
} request_meta_t;The local_tag deserves comment because it is the field most often misunderstood. It is how this device recognises a returning response as belonging to a particular outstanding read. It is a local bookkeeping handle. PCIe defines its own normative mechanism for associating responses with requests, and Module 10 covers it — the point here is only that some such association must exist and that the requester has to keep state until it resolves.
Now the interface that carries it. The engineering content is in the handshake rules, not the struct:
// Illustrative synthesizable RTL — a Transaction Layer request intake stage.
// NOT a complete PCIe controller; it models one pipeline boundary only.
module tl_request_intake #(
parameter int unsigned DEPTH = 4
) (
input logic clk,
input logic rst_n,
// From local device logic
input logic req_valid,
output logic req_ready,
input request_meta_t req_meta,
// Toward assembly / lower layers
output logic out_valid,
input logic out_ready,
output request_meta_t out_meta
);
// A small skid/staging buffer. Real designs size this from latency and
// throughput targets; DEPTH here is arbitrary and exists to show the shape.
request_meta_t mem [DEPTH];
logic [$clog2(DEPTH):0] count;
logic [$clog2(DEPTH)-1:0] wr_ptr, rd_ptr;
wire accept = req_valid && req_ready; // a request is taken this cycle
wire emit = out_valid && out_ready; // a request leaves this cycle
assign req_ready = (count != DEPTH[$clog2(DEPTH):0]);
assign out_valid = (count != '0);
assign out_meta = mem[rd_ptr];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= '0;
wr_ptr <= '0;
rd_ptr <= '0;
end else begin
if (accept) begin
mem[wr_ptr] <= req_meta;
wr_ptr <= wr_ptr + 1'b1;
end
if (emit) begin
rd_ptr <= rd_ptr + 1'b1;
end
// Net occupancy change — accept and emit in the same cycle cancel out.
case ({accept, emit})
2'b10: count <= count + 1'b1;
2'b01: count <= count - 1'b1;
default: count <= count;
endcase
end
end
endmoduleWhat this models: the boundary where a device's internal logic hands a request to the Transaction Layer, with backpressure in both directions.
What is deliberately simplified: no payload path (metadata only), no classification, no error checking, no ordering rules between different operation types, and a trivially-sized buffer. A real transmit path carries data alongside metadata and has to respect ordering constraints this module knows nothing about.
What to notice: a request is transferred only when req_valid && req_ready are both high. That single rule is what makes the interface composable — and it is the rule most often violated in a first implementation, usually by a source that drops req_valid after one cycle because it assumed the sink was listening.
7. What the Interface Must Guarantee
Two properties make this boundary trustworthy. Both are worth asserting, and both catch real bugs.
// SVA over the illustrative intake module above.
// Assumption: req_meta is driven by a source obeying the same handshake
// convention; these check the convention rather than any PCIe requirement.
// P1 — request metadata must not change while an offer is pending.
// If a source raises req_valid and the sink is not ready, the source must
// hold the request stable until it is accepted.
property p_req_meta_stable;
@(posedge clk) disable iff (!rst_n)
(req_valid && !req_ready) |=> $stable(req_meta);
endproperty
a_req_meta_stable : assert property (p_req_meta_stable);
// P2 — an offer must not be withdrawn. Once req_valid is asserted it stays
// asserted until the transfer completes.
property p_req_no_withdraw;
@(posedge clk) disable iff (!rst_n)
(req_valid && !req_ready) |=> req_valid;
endproperty
a_req_no_withdraw : assert property (p_req_no_withdraw);Why P1 matters. If metadata can change while an offer is pending, the sink may latch a different request from the one the source believes it sent. That produces a transaction with, say, the address of one request and the length of another — a corruption that is invisible at every layer below, because the packet is perfectly well-formed and crosses every Link intact. It will be delivered faithfully to the wrong place. This is the canonical example of a Transaction Layer bug that looks like a fabric problem.
Why P2 matters. A withdrawn offer leaves the sink's view and the source's view of "how many requests were sent" permanently out of step. That mismatch usually surfaces much later, as a count that does not reconcile or an outstanding entry that never clears.
Neither property is a PCIe requirement — they are properties of the handshake convention this illustrative interface uses. That distinction matters: an assertion suite is only as meaningful as the specification it is checking against, and mislabelling internal conventions as protocol requirements produces false confidence.
8. Verifying Transaction Layer Responsibility
Because this layer owns meaning, its verification is about whether the right operation was expressed and delivered to the right place — not whether bits survived.
What to assert
- Handshake integrity at every boundary: no transfer without
valid && ready, stability under stall, no withdrawal. - No accepted request is lost. Everything accepted at the intake eventually leaves toward the layer below — under an explicit assumption that the downstream eventually accepts, since otherwise the property is unprovable and a liveness failure is indistinguishable from ordinary backpressure.
- No request is emitted twice. A duplicate is a classic consequence of mishandling a stall.
- Payload boundaries are preserved: the data associated with a request stays associated with it and does not bleed into the next one.
What to score-board
Independently of the design, build a model of what should have been emitted for each accepted request, and compare. The scoreboard's job is to catch semantic corruption — right operation type, right destination, right size, right payload — which no handshake assertion can see. It should also verify that responses are matched to the correct outstanding request, since a mismatched pairing delivers correct-looking data to the wrong consumer.
What to generate
- Backpressure everywhere, including sustained stalls. Most metadata-stability bugs only appear when something is held.
- Requests that are legal but awkward: minimum and maximum sizes, addresses at boundaries, back-to-back requests with no gap.
- Malformed internal requests, to confirm they are rejected cleanly at classification rather than assembled into a nonsensical transaction.
- Enough concurrent outstanding reads to exhaust whatever tracking resource exists, then confirm the design applies backpressure instead of overwriting state.
That last case deserves emphasis. Outstanding-tracking exhaustion is a genuinely common bug and is easy to miss, because a testbench that issues one request at a time will never reach it and will report clean.
9. Debugging: Localising to This Layer
The most valuable diagnostic skill here is recognising which failures belong to this layer at all. Six signatures, and what each points at:
A request is accepted locally but never appears at the lower layer. Something between intake and hand-off consumed it. Check occupancy counters and the valid/ready handshake at each internal boundary — the transaction never got far enough for delivery or transport to be involved.
A transaction is emitted but with the wrong operation type. Classification, not transport. The packet will cross the fabric perfectly and do the wrong thing on arrival.
An arriving transaction reaches the wrong local consumer. Receive-side classification or destination routing. The transaction was fine on the wire.
Payload and metadata do not correspond. Almost always a stall-handling bug on one of the two paths, where one advanced and the other did not. Look for a stability violation.
A stall produces a duplicated transaction. The transmit side re-offered something already accepted — usually a pointer or counter updated on the wrong condition.
A transaction disappears at a layer boundary. The most instructive case. Determine which side of the boundary it was last seen on. If the Transaction Layer asserted valid and the layer below never asserted ready, that is a downstream flow-control question, not a lost packet. If it was accepted and then vanished, the loss is below.
10. Common Misconceptions
11. Understanding Check
12. Summary
The Transaction Layer expresses PCIe operations as transactions. Outbound, it converts what a local function wants into a self-contained packet carrying the operation's type, destination, size, and the context needed for any response to return. Inbound, it validates and classifies arriving transactions and delivers them to the local consumer that should act on them.
It exists because dedicated Links removed the universal visibility a shared bus provided: intent that cannot be observed by its target must travel as something self-describing.
Its boundaries are as important as its function. It owns operation semantics and deliberately does not own delivery or transport. That separation lets a Switch forward transactions without interpreting them, and lets each layer be reasoned about — and debugged — independently.
Requester and completer are per-transaction roles. Operations requiring a response make the requester hold outstanding state until it resolves, which is finite and is a common source of real bugs.
For implementation, the recurring engineering concerns are classify early, keep metadata and payload separable, and recognise that outstanding tracking couples the transmit and receive paths. At every internal interface, the handshake rules — transfer only on valid && ready, stability under stall, no withdrawal — are what make the layer composable, and violations of them produce well-formed packets that do the wrong thing.
Hold the model: this layer decides what operation is being performed; it relies on the layers below to move the result.
13. What Comes Next
The Transaction Layer hands a packet down and assumes it arrives. Chapter 3.2 — Data Link Layer examines what that assumption costs and who pays it: why PCIe dedicates a layer to getting a packet across one Link reliably, why that responsibility is hop-local rather than end-to-end, and what bookkeeping it requires.
Chapter 3.3 then takes the transport itself. Chapters 3.4 and 3.5 close the module by making the responsibility boundaries rigorous and tracing a packet's complete journey down and back up the stack.
Revisit Switched Architecture for the structure these layers operate within. Browse the full path on the PCIe tutorials index.