Skip to content

PCIe · Module 15

DLLP Types — The Packets That Are Not Transactions

Every mechanism in Module 14 exchanged control information with a neighbour. DLLPs are what carried it: Link-local packets that terminate at the port they reach, never route, and never enter the Transaction Layer — and a receive path must keep them out of it structurally.

Module 14 built one Link's reliability and named the packets that carry its control information exactly once, in passing: acknowledgement and retry are exchanged using DLLPs. Then it moved on, because the mechanisms were the subject.

They are not the only such packets, and they are not the only Link-local mechanism.

What is a Data Link Layer Packet, how is it different from a TLP, and what categories of Link-local control information does PCIe exchange using DLLPs?

1. The Verified Taxonomy

2. A DLLP Is Not a Small TLP

The most common misreading, and it is not a matter of size.

TLPDLLP
LayerTransactionData Link
Carriesan operation — Memory, Config, Completion, MessageLink control
Scopeend to end, across the whole pathone Link
Routed?yes — by address, ID, or implicitly (Chapter 11.5)never
Consumed bythe destination Function's enginesthe directly attached port
Has a Requester ID / Tag?yesno — those are transaction concepts
Reaches the Transaction Layer?yes, that is the pointno, ever

3. A DLLP Is Never Forwarded

Chapter 3.2 §2 established that reliability is per-Link. This is that fact seen from the packet side.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Endpoint A ──Link A── Switch Port X │ Switch │ Switch Port Y ──Link B── Root Complex

An acknowledgement DLLP arriving on Link A ends at Switch Port X. It is consumed there, by that port's reliability engine, and it retires that port's retained state for Link A.

If the Switch needs a corresponding action on Link B, it generates Link B's own control — from Link B's own state, at Link B's own timing, with Link B's own sequence identities (Chapter 14.5 §2).

4. The Receive Path

Packets arriving from the physical layer are separated into transaction layer packets and data link layer packets. Transaction layer packets pass upward through the data link layer's integrity and sequence checks into the transaction layer's engines. Data link layer packets are classified into acknowledgement and retry, flow control, and power management categories, each dispatched to a local control engine, and none reach the transaction layer. An unsupported class goes to an error path.From the PhysicalLayerboth packet kindsarrive hereTLP / DLLPseparationtwo kinds, two pathsTLP pathintegrity, sequence,then upwardTransaction LayerenginesMemory · Config ·CompletionDLLP classifiernormalized class,decided onceReliabilityengineACK / NAK — Module 14Flow-controlenginecredit accounting —15.2Power-managementengineLink power state —15.512
Figure 1 — one Link boundary, two packet kinds, two dispatch domains. TLPs go up to the Transaction Layer's engines; DLLPs go sideways to Link-local control engines and stop there. The two paths share the Physical Layer and nothing above it.

The figure's argument is the missing edge. There is no path from the DLLP classifier to the Transaction Layer engines — not a filtered one, not a disabled one, none. §6 is why that absence is the design rather than an omission.

5. RTL — DLLP Classifier and Dispatch

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Classify a decoded DLLP into a normalized category and
// deliver it to exactly one local control engine.
// The category set: NORMATIVE (section 1). The enum encoding, the port
// names, and the raw-type abstraction: ILLUSTRATIVE normalized metadata —
// NOT PCIe wire Type values.
package dllp_pkg;
 
  typedef enum logic [2:0] {
    DLLP_ACK         = 3'd0,
    DLLP_NAK         = 3'd1,
    DLLP_FC_INIT1    = 3'd2,
    DLLP_FC_INIT2    = 3'd3,
    DLLP_FC_UPDATE   = 3'd4,
    DLLP_PM          = 3'd5,
    DLLP_VENDOR      = 3'd6,
    DLLP_UNSUPPORTED = 3'd7    // not represented by this model
  } dllp_class_e;
 
  // Which local engine consumes each class. Separate from the class itself,
  // because "what kind of packet" and "who handles it" are different
  // questions — Chapter 11.7 section 7's separation, at this layer.
  typedef enum logic [1:0] {
    ENG_RELIABILITY = 2'd0,   // Module 14
    ENG_FLOWCTRL    = 2'd1,   // Chapter 15.2, Module 16
    ENG_POWER       = 2'd2,   // Chapter 15.5
    ENG_ERROR       = 2'd3
  } dllp_engine_e;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import dllp_pkg::*;
 
module dllp_dispatch #(
  // Opaque decoded payload. The classifier does not interpret it; each
  // engine does, according to its own chapter.
  parameter int PAYLOAD_W = 32
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- Decoded DLLP in --------------------------------------------------
  input  logic                  in_valid,
  output logic                  in_ready,
  input  dllp_class_e           in_class,      // from the framing decoder
  input  logic [PAYLOAD_W-1:0]  in_payload,
 
  // ---- Local control engines -------------------------------------------
  output logic                  rel_valid,
  input  logic                  rel_ready,
  output logic                  fc_valid,
  input  logic                  fc_ready,
  output logic                  pm_valid,
  input  logic                  pm_ready,
  output logic                  err_valid,
  input  logic                  err_ready,
 
  output dllp_class_e           out_class,
  output logic [PAYLOAD_W-1:0]  out_payload,
  output dllp_engine_e          out_engine
);
 
  // ---- Class -> engine, decided ONCE -----------------------------------
  // A single enum decode. Mutual exclusion is structural: the four valids
  // below are decodes of one value, so no configuration can assert two.
  dllp_engine_e eng;
 
  always_comb begin
    unique case (in_class)
      DLLP_ACK, DLLP_NAK:                          eng = ENG_RELIABILITY;
      DLLP_FC_INIT1, DLLP_FC_INIT2, DLLP_FC_UPDATE: eng = ENG_FLOWCTRL;
      DLLP_PM:                                     eng = ENG_POWER;
      // Vendor-specific content is not interpreted by this model, and an
      // unrepresented class is reported. Neither is a protocol judgement —
      // "not represented by this model" is a statement about the design.
      default:                                     eng = ENG_ERROR;
    endcase
  end
 
  assign out_class   = in_class;
  assign out_payload = in_payload;
  assign out_engine  = eng;
 
  assign rel_valid = in_valid && (eng == ENG_RELIABILITY);
  assign fc_valid  = in_valid && (eng == ENG_FLOWCTRL);
  assign pm_valid  = in_valid && (eng == ENG_POWER);
  assign err_valid = in_valid && (eng == ENG_ERROR);
 
  // Backpressure comes from the SELECTED engine only. ANDing every ready
  // would stall a credit update behind a busy power-management engine that
  // has nothing to do with it.
  always_comb begin
    unique case (eng)
      ENG_RELIABILITY: in_ready = rel_ready;
      ENG_FLOWCTRL:    in_ready = fc_ready;
      ENG_POWER:       in_ready = pm_ready;
      default:         in_ready = err_ready;
    endcase
  end
 
  // NOTE WHAT IS ABSENT. There is no Transaction Layer output port on this
  // module. A DLLP cannot reach the Transaction Layer because there is no
  // wire by which it could — section 6. That is stronger than any filter.
 
endmodule

Classification: synthesizable (package: compile-time).

Architecture. One enum decode driving a one-hot valid. Mutual exclusion is structural — the four valids are decodes of a single value, so no state can assert two.

Contract. The framing decoder guarantees in_class is stable while in_valid is asserted. Each engine relies on receiving a DLLP only when its own valid is high, and on never seeing one another engine also received.

Failure — three. Independent per-engine comparisons rather than one enum decode allow two engines to accept the same packet if the selection ever becomes inconsistent. ANDing all the readies stalls every class behind the busiest engine. And adding a Transaction Layer output — the one the module deliberately does not have — reintroduces §6's failure with a wire.

Deliberately simplified: no framing, no CRC, no payload interpretation; the class arrives already decoded, because the encodings belong to later chapters.

6. Keeping DLLPs Out of the Transaction Layer

A design rule, and the reason it is structural rather than a check.

A DLLP must never enter Transaction Layer request routing.

The wrong thingWhat it produces
an acknowledgement reaching the Completion enginea packet with no correlation identity presented to correlation logic (Chapter 12.3 §5)
a credit update reaching the replay buffer as a TLPan entry allocated for a packet that is not a transaction (Chapter 14.4 §4)
a power-management packet reaching the memory enginean address decode on bits that are not an address

7. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over dllp_dispatch. These assert the LOCAL dispatch contract and the
// layer separation of section 6. They assert nothing about DLLP format
// (Chapters 15.3-15.5), nothing about the acknowledgement algorithm
// (Module 14), and nothing about credit arithmetic (Chapter 15.2).
 
// P1: EXACTLY ONE ENGINE. Neither zero nor two.
property p_exactly_one_engine;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> ($countones({rel_valid, fc_valid, pm_valid, err_valid}) == 1);
endproperty
a_one_hot : assert property (p_exactly_one_engine);
 
// P2: THE LAYER-SEPARATION PROPERTY. No DLLP ever produces a Transaction
// Layer event. Bound across the boundary, because that is the only place
// section 6's rule is expressible — and a design that wired the two
// together fails here rather than in a debugging session.
property p_no_dllp_reaches_tl;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> !(tl_mem_valid || tl_cfg_valid || tl_cpl_valid || tl_msg_valid);
endproperty
a_layer_separation : assert property (p_no_dllp_reaches_tl);
 
// P3: class-to-engine mapping is exactly section 1's taxonomy.
property p_mapping_correct;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> ((in_class inside {DLLP_ACK, DLLP_NAK})       ? rel_valid :
                (in_class inside {DLLP_FC_INIT1, DLLP_FC_INIT2,
                                  DLLP_FC_UPDATE})           ? fc_valid  :
                (in_class == DLLP_PM)                        ? pm_valid  :
                                                               err_valid);
endproperty
a_mapping : assert property (p_mapping_correct);
 
// P4: an unrepresented class reaches ONLY the error path.
property p_unsupported_isolated;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && (out_engine == ENG_ERROR))
    |-> (err_valid && !rel_valid && !fc_valid && !pm_valid);
endproperty
a_unsupported : assert property (p_unsupported_isolated);
 
// P5: dispatch and payload are stable while the selected engine stalls. A
// packet must not migrate between engines mid-offer.
property p_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && !in_ready)
    |=> (in_valid && $stable({rel_valid, fc_valid, pm_valid, err_valid,
                              out_class, out_payload}));
endproperty
a_stable : assert property (p_stable_under_stall);
 
// P6: backpressure comes from the SELECTED engine only — an unrelated busy
// engine never blocks.
property p_ready_from_selected_only;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> (in_ready == ((rel_valid && rel_ready) || (fc_valid && fc_ready)
                          || (pm_valid  && pm_ready)  || (err_valid && err_ready)));
endproperty
a_selected_ready : assert property (p_ready_from_selected_only);
 
// P7: one accepted DLLP produces exactly one local control event.
// (in_count / out_count are testbench counters.)
property p_one_event_per_dllp;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && in_ready) |-> (out_count + 1 <= in_count);
endproperty
a_conserved : assert property (p_one_event_per_dllp);

P2 is the property this chapter exists to make writable. §6's rule is normally taught and never checked — binding it across the layer boundary turns "a DLLP never enters the Transaction Layer" from a paragraph into something that fails a design. And note that with §5's structure it is vacuously true, which is the point: the property confirms the architecture rather than policing a filter.

P1 uses $countones == 1 rather than a mutual-exclusion check because zero is a failure too — a DLLP reaching no engine is dropped, and a dropped credit update stalls a Link permanently with nothing reporting it.

P6 exists because ANDing readies is the natural mistake. It looks conservative and produces a real coupling: an acknowledgement waiting behind a busy power-management engine delays retirement, which fills the replay buffer, which backpressures the Transaction Layer — a Link-wide stall caused by an unrelated engine.

8. Verification

Monitors observe: the decoded DLLP input, all four engine interfaces, and the Transaction Layer engine interfaces (for P2).

The scoreboard holds its own class-to-engine table, written from §1. It must not import dllp_pkg or call the design's decode.

Per category

  • Each class in §1's taxonomy: ACK, NAK, InitFC1, InitFC2, UpdateFC, PM, vendor-specific. Verify the engine, the one-hot (P1), and the mapping (P3).
  • All three Flow Control classes. Verify they reach the same engine — the class distinction matters to Chapter 15.2, not to dispatch.
  • An unrepresented class. Verify the error path only (P4), and that the report says not represented by this model, not malformed per PCIe.

Stress and ordering

  • Back-to-back DLLPs of different classes, every ordered pair.
  • Alternating ACK and UpdateFC — the realistic steady-state mix.
  • Each engine stalled in turn while a packet for it is offered. Verify stability (P5) and that other classes still flow (P6).
  • The error path stalled with an unrepresented class offered. Verify it does not block represented ones.
  • A long randomised run with independent per-engine backpressure, checking the conservation count (P7).
  • Reset mid-offer.

Layer separation

  • Drive every DLLP class while a TLP stream runs concurrently. Verify no Transaction Layer engine ever fires on a DLLP (P2).
  • Confirm structurally, not only by simulation: the dispatcher has no Transaction Layer port, so review is the check and P2 is the confirmation.

Which mutation which check kills

Injected mutationCaught by
ACK routed to the Completion engineP2, and P3
independent per-engine comparisons instead of one decodeP1, if the selection ever becomes inconsistent
all readies ANDedP6, and an unrelated stall in the mixed-class run
unrepresented class silently droppedP1 — zero engines selected
unrepresented class routed to a real engineP4
dispatch changes mid-offerP5
a Flow Control class routed to the reliability engineP3

9. Debugging

An ACK DLLP reaches the Transaction Layer's Completion engine

A layer-separation failure, and the first question is structural.

Does a wire exist from the DLLP path to that engine? If yes, that is the bug and no amount of decode fixing will make it safe (§6). If no, then the packet was classified as a TLP by the framing decoder — the fault is upstream of this chapter entirely, in whatever separates the two kinds.

What it looks like at the Completion engine. A packet with no correlation identity presented to correlation logic. A well-built engine reports an unexpected Completion (Chapter 13.2); a less careful one matches garbage against an outstanding entry and resolves the wrong read.

A Flow Control update is received but the transmit credits never change

Two boundaries, and the classifier is only the first.

Check fc_valid fired. If it did, dispatch worked and the fault is in the flow-control engine's decode of the update — which is Chapter 15.2's subject, and its most common failure is treating a cumulative value as a delta.

If it did not fire, check what out_engine said. A Flow Control class reaching the reliability engine is a mapping bug here (P3); reaching the error path means the class was not recognised at all.

A Link-local scope violation (§3), and it is invisible point to point.

The consequence is not subtle. The forwarded acknowledgement names a Link A sequence identity, which on Link B refers to a different packet chosen by coincidence (Chapter 14.5 §2). Link B's retained state is retired arbitrarily.

The observation: capture the same logical TLP on both Links and compare the acknowledgement traffic. Link B's acknowledgements must reference Link B's own sequence values. If they mirror Link A's, the Switch is relaying what it should be terminating.

An unrecognised DLLP wedges the receive path

The error path is not draining, or it does not exist.

Check err_ready. If the error path is stalled and in_ready is derived from it, an unrecognised DLLP blocks every subsequent one, including acknowledgements and credit updates — and the Link stops for a reason that has nothing to do with either.

This is why the error path is a real destination with a real handshake rather than a default that does nothing. A packet with nowhere to go stalls the port.

10. Common Misconceptions

  • "A DLLP is a small TLP." Different layer, different scope, and it carries none of a TLP's transaction concepts — no address, no Requester ID, no Tag (§2).
  • "DLLPs are routed end to end." They terminate at the port they reach. A Switch consumes one and generates its own (§3).
  • "ACK and NAK are Transaction Layer packets." They are Data Link control and never produce a Transaction Layer event (Chapter 14.2 §2, P2).
  • "A Flow Control Update carries application data." It carries credit information about a receive buffer (Chapter 15.2). No payload of any transaction is involved.
  • "A DLLP has a Tag identifying a Request." A Tag is Transaction Layer correlation (Chapter 11.3 §6). Nothing at this layer identifies a Request.
  • "Every DLLP eventually enters the Transaction Layer." None does — that is the layer boundary (§6).
  • "DLLP type and TLP type share one semantic namespace." They are separate taxonomies at separate layers, and merging them is what lets a control packet reach transaction machinery (§6).
  • "Flow Control DLLPs control replay-buffer occupancy." Replay storage is transmit-side reliability state (Chapter 14.4 §17). Credits describe the remote receive buffer. Different resources, different sides of the Link.
  • "An ACK DLLP means a Memory Read completed." It means a neighbour received a packet (Chapter 14.2 §4). A read completes when a Completion returns.
  • "Power Management DLLPs are software configuration transactions." They are Link-local control. Configuration accesses are TLPs and are routed by ID (Chapter 11.7 §4).

11. Understanding Check

12. What's Next

This chapter established the taxonomy and the boundary: four categories of Link-local control, none of them routed, none of them reaching the Transaction Layer — enforced by a receive structure with no path between the two domains rather than by a check somewhere downstream.

Chapter 15.2 — Flow Control Updates takes the category this chapter only named: what a credit update actually carries, why its values are cumulative counters rather than deltas, and the modular arithmetic needed to turn one into "how much capacity was just returned" — the same wrap problem Chapter 14.5 solved for sequence identities, on a different counter.

Chapters 15.3 and 15.4 then own the acknowledgement and retry packets' formats and timing, and 15.5 the power-management packets. Module 16 owns the credit system itself — the pools, the consumption, and the transmitter's eligibility decision.

The idea to carry forward: a DLLP is addressed to a neighbour, not to a destination — and the cleanest way to guarantee it never goes further is to build a path that cannot take it there.