Scrambler & Descrambler in Verilog (LFSR) with Code

A scrambler is a circuit that XORs a serial data stream with a pseudo-random sequence from a linear-feedback shift register (LFSR) so the line carries few long runs of identical bits. SerDes links, PCIe, SATA, and Ethernet all scramble the payload to balance the number of ones and zeros, guarantee frequent transitions for clock recovery, and spread spectral energy so EMI is lower. This page gives working scrambler Verilog code for both common styles, each paired with its matching descrambler.

We cover the two families that appear in interviews and real standards: the self-synchronizing (multiplicative) scrambler, which needs no shared state, and the additive (frame-synchronous) scrambler, which seeds both ends with the same value. Every example below is a complete module plus a self-checking testbench that was compiled and run under Icarus Verilog in strict Verilog-2005 mode.

This self-synchronizing scrambler uses the polynomial x7+x4+1. The feedback for each bit is taken from the scrambled output, so the descrambler re-derives the same feedback purely from the bits arriving on the wire and locks on after at most 7 bits, with no shared seed.

// Self-synchronizing (multiplicative) scrambler / descrambler
// Polynomial: x^7 + x^4 + 1  (taps at positions 7 and 4)
module scrambler_mult (
    input  wire clk,
    input  wire rst_n,
    input  wire din,
    output wire dout
);
    reg [7:1] sr;
    assign dout = din ^ sr[7] ^ sr[4];
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) sr <= 7'b0;
        else        sr <= {sr[6:1], dout};
    end
endmodule

module descrambler_mult (
    input  wire clk,
    input  wire rst_n,
    input  wire din,
    output wire dout
);
    reg [7:1] sr;
    assign dout = din ^ sr[7] ^ sr[4];
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) sr <= 7'b0;
        else        sr <= {sr[6:1], din};
    end
endmodule

module tb;
    reg clk, rst_n, data_in;
    wire scr, rec;
    scrambler_mult   U_S (.clk(clk), .rst_n(rst_n), .din(data_in), .dout(scr));
    descrambler_mult U_D (.clk(clk), .rst_n(rst_n), .din(scr),     .dout(rec));
    reg [0:39] stream = 40'b1011001000000000111101010101001100001111;
    integer i, errors, checked;
    initial begin clk = 0; forever #5 clk = ~clk; end
    initial begin
        rst_n = 0; data_in = 0; errors = 0; checked = 0;
        @(negedge clk); rst_n = 1;
        for (i = 0; i < 40; i = i + 1) begin
            data_in = stream[i];
            #1;
            if (i >= 7) begin
                checked = checked + 1;
                if (rec !== data_in) begin
                    errors = errors + 1;
                    $display("FAIL i=%0d  din=%b scr=%b rec=%b", i, data_in, scr, rec);
                end else
                    $display("PASS i=%0d  din=%b scr=%b rec=%b", i, data_in, scr, rec);
            end
            @(posedge clk);
        end
        if (errors == 0 && checked == 33)
            $display("ALL TESTS PASSED (%0d bits recovered after sync)", checked);
        else
            $display("FAIL summary errors=%0d checked=%0d", errors, checked);
        $finish;
    end
endmodule
PASS i=7  din=0 scr=0 rec=0
PASS i=8  din=0 scr=1 rec=0
PASS i=9  din=0 scr=1 rec=0
PASS i=10  din=0 scr=1 rec=0
PASS i=11  din=0 scr=1 rec=0
PASS i=12  din=0 scr=1 rec=0
PASS i=13  din=0 scr=1 rec=0
PASS i=14  din=0 scr=1 rec=0
PASS i=15  din=0 scr=0 rec=0
PASS i=16  din=1 scr=1 rec=1
PASS i=17  din=1 scr=1 rec=1
PASS i=18  din=1 scr=1 rec=1
PASS i=19  din=1 scr=0 rec=1
PASS i=20  din=0 scr=0 rec=0
PASS i=21  din=1 scr=1 rec=1
PASS i=22  din=0 scr=1 rec=0
PASS i=23  din=1 scr=0 rec=1
PASS i=24  din=0 scr=1 rec=0
PASS i=25  din=1 scr=1 rec=1
PASS i=26  din=0 scr=1 rec=0
PASS i=27  din=1 scr=1 rec=1
PASS i=28  din=0 scr=0 rec=0
PASS i=29  din=0 scr=0 rec=0
PASS i=30  din=1 scr=0 rec=1
PASS i=31  din=1 scr=1 rec=1
PASS i=32  din=0 scr=1 rec=0
PASS i=33  din=0 scr=1 rec=0
PASS i=34  din=0 scr=1 rec=0
PASS i=35  din=0 scr=1 rec=0
PASS i=36  din=1 scr=0 rec=1
PASS i=37  din=1 scr=0 rec=1
PASS i=38  din=1 scr=1 rec=1
PASS i=39  din=1 scr=1 rec=1
ALL TESTS PASSED (33 bits recovered after sync)

The log shows that from bit index 7 onward every recovered bit equals the original input bit, printing 33 consecutive PASS lines and ALL TESTS PASSED. Notice the scrambled line (scr) toggles even during the all-zeros input run (indices 2–7), proving the data is whitened, while the descrambler still reconstructs the stream exactly.

The additive scrambler runs a free-running LFSR (same x7+x4+1 polynomial, parameterized seed) to produce a keystream that is XORed with the data. Both ends load the identical SEED, so descrambling is simply a second XOR with the same keystream and there is no sync delay.

module lfsr7 #(
    parameter [7:1] SEED = 7'h7F
) (
    input  wire clk,
    input  wire load,
    output wire ks
);
    reg [7:1] state;
    wire fb = state[7] ^ state[4];
    assign ks = state[7];
    always @(posedge clk) begin
        if (load) state <= SEED;
        else      state <= {state[6:1], fb};
    end
endmodule

module add_scrambler #(parameter [7:1] SEED = 7'h7F) (
    input wire clk, input wire load, input wire din, output wire dout
);
    wire ks;
    lfsr7 #(.SEED(SEED)) U (.clk(clk), .load(load), .ks(ks));
    assign dout = din ^ ks;
endmodule

module tb;
    reg clk, load, data_in;
    wire scr, rec;
    wire ks_mon;
    add_scrambler #(.SEED(7'h5B)) U_S (.clk(clk), .load(load), .din(data_in), .dout(scr));
    add_scrambler #(.SEED(7'h5B)) U_D (.clk(clk), .load(load), .din(scr),     .dout(rec));
    lfsr7 #(.SEED(7'h5B)) U_K (.clk(clk), .load(load), .ks(ks_mon));
    reg [0:31] stream = 32'b11010010000000000000111101011001;
    integer i, errors, ones_out, zero_run_ones;
    initial begin clk = 0; forever #5 clk = ~clk; end
    initial begin
        load = 1; data_in = 0; errors = 0; ones_out = 0; zero_run_ones = 0;
        @(negedge clk);
        @(posedge clk);
        load = 0;
        for (i = 0; i < 32; i = i + 1) begin
            data_in = stream[i];
            #1;
            if (rec !== data_in) begin
                errors = errors + 1;
                $display("FAIL i=%0d din=%b scr=%b rec=%b", i, data_in, scr, rec);
            end else
                $display("PASS i=%0d din=%b scr=%b rec=%b ks=%b", i, data_in, scr, rec, ks_mon);
            if (scr) ones_out = ones_out + 1;
            if (i >= 2 && i <= 17 && scr) zero_run_ones = zero_run_ones + 1;
            @(posedge clk);
        end
        if (errors == 0)
            $display("PASS recovery: all 32 bits descrambled correctly");
        if (zero_run_ones > 0)
            $display("PASS whitening: 16-bit all-zero run produced %0d ones on the wire", zero_run_ones);
        if (errors == 0 && zero_run_ones > 0)
            $display("ALL TESTS PASSED");
        else
            $display("FAIL summary errors=%0d zero_run_ones=%0d", errors, zero_run_ones);
        $finish;
    end
endmodule
PASS i=0 din=1 scr=1 rec=1 ks=0
PASS i=1 din=1 scr=0 rec=1 ks=1
PASS i=2 din=0 scr=1 rec=0 ks=1
PASS i=3 din=1 scr=1 rec=1 ks=0
PASS i=4 din=0 scr=1 rec=0 ks=1
PASS i=5 din=0 scr=1 rec=0 ks=1
PASS i=6 din=1 scr=1 rec=1 ks=0
PASS i=7 din=0 scr=0 rec=0 ks=0
PASS i=8 din=0 scr=0 rec=0 ks=0
PASS i=9 din=0 scr=0 rec=0 ks=0
PASS i=10 din=0 scr=0 rec=0 ks=0
PASS i=11 din=0 scr=1 rec=0 ks=1
PASS i=12 din=0 scr=1 rec=0 ks=1
PASS i=13 din=0 scr=0 rec=0 ks=0
PASS i=14 din=0 scr=0 rec=0 ks=0
PASS i=15 din=0 scr=1 rec=0 ks=1
PASS i=16 din=0 scr=1 rec=0 ks=1
PASS i=17 din=0 scr=0 rec=0 ks=0
PASS i=18 din=0 scr=1 rec=0 ks=1
PASS i=19 din=0 scr=0 rec=0 ks=0
PASS i=20 din=1 scr=0 rec=1 ks=1
PASS i=21 din=1 scr=1 rec=1 ks=0
PASS i=22 din=1 scr=1 rec=1 ks=0
PASS i=23 din=1 scr=0 rec=1 ks=1
PASS i=24 din=0 scr=1 rec=0 ks=1
PASS i=25 din=1 scr=0 rec=1 ks=1
PASS i=26 din=0 scr=0 rec=0 ks=0
PASS i=27 din=1 scr=1 rec=1 ks=0
PASS i=28 din=1 scr=0 rec=1 ks=1
PASS i=29 din=0 scr=1 rec=0 ks=1
PASS i=30 din=0 scr=1 rec=0 ks=1
PASS i=31 din=1 scr=0 rec=1 ks=1
PASS recovery: all 32 bits descrambled correctly
PASS whitening: 16-bit all-zero run produced 9 ones on the wire
ALL TESTS PASSED

The log confirms two independent properties. First, all 32 bits descramble back to the original stream (the rec column matches din on every line). Second, a deliberate 16-bit all-zeros input stretch still produced 9 ones on the scrambled wire — the keystream (ks) whitened the dead run. Both facts yield ALL TESTS PASSED.

Multiplicative vs additive scramblers

The two architectures differ in where the LFSR feedback comes from.

  • Multiplicative (self-synchronizing): the shift register is clocked with the scrambled stream itself. Because both scrambler and descrambler feed on the same transmitted bits, the receiver needs no knowledge of the transmitter's state — it self-synchronizes within one register length. This is the scrambler used in PCIe Gen1/Gen2, SONET/SDH, and many SerDes PHYs.
  • Additive (frame-synchronous): the LFSR free-runs independently of the data and simply generates a keystream. The transmitter and receiver must start from the same seed at the same bit position (usually reset at a frame or block boundary), hence "frame-synchronous." 10GBASE-R and USB use this style.

The LFSR polynomial

Both designs use x7+x4+1, a primitive polynomial of degree 7. "Primitive" means the LFSR cycles through all 27−1 = 127 non-zero states before repeating, giving the longest possible pseudo-random sequence for a 7-bit register. The exponents name the tap positions: feedback is the XOR of the bits at positions 7 and 4.

In the code the state is declared reg [7:1] sr; and the feedback is sr[7] ^ sr[4]. Longer registers (for example the x23+x18+1 used in some standards) give longer runs before repetition; the structure is identical — only the width and tap indices change.

Error multiplication in self-sync descramblers

The self-synchronizing descrambler's convenience has a cost: error multiplication. Each received bit is XORed with two earlier received bits (taps 4 and 7). If a single bit flips on the channel, it corrupts the output when it is the current bit and again each time it later appears at a tap — so one channel error becomes N+1 output errors, where N is the number of feedback taps (here 3 errors per single-bit flip).

The additive scrambler does not multiply errors: a flipped bit on the wire produces exactly one wrong output bit, because the keystream is generated locally and is independent of the received data. The trade-off is that the additive scheme needs reliable seed/frame synchronization, which the self-sync scheme avoids entirely.

Why scrambling helps clock recovery and DC balance

Serial receivers recover the clock from data edges using a CDR (clock-data recovery) PLL. A long run of identical bits has no transitions, so the PLL drifts and the eye closes. Scrambling breaks up these runs statistically, guaranteeing frequent transitions.

Scrambling also improves DC balance — keeping the running count of ones and zeros roughly equal. AC-coupled links and transformers cannot pass a DC offset, so a persistent imbalance causes baseline wander. By whitening the data, the scrambler flattens the spectrum and pushes energy away from a single tone, which also lowers EMI. Note that scrambling is statistical: unlike 8b/10b line coding it does not guarantee a maximum run length, so some standards combine scrambling with a coding layer.

Sync behavior and common mistakes

Synchronization

The self-sync descrambler locks automatically: after L correctly received bits (L = register length = 7 here) its shift register holds the same history as the transmitter, and from then on output equals input. That is exactly why the Example 1 testbench only starts checking at bit index 7. The additive descrambler, by contrast, is correct from the first bit only if its LFSR was loaded with the right seed at the right position — a seed or phase mismatch garbles the entire frame.

Common mistakes

  • Feeding the descrambler's own output back into its register instead of the received line — that turns a self-sync descrambler back into a scrambler.
  • Tap-position off-by-one: indexing sr[8] or sr[0] on a [7:1] register silently breaks the polynomial.
  • Seeding the LFSR to all zeros: an all-zero state is a lock-up state — the register stays at zero forever and produces no keystream. Always load a non-zero seed.
  • Forgetting error multiplication when choosing a self-sync scrambler ahead of an FEC block; the multiplied errors can exceed the code's correction capacity.
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