8b/10b Encoder in Verilog with Code
An 8b/10b encoder maps each 8-bit byte to a 10-bit symbol so the serial line stays DC-balanced and has frequent transitions for clock recovery. It is the classic line code behind PCIe Gen1/2, SATA, USB 3.0 Gen1, Gigabit Ethernet (1000BASE-X) and Fibre Channel. This page gives working 8b10b encoder Verilog code: a tabulated subset encoder that tracks running disparity, plus an invariant checker, both runnable under Icarus Verilog.
The scheme splits the byte into a 5-bit and a 3-bit field, encodes them in two sub-blocks (5b/6b and 3b/4b), and uses a one-bit running disparity (RD) to pick between two complementary code words so the number of ones and zeros on the wire stays nearly equal over time.
This encoder tabulates a subset of data characters for the 5b/6b and 3b/4b sub-blocks, selects the RD− or RD+ column from the current running disparity, and updates RD after each sub-block. The testbench drives a fixed byte sequence and checks every 10-bit symbol and the resulting disparity against known-good values computed offline from the IEEE 802.3 code tables.
// 8b/10b encoder for a tabulated subset of data characters, with running
// disparity. Input byte = {HGF, EDCBA}; output symbol bit order a b c d e i f g h j.
module enc8b10b_subset (
input clk,
input rst_n,
input [7:0] din,
output reg [9:0] dout, // encoded 10-bit symbol
output reg rd, // running disparity after last symbol: 0=RD-, 1=RD+
output reg bad // 1 if din not in the tabulated subset
);
function [3:0] ones6; input [5:0] v; integer i;
begin ones6 = 0; for (i=0;i<6;i=i+1) ones6 = ones6 + v[i]; end
endfunction
reg [4:0] x; reg [2:0] y;
reg [5:0] six; reg [3:0] four;
reg rd6, rdn, b5, b3;
reg [9:0] sym;
always @(*) begin
x = din[4:0]; y = din[7:5];
b5 = 1'b0; b3 = 1'b0;
// ---- 5b/6b sub-block, column chosen by current running disparity ----
case (x)
5'd0 : six = rd ? 6'b011000 : 6'b100111;
5'd1 : six = rd ? 6'b100010 : 6'b011101;
5'd2 : six = rd ? 6'b010010 : 6'b101101;
5'd3 : six = 6'b110001;
5'd4 : six = rd ? 6'b001010 : 6'b110101;
5'd5 : six = 6'b101001;
5'd6 : six = 6'b011001;
5'd7 : six = rd ? 6'b000111 : 6'b111000;
5'd10: six = 6'b010101;
5'd16: six = rd ? 6'b100100 : 6'b011011;
5'd24: six = rd ? 6'b001100 : 6'b110011;
5'd28: six = 6'b001110;
default: begin six = 6'b000000; b5 = 1'b1; end
endcase
// disparity after the 6b block: flips unless the block is balanced (3 ones)
rd6 = (ones6(six) == 4'd3) ? rd : ~rd;
// ---- 3b/4b sub-block, column chosen by rd6 ----
case (y)
3'd0 : four = rd6 ? 4'b0100 : 4'b1011;
3'd1 : four = 4'b1001;
3'd2 : four = 4'b0101;
3'd5 : four = 4'b1010;
default: begin four = 4'b0000; b3 = 1'b1; end
endcase
// 4b block: 1011/0100 carry disparity +/-2; 1001/0101/1010 are balanced
rdn = ((four==4'b1011)||(four==4'b0100)) ? ~rd6 : rd6;
sym = {six, four};
end
always @(posedge clk) begin
if (!rst_n) begin
dout <= 10'd0; rd <= 1'b0; bad <= 1'b0;
end else begin
dout <= sym; rd <= rdn; bad <= b5 | b3;
end
end
endmodule
module tb;
reg clk=0, rst_n=0; reg [7:0] din=0;
wire [9:0] dout; wire rd, bad;
integer errors=0, i;
enc8b10b_subset dut(.clk(clk), .rst_n(rst_n), .din(din),
.dout(dout), .rd(rd), .bad(bad));
always #5 clk = ~clk;
// stimulus and golden (start RD-), computed offline from the IEEE 802.3 tables
reg [7:0] seq [0:7];
reg [9:0] gsym [0:7];
reg grd [0:7];
initial begin
seq[0]=8'h18; gsym[0]=10'b1100110100; grd[0]=1'b0;
seq[1]=8'h00; gsym[1]=10'b1001110100; grd[1]=1'b0;
seq[2]=8'h01; gsym[2]=10'b0111010100; grd[2]=1'b0;
seq[3]=8'h0A; gsym[3]=10'b0101011011; grd[3]=1'b1;
seq[4]=8'h27; gsym[4]=10'b0001111001; grd[4]=1'b1;
seq[5]=8'h44; gsym[5]=10'b0010100101; grd[5]=1'b0;
seq[6]=8'h1C; gsym[6]=10'b0011101011; grd[6]=1'b1;
seq[7]=8'h50; gsym[7]=10'b1001000101; grd[7]=1'b0;
end
initial begin
// reset: RD- and clean state
@(negedge clk); rst_n = 1'b1;
for (i=0;i<8;i=i+1) begin
din = seq[i];
@(posedge clk); // latch at this edge
#1; // let outputs settle after the edge
if (bad !== 1'b0) begin
errors=errors+1; $display("FAIL byte %0d (%h): flagged unsupported", i, seq[i]);
end else if (dout !== gsym[i] || rd !== grd[i]) begin
errors=errors+1;
$display("FAIL byte %0d (%h): got sym=%b rd=%b exp sym=%b rd=%b",
i, seq[i], dout, grd[i], gsym[i], grd[i]);
end else begin
$display("PASS byte %0d: %h -> %b (RD->%s)",
i, seq[i], dout, rd ? "+" : "-");
end
end
if (errors==0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d", errors);
$finish;
end
endmodule
PASS byte 0: 18 -> 1100110100 (RD->-) PASS byte 1: 00 -> 1001110100 (RD->-) PASS byte 2: 01 -> 0111010100 (RD->-) PASS byte 3: 0a -> 0101011011 (RD->+) PASS byte 4: 27 -> 0001111001 (RD->+) PASS byte 5: 44 -> 0010100101 (RD->-) PASS byte 6: 1c -> 0011101011 (RD->+) PASS byte 7: 50 -> 1001000101 (RD->-) ALL TESTS PASSEDThe log shows each input byte mapping to its expected 10-bit symbol and the running disparity (shown as + or −) after each one, ending in
ALL TESTS PASSED. Because every asserted symbol and RD value matches the offline golden vectors, a wrong table entry or a broken disparity update would printFAILinstead.Rather than re-list the full code table, this variant reuses the same encoder and sweeps every tabulated byte from both disparity states (with a
REPEATknob to lengthen the stream). It self-checks the algebra that makes 8b/10b work: bounded symbol weight, the balance↔flip rule, and a running digital sum confined to a narrow band.module enc8b10b_subset ( input clk, input rst_n, input [7:0] din, output reg [9:0] dout, // encoded 10-bit symbol output reg rd, // running disparity after last symbol: 0=RD-, 1=RD+ output reg bad // 1 if din not in the tabulated subset ); function [3:0] ones6; input [5:0] v; integer i; begin ones6 = 0; for (i=0;i<6;i=i+1) ones6 = ones6 + v[i]; end endfunction reg [4:0] x; reg [2:0] y; reg [5:0] six; reg [3:0] four; reg rd6, rdn, b5, b3; reg [9:0] sym; always @(*) begin x = din[4:0]; y = din[7:5]; b5 = 1'b0; b3 = 1'b0; // ---- 5b/6b sub-block, column chosen by current running disparity ---- case (x) 5'd0 : six = rd ? 6'b011000 : 6'b100111; 5'd1 : six = rd ? 6'b100010 : 6'b011101; 5'd2 : six = rd ? 6'b010010 : 6'b101101; 5'd3 : six = 6'b110001; 5'd4 : six = rd ? 6'b001010 : 6'b110101; 5'd5 : six = 6'b101001; 5'd6 : six = 6'b011001; 5'd7 : six = rd ? 6'b000111 : 6'b111000; 5'd10: six = 6'b010101; 5'd16: six = rd ? 6'b100100 : 6'b011011; 5'd24: six = rd ? 6'b001100 : 6'b110011; 5'd28: six = 6'b001110; default: begin six = 6'b000000; b5 = 1'b1; end endcase // disparity after the 6b block: flips unless the block is balanced (3 ones) rd6 = (ones6(six) == 4'd3) ? rd : ~rd; // ---- 3b/4b sub-block, column chosen by rd6 ---- case (y) 3'd0 : four = rd6 ? 4'b0100 : 4'b1011; 3'd1 : four = 4'b1001; 3'd2 : four = 4'b0101; 3'd5 : four = 4'b1010; default: begin four = 4'b0000; b3 = 1'b1; end endcase // 4b block: 1011/0100 carry disparity +/-2; 1001/0101/1010 are balanced rdn = ((four==4'b1011)||(four==4'b0100)) ? ~rd6 : rd6; sym = {six, four}; end always @(posedge clk) begin if (!rst_n) begin dout <= 10'd0; rd <= 1'b0; bad <= 1'b0; end else begin dout <= sym; rd <= rdn; bad <= b5 | b3; end end endmodule // Invariant sweep: drive every tabulated byte from both running-disparity // states and check the DC-balance algebra, not a stored full code table. module tb; parameter REPEAT = 4; // stream length scales with this knob reg clk=0, rst_n=0; reg [7:0] din=0; wire [9:0] dout; wire rd, bad; integer errors=0, r, xi, yi, k, nones, rds, span_lo, span_hi; reg prev_rd, flipped; enc8b10b_subset dut(.clk(clk), .rst_n(rst_n), .din(din), .dout(dout), .rd(rd), .bad(bad)); always #5 clk = ~clk; reg [4:0] xs [0:11]; reg [2:0] ys [0:3]; initial begin xs[0]=0; xs[1]=1; xs[2]=2; xs[3]=3; xs[4]=4; xs[5]=5; xs[6]=6; xs[7]=7; xs[8]=10; xs[9]=16; xs[10]=24; xs[11]=28; ys[0]=0; ys[1]=1; ys[2]=2; ys[3]=5; end function [3:0] popc; input [9:0] v; integer i; begin popc=0; for (i=0;i<10;i=i+1) popc=popc+v[i]; end endfunction initial begin @(negedge clk); rst_n=1'b1; prev_rd = 1'b0; // reset leaves RD- rds = 0; span_lo = 0; span_hi = 0; k = 0; for (r=0;r<REPEAT;r=r+1) for (yi=0;yi<4;yi=yi+1) for (xi=0;xi<12;xi=xi+1) begin din = {ys[yi], xs[xi]}; @(posedge clk); #1; nones = popc(dout); flipped = (rd !== prev_rd); // invariant 1: bounded weight (4/5/6 ones) -> bounded run length & DC content if (bad!==1'b0 || !(nones==4 || nones==5 || nones==6)) begin errors=errors+1; $display("FAIL weight: din=%h sym=%b ones=%0d bad=%b",din,dout,nones,bad); end // invariant 2: balanced symbol keeps RD, unbalanced flips it if ((nones==5 && flipped) || (nones!=5 && !flipped)) begin errors=errors+1; $display("FAIL balance/flip: din=%h ones=%0d prev=%b now=%b",din,nones,prev_rd,rd); end // invariant 3: running digital sum stays inside a width-1 band rds = rds + (nones-5); if (rds<span_lo) span_lo=rds; if (rds>span_hi) span_hi=rds; if ((span_hi-span_lo) > 1) begin errors=errors+1; $display("FAIL RDS band: din=%h rds=%0d span=[%0d,%0d]",din,rds,span_lo,span_hi); end prev_rd = rd; k=k+1; end $display("PASS: %0d symbols, all 4/5/6-ones", k); $display("PASS: balance<->disparity-flip held on every symbol"); $display("PASS: running digital sum confined to [%0d,%0d] (span %0d)", span_lo, span_hi, span_hi-span_lo); if (errors==0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d", errors); $finish; end endmodulePASS: 192 symbols, all 4/5/6-ones
PASS: balance<->disparity-flip held on every symbol
PASS: running digital sum confined to [0,1] (span 1)
ALL TESTS PASSEDThe log confirms three algebraic invariants hold on all 192 streamed symbols: every symbol carries 4, 5 or 6 ones (bounded run length and DC content); a balanced 5-ones symbol keeps RD while a 4- or 6-ones symbol flips it; and the cumulative running digital sum never leaves a width-1 band. These are checked without a stored reference table, so they catch disparity-tracking bugs independently of Example 1.
Why DC balance and bounded run length matter
Serial links are AC-coupled, so a long run of the same bit value would drift the signal baseline and the receiver would lose the data. 8b/10b guarantees two properties: the disparity (difference between ones and zeros) stays bounded, keeping the average line level centered, and the maximum run length of identical bits is limited to five, guaranteeing enough edges for the receiver's clock-and-data-recovery (CDR) loop to stay locked. The 25% coding overhead (10 bits for 8) buys both.
The 5b/6b + 3b/4b split
The input byte is written as bits
HGF EDCBA. The lower five bitsEDCBAgo through the 5b/6b sub-block producing six bitsabcdei; the upper three bitsHGFgo through the 3b/4b sub-block producing four bitsfghj. The transmitted symbol is the concatenationa b c d e i f g h j. Splitting the problem keeps each lookup table small (32 and 8 entries) instead of one 256-entry table, and lets the two sub-blocks share one disparity state.Notation
A character is named
Dx.ywherexis the 5-bit value (0–31) andyis the 3-bit value (0–7). For example byte0x18isD24.0.The running disparity rule
Running disparity is a single bit of state: RD− (more zeros sent so far) or RD+ (more ones). Each sub-block has two complementary code words. The rule is simple:
- If the chosen code word is balanced (equal ones and zeros — three ones in a 6-bit block, two in a 4-bit block), RD is left unchanged.
- If it is unbalanced, the encoder must emit the code word whose disparity opposes the current RD, and RD then flips to the other state.
This is exactly the logic in the examples:
rd6 = (ones6(six)==3) ? rd : ~rd;for the 6-bit block, and the analogous update for the 4-bit block. Picking the column by the current RD (rd ? plus_column : minus_column) is what keeps the line balanced over time.Control symbols vs data symbols (the K28.5 comma)
Besides the 256 data characters (Dx.y), 8b/10b defines twelve control characters (Kx.y) used for framing and link management. The most important is K28.5, the comma: its 10-bit pattern (
0011111010/1100000101) contains the bit sequence0011111or1100000that cannot appear across any data-symbol boundary, so the receiver uses it to find symbol alignment. A full encoder adds aKinput select that routes the byte through the separate control-code tables; the examples here cover data characters only.Scope note and common mistakes
Scope: Example 1 tabulates only a subset of the 256 data characters (twelve 5b/6b entries and four 3b/4b entries) so the code stays readable; it is not a complete 8b/10b codec. A production encoder fills in all 32 5b/6b and 8 3b/4b rows, adds the control characters, and typically pairs with a matching decoder that also flags disparity and code-violation errors.
Common mistakes
- Wrong bit order. The wire order is
a b c d e i f g h j, notdin[9:0]. Gettingiand the 3b/4b block order wrong silently corrupts alignment.- Not threading disparity between sub-blocks. The 3b/4b block must use the RD after the 6b block, not the byte's starting RD.
- Forgetting the D.7 / Dx.7 alternates. D.7 and the y=7 column have primary/alternate code words chosen to avoid long runs at the junction; a naive single-entry table violates the run-length limit.
- Treating balanced codes as flipping RD. A 5-ones symbol must leave RD unchanged — Example 2 checks exactly this.
Synthesis notes
The combinational core (two case statements plus the ones-count functions) synthesizes to a small LUT block; the only state is the 1-bit
rdregister plus the output pipeline register. On FPGAs the whole byte-rate encoder is a handful of LUTs and flops and closes timing easily at line-rate/10. For gigabit serial rates the encoder runs in the parallel (byte) clock domain and a separate SerDes does the 10:1 serialization. Theforloops in the ones-count functions are unrolled at elaboration, so they cost no hardware beyond the adder tree.FAQ
Is 8b/10b self-synchronizing?
Yes — the bounded run length guarantees transitions for CDR, and the comma character (K28.5) provides symbol-boundary alignment.
How much bandwidth does it cost?
25%: ten line bits per eight data bits. Newer links (PCIe Gen3+, USB 3.1 Gen2) switched to 128b/130b scrambling to cut that overhead, but 8b/10b remains common and is a frequent interview topic.
Why track disparity as one bit?
Because the code words are defined so the running disparity only ever sits at RD− or RD+ at symbol boundaries; a single sign bit is sufficient state, as Example 2's width-1 digital-sum band demonstrates.
Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres