A four-lane serial link sends each 32-bit word as four bytes, one per lane, in the same cycle. At the receiver each lane recovers its own clock and delivers its bytes perfectly, yet the reassembled words are scrambled: byte 0 of word 5 sits next to byte 1 of word 3. Every lane is right; together they are wrong.
When each lane arrives at its own time, how does the receiver know which bytes belong together?
The obvious answer is to route the lanes with equal lengths so they arrive together. Matching helps, but it cannot be exact: connectors, cables, package routing and the receivers' own recovered clocks each add different delays, and at high rates even a few millimetres of difference is a whole bit. Worse, each lane's CDR may lock at a different point in its first word. The skew between lanes is unknown until the link is running, so it has to be measured, not designed away.
The transmitter periodically sends a special alignment marker on all lanes in the same cycle, something the receiver can recognise and that never appears in normal data. Each lane's receiver notes when its marker arrives and buffers its data from that point on. When every lane has seen its marker, the receiver reads all the lane buffers together. Bytes that left together come out together, however late each lane was.
The buffers absorb the difference: the earliest lane waits in its buffer for the latest one. So each lane's buffer must be at least as deep as the largest skew the link must tolerate, in cycles.
The testbench splits words across four lanes and sends a marker byte BC on every lane in one cycle, then six words. The channel delays the lanes by 0, 2, 1 and 3 cycles. The receiver writes each lane into its own small buffer from its marker onwards and reads all four with one shared read pointer once every lane has seen its marker. A checker confirms the six words come out whole and in order.
// Lane deskew with an alignment marker. A word is split across four lanes, one byte
// per lane per cycle. The transmitter sends a marker byte (BC) on every lane in the same
// cycle, then data. The lanes arrive with different delays (0, 2, 1 and 3 cycles). Each
// lane writes into its own small buffer starting at its marker; once every lane has
// seen its marker, all buffers are read together with one read pointer, so the bytes
// that left together come out together.
module tb;
localparam logic [7:0] MARK = 8'hBC;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
// Transmitter: idle (00), one cycle of markers, then words 0..5; lane i carries byte 4k+i+0x10.
logic [3:0] t = 0;
always @(posedge clk) if (rst_n && t != 4'd15) t <= t + 1;
function automatic logic [7:0] tx_byte(input int lane, input logic [3:0] tt);
if (tt == 2) return MARK;
if (tt >= 3 && tt <= 8) return 8'h10 + 4 * (tt - 3) + lane;
return 8'h00;
endfunction
// Channel: lane i delayed by DLY[i] cycles.
localparam int D0 = 0, D1 = 2, D2 = 1, D3 = 3;
logic [7:0] pipe [0:3][0:3];
initial for (int l = 0; l < 4; l++) for (int k = 0; k < 4; k++) pipe[l][k] = 8'h00;
logic [7:0] in0, in1, in2, in3; // bytes arriving at the receiver, per lane
always @(posedge clk)
for (int l = 0; l < 4; l++) begin
pipe[l][0] <= tx_byte(l, t);
for (int k = 1; k < 4; k++) pipe[l][k] <= pipe[l][k-1];
end
assign in0 = pipe[0][D0];
assign in1 = pipe[1][D1];
assign in2 = pipe[2][D2];
assign in3 = pipe[3][D3];
logic [7:0] lane_in [0:3];
always @* begin lane_in[0] = in0; lane_in[1] = in1; lane_in[2] = in2; lane_in[3] = in3; end
// Receiver: per-lane buffer written from the marker on; one shared read pointer.
logic [7:0] mem [0:3][0:7];
logic [3:0] seen = 0;
logic [2:0] wp [0:3];
logic [2:0] rp = 0; // saturates at 7: the six words have been read
initial for (int l = 0; l < 4; l++) wp[l] = 0;
wire aligned = &seen;
always @(posedge clk) if (rst_n) begin
for (int l = 0; l < 4; l++) begin
if (!seen[l] && lane_in[l] == MARK) begin
seen[l] <= 1; mem[l][0] <= MARK; wp[l] <= 1;
end else if (seen[l]) begin
mem[l][wp[l]] <= lane_in[l]; wp[l] <= wp[l] + 1;
end
end
if (aligned && rp != 3'd7) rp <= rp + 1;
end
wire [31:0] out = aligned ? {mem[3][rp], mem[2][rp], mem[1][rp], mem[0][rp]} : 32'h0;
// Checker: after the marker word, words 0..5 must come out whole and in order.
function automatic logic [31:0] expected(input int k);
logic [7:0] b0, b1, b2, b3;
b0 = 8'h10 + 4 * k; b1 = b0 + 1; b2 = b0 + 2; b3 = b0 + 3;
return {b3, b2, b1, b0};
endfunction
int errors = 0, words = 0;
always @(posedge clk) if (aligned && rp != 3'd7) begin
if (rp == 0 && out != {4{MARK}}) errors++;
if (rp >= 1 && rp <= 6) begin
if (out != expected(rp - 1)) errors++;
words++;
end
end
initial begin
#200;
if (errors == 0 && words == 6) $display("PASS: lanes skewed by 0, 2, 1 and 3 cycles realigned; 6 words intact");
else $display("FAIL: %0d errors, %0d words", errors, words);
$finish;
end
endmoduleHow to read it, slot by slot:
Slots 3 to 6: the marker bc arrives on lane 0 in slot 3, lane 2 in slot 4, lane 1 in slot 5 and lane 3 in slot 6. The bytes after each marker belong to the same words, but they arrive staggered.
Slots 4 to 7: seen collects one bit per lane as markers arrive: 1, 5 (lanes 0 and 2), 7, then f in slot 7 when the slowest lane has its marker.
Slot 7: with every lane aligned, the shared read pointer (rp) starts at 0, and out shows the four markers together: bcbcbcbc.
Slots 8 to 13: out delivers words 0 to 5 whole: 13121110 is bytes 10, 11, 12 and 13 from lanes 0 to 3, which left the transmitter in the same cycle and arrived up to three cycles apart.
Slot 14: after the six words, rp stops at 7 and the output goes idle.
Lane 0's bytes waited three cycles in its buffer; lane 3's waited none. The buffers turned an unknown skew into a fixed latency equal to the worst lane's delay.
Skew is set by physical differences, so it is fixed in time, while the bit time shrinks with rate. With illustrative numbers:
Lane-to-lane skew to tolerate: 2 ns (cables, connectors, packages, receivers)
At 2.5 Gb/s per lane: bit = 400 ps -> skew = 5 bits
At 16 Gb/s per lane: bit = 62.5 ps -> skew = 32 bits
With 32-bit words per lane per cycle, 32 bits of skew is one word of buffering per lane,
plus a margin: the buffer depth grows with the rate even though the skew does not.Choice | What it costs | When it wins |
|---|---|---|
Tight length matching only | Routing effort; no tolerance for anything unmatched | Short, slow, parallel links |
Alignment markers + per-lane buffers (this lesson) | Marker bandwidth; buffer per lane; latency equal to the worst skew | Multi-lane serial links: the standard answer |
Train once at start-up, then hold | Must retrain if skew drifts | Links whose skew is stable once warm |
Markers repeated periodically | A little more overhead | Links that must also detect or recover from lane slips while running |
Protocol | Its choice |
|---|---|
PCI Express | Links have 1 to 32 lanes, each with its own recovered clock. The receiver must remove lane-to-lane skew, using ordered sets that the transmitter sends on all lanes at the same time as alignment points. |
DDR memory | Not serial, but the same problem: each data group has its own strobe, so skew only has to be controlled within a group, and the controller trains each group's capture timing separately. |
A design sizes its deskew buffers for the skew of the reference board (illustrative). A customer uses a longer cable whose pairs differ more in length, and the worst lane arrives one word later than the buffer can hold. The receiver then aligns the lanes one word apart: every reassembled word mixes bytes from two different words, but each lane's data is perfect and each lane reports no errors.
Symptom: link trains, lanes report clean, but upper-layer data is garbled.
Evidence: markers on one lane are detected a word later than the deskew logic allows.
Fix: deeper buffers sized from the specification's skew limit, not the bench; and a check that flags "marker outside the window" instead of silently misaligning.
Three: lane 0 had no channel delay and lane 3 had three, so lane 0 must wait for lane 3's matching bytes before a whole word can be output.
So a lane cannot mistake a data byte for a marker and align on the wrong cycle. Protocols use control symbols or ordered sets for this (lesson 2.9).
The skew in bits doubles, so the buffer, measured in bits or words, must double too.
Lane-to-lane skew comes from physical differences and recovered clocks; it is unknown until the link runs, so it must be measured.
A marker sent on every lane at once lets each lane buffer from its marker; reading all buffers together realigns the words.
Buffers must cover the specified maximum skew at the highest rate; exceeding them should be detected, never silently misaligned.