Skip to content

PCIe · Module 6

Lane Aggregation — How Many Lanes Become One Link

Distribution, lane-local transport, relative arrival, and reassembly — the mechanism every width chapter deferred. Why positional pairing only works if lanes are aligned to begin with, and why lane-local health can never prove aggregate correctness.

Every chapter since x2 has made the same promise and declined to keep it. Information must be distributed across lanes and correctly reconstructed — and each chapter said "6.6 owns the mechanism" and moved on.

This is 6.6.

How can an x2, x4, x8, or x16 Link behave as one logical connection when the physical data moves across multiple independent lane paths?

The whole subject is the gap between two descriptions of one thing.

The logical view. One ordered stream of information moving between two components. Transactions have an order, that order means something, and nothing about it mentions lanes.

The physical view. N independent lanes, each a separate path with its own transmitter, receiver, route, and timing.

Aggregation is the layer that reconciles them. Everything above it works in the logical view and never sees a lane; everything below it works per lane and never sees the stream.

2. The Five Responsibilities

Distribution. At the transmitter, information from the logical stream is divided so each active lane carries a portion. The rule must be known to both ends.

Lane-local transport. Each lane carries its portion using the mechanisms of Chapter 6.1 — its own transmit and receive paths, independent of the others.

Relative arrival difference. The lanes need not deliver at the same instant. The receiver cannot assume simultaneity.

Reassembly. The receiver recombines the per-lane portions into the original logical stream, in the original order, with nothing lost, duplicated, or misplaced.

Link-level coordination. Throughout, the two components behave as one connection: one configuration, one status, one flow of traffic.

The lane aggregation path for a four-lane Link. A logical transmit stream feeds a distributor, which feeds lanes 0 through 3. Each lane carries its portion independently. The four lanes feed a reassembler, which produces the logical receive stream.Logical TXstreamone ordered streamDistributordivides by a knownruleLane 0independent pathLane 1independent pathLane 2independent pathLane 3independent pathReassemblerwaits for everyportionLogical RXstreamoriginal orderrestored12
Figure 1 — the aggregation path for a four-lane Link. One logical stream is divided by the distributor, carried independently on each lane, and recombined by the reassembler into the original stream. The lanes in the middle are genuinely independent paths; the two ends see only the stream.

3. Distribution — A Teaching Model

Take eight consecutive units of information from the logical stream:

A B C D E F G H

A straightforward way to spread them across four lanes is to deal them out in turn and wrap:

first roundsecond round
Lane 0AE
Lane 1BF
Lane 2CG
Lane 3DH

The receiver reverses it, taking one unit from each lane in the same order, and recovers A B C D E F G H.

The failure mode this makes concrete. If the transmitter deals A B C D to lanes 0–3 and the receiver reads them back as lanes 3–0, every unit arrives intact and uncorrupted — and the reconstructed stream is D C B A. Nothing reports an error. No lane is unhealthy. The data is simply wrong.

That is the signature of an aggregation defect, and it is why §9 exists.

4. Why Reassembly Needs State

Even when the transmitter distributes in a known order, the receiver cannot simply concatenate whatever is available, because the lanes need not present their portions at the same instant.

Contributors to that difference include routing length, package traversal, receive pipeline depth, and lane-local implementation variation. Their relative arrival difference is called skew.

Portions of one logical beat arriving on four lanes at different times. Lane 0 arrives first and is held. Lane 2 arrives next, giving two of four. Lane 1 arrives third, giving three of four. Lane 3 arrives last, at which point all four portions are present and the reconstructed beat is released.1Lane 0 portion arrivesheld; 1 of 4 present2Lane 2 portion arrivesheld; 2 of 4 present3Lane 1 portion arrivesheld; 3 of 4 present4Lane 3 portion arrives4 of 4 — beat reconstructed
Figure 2 — portions of one logical beat becoming available on four lanes at different times. The reassembler holds each as it arrives and releases the reconstructed beat only when every portion is present. The arrival order shown is arbitrary; what matters is that it need not be lane order.

Deskew means compensating for those relative arrival differences between lanes participating in one Link.

5. Microarchitecture

TX: a logical stream buffer → a distribution stage → per-lane holding → the lane interfaces.

RX: the lane interfaces → per-lane holding → an alignment and reassembly stage → a logical stream buffer.

Illustrative digital architecture — not PCIe-mandated implementation. Real designs pipeline, buffer, and place the boundary differently.

The structural facts that hold regardless:

  • Lane-local storage exists on both sides. The transmitter needs somewhere to hold a portion whose lane is not yet ready; the receiver needs somewhere to hold a portion whose partners have not arrived.
  • Fragment ownership must be unambiguous. Every portion belongs to exactly one logical beat, and that association must be established somewhere it cannot be lost.
  • Aggregate readiness is a reduction. The Link accepts work only when the lanes collectively can take it, which is a different question from any single lane being ready.
  • A stall anywhere becomes a stall everywhere. One lane unable to make progress stalls the aggregate stream, because the beat it holds cannot complete without it.

6. RTL — Distribution

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Generic N-lane distributor.
// The distribution RULE is a teaching placeholder. The COORDINATION behaviour
// around it is the point. NOT PCIe striping logic.
module lane_distributor #(
  parameter int LANES  = 4,
  parameter int LANE_W = 32
) (
  input  logic                    clk,
  input  logic                    rst_n,
 
  // Aggregate ingress: one logical beat spans all lanes.
  input  logic                    in_valid,
  output logic                    in_ready,
  input  logic [LANES*LANE_W-1:0] in_data,
  input  logic                    in_last,
 
  // Per-lane egress.
  output logic [LANES-1:0]        lane_valid,
  input  logic [LANES-1:0]        lane_ready,
  output logic [LANE_W-1:0]       lane_data [LANES],
  output logic [LANES-1:0]        lane_last
);
 
  initial begin
    if (LANES  < 2) $fatal(1, "aggregation requires at least two lanes");
    if (LANE_W < 1) $fatal(1, "LANE_W must be positive");
  end
 
  logic [LANE_W-1:0] d_q [LANES];
  logic [LANES-1:0]  v_q, last_q;
 
  // A lane slot can take a new portion if it is empty or is being emptied.
  logic [LANES-1:0] slot_free;
  always_comb
    for (int i = 0; i < LANES; i++)
      slot_free[i] = !v_q[i] || lane_ready[i];
 
  // ATOMIC ACCEPTANCE: the beat is taken only when EVERY lane slot can hold
  // its portion. in_ready depends on lane_ready (legal: ready may depend on
  // downstream ready) but never on in_valid, so there is no handshake loop.
  assign in_ready = &slot_free;
 
  wire accept = in_valid && in_ready;
 
  // Lane valids are REGISTERED and never observe their own ready.
  assign lane_valid = v_q;
  assign lane_last  = last_q;
  always_comb
    for (int i = 0; i < LANES; i++)
      lane_data[i] = d_q[i];
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_q    <= '0;
      last_q <= '0;
      for (int i = 0; i < LANES; i++) d_q[i] <= '0;
    end else begin
      // Lanes release INDEPENDENTLY. A portion already taken by lane 0 is
      // gone even while lane 3 still holds its partner. Association was fixed
      // at load time, so independent release cannot mix beats.
      for (int i = 0; i < LANES; i++)
        if (v_q[i] && lane_ready[i]) v_q[i] <= 1'b0;
 
      // ATOMIC LOAD: every portion of one beat is written in the SAME cycle,
      // so no partial beat can ever exist in the lane registers. The loads
      // below override the releases above; last assignment wins, which is the
      // back-to-back case.
      if (accept)
        for (int i = 0; i < LANES; i++) begin
          d_q[i]    <= in_data[i*LANE_W +: LANE_W];  // teaching rule
          last_q[i] <= in_last;                      // replicated, not split
          v_q[i]    <= 1'b1;
        end
    end
  end
 
endmodule

Classification: synthesizable.

What it models: the transmit half of aggregation — dividing one logical beat into per-lane portions with unambiguous ownership.

What it teaches: that acceptance must be atomic while release need not be. Taking a beat when only some lanes can hold their portion would require storing the remainder somewhere, and that somewhere is where portions get separated. Writing all portions in one cycle makes them associated by construction, which is why they can then be released independently without risk.

Deliberately simplified: one portion of holding per lane, so a single slow lane immediately stalls ingress; the distribution rule is arbitrary; there is no notion of a lane being inactive; and in_last is replicated to every lane because it describes the beat rather than any portion.

Production implication: a real transmit path needs per-lane skid buffering so a briefly stalled lane does not stall the aggregate, the specified distribution rule rather than a placeholder, handling for the active-width the Link actually negotiated, and defined behaviour when a lane becomes unusable mid-beat.

7. RTL — Reassembly

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Generic N-lane reassembler — the inverse of lane_distributor.
// Accepts each lane's portion INDEPENDENTLY, which is what absorbs relative
// arrival differences, then releases the beat once every portion is present.
module lane_reassembler #(
  parameter int LANES    = 4,
  parameter int LANE_W   = 32,
  parameter int SPREAD_W = 8      // width of the arrival-spread observability
) (
  input  logic                    clk,
  input  logic                    rst_n,
 
  input  logic [LANES-1:0]        lane_valid,
  output logic [LANES-1:0]        lane_ready,
  input  logic [LANE_W-1:0]       lane_data [LANES],
  input  logic [LANES-1:0]        lane_last,
 
  output logic                    out_valid,
  input  logic                    out_ready,
  output logic [LANES*LANE_W-1:0] out_data,
  output logic                    out_last,
 
  // Observability. Not required by PCIe; genuinely useful in practice.
  output logic [LANES-1:0]        lanes_present,  // partial-arrival visibility
  output logic [SPREAD_W-1:0]     max_spread,     // worst wait, in cycles
  output logic                    last_mismatch   // sticky: lanes disagreed
);
 
  initial begin
    if (LANES < 2) $fatal(1, "aggregation requires at least two lanes");
  end
 
  logic [LANE_W-1:0]   f_q [LANES];
  logic [LANES-1:0]    v_q, last_q;
  logic [SPREAD_W-1:0] wait_q, max_q;
  logic                mism_q;
 
  wire all_present = &v_q;
  wire beat_taken  = all_present && out_ready;
 
  // out_valid is derived only from registers. It never observes out_ready.
  assign out_valid     = all_present;
  assign out_last      = last_q[0];
  assign lanes_present = v_q;
  assign max_spread    = max_q;
  assign last_mismatch = mism_q;
 
  always_comb
    for (int i = 0; i < LANES; i++)
      out_data[i*LANE_W +: LANE_W] = f_q[i];
 
  // INDEPENDENT per-lane acceptance. THIS is the arrival-difference tolerance:
  // a lane may deliver many cycles before its partners. A lane that has run
  // ahead is then backpressured until the beat completes and is consumed.
  always_comb
    for (int i = 0; i < LANES; i++)
      lane_ready[i] = !v_q[i] || beat_taken;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_q <= '0; last_q <= '0; wait_q <= '0; max_q <= '0; mism_q <= 1'b0;
      for (int i = 0; i < LANES; i++) f_q[i] <= '0;
    end else begin
      // Consume the completed beat first; the loads below may refill in the
      // same cycle, giving back-to-back aggregate beats.
      if (beat_taken) begin
        v_q <= '0;
        if (wait_q > max_q) max_q <= wait_q;   // high-water mark
        wait_q <= '0;
      end else if (|v_q && !all_present) begin
        // Count cycles spent partially assembled. This measures INTER-LANE
        // spread only: it stops once every portion is present, so sink
        // backpressure is not counted as arrival difference.
        if (wait_q != {SPREAD_W{1'b1}}) wait_q <= wait_q + 1'b1;
      end
 
      for (int i = 0; i < LANES; i++)
        if (lane_valid[i] && lane_ready[i]) begin
          f_q[i]    <= lane_data[i];
          last_q[i] <= lane_last[i];
          v_q[i]    <= 1'b1;
        end
 
      // A complete beat whose portions disagree about end-of-item means the
      // lanes have lost alignment. Sticky: a later consistent beat must not
      // erase the evidence.
      if (all_present && (last_q != '0) && (last_q != {LANES{1'b1}}))
        mism_q <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

What it models: the receive half of aggregation — holding portions as they arrive and releasing a reconstructed beat once all are present.

What it teaches — four things:

  1. Independent per-lane acceptance is the arrival-difference tolerance. Each lane loads into its own slot without waiting for the others, so an arbitrary difference is absorbed up to the depth provided.
  2. Depth bounds what can be absorbed. One portion per lane means a lane running more than one portion ahead is backpressured. How much depth a real receiver needs is a physical-layer question this chapter does not answer.
  3. A beat is never released partially assembled. out_valid requires every portion. Releasing with portions missing would publish whatever the empty registers happened to hold.
  4. Misalignment is detectable and must be sticky. last_mismatch catches portions paired together that do not belong to one beat.

On max_spread. It records the largest number of cycles a beat spent partially assembled. It is not a measurement of physical skew — it is a digital observation of the consequence, in cycles, after everything the receive path does to the signal. It is useful for noticing that arrival differences have grown, and it is not a substitute for physical characterisation.

Deliberately simplified: one portion of depth per lane; last_mismatch reports but takes no recovery action; there is no alignment mechanism, only the assumption that alignment exists; and reset discards a partially assembled beat.

Production implication: depth sized to the arrival difference that must actually be tolerated, a defined recovery path when alignment is lost rather than a status bit, the alignment mechanism itself (Module 17.2 and 17.3), and per-lane error attribution.

8. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the two modules above. Implementation invariants for THESE
// designs — not PCIe protocol requirements.
 
// ── Distributor ──────────────────────────────────────────────────────────
 
// COHERENCE — P1: an accepted beat produces exactly one portion on every
// lane, in the same cycle. No partial beat can exist in the lane registers.
property p_atomic_portion_load;
  @(posedge clk) disable iff (!rst_n)
  accept |=> (v_q == '1);
endproperty
a_atomic_load : assert property (p_atomic_portion_load);
 
// COHERENCE — P2: no lane valid rises without an accepted aggregate beat.
// Catches a lane fabricating a portion its partners know nothing about.
property p_no_lone_portion;
  @(posedge clk) disable iff (!rst_n)
  ((v_q & ~$past(v_q)) != '0) |-> $past(accept);
endproperty
a_no_lone_portion : assert property (p_no_lone_portion);
 
// COHERENCE — P3: portions held together agree on beat metadata. `last`
// describes the beat, so it is replicated; disagreement means the held
// portions do not describe one beat.
property p_metadata_agrees;
  @(posedge clk) disable iff (!rst_n)
  (v_q == '1) |-> ((last_q == '0) || (last_q == '1));
endproperty
a_metadata_agrees : assert property (p_metadata_agrees);
 
// STABILITY — P4: a portion held for a stalled lane is stable. Each lane
// independently owes the handshake contract to its own sink.
generate for (genvar gi = 0; gi < LANES; gi++) begin : g_stable
  property p_lane_stable_under_stall;
    @(posedge clk) disable iff (!rst_n)
    (lane_valid[gi] && !lane_ready[gi])
      |=> (lane_valid[gi] && $stable(lane_data[gi]) && $stable(lane_last[gi]));
  endproperty
  a_lane_stable : assert property (p_lane_stable_under_stall);
end endgenerate
 
// ── Reassembler ──────────────────────────────────────────────────────────
 
// COHERENCE — P5: no beat is released before every portion is present. The
// single most valuable property here: releasing early publishes whatever the
// empty registers held, with no error anywhere and no lane unhealthy.
property p_no_partial_release;
  @(posedge clk) disable iff (!rst_n)
  out_valid |-> (v_q == '1);
endproperty
a_no_partial_release : assert property (p_no_partial_release);
 
// SAFETY — P6: a held portion is immutable until its beat is consumed.
// Catches a lane overwriting a waiting portion with the NEXT one, which
// silently mixes two logical beats into one reconstructed beat.
generate for (genvar gj = 0; gj < LANES; gj++) begin : g_immutable
  property p_held_portion_immutable;
    @(posedge clk) disable iff (!rst_n)
    (v_q[gj] && !beat_taken) |=> ($stable(f_q[gj]) && $stable(last_q[gj]));
  endproperty
  a_portion_immutable : assert property (p_held_portion_immutable);
end endgenerate
 
// STABILITY — P7: the reconstructed beat is stable while the sink stalls.
property p_beat_stable_under_stall;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> (out_valid && $stable(out_data) && $stable(out_last));
endproperty
a_beat_stable : assert property (p_beat_stable_under_stall);
 
// SAFETY — P8: a detected misalignment is sticky.
property p_mismatch_sticky;
  @(posedge clk) disable iff (!rst_n)
  last_mismatch |=> last_mismatch;
endproperty
a_mismatch_sticky : assert property (p_mismatch_sticky);
 
// SAFETY — P9: the arrival-spread high-water mark never decreases. A mark
// that can fall reports the most recent spread, not the worst one.
property p_max_spread_monotonic;
  @(posedge clk) disable iff (!rst_n)
  max_spread >= $past(max_spread);
endproperty
a_spread_monotonic : assert property (p_max_spread_monotonic);
 
// LIVENESS — P10: a completed beat is eventually consumed.
// ASSUMPTION, stated explicitly: this holds only if the sink eventually
// asserts out_ready. A permanently stalled sink is not a design bug, so the
// environment must constrain out_ready to be eventually asserted.
property p_complete_beat_progresses;
  @(posedge clk) disable iff (!rst_n)
  out_valid |-> s_eventually (out_valid && out_ready);
endproperty
a_beat_progresses : assert property (p_complete_beat_progresses);

P5 is the property this chapter exists for. A reassembler that releases with portions missing produces a beat in which part of the data is stale. Every lane delivered exactly what it was given; no lane reports an error; the per-lane telemetry from Chapter 6.5 is entirely clean. The only symptom is wrong data at the far end, and it is found in system integration if at all.

P6 is its transmit-side twin, catching the other way beats get mixed: a portion overwritten while waiting for its partners.

P1, P2, P5, and P6 together are what "one Link" means as a checkable claim. None of them has any analogue at x1, because at x1 there is no such thing as part of a beat.

9. Verification

The correlation ID

Reconstruction correctness cannot be checked by watching any single lane. It needs an independent model that knows which portion belongs to which logical beat — and the design deliberately does not carry that information, because it relies on positional alignment instead.

The environment supplies it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY. NOT synthesizable, NOT PCIe protocol state.
// The correlation ID exists ONLY in the testbench. It is not transmitted, has
// no hardware counterpart, and the design neither knows nor needs it. Its
// purpose is to let the checker say WHICH beat a portion belonged to.
class aggregation_scoreboard #(int LANES = 4, int LANE_W = 32);
 
  typedef struct {
    bit [LANE_W-1:0]        frag [LANES];   // expected portion, per lane
    bit [LANES*LANE_W-1:0]  beat;           // expected reconstruction
  } item_t;
 
  protected item_t       expect_by_id [int unsigned];
  protected int unsigned next_ingress_id = 0;
  protected int unsigned next_lane_id [LANES];   // per-lane cursor
  protected int unsigned next_egress_id  = 0;
  protected int unsigned errors = 0;
 
  // Called when the DUT accepts an aggregate beat at ingress.
  function void observe_ingress(bit [LANES*LANE_W-1:0] beat);
    item_t it;
    it.beat = beat;
    // INDEPENDENT model of the distribution rule. Deriving this by calling
    // the design's own slicing would make every check below vacuous — the
    // model would agree with the design about a distribution bug.
    for (int i = 0; i < LANES; i++)
      it.frag[i] = beat[i*LANE_W +: LANE_W];
    expect_by_id[next_ingress_id++] = it;
  endfunction
 
  // Called on each per-lane portion transfer.
  function void observe_lane(int lane, bit [LANE_W-1:0] data);
    int unsigned id = next_lane_id[lane];
    if (!expect_by_id.exists(id)) begin
      $error("lane %0d produced portion %0d with no matching ingress beat", lane, id);
      errors++; return;
    end
    if (data !== expect_by_id[id].frag[lane]) begin
      // Fires on a wrong rule, a swapped lane, or a portion from another beat.
      $error("lane %0d portion for beat %0d mismatched", lane, id);
      errors++;
    end
    next_lane_id[lane]++;
  endfunction
 
  // Called on each reconstructed aggregate beat at egress.
  function void observe_egress(bit [LANES*LANE_W-1:0] beat);
    int unsigned id = next_egress_id;
    if (!expect_by_id.exists(id)) begin
      $error("egress beat %0d with nothing outstanding", id);
      errors++; return;
    end
    if (beat !== expect_by_id[id].beat) begin
      $error("reconstruction mismatch for beat %0d", id);
      errors++;
    end
    next_egress_id++;
    // Retire once every lane and the egress have passed this id.
    begin
      int unsigned lo = next_egress_id;
      foreach (next_lane_id[i]) if (next_lane_id[i] < lo) lo = next_lane_id[i];
      for (int unsigned k = 0; k < lo; k++)
        if (expect_by_id.exists(k)) expect_by_id.delete(k);
    end
  endfunction
 
  // End of test: every lane and the egress must have consumed every beat.
  // This is the NO-LOSS check — a portion silently dropped shows up here and
  // nowhere else, because a dropped portion simply stalls rather than erring.
  function bit final_check();
    bit ok = (errors == 0);
    foreach (next_lane_id[i])
      if (next_lane_id[i] != next_ingress_id) begin
        $error("lane %0d saw %0d portions, expected %0d",
               i, next_lane_id[i], next_ingress_id);
        ok = 0;
      end
    if (next_egress_id != next_ingress_id) begin
      $error("egress produced %0d beats, expected %0d", next_egress_id, next_ingress_id);
      ok = 0;
    end
    return ok;
  endfunction
 
endclass

Classification: verification-only.

What it teaches — and this is the part worth carrying into any multi-lane environment:

  • Check at two levels. Per-lane portions and the reconstruction. End-to-end checking alone passes when distribution and reassembly are wrong in matching ways — the case that only fails against another vendor's silicon. Per-lane checking alone misses pairing and ordering errors.
  • The end-of-test check is not optional. A dropped portion produces no error at any interface; it simply stalls, and the test ends with fewer beats than it started. final_check is the only place that surfaces it.
  • The ID never enters the design. It is testbench bookkeeping that makes "which beat" expressible. Treating it as protocol state would be a category error.

Deliberately simplified: in-order per lane, one outstanding stream, and no modelling of intentionally inactive lanes.

Production implication: a real environment would additionally model the negotiated active width, handle streams that reconfigure, and check ordering against the transaction layer rather than against a beat counter.

The skew model

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY. A digital delay abstraction that reproduces the
// OBSERVABLE consequence of skew — portions of one beat becoming available at
// different times. It does NOT model PCIe electrical skew, its causes, or its
// limits. Its unit is simulation cycles, not physical time.
class lane_delay_profile #(int LANES = 4);
  int unsigned delay [LANES];
 
  // Give lanes DIFFERENT delays. A profile with equal delays exercises no
  // arrival difference at all and would pass against a reassembler with zero
  // tolerance — see the callout below.
  function new();
    foreach (delay[i]) delay[i] = 0;
  endfunction
 
  function void set(int lane, int unsigned cycles);
    delay[lane] = cycles;
  endfunction
 
  // Largest relative difference in the profile — the parameter that matters.
  function int unsigned spread();
    int unsigned lo = delay[0], hi = delay[0];
    foreach (delay[i]) begin
      if (delay[i] < lo) lo = delay[i];
      if (delay[i] > hi) hi = delay[i];
    end
    return hi - lo;
  endfunction
endclass

A representative profile: lane 0 at 0 cycles, lane 1 at 2, lane 2 at 1, lane 3 at 3 — spread 3. Those cycle counts do not correspond to any PCIe physical skew figure.

Scenarios

  • All lanes aligned. Baseline: continuous traffic, one reconstructed beat per offered beat, in order, at full rate.
  • One lane delayed. Spread of one, then several. Verify correct reconstruction and that max_spread reflects the delay.
  • Several lanes delayed differently. The representative profile above. Verify reconstruction is unaffected by which lane is latest.
  • Spread beyond the depth. Push past one portion of holding. Verify the design backpressures rather than mispairing — this is where a design with insufficient depth silently starts producing mixed beats.
  • A portion dropped. Suppress one lane's portion for one beat. Verify the reassembler stalls rather than releasing a partial beat (P5), and that final_check reports the loss.
  • A portion duplicated. Present an extra portion on one lane. Verify the scoreboard's per-lane comparison fires — the duplicate shifts that lane's cursor and every subsequent portion mismatches.
  • Portions from adjacent beats interleaved. Force a lane one portion ahead. This is the alignment failure §4 warned about: the design will not detect it, positional pairing will produce plausible-looking beats, and only the scoreboard catches it. Running this scenario is what demonstrates that aggregation assumes alignment rather than establishing it.
  • Backpressure at the output. Drop out_ready for varying durations including a single cycle. Verify stability (P7) and that lanes backpressure rather than overwriting (P6).
  • Reset with partial reassembly. Reset with some portions held. Verify a clean restart with no stale portion paired into the first post-reset beat.
  • Sustained full-rate traffic. Long run with both sides always ready. Verify no bubbles, no drift, and final_check clean.

Coverage should include: every lane index as the latest arriver; spread from zero to beyond the depth; in_last at each position; out_ready patterns including single-cycle assertion; reset at each lanes_present value; and the cross of spread against output backpressure.

10. Debugging

The reference scenario for this chapter: every lane passes individually, but the wide Link corrupts data.

This is the failure that no amount of lane-level work will find, and Module 6 has been building toward it since x2.

Why the individual tests eliminate so much. Exercising lane i alone tests its datapath, its physical path, and its PHY instances. Every lane passing means every lane is individually sound, and the entire lane-local hypothesis space from Chapter 6.3 and Chapter 6.5 is consumed.

Why they eliminate nothing about aggregation. None of those tests runs distribution, reassembly, pairing, or arrival-difference handling — that logic only executes when multiple lanes carry one stream. It is entirely untested by the passing experiments.

Lane-local health proves lane transport. It does not prove aggregate correctness.

Prime suspects, roughly ordered by how often they are the cause:

  • The reassembler releasing before every portion is present (P5). Part of the beat is stale; nothing errors.
  • A portion overwritten while waiting for its partners (P6). Two beats mixed into one.
  • Insufficient depth for the actual arrival difference. Correct at low spread, silently wrong beyond it — which makes it load- and condition-dependent and therefore intermittent.
  • Distribution and reassembly rules that do not match. Every unit arrives intact and the stream is assembled wrongly. Symmetrically-wrong rules survive end-to-end checking entirely and fail only against a correctly-implemented partner.
  • Lost alignment. Positional pairing across lanes that are offset. Produces plausible data indefinitely.
  • Beat metadata split across lanes rather than replicated. Each lane's view of the stream becomes incoherent.

What to read first. lanes_present under a stall shows which lanes are waiting on which; max_spread shows whether arrival differences have grown; last_mismatch shows whether alignment has been lost. A design without those three answers "the Link corrupts data" and nothing more.

11. Common Misconceptions

  • "Each lane carries unrelated transactions." One logical stream is distributed across all active lanes. A single transaction's information can occupy every lane, which is precisely why reassembly exists.
  • "Lane aggregation is just wiring buses together." A wide on-chip bus is synchronous by construction. PCIe lanes are physically separate paths that need not deliver together, which is what requires holding, alignment, and reassembly.
  • "Deskew is the same as clock skew." Different phenomena at different levels — one is edge arrival within a design, the other is information arrival across separate paths between two components.
  • "If all lanes pass individually, the wide Link must pass." Individual lane tests exercise no distribution, no reassembly, no pairing, and no arrival-difference handling. Every lane passing alone is entirely consistent with the aggregate being wrong, and it is the signature that points hardest at aggregation.
  • "Reassembly requires no state." It requires holding for every lane, because portions of one beat need not be simultaneously available. The depth of that holding bounds what can be absorbed.
  • "Striping determines transaction ordering." Distribution decides which lane carries which portion of the transport. Transaction ordering is a protocol-level property preserved through aggregation, not created by it. Modules 10 onward own ordering.
  • "A wider Link is one large physical signal." It is N independent lanes, each with its own transmit and receive paths (Chapter 6.1). Nothing is widened physically.
  • "This chapter's striping model is PCIe's algorithm." The slice-by-lane rule here is an arbitrary placeholder chosen to make coordination visible. The specification's rule, unit size, and boundary handling are not stated here.
  • "One delayed lane necessarily corrupts traffic." Within the tolerance a design provides, an arrival difference is absorbed and reconstruction is correct. Corruption occurs when the difference exceeds what the design can hold — which is why the tolerance is a design parameter rather than a hope.
  • "More lanes means more independent Links." N lanes form one Link. Independence appears only when a lane pool is allocated into separate Links, which is Chapter 6.4's subject and a different configuration entirely.

12. Understanding Check

13. What's Next

Aggregation explains how N lanes act as one Link. It says nothing about how much that Link can carry.

Chapter 6.7 — Throughput Calculations answers that. Module 5 derived per-lane capacity from signalling rate and encoding; Module 6 established lane count; and no chapter has yet multiplied them. 6.7 does — with the units, direction, and basis discipline that makes the result trustworthy rather than merely tidy, and with the distinction between what a Link can theoretically carry and what a workload actually gets.