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?
1. Two Views of the Same Link
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.
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 round | second round | |
|---|---|---|
| Lane 0 | A | E |
| Lane 1 | B | F |
| Lane 2 | C | G |
| Lane 3 | D | H |
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.
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
// 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
endmoduleClassification: 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
// 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
endmoduleClassification: 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:
- 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.
- 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.
- A beat is never released partially assembled.
out_validrequires every portion. Releasing with portions missing would publish whatever the empty registers happened to hold. - Misalignment is detectable and must be sticky.
last_mismatchcatches 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
// 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:
// 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
endclassClassification: 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_checkis 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
// 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
endclassA 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_spreadreflects 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_checkreports 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_readyfor 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_checkclean.
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.