A 64b/66b receiver powers up and starts collecting bits. It knows that every 66 bits there is a two-bit header that must be 01 or 10, but it has no idea which of the 66 possible positions is the right one. Somewhere in the stream, scrambled payload bits will also form 01 or 10 by chance, about half the time.
How does a receiver find the true block boundary, avoid being fooled by payload that looks like a header, and notice if it ever loses its place?
The obvious answer is to check one position, and if the header looks valid, declare it the boundary. Since half of all random two-bit patterns are 01 or 10, the receiver would be fooled half the time at a wrong position. The opposite mistake is just as bad: once locked, giving up after a single bad header would make every bit error on the line throw away the alignment and start a long hunt. The receiver needs a rule with memory: several good headers in a row to believe a position, several bad ones in a short window to abandon it.
Block lock is a small state machine:
HUNT. Check the header at the current position. If it is invalid, this position is wrong: slip the boundary by one bit and try again. If it is valid, count it.
Confirm. A wrong position passes each check with probability ½, so N valid headers in a row happen by chance only with probability (½)ᴺ. After N, declare LOCKED.
LOCKED. Keep checking every header. Isolated errors are expected (bit errors happen), so tolerate a few. If too many bad headers arrive within a window, the boundary has probably moved: go back to HUNT.
To keep the waveform short, the listing shrinks 64b/66b to 10-bit blocks: a 2-bit header (01 or 10) and 8 payload bits. The receiver gets 10-bit words starting at an unknown bit offset (the true one is 3). In HUNT it slips one bit on every bad header and locks after four good ones in a row. While locked, three bad headers within a window of eight blocks drop it back to HUNT. The testbench later corrupts four headers in a row to show loss of lock and relock. Note that slip acts in the same cycle: a registered slip would act a cycle late, and the receiver would slip again on a word that was still from the old offset.
// Block lock on a 2-bit sync header (the 64b/66b idea, shrunk to 10-bit blocks so
// the waveform stays short). Each block is a header, 01 or 10, then 8 payload bits.
// The receiver gets 10-bit words at an unknown bit offset. In HUNT it checks each
// word's header: a bad header means "wrong offset", so it asks the deserialiser to
// slip one bit and starts counting again. Four good headers in a row: LOCKED. While
// locked, three bad headers within eight blocks mean lock was lost: back to HUNT.
module block_lock (
input logic clk, rst_n,
input logic [9:0] word, // word[9:8] is where the header should be
output logic slip, // move the word boundary by one bit at this edge
output logic locked,
output logic hdr_ok
);
logic [2:0] good; // consecutive good headers while hunting
logic [2:0] seen, bad; // blocks and bad headers in the current window (locked)
assign hdr_ok = (word[9:8] == 2'b01) || (word[9:8] == 2'b10);
// Combinational, so the deserialiser moves the boundary at this same edge and the
// next word is already at the new offset (a registered slip would act a cycle late
// and the FSM would slip again on the stale word).
assign slip = !locked && !hdr_ok;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
locked <= 0; good <= 0; seen <= 0; bad <= 0;
end else begin
if (!locked) begin
if (!hdr_ok) good <= 0;
else if (good == 3) begin locked <= 1; seen <= 0; bad <= 0; end
else good <= good + 1;
end else begin
seen <= seen + 1;
if (!hdr_ok) bad <= bad + 1;
if (!hdr_ok && bad == 2) begin locked <= 0; good <= 0; end // third bad header
else if (seen == 7) begin seen <= 0; bad <= 0; end // window of 8 ends
end
end
end
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
// The bit stream: 3 junk bits, then 40 blocks. Headers alternate in a fixed mix of
// 01 and 10; payloads come from a simple counter pattern.
localparam int NB = 40, OFFSET = 3;
logic stream [0:NB*10 + 30];
logic [7:0] payload [0:NB-1];
initial begin
stream[0] = 1; stream[1] = 1; stream[2] = 1; // junk before the first block
for (int b = 0; b < NB; b++) begin
logic [1:0] h;
h = (b % 3 == 1) ? 2'b10 : 2'b01;
payload[b] = 8'(b * 37 + 11);
stream[OFFSET + 10*b] = h[1];
stream[OFFSET + 10*b + 1] = h[0];
for (int k = 0; k < 8; k++) stream[OFFSET + 10*b + 2 + k] = payload[b][7-k];
end
for (int j = OFFSET + 10*NB; j < NB*10 + 31; j++) stream[j] = 0;
end
// Deserialiser model: delivers word c from bit position 10*c + k; a slip adds one to k.
int c = 0, k = 0;
logic [9:0] word;
wire slip, locked, hdr_ok;
wire [2:0] good = dut.good; // copy for the waveform
always @* for (int n = 0; n < 10; n++) word[9-n] = stream[10*c + k + n];
// Corrupt four headers in a row (blocks 30 to 33) so at least three fall in one window.
wire [9:0] word_in = (c >= 30 && c <= 33) ? {2'b11, word[7:0]} : word;
always @(posedge clk) if (rst_n) begin
c <= c + 1;
if (slip) k <= k + 1;
end
block_lock dut (.clk, .rst_n, .word(word_in), .slip, .locked, .hdr_ok);
// Checker: once locked (and outside the corrupted blocks), every payload is the next expected one.
int errors = 0, locks = 0, losses = 0, good_payloads = 0;
logic locked_q = 0;
always @(posedge clk) if (rst_n) begin
if (locked && !locked_q) locks++;
if (!locked && locked_q) losses++;
locked_q <= locked;
if (locked && hdr_ok && !(c >= 30 && c <= 33)) begin
// A boundary that is a whole block later is also correct alignment.
if ((k - OFFSET) % 10 != 0 || word_in[7:0] != payload[c + (k - OFFSET) / 10]) errors++;
else good_payloads++;
end
end
initial begin
#400;
if (errors == 0 && locks == 2 && losses == 1 && good_payloads > 20)
$display("PASS: locked at offset %0d, lost lock on 3 bad headers, relocked; %0d payloads checked", k, good_payloads);
else $display("FAIL: errors %0d, locks %0d, losses %0d, offset %0d", errors, locks, losses, k);
$finish;
end
endmoduleHow to read it, slot by slot:
Slots 0 to 2: the words start at offsets 0, 1 and 2 (k), so their top two bits are payload, not header, and happen to be 11 or 00: hdr_ok is low, slip is high, and k steps up each slot.
Slot 3: at offset 3 the header is valid. Slipping stops.
Slots 3 to 6: four valid headers in a row. The counter good climbs 0, 1, 2, 3.
Slot 7: after the fourth good header the receiver is LOCKED, at the true offset of 3.
The run continues beyond the waveform. Four corrupted headers later drop the lock, the next good headers relock it at the same offset, and the checker confirms that every payload received while locked is the right one.
At a wrong position, each header check passes by chance with probability ½ (scrambled bits are about equally likely to be 01, 10, 00 or 11). Requiring N good headers in a row bounds the chance of a false lock at each wrong position:
N = 4: (1/2)^4 = 1 in 16 -> false locks are common; recovered by the LOCKED-state check
N = 16: (1/2)^16 = 1 in 65,536
N = 64: (1/2)^64 = about 1 in 1.8 x 10^19
Time to lock grows with N: up to (number of positions) slips plus N blocks.The listing uses N = 4 to keep the waveform short; real links use much larger counts, trading a slightly slower first lock for false-lock odds that are effectively zero.
Parameter | Larger value | Smaller value |
|---|---|---|
Good headers to lock (N) | Fewer false locks; slower to lock | Faster lock; false locks more likely |
Bad headers to unlock (and window) | Survives bursts of bit errors without dropping lock; slower to notice a real loss | Notices real loss quickly; drops lock on noise |
Search strategy | Check several positions in parallel: faster lock, more logic | One position at a time with slips: least logic |
Protocol family | What the receiver searches for |
|---|---|
64b/66b (for example 10GBASE-R) | The 2-bit sync header, 01 for data or 10 for control, at the start of every 66-bit block, using a hunt-and-confirm state machine like the one above. |
PCI Express 128b/130b | The 2-bit sync header (10 data, 01 ordered set) on every 130-bit block; it is left unscrambled so it can be found first. |
8b/10b links | The comma pattern (0011111 or 1100000) inside the K28.5 control character, which cannot appear across data code groups, so a single comma gives the word boundary. |
8b/10b's comma gives alignment in one step, because the code is designed so data never produces it. Header-based codes cannot promise that, which is why they need hunt-and-confirm with memory.
A receiver unlocks after one bad header (illustrative). On a clean bench it never matters. On a noisy channel with a bit error rate where a header is hit every few seconds, the link drops lock, hunts, relocks and loses the traffic in between, every few seconds, and reports it as link instability. The fix is a window: tolerate a few bad headers per window, so random errors are absorbed and only a real loss of alignment triggers a hunt.
(½)⁸ = 1 in 256.
slip combinational?So the deserialiser moves the boundary at the same edge that judged the header bad. A registered slip takes effect a cycle later, the FSM sees one more word at the old offset, judges it bad too, and slips twice.
The comma pattern is designed never to appear in or across data code groups, so one sighting is conclusive. A 2-bit header pattern appears in scrambled payload half the time, so only a long run of matches is convincing.
Block lock hunts by slipping one bit at a time until headers are valid, then requires N good headers in a row.
N sets the false-lock odds, about (½)ᴺ per wrong position; a window of tolerated bad headers keeps noise from dropping lock.
Codes with unique comma patterns align in one step; header-based codes need hunt-and-confirm with memory.