Skip to content

RTL Design Patterns · Chapter 2 · Registers & Sequential Building Blocks

The Register Pattern (D-FF)

Combinational logic forgets. A multiplexer, a decoder, or a comparator recomputes its output from scratch each instant and remembers nothing. But a real chip is defined by what it remembers: a counter that knows how far it has counted, an FSM that knows its state, a pipeline that carries a value forward. To remember, hardware needs a place to store a value across a clock cycle, and the primitive that does exactly that is the register, the D flip-flop. It is the atom of sequential logic, since every counter, FSM, FIFO, and pipeline is a bank of registers wrapped in combinational logic. This lesson treats the register as a sample-and-hold on the clock edge that captures D at the rising edge and holds Q steady for the whole cycle, and shows why non-blocking assignment makes a whole bank sample the old values at once and keeps clocked logic race-free. Everything is built and self-checked in SystemVerilog, Verilog, and VHDL.

Foundation12 min readRTL Design PatternsRegisterD Flip-FlopNon-Blocking AssignmentSequential LogicNBA Region

Chapter 2 · Section 2.1 · Registers & Sequential Building Blocks

1. The Engineering Problem

You are building the simplest possible datapath: a value comes in on a bus this cycle, and next cycle you need to do something with the value that arrived last cycle — add it to the new one, compare it, forward it down a pipeline. The value from last cycle is gone the instant the wire changes. Combinational logic cannot help you here: a mux, an adder, a decoder all recompute their output from their current inputs every instant and retain nothing. The moment the input changes, the old output is overwritten and unrecoverable. Combinational logic is memoryless by construction.

But every design that does anything interesting must remember state across cycles. A counter must remember how far it has counted. An FSM must remember which state it is in so it can decide the next one. A pipeline must carry a partial result from one stage to the next, cycle after cycle. A FIFO must remember every word written into it until it is read out. None of that is possible with combinational logic alone, because none of it is a function of this instant's inputs — it is a function of history. To have history, hardware needs a place to store a value and hold it stable until it is deliberately updated.

That storage element — the one that stores exactly one value and updates it in lockstep with a clock — is the register, built from D flip-flops. It is Chapter 2's opening pattern for the same reason the multiplexer opened Chapter 1: it is the atom. A counter is registers plus an adder. An FSM is a state register plus next-state mux logic. A pipeline is a chain of registers. A FIFO is a register array plus pointers. Learn the register — really learn it, including why it must be written with non-blocking assignment — and every sequential pattern in the rest of the track is a composition of the thing you learned here.

the-need.v — combinational forgets; the design must remember
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // COMBINATIONAL: output is a function of THIS instant's inputs only.
   //   assign sum = a + b;   // when a or b changes, the old sum is GONE.
   //   There is no way to ask "what was a last cycle?" — no memory exists.
 
   // The design needs to remember the value that arrived LAST cycle:
   //   next_sum = a + prev_a;   // ...but where does prev_a come from?
 
   // It comes from a REGISTER — storage that captures a on the clock edge and
   // holds it for the whole next cycle, so combinational logic can use it:
   //   always @(posedge clk) prev_a <= a;   // sample-and-hold — the atom
   //   assign next_sum = a + prev_a;         // now history is available

2. Mental Model

3. Pattern Anatomy

The register is built from one clocked process and a handful of load-bearing decisions.

The clocked process. The entire pattern is a single edge-sensitive block: SystemVerilog always_ff @(posedge clk), Verilog always @(posedge clk), VHDL process(clk) ... if rising_edge(clk). The sensitivity to posedge clk (or rising_edge(clk)) is what makes it sequential: the block's outputs update only at the rising edge, never in between. That single fact — updates gated to the edge — is the difference between the storage of Chapter 2 and the pure combinational logic of Chapter 1.

The three ports — D, Q, clk. A register has a data input d (what to store), a data output q (what is currently stored), and a clock clk (when to store it). D is sampled; Q is held; clk is the strobe. There is no other logic in the pure register — no reset yet (that is 2.3), no enable yet (that is 2.2). It is the minimal storage atom: capture D at the edge, present it on Q.

Sample-on-edge, hold-for-cycle. At each rising edge, Q takes the value D had at that edge; for the rest of the cycle Q is constant regardless of what D does. This is the behaviour that gives a clocked design a definite "value this cycle" and lets you reason about the circuit one cycle at a time.

The WIDTH parameter. A register is naturally W bits wide — a 1-bit flag, an 8-bit byte, a 32-bit word. The pattern parameterizes that width (parameter WIDTH / VHDL generic), so one source is a 1-bit register or a 64-bit register with no rewrite. Under the hood it is simply WIDTH independent flip-flops sharing one clock, each storing one bit.

The NBA (<=) discipline vs blocking (=). Inside the clocked process the assignment must be non-blocking (<=). Non-blocking assignment splits the edge into two phases: first evaluate every right-hand side (reading the pre-edge values), then update every left-hand side together in the NBA region. Blocking assignment (=) instead evaluates-and-updates immediately, in order, so a later statement sees an earlier one's new value in the same edge. For a single register the two often look identical; for a bank of registers they are worlds apart — which is the next point.

Why a register bank updates atomically. Consider three registers chained: STAGE1 gets D, STAGE2 gets STAGE1, STAGE3 gets STAGE2 (a shift register). With <=, at the edge all three right-hand sides are read first (the old STAGE1, STAGE2, D), then all three left-hand sides are written — so every stage advances by exactly one, atomically, no matter what order you wrote the lines. With =, STAGE1 would take D, then STAGE2 would take the just-updated STAGE1 (which is now D), then STAGE3 the just-updated STAGE2 — and the value races straight through all three in a single edge, collapsing the shift register to a single stage. The atomic "read all old, then write all new" behaviour of <= is exactly what makes a register bank a synchronous structure and clocked logic race-free.

The register — sample D on the edge, hold Q for the cycle; a bank updates atomically

data flow
The register — sample D on the edge, hold Q for the cycle; a bank updates atomicallyD (data in)may wiggle any time in the cyclerising edge ofclkthe strobe — the only update instantQ (held output)= D sampled AT the edge; then constant<= reads OLDfirstevaluate all RHS, then update all LHSregister bankwhole bank samples pre-edge values atomically= would racelater stmt sees new value → order-dependent bug
D is sampled at the rising edge and held on Q for the whole cycle. Non-blocking assignment reads every right-hand side (the OLD Q values) before it writes any left-hand side, so a whole bank of registers captures the pre-edge snapshot together, atomically and independent of statement order. Blocking assignment would let a later statement see an earlier one's new value in the same edge — an order-dependent race. This is language-independent: it is the same in SystemVerilog, Verilog, and VHDL.

One decision is deferred on purpose: this pure register has no reset, so at power-up Q is unknown (X in simulation) until the first edge loads a defined D. That is not an oversight — it is the boundary of this pattern, and it is exactly what motivates the reset patterns of 2.3 (synchronous) and 2.4 (asynchronous). Here the register does one thing — sample and hold — and does it correctly.

4. Real RTL Implementation

This is the core of the page. We build two patterns — (a) a parameterized WIDTH-generic D-FF register, and (b) the blocking-vs-non-blocking shift-register 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.

One discipline runs through every clocked RTL block on this page: the update is non-blocking (<=), gated to the rising edge. That single rule is what makes each block a real register that samples the pre-edge value, and it is what pattern (b) violates.

4a. The parameterized D-FF register — WIDTH-generic, three ways

Start with the atom. A register is q <= d at the rising edge, WIDTH bits wide. There is no reset and no enable — it captures D every edge and holds Q between edges. The SystemVerilog form uses always_ff, which additionally tells the tool "this is intended to be sequential," so a lint tool flags it if you accidentally write it in a way that infers combinational logic or a latch.

reg_ff.sv — the parameterized D-FF register (always_ff, non-blocking)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reg_ff #(
       parameter int WIDTH = 8              // one source, any width: 1-bit flag ... 64-bit word
   )(
       input  logic             clk,        // the strobe — updates happen only on its rising edge
       input  logic [WIDTH-1:0] d,          // data in: sampled AT the rising edge
       output logic [WIDTH-1:0] q           // data out: holds the sampled value for the whole cycle
   );
       // Sequential process. `always_ff` documents the INTENT (a flop, not comb/latch)
       // so the tool can check it. `<=` is NON-BLOCKING: at the edge, the RHS (d) is
       // sampled, then q is updated in the NBA region — so a whole bank of these
       // updates atomically from the pre-edge values (race-free).
       always_ff @(posedge clk) begin
           q <= d;                          // sample d on the edge, hold q until the next edge
       end
   endmodule

The SystemVerilog testbench is the first clocked testbench on the page, and its shape is the template for all six §4 testbenches: generate a clock, drive D, and after each rising edge assert Q equals the D presented before the edge — a one-cycle reference model. It also checks the hold behaviour by wiggling D mid-cycle and confirming Q does not move until the next edge.

reg_ff_tb.sv — clocked, self-checking: after each edge, q === d-before-edge; and q holds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reg_ff_tb;
       localparam int WIDTH = 8;
       logic             clk = 0;
       logic [WIDTH-1:0] d, q;
       logic [WIDTH-1:0] expected;                 // one-cycle reference model
       int               errors = 0;
 
       reg_ff #(.WIDTH(WIDTH)) dut (.clk(clk), .d(d), .q(q));
 
       always #5 clk = ~clk;                        // 10-time-unit clock: rising edge every 10
 
       initial begin
           d = '0; expected = '0;
           @(negedge clk);                          // align stimulus changes to the FALLING edge,
                                                     //   so d is stable before each rising edge
           for (int cyc = 0; cyc < 20; cyc++) begin
               d        = $urandom;                 // present new data for this cycle
               expected = d;                        // reference: q should become THIS d at next edge
               @(posedge clk);                      // the register samples d here
               #1;                                  // let the NBA update settle
               // CHECK 1 — the register captured the value presented before the edge:
               assert (q === expected)
                   else begin $error("cyc=%0d q=%h exp=%h", cyc, q, expected); errors++; end
               // CHECK 2 — HOLD: change d mid-cycle; q must NOT move until the next edge.
               d = ~d;                              // wiggle d now (between edges)
               #1;
               assert (q === expected)
                   else begin $error("cyc=%0d HOLD broke: q=%h exp=%h", cyc, q, expected); errors++; end
               @(negedge clk);                      // return to the falling edge to set up next cycle
           end
           if (errors == 0) $display("PASS: reg_ff samples d on the edge and holds q");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the only differences are reg/wire typing and $random instead of $urandom. The <= is just as non-blocking, so it samples the pre-edge value in exactly the same way.

reg_ff.v — the parameterized D-FF register in Verilog (posedge clk, non-blocking)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reg_ff #(
       parameter WIDTH = 8
   )(
       input  wire             clk,
       input  wire [WIDTH-1:0] d,
       output reg  [WIDTH-1:0] q            // `reg` because it is assigned in a procedural block
   );
       // Non-blocking assignment in a clocked block: sample d on the rising edge,
       // update q in the NBA region so a bank of these is race-free.
       always @(posedge clk) begin
           q <= d;                          // sample-and-hold
       end
   endmodule
reg_ff_tb.v — clocked, self-checking Verilog testbench (compare, print PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module reg_ff_tb;
       parameter WIDTH = 8;
       reg              clk;
       reg  [WIDTH-1:0] d;
       wire [WIDTH-1:0] q;
       reg  [WIDTH-1:0] expected;
       integer          cyc, errors;
 
       reg_ff #(.WIDTH(WIDTH)) dut (.clk(clk), .d(d), .q(q));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;                        // rising edge every 10 time units
 
       initial begin
           errors = 0;
           d      = {WIDTH{1'b0}};
           @(negedge clk);                          // drive on the falling edge; sample on the rising
           for (cyc = 0; cyc < 20; cyc = cyc + 1) begin
               d        = $random;                  // new data for this cycle
               expected = d;                        // reference: q should take this d next edge
               @(posedge clk);                      // register samples here
               #1;                                  // let NBA settle
               if (q !== expected) begin            // CHECK 1 — captured the pre-edge value
                   $display("FAIL: cyc=%0d q=%h exp=%h", cyc, q, expected);
                   errors = errors + 1;
               end
               d = ~d;                              // CHECK 2 — HOLD: wiggle d mid-cycle
               #1;
               if (q !== expected) begin            // q must not move until the next edge
                   $display("FAIL: cyc=%0d HOLD broke q=%h exp=%h", cyc, q, expected);
                   errors = errors + 1;
               end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: reg_ff matches reference and holds between edges");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the same register is a process(clk) guarded by rising_edge(clk), and the signal assignment <= is already non-blocking by the language's semantics (signals update at the end of the process, not immediately). The generic plays the role of the Verilog parameter.

reg_ff.vhd — the parameterized D-FF register in VHDL (rising_edge, signal assignment)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity reg_ff is
       generic ( WIDTH : positive := 8 );                 -- generic == Verilog parameter
       port (
           clk : in  std_logic;                           -- the strobe
           d   : in  std_logic_vector(WIDTH-1 downto 0);  -- sampled AT the rising edge
           q   : out std_logic_vector(WIDTH-1 downto 0)   -- held for the whole cycle
       );
   end entity;
 
   architecture rtl of reg_ff is
   begin
       -- Clocked process: only the rising edge updates q. A VHDL signal assignment
       -- (<=) is inherently non-blocking — q's new value is scheduled and applied
       -- at the end of the delta, so a bank of these samples the pre-edge values
       -- together (race-free), exactly like SV/Verilog non-blocking.
       process (clk)
       begin
           if rising_edge(clk) then
               q <= d;                                    -- sample-and-hold
           end if;
       end process;
   end architecture;

The VHDL testbench generates a clock with a concurrent process, drives D on the falling edge, and self-checks with assert ... report ... severity error after each rising edge — the VHDL idiom that plays the role SystemVerilog's assert/$error and Verilog's if (...) $display("FAIL") play.

reg_ff_tb.vhd — clocked, self-checking VHDL testbench (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity reg_ff_tb is
   end entity;
 
   architecture sim of reg_ff_tb is
       constant WIDTH : positive := 8;
       signal clk : std_logic := '0';
       signal d   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal q   : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.reg_ff generic map (WIDTH => WIDTH)
                                port map (clk => clk, d => d, q => q);
 
       clk <= not clk after 5 ns;                          -- rising edge every 10 ns
 
       stim : process
           variable expected : std_logic_vector(WIDTH-1 downto 0);
       begin
           wait until falling_edge(clk);                   -- drive on the falling edge
           for cyc in 0 to 19 loop
               d        <= std_logic_vector(to_unsigned((cyc*29 + 7) mod 256, WIDTH));
               expected := std_logic_vector(to_unsigned((cyc*29 + 7) mod 256, WIDTH));
               wait until rising_edge(clk);                -- register samples here
               wait for 1 ns;                              -- let the update settle
               -- CHECK 1 — captured the value presented before the edge:
               assert q = expected
                   report "reg_ff mismatch: q /= d presented before the edge" severity error;
               d <= not d;                                 -- CHECK 2 — HOLD: wiggle d mid-cycle
               wait for 1 ns;
               assert q = expected
                   report "reg_ff hold broke: q moved between edges" severity error;
               wait until falling_edge(clk);
           end loop;
           report "reg_ff self-check complete" severity note;
           wait;
       end process;
   end architecture;

The testbenches above are written for WIDTH = 8, but the register is genuinely width-generic: re-instantiate the DUT with WIDTH = 1 (a single flip-flop) and WIDTH = 32 (a full word) and the same clock-and-check loop proves each — a parameterized register must be verified generic, not assumed generic. The reference model does not change; only the vectors get wider.

4b. The blocking-vs-non-blocking shift register — buggy = vs fixed <=

Now the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. Build a 3-stage shift register: STAGE1 samples D, STAGE2 samples STAGE1, STAGE3 samples STAGE2 — so a value entered at D should appear at STAGE3 exactly three cycles later. Written with <= (non-blocking) it does exactly that. Written with = (blocking) it collapses: within a single edge, STAGE1 takes D, then STAGE2 takes the just-updated STAGE1 (now D), then STAGE3 takes the just-updated STAGE2 (now D) — the value races through all three stages in one edge, and the "3-stage delay" becomes a 1-stage delay (or, across simulators that order the statements differently, a nondeterministic mess).

shift_bug.sv — BUGGY (blocking =) vs FIXED (non-blocking <=)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: blocking `=` makes STAGE2 see the JUST-UPDATED STAGE1 in the same edge,
   //        so d races through all three stages in ONE edge → the 3-cycle delay
   //        collapses to 1 cycle (and is order-dependent across simulators).
   module shift3_bad (
       input  logic clk,
       input  logic d,
       output logic q            // should be d delayed by 3 cycles
   );
       logic s1, s2;
       always_ff @(posedge clk) begin
           s1 = d;               // = : updates s1 immediately...
           s2 = s1;              //     ...so s2 sees the NEW s1 (= d), not the old one
           q  = s2;              //     ...and q sees the NEW s2 (= d) → 1-cycle delay, not 3
       end
   endmodule
 
   // FIXED: non-blocking `<=` reads ALL right-hand sides (old s1, old s2, old d)
   //        BEFORE writing any → each stage advances by exactly one → true 3-cycle delay.
   module shift3_good (
       input  logic clk,
       input  logic d,
       output logic q
   );
       logic s1, s2;
       always_ff @(posedge clk) begin
           s1 <= d;              // all RHS sampled first (pre-edge values)...
           s2 <= s1;             // s2 takes the OLD s1
           q  <= s2;             // q takes the OLD s2 → each stage moves one → 3-cycle delay
       end
   endmodule
shift_bug_tb.sv — clocked self-check: a pulse at d must appear at q 3 cycles later
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module shift_bug_tb;
       logic clk = 0;
       logic d, q;
       logic pipe [3];                          // reference model: a 3-deep delay line
       int   errors = 0;
 
       // Point the DUT at shift3_good to PASS; at shift3_bad to see the collapse.
       shift3_good dut (.clk(clk), .d(d), .q(q));
 
       always #5 clk = ~clk;
 
       initial begin
           d = 0; foreach (pipe[i]) pipe[i] = 0;
           @(negedge clk);
           for (int cyc = 0; cyc < 24; cyc++) begin
               d = (cyc == 3);                  // drive a single 1-cycle pulse at cycle 3
               @(posedge clk); #1;
               // Reference model advances one stage per edge (a true 3-deep pipe):
               pipe[2] = pipe[1];  pipe[1] = pipe[0];  pipe[0] = d;
               // q must equal what the reference says exited the 3-deep delay line:
               assert (q === pipe[2])
                   else begin $error("cyc=%0d q=%b exp=%b (delay collapsed?)", cyc, q, pipe[2]); errors++; end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: shift3 delays d by exactly 3 cycles");
           else             $display("FAIL: %0d mismatches (blocking = collapsed the shift register)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg stages and always @(posedge clk). The reference model is a shift of a small array; the check fires the moment the blocking version races the pulse through in one edge.

shift_bug.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: blocking `=` → d races through all three stages in one edge.
   module shift3_bad (
       input  wire clk,
       input  wire d,
       output reg  q
   );
       reg s1, s2;
       always @(posedge clk) begin
           s1 = d;               // immediate update
           s2 = s1;              // sees NEW s1 (= d)
           q  = s2;              // sees NEW s2 (= d) → 1-cycle delay, not 3
       end
   endmodule
 
   // FIXED: non-blocking `<=` → each stage advances by exactly one → 3-cycle delay.
   module shift3_good (
       input  wire clk,
       input  wire d,
       output reg  q
   );
       reg s1, s2;
       always @(posedge clk) begin
           s1 <= d;              // all RHS read first...
           s2 <= s1;             // s2 takes OLD s1
           q  <= s2;             // q takes OLD s2 → true 3-cycle delay
       end
   endmodule
shift_bug_tb.v — clocked self-check; a pulse must arrive 3 cycles later
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module shift_bug_tb;
       reg     clk;
       reg     d;
       wire    q;
       reg     p0, p1, p2;                       // 3-deep reference delay line
       integer cyc, errors;
 
       // Bind to shift3_good to PASS; to shift3_bad to observe the collapse.
       shift3_good dut (.clk(clk), .d(d), .q(q));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0;
           d = 1'b0; p0 = 0; p1 = 0; p2 = 0;
           @(negedge clk);
           for (cyc = 0; cyc < 24; cyc = cyc + 1) begin
               d = (cyc == 3);                   // one-cycle pulse at cycle 3
               @(posedge clk); #1;
               p2 = p1; p1 = p0; p0 = d;         // reference advances one stage per edge
               if (q !== p2) begin
                   $display("FAIL: cyc=%0d q=%b exp=%b (delay collapsed?)", cyc, q, p2);
                   errors = errors + 1;
               end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: shift3 delays d by exactly 3 cycles");
           else             $display("FAIL: %0d mismatches (blocking = collapsed the pipe)", errors);
           $finish;
       end
   endmodule

In VHDL the same collapse appears when the stages are written as variables assigned with := (VHDL's blocking assignment, updated immediately) instead of signals assigned with <= (non-blocking, updated at the end of the process). The signal form is the correct register; the variable form races the value through in one edge.

shift_bug.vhd — BUGGY (variables :=) vs FIXED (signals <=)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   -- BUGGY: variables with := update IMMEDIATELY, so s2 sees the new s1 in the same
   --        edge → d races through all three stages → 1-cycle delay, not 3.
   entity shift3_bad is
       port ( clk : in std_logic; d : in std_logic; q : out std_logic );
   end entity;
   architecture rtl of shift3_bad is
   begin
       process (clk)
           variable s1, s2 : std_logic;          -- variables: immediate (blocking) update
       begin
           if rising_edge(clk) then
               s1 := d;                          -- updates now
               s2 := s1;                         -- sees NEW s1 (= d)
               q  <= s2;                         -- gets NEW s2 (= d) → collapses to 1 stage
           end if;
       end process;
   end architecture;
 
   -- FIXED: signals with <= are non-blocking — all RHS read before any LHS updates,
   --        so each stage advances by exactly one → true 3-cycle delay.
   library ieee;
   use ieee.std_logic_1164.all;
   entity shift3_good is
       port ( clk : in std_logic; d : in std_logic; q : out std_logic );
   end entity;
   architecture rtl of shift3_good is
       signal s1, s2 : std_logic;                -- signals: non-blocking update
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               s1 <= d;                          -- all RHS sampled first...
               s2 <= s1;                         -- s2 takes OLD s1
               q  <= s2;                         -- q takes OLD s2 → 3-cycle delay
           end if;
       end process;
   end architecture;
shift_bug_tb.vhd — clocked self-check; pulse must arrive 3 cycles later (assert report)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
 
   entity shift_bug_tb is
   end entity;
 
   architecture sim of shift_bug_tb is
       signal clk : std_logic := '0';
       signal d   : std_logic := '0';
       signal q   : std_logic;
       -- 3-deep reference delay line:
       signal p0, p1, p2 : std_logic := '0';
   begin
       -- Bind to shift3_good to PASS; to shift3_bad to observe the collapse.
       dut : entity work.shift3_good port map (clk => clk, d => d, q => q);
 
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           wait until falling_edge(clk);
           for cyc in 0 to 23 loop
               if cyc = 3 then d <= '1'; else d <= '0'; end if;   -- one-cycle pulse at cycle 3
               wait until rising_edge(clk);
               wait for 1 ns;
               p2 <= p1;  p1 <= p0;  p0 <= d;                     -- reference advances one stage
               wait for 1 ns;
               assert q = p2
                   report "shift3 mismatch: q /= 3-cycle-delayed d (delay collapsed?)" severity error;
               wait until falling_edge(clk);
           end loop;
           report "shift_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: in a clocked process, use non-blocking assignment (<= on signals) so every register in the bank samples the pre-edge values together — blocking (=, or VHDL := on a variable) lets one stage see another's new value in the same edge and collapses the shift register.

5. Verification Strategy

A register is sequential, so its correctness is not a truth table — it is a behaviour over time, and the single invariant that captures a correct register is:

After every rising edge, Q equals the value D held just before that edge; and between edges, Q holds that value unchanged no matter what D does.

Everything below is that one invariant, made checkable, and it maps directly onto the clocked testbenches in §4.

  • A clocked, self-checking testbench. Generate a free-running clock (always #5 clk = ~clk; in SV/Verilog, clk <= not clk after 5 ns; in VHDL). Drive D on the falling edge so it is stable before each rising edge, capture the D you presented into a one-cycle reference model (expected = d), then after the rising edge assert q === expected. This is the core self-check: a one-line reference model of "Q this cycle = D last cycle," compared automatically every cycle. The three reg_ff testbenches in §4a do exactly this — SV assert (q === expected), Verilog if (q !== expected) $display("FAIL..."), VHDL assert q = expected report ... severity error.
  • The sample-on-edge invariant. Stated as a property that holds every edge: q after edge n equals d sampled at edge n. If it ever fails, the register is not sampling correctly — either the clocking is wrong or the assignment is not gated to the edge.
  • The hold invariant — prove Q is stable between edges. This is the half most learners forget to test. After the edge-sample check, deliberately change D mid-cycle and assert Q does not move until the next edge. A register that fails this is not holding — it has become transparent (a latch) or combinational. The §4a testbenches do this explicitly: wiggle D between edges and re-assert q === expected.
  • The shift-register / atomicity check. To prove the bank updates atomically (not just a single register), verify a 3-stage shift register: drive a one-cycle pulse at D and assert it appears at Q exactly three cycles later, using a 3-deep reference delay line that advances one stage per edge. This is the check that distinguishes a correct <= register bank from a blocking-= collapse — the §4b testbenches are built around it, and pointing the DUT at the buggy module makes the assertion fail on the very first pulse.
  • Parameter-generic verification (WIDTH = 1, 8, 32). A width-generic register must be verified generic, not assumed generic. Re-instantiate the DUT at WIDTH = 1 (a single flip-flop), WIDTH = 8, and WIDTH = 32 (a full word) and re-run the same edge-sample + hold checks. The reference model is unchanged; only the vector width grows. A register that passes at 8 bits but was written with an accidental fixed width will fail the moment you re-instantiate it at 32.
  • Expected waveform. On a waveform, a correct register shows Q changing only at rising edges of clk, and at each edge stepping to whatever D was immediately before that edge; between edges Q is a flat, steady line even while D toggles. The visual signature of the blocking-= bug is a value racing through what should be a multi-cycle delay — the shift register's output moving on the same edge the input did, instead of three edges later.

6. Common Mistakes

Blocking = in a clocked process. The signature register bug. Inside always @(posedge clk) (or always_ff), using blocking = on registers that feed each other lets a later statement see an earlier one's just-updated value in the same edge. A shift register written with = collapses — the value races through every stage in one edge instead of advancing one stage per edge — and because the outcome depends on the order the tool evaluates the statements, different simulators can even give different results (a race). In a clocked process, non-blocking <= is not a style preference; it is what makes the register bank sample the pre-edge values simultaneously. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4b.)

Reading Q and expecting the new value in the same cycle. Q updates after the edge, in the NBA region — so within the same cycle that you drove D, Q still holds the old value. Combinational logic reading Q this cycle sees last cycle's value; the D you just presented does not appear on Q until the next edge. Expecting q == d in the same cycle you set D is the classic off-by-one-cycle mistake, and it is exactly why the reference model in §5 is "Q this cycle = D last cycle."

Combinational feedback mistaken for a register. Writing assign q = ... q ... (a combinational loop that feeds its own output back) is not a register — it is an unclocked feedback path that oscillates or latches unpredictably, with no defined "value this cycle." A register stores state because the storage is gated to a clock edge; combinational feedback has no edge and no defined sampling instant. If you need to remember a value, it must be updated in a clocked process, not fed back combinationally.

Forgetting this register has no reset yet. The pure register on this page has no reset, so at power-up Q is unknown — X in simulation — until the first edge loads a defined D. That is correct for this pattern (the register does one thing: sample and hold), but it means Q is X until first load, and any logic that reads Q before it has been loaded sees X. This is not a bug to fix here; it is the exact motivation for the reset patterns of 2.3 (synchronous reset) and 2.4 (asynchronous reset), which add a defined power-up value.

7. DebugLab

The delay line that lost two-thirds of its delay — blocking assignment in clocked logic

The engineering lesson: in clocked logic, non-blocking assignment is not a style preference — it is the mechanism that makes a whole bank of registers sample their inputs simultaneously from the pre-edge values. Blocking = in a clocked process turns a register bank into an ordered chain of immediate updates, so one stage sees another's new value in the same edge — a race whose outcome depends on statement order and even on the simulator. The tell is a bug that a steady input hides but a moving edge or pulse exposes (a delay line that lost cycles), and a waveform that changes between simulators: order-dependence in something you designed to be synchronous means you wrote blocking assignment where the design needs non-blocking. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: use <= (non-blocking, on signals) for every assignment inside a clocked process, and verify with a moving pulse against a reference delay line, never with a steady level.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "sample on the edge, hold for the cycle, update the bank atomically" habit.

Exercise 1 — Trace the shift register: = vs <=

A 3-stage shift register has stages S1, S2, S3 (output), all initially 0, written in the order S1 ⟵ d; S2 ⟵ S1; S3 ⟵ S2; inside always @(posedge clk). A single 1-cycle pulse is applied to d at cycle 3. For each of the next five edges, tabulate S1, S2, S3 (a) if the assignments are non-blocking (<=), and (b) if they are blocking (=). State at which cycle the pulse reaches S3 in each case, and explain in one line why the blocking version collapses the delay.

Exercise 2 — Why last cycle, not this cycle?

A teammate writes always @(posedge clk) q <= d; and then, in the same cycle they drive d, reads q in a combinational block and is surprised it still shows the old value. Explain precisely why q does not equal the new d until the next edge — reference the NBA region and the sample-and-hold model — and state the general rule "Q this cycle = D ___ cycle" that they should have used to predict it.

Exercise 3 — Register or not?

For each, say whether it is a correct register, and why in one line: (a) always @(posedge clk) q <= d;; (b) assign q = clk ? d : q;; (c) always @(*) q = d;; (d) always @(posedge clk) q = d; used standalone (single stage, no feedback). (Hint: which ones are edge-gated storage, which are combinational, which are a latch or feedback, and for (d) — does it work as a single register even though it is bad practice in a bank?)

Exercise 4 — Verify the hold, not just the sample

Two engineers test the same register. Engineer A only checks that q takes the right value at each edge. Engineer B also changes d mid-cycle and asserts q does not move until the next edge. Describe a broken "register" that passes A's test but fails B's, and name what B's extra check is actually proving about the storage element. (Hint: think about what a transparent latch would do.)

Continue in RTL Design Patterns (this-batch siblings go live together; forward links unlock as they ship):

  • Register with Enable / Load — Chapter 2.2; add a load-enable so the register holds when told to — and a register-with-enable is exactly a 2:1 mux (en ? d : q) feeding this flop.
  • Synchronous Reset — Chapter 2.3; give the register a defined power-up value, closing the "Q is X until first load" gap this page deliberately left open.
  • Asynchronous Reset — Chapter 2.4; the other reset style and its recovery trade-offs.
  • Shift Registers — Chapter 3; the serial chain of registers whose collapse under blocking assignment is this page's DebugLab.
  • Up/Down Counters — Chapter 3; the simplest stateful datapath — a register plus an adder feeding its D.
  • FSM Fundamentals — Chapter 4.1; the state register plus next-state logic — the register as the heart of control.

Backward / method:

  • Multiplexers — Chapter 1.1; a register with a load-enable is a 2:1 mux feeding a flop — where selection (Chapter 1) meets state (Chapter 2).
  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applied to the atom of state.
  • What Is an RTL Design Pattern? — Chapter 0.1; its §4 sketched the register as a pattern — this page teaches it in full.

Combinational primitives (Chapter 1, the datapath atoms this state pattern sits alongside):

Verilog v1 prerequisites (the grammar this clocked pattern relies on):

  • Blocking and Non-Blocking Assignments — the assignment discipline this whole page rests on: <= for sequential, = for combinational, and the NBA region that makes a register bank atomic.
  • Race Conditions & Determinism — why non-blocking assignment keeps clocked logic deterministic across simulators, and what a blocking-in-clocked-logic race looks like.

11. Summary

  • A register is a sample-and-hold on the clock edge. It captures D at the rising edge and holds Q rock-steady for the whole cycle; always_ff @(posedge clk) / always @(posedge clk) / rising_edge(clk) are three spellings of the same edge-triggered storage. Q changes only at the edge and equals the D that existed just before it — so "Q this cycle = D last cycle."
  • It is the atom of sequential logic. Every counter, FSM, FIFO, pipeline, and shift register is a bank of registers wrapped in combinational logic; the storage comes entirely from the register, and the combinational logic only decides what to store next.
  • Non-blocking (<=) is the discipline, not a style choice. In a clocked process, <= reads every right-hand side (the pre-edge values) before writing any left-hand side, so a whole register bank samples simultaneously and updates atomically — independent of statement order. That is exactly what makes clocked logic race-free. Blocking (=, or VHDL := on a variable) lets one stage see another's new value in the same edge and collapses a shift register — the DebugLab of §7.
  • Verify both halves, over time. A clocked, self-checking testbench generates a clock, drives D, and checks that after each edge Q equals the D presented before it (sample), and that Q holds between edges (hold) — plus a pulse-through-a-3-stage-shift-register check for atomicity, at WIDTH = 1, 8, 32. The self-checking testbenches are shown here in SystemVerilog, Verilog, and VHDL.
  • This pure register has no reset — on purpose. Q is X until the first edge loads it; that gap is the exact motivation for the synchronous and asynchronous reset patterns that follow in Chapter 2.