Skip to content

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.

the-need.v — one wire, one bit per clock, reassemble on the far side
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 si

2. 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 = 00hold (each flop keeps its own value; the disabled-register feedback of 2.2).
  • mode = 01shift-right (q <= {si_right, q[WIDTH-1:1]}; a bit enters at the MSB end, the LSB falls off).
  • mode = 10shift-left (q <= {q[WIDTH-2:0], si_left}; a bit enters at the LSB end, the MSB falls off).
  • mode = 11parallel-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 flow
The shift-register family — one register chain, three taps, one mode muxregister chainWIDTH D-flops, each D <- neighbourSIPO(deserialize)si in, po = whole word, no loadPISO (serialize)pi loaded, so = dropped bit, needs loadmode mux (1.1)4:1 select on each flop's D inputuniversal SRhold / shift-L / shift-R / loadfill bit +directionMSB-first vs LSB-first = the contract
Everything is the same WIDTH-flop register chain (2.1). Tap it serial-in / parallel-out and it is a SIPO deserializer; tap it parallel-in-with-load / serial-out and it is a PISO serializer; put a 4:1 mode mux (1.1) in front of every flop's D input, gated like an enable/load (2.2), and it is the universal shift register that does hold, shift-left, shift-right, and parallel-load. In every form the fill bit and the shift direction define the MSB-first-vs-LSB-first contract that both ends of a link must share. Language-independent: the same structure exists in SystemVerilog, Verilog, and VHDL.

One 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.

sipo.sv — serial-in parallel-out, WIDTH-generic (MSB-first deserializer)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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)
   endmodule

The 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.

sipo_tb.sv — clocked self-check: shift WIDTH bits in, assert po == word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The Verilog form is identical in intent — reg/wire typing, always @(posedge clk), the same concatenation. The reset and enable behave exactly the same.

sipo.v — serial-in parallel-out in Verilog (same chain, same fill)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
sipo_tb.v — clocked self-check (drive MSB-first, compare, PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

sipo.vhd — serial-in parallel-out in VHDL (rising_edge, & concatenation)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
sipo_tb.vhd — clocked self-check (assert po = word, severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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).

piso.sv — parallel-in serial-out with load (MSB-first serializer)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

piso_tb.sv — clocked self-check: load, then assert so == word[i] each cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

piso.v — parallel-in serial-out in Verilog (load priority over shift)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
piso_tb.v — clocked self-check; compare serial stream to word[i]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

piso.vhd — parallel-in serial-out in VHDL (load elsif shift, MSB-first)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
piso_tb.vhd — clocked self-check; assert so = word(i) each cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

usr.sv — universal shift register (hold / shift-L / shift-R / load)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

usr_tb.sv — clocked self-check: drive every mode, assert the result
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

usr.v — universal shift register in Verilog (case mode, concatenation)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
usr_tb.v — clocked self-check; precomputed expected per mode
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

usr.vhd — universal shift register in VHDL (case mode, & fill bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
usr_tb.vhd — clocked self-check; assert result per mode (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

order_bug.sv — BUGGY (RX shifts wrong way) vs FIXED (matched direction)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
   endmodule

The 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.

order_bug_tb.sv — TX MSB-first into RX; non-palindrome exposes the reversal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

order_bug.v — BUGGY vs FIXED RX direction in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
order_bug_tb.v — non-palindrome byte exposes the reversal (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

order_bug.vhd — BUGGY (shift-right) vs FIXED (shift-left) RX in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
order_bug_tb.vhd — non-palindrome byte, assert po = word (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 WIDTH cycles (the sipo_tb of §4a); load a known word into a PISO and assert the serial output equals word[i] on each successive cycle (the piso_tb of §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 assert q is unchanged; shift-left one step and assert q == {q_prev[WIDTH-2:0], si_l}; shift-right one step and assert q == {si_r, q_prev[WIDTH-1:1]}. The usr_tb in §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 load and en simultaneously and confirm the loaded word survives (q == pi, not a shifted pi). 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 for WIDTH = 4, 8, 16 (and an odd width like 5) so an off-by-one on the fill/last bit — a chain that shifts WIDTH-1 times instead of WIDTH, or fills the wrong end at the boundary — surfaces. A form correct at WIDTH=8 can be off-by-one at WIDTH=5.
  • Expected waveform. On a waveform, a correct SIPO shows po gaining one bit of the incoming pattern per rising edge, the word "filling up" from one end; a correct PISO shows so presenting successive word bits, one per edge, MSB-first; a correct universal SR shows q sliding by exactly one position on each shift edge and standing still on a hold. A q that moves by two positions per edge (or collapses to the input) is the blocking-assignment failure of §6; a po that 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.

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.)

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_n that 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 WIDTH D flip-flops (2.1) wired D-to-neighbour-Q; the concatenation q <= {q[WIDTH-2:0], si} is that wiring, plus the fill bit at the vacated end. Same silicon as a WIDTH-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); count WIDTH shifts 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.