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.
📘 New to the concept? Read the theory first: Shift Registers: SISO, SIPO, PISO, PIPO.
The four types at a glance
| Type | Input | Output | Typical use |
|---|---|---|---|
| SISO | Serial (1 bit/clk) | Serial | Fixed delay line |
| SIPO | Serial | Parallel (all bits) | Serial-to-parallel receiver |
| PISO | Parallel (load) | Serial | Parallel-to-serial transmitter |
| Universal | Serial or parallel | Parallel | Hold / 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 newdin, 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 withdin. - Because every branch is covered, the
caseinfers 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}, notsr[WIDTH-1:0]. TheWIDTH-2is 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
casewith nodefault. If you leave a mode uncovered without covering all values, synthesis infers a latch. Either cover every pattern (as above) or adddefault: 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
qone clock too early in a testbench. Non-blocking updates settle after the edge — sample on the opposite edge (this is why the testbenches drive onnegedge).
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 <=.
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