Shift Register Verilog Code: SISO, SIPO, PISO & Universal

A shift register is just a chain of flip-flops that hands a bit from one stage to the next on every clock edge. This page gives you four working, simulation-tested modules of shift register Verilog code — SISO, SIPO, PISO, and a parameterized universal shift register — each with its own testbench. Every block below was compiled and run with Icarus Verilog using iverilog -g2005 -o sim file.v && vvp sim, and the real simulation log is shown next to the code. Copy a block, run it, and watch the bits move.

The four types at a glance

TypeInputOutputTypical use
SISOSerial (1 bit/clk)SerialFixed delay line
SIPOSerialParallel (all bits)Serial-to-parallel receiver
PISOParallel (load)SerialParallel-to-serial transmitter
UniversalSerial or parallelParallelHold / shift L / shift R / load

1. SISO — Serial-In Serial-Out

The simplest shift register. One bit enters each clock; after WIDTH clocks the same bit leaves the far end. It behaves as a programmable delay line.

// siso.v - 4-bit Serial-In Serial-Out shift register
module siso #(parameter WIDTH = 4) (
    input  wire clk,
    input  wire rst_n,   // active-low synchronous reset
    input  wire din,     // serial data in
    output wire dout     // serial data out
);
    reg [WIDTH-1:0] sr;

    always @(posedge clk) begin
        if (!rst_n)
            sr <= {WIDTH{1'b0}};
        else
            sr <= {sr[WIDTH-2:0], din};  // shift left, din enters LSB
    end

    assign dout = sr[WIDTH-1];           // oldest bit leaves from MSB
endmodule

// -------------------- testbench --------------------
module tb_siso;
    reg clk = 0, rst_n = 0, din = 0;
    wire dout;
    integer i;
    reg [3:0] pattern = 4'b1011;   // pushed in LSB first: 1,1,0,1

    siso #(.WIDTH(4)) dut (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout));

    always #5 clk = ~clk;          // 10 ns clock period

    initial begin
        $display("=== SISO: data appears at dout WIDTH clocks after it enters ===");
        $monitor("t=%0t  din=%b  dout=%b", $time, din, dout);
        @(negedge clk) rst_n = 0;            // hold in reset
        @(negedge clk) rst_n = 1;            // release reset
        for (i = 0; i < 4; i = i + 1) begin
            din = pattern[i];                // feed LSB..MSB
            @(negedge clk);
        end
        din = 0;
        for (i = 0; i < 4; i = i + 1) @(negedge clk);  // flush the pipe
        $display("=== done: the 4 bits came out in the same order they went in ===");
        $finish;
    end
endmodule
=== SISO: data appears at dout WIDTH clocks after it enters ===
t=0  din=0  dout=x
t=5  din=0  dout=0
t=20  din=1  dout=0
t=40  din=0  dout=0
t=50  din=1  dout=0
t=55  din=1  dout=1
t=60  din=0  dout=1
t=75  din=0  dout=0
t=85  din=0  dout=1
t=95  din=0  dout=0
=== done: the 4 bits came out in the same order they went in ===
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp5llo_z9e/siso.v:43: $finish called at 100 (1s)

How it reads, line by line

  • #(parameter WIDTH = 4) makes the length a knob — change it to 8 or 32 without touching the body.
  • reg [WIDTH-1:0] sr; is the flip-flop chain itself.
  • sr <= {sr[WIDTH-2:0], din}; is the whole shift: concatenate the lower bits with the new din, which pushes everything one position toward the MSB. Use the non-blocking <= so all stages update together.
  • assign dout = sr[WIDTH-1]; taps the top bit, the one that has travelled the full length.

2. SIPO — Serial-In Parallel-Out

Same shifting, but instead of tapping one end we expose the entire register as a bus. After WIDTH serial bits arrive, q holds the full word — the classic serial-to-parallel converter (UART receive, SPI capture).

// sipo.v - Serial-In Parallel-Out shift register (4-bit)
module sipo #(parameter WIDTH = 4) (
    input  wire             clk,
    input  wire             rst_n,  // active-low synchronous reset
    input  wire             din,    // one bit per clock
    output reg  [WIDTH-1:0] q       // all bits available in parallel
);
    always @(posedge clk) begin
        if (!rst_n)
            q <= {WIDTH{1'b0}};
        else
            q <= {q[WIDTH-2:0], din}; // new bit enters LSB, rest shift left
    end
endmodule

// -------------------- testbench --------------------
module tb_sipo;
    localparam WIDTH = 4;
    reg clk = 0, rst_n = 0, din = 0;
    wire [WIDTH-1:0] q;
    integer i;
    reg [WIDTH-1:0] stream = 4'b1101;  // sent MSB first: 1,1,0,1

    sipo #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .din(din), .q(q));

    always #5 clk = ~clk;

    initial begin
        $display("=== SIPO: shift 4 serial bits, read parallel q ===");
        $monitor("t=%0t  din=%b  q=%b", $time, din, q);
        @(negedge clk) rst_n = 0;
        @(negedge clk) rst_n = 1;
        for (i = WIDTH-1; i >= 0; i = i - 1) begin
            din = stream[i];
            @(negedge clk);
        end
        $display(">> after %0d clocks, parallel output q = %b (expected %b)",
                 WIDTH, q, stream);
        $finish;
    end
endmodule
=== SIPO: shift 4 serial bits, read parallel q ===
t=0  din=0  q=xxxx
t=5  din=0  q=0000
t=20  din=1  q=0000
t=25  din=1  q=0001
t=35  din=1  q=0011
t=40  din=0  q=0011
t=45  din=0  q=0110
t=50  din=1  q=0110
t=55  din=1  q=1101
>> after 4 clocks, parallel output q = 1101 (expected 1101)
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmped2hhf6d/sipo.v:39: $finish called at 60 (1s)

Notice q is declared output reg because it is assigned inside an always block, and it is the storage — no separate sr needed. The testbench feeds stream MSB first, so the final q matches the original word exactly.

3. PISO — Parallel-In Serial-Out

This one has a mode control. When load is high it grabs all bits in a single clock; when load is low it shifts them out one per clock, MSB first. It is the transmit side of a serializer.

// piso.v - Parallel-In Serial-Out shift register (4-bit)
module piso #(parameter WIDTH = 4) (
    input  wire             clk,
    input  wire             rst_n,  // active-low synchronous reset
    input  wire             load,   // 1 = parallel load, 0 = shift
    input  wire [WIDTH-1:0] din,    // parallel data in
    output wire             dout    // serial data out (MSB first)
);
    reg [WIDTH-1:0] sr;

    always @(posedge clk) begin
        if (!rst_n)
            sr <= {WIDTH{1'b0}};
        else if (load)
            sr <= din;                     // capture all bits at once
        else
            sr <= {sr[WIDTH-2:0], 1'b0};   // shift left, feed 0 behind
    end

    assign dout = sr[WIDTH-1];             // MSB streams out first
endmodule

// -------------------- testbench --------------------
module tb_piso;
    localparam WIDTH = 4;
    reg clk = 0, rst_n = 0, load = 0;
    reg [WIDTH-1:0] din = 0;
    wire dout;
    integer i;

    piso #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n),
                               .load(load), .din(din), .dout(dout));

    always #5 clk = ~clk;

    initial begin
        $display("=== PISO: load 1010 in parallel, read it out serially ===");
        @(negedge clk) rst_n = 0;
        @(negedge clk) rst_n = 1; load = 1; din = 4'b1010;  // load
        @(negedge clk) load = 0;                            // now shift
        $display("loaded din=%b", 4'b1010);
        for (i = 0; i < WIDTH; i = i + 1) begin
            $display("clock %0d: dout = %b", i, dout);
            @(negedge clk);
        end
        $display("=== done (serial stream was MSB-first) ===");
        $finish;
    end
endmodule
=== PISO: load 1010 in parallel, read it out serially ===
loaded din=1010
clock 0: dout = 1
clock 1: dout = 0
clock 2: dout = 1
clock 3: dout = 0
=== done (serial stream was MSB-first) ===
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp_1sgjns8/piso.v:47: $finish called at 70 (1s)

The if (load) ... else ... priority is the heart of it: load wins, otherwise shift. Reset still has top priority so the register always starts known.

4. Universal shift register (parameterized)

A universal register folds every behaviour above into one module selected by a 2-bit mode: hold, shift right, shift left, or parallel load. This is the 74194 in parameterized form — set WIDTH to anything you need.

// univ_shift.v - Parameterized universal shift register
// mode: 00 hold | 01 shift right (sr_in -> MSB) | 10 shift left (sl_in -> LSB) | 11 load
module univ_shift #(parameter WIDTH = 8) (
    input  wire             clk,
    input  wire             rst_n,  // active-low synchronous reset
    input  wire [1:0]       mode,   // operation select
    input  wire             sr_in,  // serial in for right shift
    input  wire             sl_in,  // serial in for left shift
    input  wire [WIDTH-1:0] din,    // parallel load data
    output reg  [WIDTH-1:0] q       // register contents
);
    always @(posedge clk) begin
        if (!rst_n)
            q <= {WIDTH{1'b0}};
        else begin
            case (mode)
                2'b00: q <= q;                        // hold
                2'b01: q <= {sr_in, q[WIDTH-1:1]};    // shift right
                2'b10: q <= {q[WIDTH-2:0], sl_in};    // shift left
                2'b11: q <= din;                      // parallel load
            endcase
        end
    end
endmodule

// -------------------- testbench --------------------
module tb_univ_shift;
    localparam WIDTH = 8;
    reg clk = 0, rst_n = 0, sr_in = 0, sl_in = 0;
    reg [1:0] mode = 2'b00;
    reg [WIDTH-1:0] din = 0;
    wire [WIDTH-1:0] q;

    univ_shift #(.WIDTH(WIDTH)) dut (
        .clk(clk), .rst_n(rst_n), .mode(mode),
        .sr_in(sr_in), .sl_in(sl_in), .din(din), .q(q));

    always #5 clk = ~clk;

    task step(input [1:0] m, input r, input l, input [WIDTH-1:0] d,
              input [8*8:1] name);
        begin
            mode = m; sr_in = r; sl_in = l; din = d;
            @(negedge clk);
            $display("%-6s mode=%b q=%b", name, m, q);
        end
    endtask

    initial begin
        $display("=== Universal %0d-bit shift register ===", WIDTH);
        @(negedge clk) rst_n = 0;
        @(negedge clk) rst_n = 1;
        step(2'b11, 0, 0, 8'b0000_1111, "LOAD");
        step(2'b00, 0, 0, 0,            "HOLD");
        step(2'b01, 1, 0, 0,            "SR");    // 1 enters MSB
        step(2'b01, 0, 0, 0,            "SR");
        step(2'b10, 0, 1, 0,            "SL");    // 1 enters LSB
        step(2'b10, 0, 0, 0,            "SL");
        step(2'b11, 0, 0, 8'hA5,        "LOAD");
        $display("=== done ===");
        $finish;
    end
endmodule
=== Universal 8-bit shift register ===
LOAD   mode=11 q=00001111
HOLD   mode=00 q=00001111
SR     mode=01 q=10000111
SR     mode=01 q=01000011
SL     mode=10 q=10000111
SL     mode=10 q=00001110
LOAD   mode=11 q=10100101
=== done ===
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpq0hv3htl/univ_shift.v:61: $finish called at 90 (1s)

Reading the four modes

  • Shift right {sr_in, q[WIDTH-1:1]}: drop the LSB, push a new bit in at the MSB.
  • Shift left {q[WIDTH-2:0], sl_in}: drop the MSB, push a new bit in at the LSB.
  • Hold keeps q; load overwrites it with din.
  • Because every branch is covered, the case infers clean flip-flops with no accidental latch.

Common mistakes

  • Blocking = instead of non-blocking <= in clocked logic. Blocking assignments can make one stage see the new value of its neighbour in the same clock, collapsing the chain. Always use <= inside @(posedge clk).
  • Off-by-one in the slice. Shift left is {sr[WIDTH-2:0], din}, not sr[WIDTH-1:0]. The WIDTH-2 is what drops the bit that falls off the end.
  • Forgetting direction. “Shift right” moves bits toward the LSB and injects at the MSB. Mixing this up silently reverses your data; check against the simulation log.
  • Incomplete case with no default. If you leave a mode uncovered without covering all values, synthesis infers a latch. Either cover every pattern (as above) or add default: q <= q;.
  • Reset that is not truly synchronous. Here reset is inside the clocked block, so it is synchronous. If you want async reset, move it to the sensitivity list: always @(posedge clk or negedge rst_n).
  • Reading q one clock too early in a testbench. Non-blocking updates settle after the edge — sample on the opposite edge (this is why the testbenches drive on negedge).

Quick check

How many clock cycles until the first bit appears at dout of a WIDTH=4 SISO?

Four. The bit must travel through all four flip-flop stages, so dout reflects an input bit WIDTH clocks after it entered.

Why is SIPO’s q declared output reg while SISO’s dout is a wire?

q is assigned inside an always block and is the register, so it must be reg. SISO’s dout is driven by a continuous assign that only taps the top flip-flop, so it is a wire.

In the universal register, which concatenation performs a shift right, and where does the new bit enter?

q <= {sr_in, q[WIDTH-1:1]}. The new bit sr_in enters at the MSB and the old LSB is discarded as all bits move toward the LSB.

What bug appears if you write q = ... (blocking) instead of q <= ... inside the clocked block?

Stages can see each other’s updated values within the same clock, so bits may jump multiple positions or the chain collapses. Clocked sequential logic must use non-blocking <=.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

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