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 PASSED

The 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 print FAIL instead.

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 REPEAT knob 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
endmodule
PASS: 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 PASSED

The 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 bits EDCBA go through the 5b/6b sub-block producing six bits abcdei; the upper three bits HGF go through the 3b/4b sub-block producing four bits fghj. The transmitted symbol is the concatenation a 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.y where x is the 5-bit value (0–31) and y is the 3-bit value (0–7). For example byte 0x18 is D24.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 sequence 0011111 or 1100000 that cannot appear across any data-symbol boundary, so the receiver uses it to find symbol alignment. A full encoder adds a K input 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, not din[9:0]. Getting i and 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 rd register 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. The for loops 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 Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD