RTL Design Patterns · Chapter 2 · Registers & Sequential Building Blocks
Shift Registers (SIPO/PISO/universal)
A wire carries one bit at a time; a register holds a whole word at once. Every serial interface a chip touches, from UART to SPI to JTAG to MDIO, lives on the boundary between those two, and the structure that crosses it is the shift register: a chain of registers where each flop hands its value to its neighbour on every clock edge. Point that chain inward from a pin and you have a SIPO, a deserializer that reassembles a serial stream into a parallel word. Point it outward toward a pin and you have a PISO, a serializer that clocks a parallel word out one bit per cycle. Add a mux on each flop that chooses hold, shift-left, shift-right, or parallel-load, and you have the universal shift register. This lesson also shows the most damaging way two ends of a link disagree, a bit-order mismatch that arrives byte-reversed, built in SystemVerilog, Verilog, and VHDL.
Foundation16 min readRTL Design PatternsShift RegisterSIPOPISOSerializerBit Order
Chapter 2 · Section 2.6 · Registers & Sequential Building Blocks
1. The Engineering Problem
You are bringing up a serial link. Your block has an 8-bit result it must send to another chip, but the two chips are connected by exactly one data wire and a shared clock — there is no room for eight parallel traces, and even if there were, the receiving chip's pin budget could not spare them. So the byte cannot cross as a byte. It has to cross the way every serial protocol crosses data: one bit per clock, MSB first, over that single wire, and be reassembled into a byte on the far side. UART, SPI, JTAG, MDIO, I²C — every one of them is, at its core, this exact problem: a parallel word on one side, a wire in the middle, a parallel word on the other side.
Combinational logic cannot do this, and neither can a plain register. A register (2.1) captures a whole word at an edge and holds it; it has no notion of "move the bits along by one position each cycle." What you need is a structure where, on every clock edge, each bit-position passes its value to its neighbour — so a value entering at one end walks across the word, one position per cycle, and after WIDTH cycles the whole word has either been swallowed in from a wire (deserialized) or clocked out onto a wire (serialized). You also need to build delay lines (a signal delayed by exactly N cycles is N registers in a chain) and LFSRs (a shift register with feedback), both of which are the same chain with a different tap.
That structure is the shift register, and it is the first pattern in Chapter 2 that is not a single register but a chain of registers wired into a datapath — each flop's input coming from its neighbour, its output going to the next. It is where the register of 2.1, the enable/load of 2.2, and the selection of 1.1 stop being separate ideas and compose into one reusable structure.
// A register captures a WHOLE word at the edge — it cannot "walk" bits along:
// always @(posedge clk) q <= d; // 8 bits in, 8 bits held. No motion.
// The link has ONE data wire. The byte must cross one bit per clock:
// TX side: clock 8'hA5 out MSB-first, one bit/cycle -> so (serialize)
// RX side: sample so each cycle, rebuild the byte -> po (deserialize)
// The structure that does it: a CHAIN of registers, each cycle every flop
// copies its neighbour, so a value entering one end walks across the word:
// always @(posedge clk) q <= {q[WIDTH-2:0], si}; // shift-left, fill with si2. Mental Model
3. Pattern Anatomy
The whole family is one register chain with different taps for input and output, plus an optional mode mux.
SIPO — serial-in, parallel-out (the deserializer). One serial input si, a parallel output po that is the whole register, and no serial output that matters. Each edge, the word shifts by one and si fills the vacated end: q <= {q[WIDTH-2:0], si}. After WIDTH clocks, the last WIDTH serial bits are sitting in po as a parallel word. This is how a receiver turns a stream of bits on a pin into a byte — a deserializer. It needs no load: it is always shifting.
PISO — parallel-in, serial-out (the serializer). A parallel input pi, one serial output so, and — critically — a load control. On a load cycle the parallel word is captured into the chain (q <= pi); on every subsequent cycle the chain shifts and the bit falling off the end appears on so (so = q[WIDTH-1] for MSB-first). After the load plus WIDTH-1 shifts, the whole word has been clocked out one bit per cycle. This is how a transmitter turns a byte into a stream on a pin — a serializer. The load is exactly the load mux of 2.2: on the load cycle each flop's D comes from pi; otherwise it comes from its neighbour.
The universal shift register — the mode mux. Combine both directions plus hold plus parallel-load under a 2-bit mode, and you have the universal shift register:
mode = 00— hold (each flop keeps its own value; the disabled-register feedback of 2.2).mode = 01— shift-right (q <= {si_right, q[WIDTH-1:1]}; a bit enters at the MSB end, the LSB falls off).mode = 10— shift-left (q <= {q[WIDTH-2:0], si_left}; a bit enters at the LSB end, the MSB falls off).mode = 11— parallel-load (q <= pi).
That is literally a 4:1 mux (1.1) selecting each flop's next value, feeding the register (2.1), controlled the way an enable/load controls it (2.2). Nothing in the universal shift register is new — it is the first time all three prior patterns appear in one structure.
Shift direction and the fill bit. Every shift vacates one bit-position and drops another. The fill bit is what enters the vacated position: the serial-in bit (si) for a SIPO/PISO on a link, a 0 for a logical shift, the LSB for a rotate, or a feedback XOR for an LFSR. The dropped bit is the serial output. Which end is which is the direction, and it doubles as the bit-order convention: shift-left with so = q[WIDTH-1] sends the MSB first; shift-right with so = q[0] sends the LSB first. This convention is the contract of §7.
The shift-register family — one register chain, three taps, one mode mux
data flowOne discipline runs through all of it, inherited straight from 2.1: the chain is clocked with non-blocking assignment (<= in SV/Verilog, signal <= under rising_edge in VHDL). Because every flop samples the pre-edge neighbour value simultaneously, the whole word shifts by exactly one position per edge, atomically. Write it with blocking = and the chain collapses — that is the 2.1 failure, revisited in §6.
4. Real RTL Implementation
This is the core of the page. We build four patterns — (a) a SIPO deserializer, (b) a PISO serializer with load, (c) the universal shift register (the mode mux), and (d) the bit-order / direction bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking clocked testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.
Two disciplines run through every RTL block. First, everything is clocked with non-blocking assignment so the chain shifts atomically (2.1). Second, every design states its reset explicitly — a synchronous, active-low reset (rst_n) that clears the chain — and, where relevant, its load-vs-shift priority, because a load that a shift can overwrite is a bug (§6).
4a. SIPO — serial-in, parallel-out (the deserializer)
Start with the deserializer. There is no load and no mode: every enabled edge the word shifts left by one and the serial input si fills the LSB. After WIDTH edges, the parallel output po holds the last WIDTH serial bits — MSB-first, because the first bit in has been shifted the farthest toward the MSB.
module sipo #(
parameter int WIDTH = 8 // width of the reassembled word
)(
input logic clk,
input logic rst_n, // synchronous, active-low
input logic en, // shift-enable (hold when 0)
input logic si, // serial input — one bit per cycle
output logic [WIDTH-1:0] po // parallel output = the whole chain
);
// The chain: each edge, every bit moves one position toward the MSB and si
// fills the LSB. The concatenation {q[WIDTH-2:0], si} IS the wiring of each
// flop's D to its neighbour's Q — a bucket brigade, not a slicing trick.
always_ff @(posedge clk) begin
if (!rst_n) po <= '0;
else if (en) po <= {po[WIDTH-2:0], si}; // shift-left, fill LSB with si
end // (else: hold — no clock gating)
endmoduleThe SIPO testbench is inherently clocked: it generates a clock, drives a known serial pattern one bit per cycle for WIDTH cycles (MSB first), then checks that the parallel output equals the original word. This is the serial→parallel half of the round-trip.
module sipo_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n, en, si;
logic [WIDTH-1:0] po;
logic [WIDTH-1:0] word = 8'hA5; // the byte we serialize into the DUT
int errors = 0;
sipo #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .si(si), .po(po));
always #5 clk = ~clk; // 100 MHz test clock
initial begin
rst_n = 0; en = 0; si = 0;
@(posedge clk); rst_n = 1; en = 1; // release reset, start shifting
// Feed the byte MSB-first, one bit per rising edge.
for (int i = WIDTH-1; i >= 0; i--) begin
si = word[i];
@(posedge clk);
end
#1; // settle after the last edge
// Invariant: after WIDTH shifts, po reconstructs the serialized word.
assert (po === word)
else begin $error("SIPO: po=%h exp=%h", po, word); errors++; end
if (errors == 0) $display("PASS: SIPO reconstructed %h", word);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent — reg/wire typing, always @(posedge clk), the same concatenation. The reset and enable behave exactly the same.
module sipo #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
input wire si,
output reg [WIDTH-1:0] po
);
always @(posedge clk) begin
if (!rst_n) po <= {WIDTH{1'b0}};
else if (en) po <= {po[WIDTH-2:0], si}; // shift-left, fill LSB
end
endmodule module sipo_tb;
parameter WIDTH = 8;
reg clk, rst_n, en, si;
wire [WIDTH-1:0] po;
reg [WIDTH-1:0] word;
integer i, errors;
sipo #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .si(si), .po(po));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0; word = 8'hA5;
rst_n = 0; en = 0; si = 0;
@(posedge clk); rst_n = 1; en = 1;
for (i = WIDTH-1; i >= 0; i = i - 1) begin
si = word[i]; // MSB-first
@(posedge clk);
end
#1;
if (po !== word) begin
$display("FAIL: SIPO po=%h exp=%h", po, word);
errors = errors + 1;
end
if (errors == 0) $display("PASS: SIPO reconstructed %h", word);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the chain is a concatenation with the & operator inside rising_edge(clk). po(WIDTH-2 downto 0) & si is the exact counterpart of the SV/Verilog concatenation — same bucket brigade, VHDL spelling.
library ieee;
use ieee.std_logic_1164.all;
entity sipo is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
rst_n : in std_logic; -- synchronous, active-low
en : in std_logic; -- shift-enable
si : in std_logic; -- serial input
po : out std_logic_vector(WIDTH-1 downto 0) -- parallel output
);
end entity;
architecture rtl of sipo is
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
q <= (others => '0');
elsif en = '1' then
q <= q(WIDTH-2 downto 0) & si; -- shift-left, fill LSB
end if;
end if;
end process;
po <= q;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity sipo_tb is
end entity;
architecture sim of sipo_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n, en, si : std_logic := '0';
signal po : std_logic_vector(WIDTH-1 downto 0);
constant word : std_logic_vector(WIDTH-1 downto 0) := x"A5";
begin
dut : entity work.sipo generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, en => en, si => si, po => po);
clk <= not clk after 5 ns; -- clock generator
stim : process
begin
rst_n <= '0'; en <= '0'; si <= '0';
wait until rising_edge(clk);
rst_n <= '1'; en <= '1';
for i in WIDTH-1 downto 0 loop -- MSB-first
si <= word(i);
wait until rising_edge(clk);
end loop;
wait for 1 ns;
assert po = word
report "SIPO mismatch: po /= serialized word" severity error;
report "SIPO self-check complete" severity note;
wait;
end process;
end architecture;4b. PISO — parallel-in, serial-out (the serializer, with load)
Now the serializer. Unlike the SIPO it needs a load control: on a load cycle it captures the parallel word (q <= pi); on every other enabled cycle it shifts and the bit falling off the MSB end appears on so. The load-vs-shift decision is a mux on each flop's D — the load mux of 2.2 — and its priority matters: load must win over shift, or a shift on the same cycle overwrites the word you just loaded (§6).
module piso #(
parameter int WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic load, // capture pi this cycle (priority over shift)
input logic en, // shift-enable when not loading
input logic [WIDTH-1:0] pi, // parallel word to serialize
output logic so // serial output = the dropped MSB
);
logic [WIDTH-1:0] q;
always_ff @(posedge clk) begin
if (!rst_n) q <= '0;
else if (load) q <= pi; // LOAD WINS over shift
else if (en) q <= {q[WIDTH-2:0], 1'b0}; // shift-left, drop MSB
end
assign so = q[WIDTH-1]; // MSB-first: top bit leaves first
endmoduleThe PISO testbench loads a known word, then clocks WIDTH shifts and checks the serial stream bit-by-bit against the expected MSB-first order. This is the parallel→serial half of the round-trip, and it is what the SIPO of 4a would have to receive.
module piso_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n, load, en;
logic [WIDTH-1:0] pi, word = 8'h3C;
logic so;
int errors = 0;
piso #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .load(load), .en(en), .pi(pi), .so(so));
always #5 clk = ~clk;
initial begin
rst_n = 0; load = 0; en = 0; pi = word;
@(posedge clk); rst_n = 1;
load = 1; @(posedge clk); // capture the word
load = 0; en = 1; // now shift it out
// After the load edge, so already presents the MSB; each edge advances.
for (int i = WIDTH-1; i >= 0; i--) begin
#1; // sample so before the next edge
assert (so === word[i])
else begin $error("PISO bit %0d: so=%b exp=%b", i, so, word[i]); errors++; end
@(posedge clk);
end
if (errors == 0) $display("PASS: PISO streamed %h MSB-first", word);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog PISO is the same with reg typing. Note the if (load) … else if (en) chain: the ordering is the load-over-shift priority, and getting that order right is the whole load discipline.
module piso #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire load,
input wire en,
input wire [WIDTH-1:0] pi,
output wire so
);
reg [WIDTH-1:0] q;
always @(posedge clk) begin
if (!rst_n) q <= {WIDTH{1'b0}};
else if (load) q <= pi; // LOAD WINS
else if (en) q <= {q[WIDTH-2:0], 1'b0}; // shift-left, drop MSB
end
assign so = q[WIDTH-1]; // MSB-first
endmodule module piso_tb;
parameter WIDTH = 8;
reg clk, rst_n, load, en;
reg [WIDTH-1:0] pi, word;
wire so;
integer i, errors;
piso #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .load(load), .en(en), .pi(pi), .so(so));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0; word = 8'h3C; pi = word;
rst_n = 0; load = 0; en = 0;
@(posedge clk); rst_n = 1;
load = 1; @(posedge clk);
load = 0; en = 1;
for (i = WIDTH-1; i >= 0; i = i - 1) begin
#1; // MSB-first: so should equal word[i]
if (so !== word[i]) begin
$display("FAIL: PISO bit %0d so=%b exp=%b", i, so, word[i]);
errors = errors + 1;
end
@(posedge clk);
end
if (errors == 0) $display("PASS: PISO streamed %h MSB-first", word);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the load-vs-shift priority is the if/elsif order inside rising_edge(clk), and so is a concurrent read of the top bit. Same structure, VHDL spelling.
library ieee;
use ieee.std_logic_1164.all;
entity piso is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
rst_n : in std_logic;
load : in std_logic; -- capture pi (priority)
en : in std_logic;
pi : in std_logic_vector(WIDTH-1 downto 0);
so : out std_logic -- serial output (dropped MSB)
);
end entity;
architecture rtl of piso is
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
q <= (others => '0');
elsif load = '1' then
q <= pi; -- LOAD WINS
elsif en = '1' then
q <= q(WIDTH-2 downto 0) & '0'; -- shift-left, drop MSB
end if;
end if;
end process;
so <= q(WIDTH-1); -- MSB-first
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity piso_tb is
end entity;
architecture sim of piso_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n, load, en, so : std_logic := '0';
signal pi : std_logic_vector(WIDTH-1 downto 0);
constant word : std_logic_vector(WIDTH-1 downto 0) := x"3C";
begin
dut : entity work.piso generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, load => load, en => en, pi => pi, so => so);
clk <= not clk after 5 ns;
stim : process
begin
pi <= word; rst_n <= '0'; load <= '0'; en <= '0';
wait until rising_edge(clk);
rst_n <= '1';
load <= '1'; wait until rising_edge(clk); -- capture the word
load <= '0'; en <= '1';
for i in WIDTH-1 downto 0 loop -- MSB-first stream
wait for 1 ns;
assert so = word(i)
report "PISO mismatch: so /= word(i)" severity error;
wait until rising_edge(clk);
end loop;
report "PISO self-check complete" severity note;
wait;
end process;
end architecture;4c. The universal shift register — the mode mux
Combine both directions plus hold plus parallel-load under a 2-bit mode, and the SIPO and PISO are just two special cases. The body is a case (mode) — a 4:1 mux (1.1) selecting each flop's next value — feeding the clocked chain (2.1). si_l fills the LSB on a left shift; si_r fills the MSB on a right shift; pi is the parallel-load source.
module usr #(
parameter int WIDTH = 8
)(
input logic clk,
input logic rst_n,
input logic [1:0] mode, // 00 hold, 01 shift-R, 10 shift-L, 11 load
input logic si_l, // fill bit for shift-LEFT (enters LSB)
input logic si_r, // fill bit for shift-RIGHT (enters MSB)
input logic [WIDTH-1:0] pi, // parallel-load source
output logic [WIDTH-1:0] q // parallel output (also taps for serial)
);
// The mode mux: a 4:1 select (1.1) on each flop's D input, clocked (2.1).
always_ff @(posedge clk) begin
if (!rst_n) q <= '0;
else unique case (mode)
2'b00: q <= q; // HOLD
2'b01: q <= {si_r, q[WIDTH-1:1]}; // shift-RIGHT: MSB<-si_r, LSB drops
2'b10: q <= {q[WIDTH-2:0], si_l}; // shift-LEFT: LSB<-si_l, MSB drops
2'b11: q <= pi; // parallel LOAD
endcase
end
endmoduleThe universal-SR testbench drives the DUT through each mode and asserts the result: parallel-load a word, hold and confirm it is unchanged, shift-left one step and check the expected bit-slide, shift-right one step and check the other direction. Each mode is a directed check with a reference model.
module usr_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n;
logic [1:0] mode;
logic si_l, si_r;
logic [WIDTH-1:0] pi, q;
int errors = 0;
usr #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .mode(mode),
.si_l(si_l), .si_r(si_r), .pi(pi), .q(q));
always #5 clk = ~clk;
task step(input [1:0] m); mode = m; @(posedge clk); #1; endtask
initial begin
rst_n = 0; mode = 2'b00; si_l = 0; si_r = 0; pi = 8'h96;
@(posedge clk); rst_n = 1;
step(2'b11); // LOAD 0x96
assert (q === 8'h96) else begin $error("load: q=%h", q); errors++; end
step(2'b00); // HOLD
assert (q === 8'h96) else begin $error("hold: q=%h", q); errors++; end
si_l = 1; step(2'b10); // shift-LEFT, fill LSB=1
assert (q === {8'h96[6:0], 1'b1}) else begin $error("shl: q=%h", q); errors++; end
pi = 8'h96; step(2'b11); si_r = 0; step(2'b01); // reload, then shift-RIGHT, fill MSB=0
assert (q === {1'b0, 8'h96[7:1]}) else begin $error("shr: q=%h", q); errors++; end
if (errors == 0) $display("PASS: USR all four modes correct");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog universal SR uses the same case (mode) with concatenation. Verilog cannot slice a literal inline, so the testbench precomputes the expected values — the engineering content is unchanged.
module usr #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire [1:0] mode,
input wire si_l,
input wire si_r,
input wire [WIDTH-1:0] pi,
output reg [WIDTH-1:0] q
);
always @(posedge clk) begin
if (!rst_n) q <= {WIDTH{1'b0}};
else case (mode)
2'b00: q <= q; // HOLD
2'b01: q <= {si_r, q[WIDTH-1:1]}; // shift-RIGHT
2'b10: q <= {q[WIDTH-2:0], si_l}; // shift-LEFT
2'b11: q <= pi; // LOAD
default: q <= q; // defined: hold on illegal mode
endcase
end
endmodule module usr_tb;
parameter WIDTH = 8;
reg clk, rst_n;
reg [1:0] mode;
reg si_l, si_r;
reg [WIDTH-1:0] pi;
wire [WIDTH-1:0] q;
reg [WIDTH-1:0] exp;
integer errors;
usr #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .mode(mode),
.si_l(si_l), .si_r(si_r), .pi(pi), .q(q));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0;
rst_n = 0; mode = 2'b00; si_l = 0; si_r = 0; pi = 8'h96;
@(posedge clk); rst_n = 1;
mode = 2'b11; @(posedge clk); #1; // LOAD 0x96
if (q !== 8'h96) begin $display("FAIL load q=%h", q); errors=errors+1; end
mode = 2'b00; @(posedge clk); #1; // HOLD
if (q !== 8'h96) begin $display("FAIL hold q=%h", q); errors=errors+1; end
si_l = 1; mode = 2'b10; @(posedge clk); #1; // shift-LEFT, fill LSB=1
exp = {8'h96 << 1} | 8'h01;
if (q !== exp) begin $display("FAIL shl q=%h exp=%h", q, exp); errors=errors+1; end
pi = 8'h96; mode = 2'b11; @(posedge clk); #1; // reload
si_r = 0; mode = 2'b01; @(posedge clk); #1; // shift-RIGHT, fill MSB=0
exp = 8'h96 >> 1;
if (q !== exp) begin $display("FAIL shr q=%h exp=%h", q, exp); errors=errors+1; end
if (errors == 0) $display("PASS: USR all four modes correct");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the mode mux is a case mode is inside rising_edge(clk), with & for the fill bit. The when others => arm gives a defined behaviour on any illegal mode — VHDL's exhaustiveness discipline applied to a clocked block.
library ieee;
use ieee.std_logic_1164.all;
entity usr is
generic ( WIDTH : positive := 8 );
port (
clk : in std_logic;
rst_n : in std_logic;
mode : in std_logic_vector(1 downto 0); -- 00 hold,01 shr,10 shl,11 load
si_l : in std_logic; -- fill for shift-LEFT (LSB)
si_r : in std_logic; -- fill for shift-RIGHT (MSB)
pi : in std_logic_vector(WIDTH-1 downto 0);
q : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of usr is
signal r : std_logic_vector(WIDTH-1 downto 0);
begin
process (clk)
begin
if rising_edge(clk) then
if rst_n = '0' then
r <= (others => '0');
else
case mode is
when "00" => r <= r; -- HOLD
when "01" => r <= si_r & r(WIDTH-1 downto 1); -- shift-RIGHT
when "10" => r <= r(WIDTH-2 downto 0) & si_l; -- shift-LEFT
when "11" => r <= pi; -- LOAD
when others => r <= r; -- defined default
end case;
end if;
end if;
end process;
q <= r;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity usr_tb is
end entity;
architecture sim of usr_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n, si_l, si_r : std_logic := '0';
signal mode : std_logic_vector(1 downto 0) := "00";
signal pi, q : std_logic_vector(WIDTH-1 downto 0);
constant W : std_logic_vector(WIDTH-1 downto 0) := x"96";
begin
dut : entity work.usr generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, mode => mode,
si_l => si_l, si_r => si_r, pi => pi, q => q);
clk <= not clk after 5 ns;
stim : process
begin
pi <= W; rst_n <= '0';
wait until rising_edge(clk);
rst_n <= '1';
mode <= "11"; wait until rising_edge(clk); wait for 1 ns; -- LOAD
assert q = W report "USR load mismatch" severity error;
mode <= "00"; wait until rising_edge(clk); wait for 1 ns; -- HOLD
assert q = W report "USR hold mismatch" severity error;
si_l <= '1'; mode <= "10"; wait until rising_edge(clk); wait for 1 ns; -- shift-LEFT
assert q = (W(WIDTH-2 downto 0) & '1') report "USR shl mismatch" severity error;
pi <= W; mode <= "11"; wait until rising_edge(clk); wait for 1 ns; -- reload
si_r <= '0'; mode <= "01"; wait until rising_edge(clk); wait for 1 ns; -- shift-RIGHT
assert q = ('0' & W(WIDTH-1 downto 1)) report "USR shr mismatch" severity error;
report "USR self-check complete" severity note;
wait;
end process;
end architecture;4d. The bit-order / direction bug — buggy vs fixed, in all three HDLs
Pattern (d) is the signature failure, shown here as buggy vs fixed RTL, then dramatized narratively in §7. The bug is a contract mismatch across two ends: a PISO serializer sends MSB-first (so = q[WIDTH-1], shift-left), but the receiving SIPO deserializes as if the stream were LSB-first (it fills the MSB and shifts right). Each block is internally correct; together they reverse every byte. The fix is to make both ends shift the same direction so the bit-order convention matches.
// TX (given, correct): PISO sends MSB-first — shift-left, so = top bit.
// assign so = q[WIDTH-1]; q <= {q[WIDTH-2:0], 1'b0};
// BUGGY RX: this SIPO deserializes as if the stream were LSB-first — it fills
// the MSB and shifts RIGHT, so the first (MSB) bit ends up in the LSB.
// Result: every received byte is BIT-REVERSED versus what TX sent.
module sipo_rx_bad #(parameter int WIDTH = 8)(
input logic clk, rst_n, en, si,
output logic [WIDTH-1:0] po
);
always_ff @(posedge clk)
if (!rst_n) po <= '0;
else if (en) po <= {si, po[WIDTH-1:1]}; // shift-RIGHT: WRONG for an MSB-first TX
endmodule
// FIXED RX: match the TX direction — shift-LEFT, fill the LSB. Now the first
// serial bit (the MSB) walks to the MSB position, bit-order preserved.
module sipo_rx_good #(parameter int WIDTH = 8)(
input logic clk, rst_n, en, si,
output logic [WIDTH-1:0] po
);
always_ff @(posedge clk)
if (!rst_n) po <= '0;
else if (en) po <= {po[WIDTH-2:0], si}; // shift-LEFT: matches MSB-first TX
endmoduleThe testbench serializes a known byte MSB-first (playing the TX) into the RX and checks the reconstructed word. Bind it to sipo_rx_good and it passes; bind it to sipo_rx_bad and it fails on any non-palindrome byte — the value comes out bit-reversed.
module order_bug_tb;
localparam int WIDTH = 8;
logic clk = 0, rst_n, en, si;
logic [WIDTH-1:0] po;
logic [WIDTH-1:0] word = 8'hB2; // NON-palindrome: reveals bit-reversal
int errors = 0;
// Bind to sipo_rx_good to PASS; to sipo_rx_bad to observe the reversal.
sipo_rx_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .si(si), .po(po));
always #5 clk = ~clk;
initial begin
rst_n = 0; en = 0; si = 0;
@(posedge clk); rst_n = 1; en = 1;
for (int i = WIDTH-1; i >= 0; i--) begin // TX drives MSB-first
si = word[i];
@(posedge clk);
end
#1;
// A matched RX reconstructs word; a wrong-direction RX yields its reverse.
assert (po === word)
else begin $error("bit-order: po=%h exp=%h (reversed?)", po, word); errors++; end
if (errors == 0) $display("PASS: bit-order matched, po=%h", po);
else $display("FAIL: %0d mismatches (direction/bit-order)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg typing. A palindrome byte like 8'h00 or 8'hFF would hide the bug — only a non-palindrome (8'hB2) exposes the reversal, which is exactly why the test pattern choice matters.
// BUGGY RX: shift-RIGHT while TX sends MSB-first → received byte is reversed.
module sipo_rx_bad #(parameter WIDTH = 8)(
input wire clk, rst_n, en, si,
output reg [WIDTH-1:0] po
);
always @(posedge clk)
if (!rst_n) po <= {WIDTH{1'b0}};
else if (en) po <= {si, po[WIDTH-1:1]}; // WRONG direction for MSB-first TX
endmodule
// FIXED RX: shift-LEFT, fill LSB — matches the MSB-first TX, bit-order preserved.
module sipo_rx_good #(parameter WIDTH = 8)(
input wire clk, rst_n, en, si,
output reg [WIDTH-1:0] po
);
always @(posedge clk)
if (!rst_n) po <= {WIDTH{1'b0}};
else if (en) po <= {po[WIDTH-2:0], si}; // matched direction
endmodule module order_bug_tb;
parameter WIDTH = 8;
reg clk, rst_n, en, si;
wire [WIDTH-1:0] po;
reg [WIDTH-1:0] word;
integer i, errors;
// Bind to _good to PASS; to _bad to see the bit-reversal.
sipo_rx_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst_n(rst_n), .en(en), .si(si), .po(po));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0; word = 8'hB2; // non-palindrome
rst_n = 0; en = 0; si = 0;
@(posedge clk); rst_n = 1; en = 1;
for (i = WIDTH-1; i >= 0; i = i - 1) begin
si = word[i]; // MSB-first
@(posedge clk);
end
#1;
if (po !== word) begin
$display("FAIL: bit-order po=%h exp=%h (reversed?)", po, word);
errors = errors + 1;
end
if (errors == 0) $display("PASS: bit-order matched, po=%h", po);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the buggy RX shifts right (si & po(WIDTH-1 downto 1)) against an MSB-first TX; the fixed RX shifts left (po(WIDTH-2 downto 0) & si) to match. The bug and fix are the direction of the & concatenation — the fill end.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY RX: shifts RIGHT while TX sends MSB-first → byte arrives bit-reversed.
entity sipo_rx_bad is
generic ( WIDTH : positive := 8 );
port ( clk, rst_n, en, si : in std_logic;
po : out std_logic_vector(WIDTH-1 downto 0) );
end entity;
architecture rtl of sipo_rx_bad is
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
process (clk) begin
if rising_edge(clk) then
if rst_n = '0' then q <= (others => '0');
elsif en = '1' then q <= si & q(WIDTH-1 downto 1); -- shift-RIGHT (wrong)
end if;
end if;
end process;
po <= q;
end architecture;
-- FIXED RX: shift-LEFT, fill LSB — matches the MSB-first TX.
entity sipo_rx_good is
generic ( WIDTH : positive := 8 );
port ( clk, rst_n, en, si : in std_logic;
po : out std_logic_vector(WIDTH-1 downto 0) );
end entity;
architecture rtl of sipo_rx_good is
signal q : std_logic_vector(WIDTH-1 downto 0);
begin
process (clk) begin
if rising_edge(clk) then
if rst_n = '0' then q <= (others => '0');
elsif en = '1' then q <= q(WIDTH-2 downto 0) & si; -- shift-LEFT (matched)
end if;
end if;
end process;
po <= q;
end architecture; library ieee;
use ieee.std_logic_1164.all;
entity order_bug_tb is
end entity;
architecture sim of order_bug_tb is
constant WIDTH : positive := 8;
signal clk : std_logic := '0';
signal rst_n, en, si : std_logic := '0';
signal po : std_logic_vector(WIDTH-1 downto 0);
constant word : std_logic_vector(WIDTH-1 downto 0) := x"B2"; -- non-palindrome
begin
-- Bind to sipo_rx_good to PASS; to sipo_rx_bad to observe the reversal.
dut : entity work.sipo_rx_good generic map (WIDTH => WIDTH)
port map (clk => clk, rst_n => rst_n, en => en, si => si, po => po);
clk <= not clk after 5 ns;
stim : process
begin
rst_n <= '0'; en <= '0'; si <= '0';
wait until rising_edge(clk);
rst_n <= '1'; en <= '1';
for i in WIDTH-1 downto 0 loop -- TX MSB-first
si <= word(i);
wait until rising_edge(clk);
end loop;
wait for 1 ns;
assert po = word
report "bit-order mismatch: po /= word (reversed?)" severity error;
report "order_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: a shift register's direction and bit-order are a contract — both ends of a serial link must shift the same way, or every word arrives reversed.
5. Verification Strategy
A shift register is clocked, so — unlike the mux of 1.1 — its correctness is not a truth table you sweep instantly; it is a cycle-by-cycle round-trip you must clock through. The single invariant that captures a correct shift register is:
A word serialized out one end and deserialized back in the other end, with matched direction, reconstructs the original word bit-for-bit — and each intermediate cycle shows exactly one bit-position of motion.
Everything below is that invariant, made checkable, and it maps directly onto the clocked testbenches in §4.
- The serial↔parallel round-trip (self-checking). The core check is the round-trip: drive a known word into a SIPO one bit per cycle (MSB-first) and assert the parallel output equals the word after
WIDTHcycles (thesipo_tbof §4a); load a known word into a PISO and assert the serial output equalsword[i]on each successive cycle (thepiso_tbof §4b). Chain a PISO into a SIPO with matched direction and the output must equal the input — that composed round-trip is the strongest single check, and it is exactly what §4d's testbench exercises. - Per-mode assertion for the universal SR. Drive the universal shift register through each mode with a reference model: parallel-load a word and assert
q == pi; hold and assertqis unchanged; shift-left one step and assertq == {q_prev[WIDTH-2:0], si_l}; shift-right one step and assertq == {si_r, q_prev[WIDTH-1:1]}. Theusr_tbin §4c does exactly this — one directed check per mode, each against a computed expected value. - Load-vs-shift priority as an invariant. For the PISO/universal SR, the property "a load on cycle k is not overwritten by a shift on cycle k" must hold — load wins. Test it directly: assert
loadandensimultaneously and confirm the loaded word survives (q == pi, not a shiftedpi). An inverted priority is the §6 mistake, and this is the check that catches it. - Bit-order with a non-palindrome pattern. A round-trip that only ever uses palindrome bytes (
8'h00,8'hFF,8'h81) cannot catch a direction/bit-order mismatch — the reversed word equals the original. Verification must use a non-palindrome test pattern (8'hB2,8'hA5) so a reversal is visible. This is precisely why §7's bug "passes a palindrome test-pattern, fails on real data": the test vector choice is part of the check. - Parameter-generic verification. A
WIDTH-generic shift register must be verified generic, not assumed generic. Re-run the round-trip forWIDTH = 4, 8, 16(and an odd width like5) so an off-by-one on the fill/last bit — a chain that shiftsWIDTH-1times instead ofWIDTH, or fills the wrong end at the boundary — surfaces. A form correct atWIDTH=8can be off-by-one atWIDTH=5. - Expected waveform. On a waveform, a correct SIPO shows
pogaining one bit of the incoming pattern per rising edge, the word "filling up" from one end; a correct PISO showssopresenting successivewordbits, one per edge, MSB-first; a correct universal SR showsqsliding by exactly one position on each shift edge and standing still on a hold. Aqthat moves by two positions per edge (or collapses to the input) is the blocking-assignment failure of §6; apothat fills up reversed is the §7 bit-order bug.
6. Common Mistakes
Shift direction / bit-order swapped (MSB-first vs LSB-first mismatch). The signature shift-register bug. A serializer sends MSB-first (shift-left, so = q[WIDTH-1]) but the deserializer reads LSB-first (shift-right, fill the MSB), or the universal SR's shift-left and shift-right modes are wired backwards. Each block is internally consistent, so each passes its own unit test — but composed on a link they disagree, and every word arrives bit-reversed. It hides behind palindrome test patterns and appears only on real data. Direction and bit-order are a contract: fix it by making both ends shift the same way, and always test with a non-palindrome pattern. (Full post-mortem in §7; buggy-vs-fixed RTL in §4d.)
Load-vs-shift priority wrong. In a PISO or universal SR the update logic must decide, on a cycle where both load and shift-enable are asserted, which wins. Write if (en) … else if (load) … and the shift swallows the word you meant to load — the parallel data never enters the chain, or enters already shifted by one. The correct order is load first: if (load) q <= pi; else if (en) q <= shifted;. The mirror bug — a load that a reset overrides unintentionally, or a shift that a stale load keeps clobbering — is the same priority mistake. Order the if/elsif chain so the intended winner is checked first, and test both controls asserted together.
Blocking = collapsing the chain. A shift register is a bank of registers that must all sample their neighbour's pre-edge value simultaneously (2.1). Written with non-blocking <=, the whole chain shifts one position atomically. Written with blocking = in a clocked block — q[1] = q[0]; q[2] = q[1]; — each statement sees the just-updated value of the previous one, so a bit races all the way down the chain in a single edge and the multi-stage shift register collapses to one stage. Same code shape as a correct register bank, catastrophically different hardware. Always use <= in the clocked shift process; this is the 2.1 discipline, and a shift register is where its violation is most visible.
Off-by-one on the fill / last bit. A WIDTH-bit word needs WIDTH serial cycles to fully serialize or deserialize — not WIDTH-1. Shift one time too few and the last bit never makes it (the MSB is left in the chain, or the final serial bit is never emitted); shift one too many and you push in a stale fill bit and lose the first real bit. The same boundary error hides in the fill: filling the wrong end on the last cycle, or asserting the load one cycle late so the first shifted bit is garbage. Count the cycles deliberately — WIDTH shifts, indexed WIDTH-1 down to 0 — and verify at an odd WIDTH where the off-by-one cannot hide behind a power-of-two coincidence.
Reset that misses, or the wrong reset value. A shift register carrying a link's framing must power up to a defined value, or the first few "received" words are garbage X that only clears after WIDTH shifts. State the reset explicitly (synchronous or async, active level) and clear the chain to a known pattern; do not rely on the incoming stream to flush out the unknown state, because until it does, the deserialized words are wrong.
7. DebugLab
The link that only worked on test patterns — a serializer and deserializer that disagree on bit-order
The engineering lesson: a shift register's shift direction and bit-order are a contract, not an implementation detail — and a contract is only honoured if both ends agree on it. The tell is a bug that passes symmetric test data and fails on real, asymmetric data: bit-reversal is invisible to palindromes, so a test suite full of 0x00/0xFF/0x81 will bless a broken link. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: make the serializer and deserializer shift the same way and write the MSB-first-vs-LSB-first convention into the interface spec both cite, and verify the round-trip with a non-palindrome pattern so any reversal is caught the first time.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "it's registers plus a mode mux, and direction is a contract" habit.
Exercise 1 — Classify the structure
For each requirement, state whether you would build a SIPO, a PISO, or a universal shift register, and why in one line: (a) the receive path of a UART that turns the incoming serial line into parallel bytes; (b) the transmit path of an SPI master that clocks a byte out on MOSI; (c) a general-purpose block that must sometimes load a word, sometimes shift it left, sometimes shift it right, and sometimes hold it under software control; (d) a 4-cycle delay line on a single-bit signal. (Hint: which need a load, and which need a mode mux?)
Exercise 2 — Predict the collapse
An engineer writes a 4-stage shift register in a clocked block as q1 = q0; q2 = q1; q3 = q2; using blocking assignment. Describe (i) what the synthesized hardware actually becomes versus the 4-flop chain intended, (ii) exactly why the blocking semantics cause it, referencing the pre-edge/post-edge sampling of 2.1, and (iii) how the same four lines with <= produce the correct chain. Then state what a waveform of the buggy version looks like when you drive a single 1 into q0.
Exercise 3 — Break the link on paper
A PISO serializer sends MSB-first. A colleague's SIPO deserializer shifts right and fills the MSB. State (i) precisely what relationship holds between the transmitted byte and the received byte, (ii) which test bytes would pass end-to-end despite the bug and why, and (iii) the single non-palindrome byte you would use to expose it, showing what value it reconstructs to. Then give the one-line RTL change to the RX that fixes it, and name where the bit-order convention should be documented.
Exercise 4 — Count the cycles
You must serialize a WIDTH-bit word out of a PISO and deserialize it back into a SIPO, matched direction. State (i) exactly how many clock cycles the full round-trip takes from "load the PISO" to "the word is valid in the SIPO's parallel output", (ii) what goes wrong if you sample the SIPO one cycle too early or one too late, and (iii) why you would run this test at an odd WIDTH (say 5) as well as WIDTH = 8. (Hint: an off-by-one hides behind a power-of-two.)
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Up/Down Counters — Chapter 3; a counter is registers plus an adder, the sequential sibling of the register-plus-mux shift register — and a shift-register ring counter is a counter built from this exact chain.
- UART Case Study — Chapter 13; the capstone that composes this shift register with an FSM and a baud-rate timer into a full serial transmitter/receiver.
- SPI Master Case Study — Chapter 13; two shift registers clocked into a ring for a full-duplex byte exchange — the shift register in its most important protocol role.
In-track dependencies (this pattern is built from these):
- The Register Pattern (D-FF) — Chapter 2.1; each stage of the chain is exactly this flop, and the non-blocking discipline that keeps the chain from collapsing comes from here.
- Enables & Loads — Chapter 2.2; the load control on a PISO and the mode mux on a universal SR are the load/feedback muxes taught here.
- Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the mode mux that selects hold/shift-left/shift-right/load in front of each flop's D input is a 4:1 mux.
- Synchronous Reset — Chapter 2.3; how the
rst_nthat clears every chain in this page is written and why it must be sampled by a running clock.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control lenses this page applies to serialize/deserialize (a shift register is state + datapath + a control mux).
- What Is an RTL Design Pattern? — Chapter 0.1; why the shift register earns the name — a recurring problem (serialize/deserialize), a reusable form (the mode mux), and a signature failure (bit-order).
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Shift Operators — the
<</>>operators and how they relate to a structural shift-register chain. - Concatenation — the
{q[WIDTH-2:0], si}idiom that wires each flop's D to its neighbour and the fill bit. - Blocking and Non-Blocking Assignments — why the chain must use
<=, and how blocking=collapses a multi-stage shift register. - Case Statements — the
case (mode)behind the universal shift register's mode mux. - Generate Blocks — the elaboration-time replication used to parameterize a
WIDTH-generic chain.
11. Summary
- A shift register is a chain of registers, each copying its neighbour every clock. It is
WIDTHD flip-flops (2.1) wired D-to-neighbour-Q; the concatenationq <= {q[WIDTH-2:0], si}is that wiring, plus the fill bit at the vacated end. Same silicon as aWIDTH-bit register — the difference is entirely in where each flop's D comes from. - SIPO deserializes, PISO serializes. A SIPO (serial-in, parallel-out) shifts one bit per cycle and reassembles a word — the receive side, no load needed. A PISO (parallel-in, serial-out) loads a word then shifts it out one bit per cycle — the transmit side, load required. Together they cross a one-wire serial link (UART, SPI, JTAG).
- The universal shift register is a mode mux on each flop's D. A 4:1 mux (1.1) selects hold / shift-left / shift-right / parallel-load, feeding the register (2.1), gated like an enable/load (2.2). SIPO and PISO are special cases with the mode fixed — nothing new, just composition made explicit.
- Direction and bit-order are a contract both ends must share. MSB-first vs LSB-first is set by the shift direction and output tap; if the serializer and deserializer disagree, every word arrives bit-reversed — the §7 bug that passes palindrome patterns and fails real data. Fix by shifting the same way on both ends and documenting the convention.
- The disciplines are inherited. Use non-blocking
<=so the chain shifts atomically and does not collapse (2.1); order load before shift so a load is never overwritten (2.2); countWIDTHshifts to avoid the off-by-one on the last/fill bit; and verify the serial↔parallel round-trip with a non-palindrome pattern — self-checking clocked testbenches shown here in SystemVerilog, Verilog, and VHDL.