18 min read | Beginner to Intermediate
What You'll Learn
- How to implement sequential logic circuits like flip-flops, counters, and shift registers using always blocks
- The critical difference between combinational and sequential always blocks and when to use each
- How to properly handle clock signals, reset signals, and asynchronous vs. synchronous reset strategies
- How to write synthesizable sequential logic that correctly maps to hardware flip-flops and meets timing requirements
A previous article showed different examples of using an always block to implement combinational logic. An always block is also mainly used to implement sequential logic which has memory elements like flip flops that can hold values.
Real-World Application
Intel's CPU Pipeline Registers: In Intel's latest processors (Raptor Lake, Meteor Lake), the instruction execution pipeline has 15-20 stages, and between every stage are banks of flip-flops (pipeline registers) that hold intermediate results. Each clock cycle, data flows from one stage to the next through these sequential elements. These pipeline registers are implemented using the exact sequential logic patterns you'll learn here--always blocks sensitive to clock edges, synchronous resets, and carefully controlled data flow. A single Core i9 processor contains over 1 billion flip-flops, all working in concert at 5+ GHz. Understanding sequential logic is understanding how modern CPUs work at their most fundamental level.
Sequential vs. Combinational Logic
Key Differences:
- Combinational Logic: Output depends only on current inputs (no memory). Examples: adders, multiplexers, decoders. Implemented with
always @(*)orassign. - Sequential Logic: Output depends on current inputs AND past history (has memory). Examples: flip-flops, counters, shift registers, state machines. Implemented with
always @(posedge clk).
Synthesis Insight: Sequential logic always blocks synthesize to flip-flops and registers, while combinational always blocks synthesize to gates (AND, OR, NOT, MUX). When you write always @(posedge clk), you're telling the synthesis tool "create flip-flops here." This is why understanding the difference is critical--it directly affects your hardware implementation and timing behavior.
JK Flip Flop
A JK flip flop is one of the many types of flops used to store values and has two data inputs j and k along with one for reset rstn and another for clock clk. The truth table for a JK flop is shown below and is typically implemented using NAND gates.
| rstn | j | k | q | Comments |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | When reset is asserted, output is always zero |
| 1 | 0 | 0 | Hold value | When both j and k are 0, output remains the same as before |
| 1 | 0 | 1 | 0 | When j=0, k=1 (Reset): output is cleared to 0 |
| 1 | 1 | 0 | 1 | When j=1, k=0 (Set): output is set to 1 |
| 1 | 1 | 1 | Toggle value | When j=1, k=1: output toggles the current value |
The behavioral Verilog code for a JK flip-flop can be written as shown below:
// JK Flip-Flop with asynchronous active-low reset
module jk_ff (
input j, // Input J: when high with K low, sets Q to 1
input k, // Input K: when high with J low, resets Q to 0
input rstn, // Active-low async reset: immediately clears Q
input clk, // Clock: triggers state changes on rising edge
output reg q // Output Q: stored state value
);
// Sequential logic: sensitive to clock edge and reset edge
always @ (posedge clk or negedge rstn) begin
if (!rstn) begin
// Asynchronous reset: Q becomes 0 immediately when rstn goes low
// This happens independent of clock edge
q <= 0;
end else begin
// JK flip-flop logic (evaluated only on rising clock edge):
// j=0,k=0: hold current value (q unchanged)
// j=0,k=1: reset Q to 0
// j=1,k=0: set Q to 1
// j=1,k=1: toggle Q
// Boolean expression: Q_next = (J & ~Q) | (~K & Q)
q <= (j & ~q) | (~k & q);
end
end
endmodule
Critical Sequential Logic Concepts:
- Sensitivity List:
@(posedge clk or negedge rstn)means "trigger this block on rising clock edge OR falling reset edge" - Asynchronous Reset: Reset takes effect immediately, not waiting for clock edge (important for initialization)
- Non-Blocking Assignment:
<=is mandatory for sequential logic to avoid race conditions - Output Type:
output reg qis required becauseqis assigned inside a procedural block
Common Error:
Never use blocking assignments (=) in sequential always blocks! Using q = (j & ~q) | (~k & q); instead of q <= creates race conditions where the order of execution within the same clock cycle becomes unpredictable. This works in simulation by chance but fails in real hardware timing analysis. Always use non-blocking (<=) for sequential logic to ensure proper flip-flop inference and avoid timing violations.Testbench
First declare all variables used in the testbench and start a clock using a simple always block that can be driven to the design. Then instantiate the design and connect its ports with corresponding testbench variables. Note that q is of type wire because it is connected to an output of the design which will be actively driving it. All other inputs to the design are of type reg so that they can be driven within a procedural block such as initial.
The stimulus first initializes all inputs to the design to zero and then de-asserts reset after some time. A for loop is used to drive different values to j and k which are driven at random times. Once the loop is done, wait for some more time and finish the simulation.
module tb;
// Declare testbench variables
reg j, k, rstn, clk;
wire q; // Output from DUT (must be wire)
integer i;
reg [2:0] dly;
// Generate clock: 50% duty cycle, 20ns period (50 MHz)
always #10 clk = ~clk;
// Instantiate the JK flip-flop design under test
jk_ff u0 (
.j(j),
.k(k),
.clk(clk),
.rstn(rstn),
.q(q)
);
// Stimulus generation
initial begin
// Initialize all inputs to zero at time 0
{j, k, rstn, clk} <= 0;
// Release reset after 10ns to start operation
#10 rstn <= 1;
// Apply random stimulus: 10 iterations
for (i = 0; i < 10; i = i+1) begin
dly = $random; // Random delay (0-7 ns)
#(dly) j <= $random; // Random J input
#(dly) k <= $random; // Random K input
end
// Let simulation run a bit longer, then finish
#20 $finish;
end
// Monitor output changes
initial begin
$monitor("Time=%0t | rstn=%b j=%b k=%b q=%b",
$time, rstn, j, k, q);
end
// Waveform dumping for analysis
initial begin
$dumpfile("jk_ff.vcd");
$dumpvars(0, tb);
end
endmodule
Note from the simulation wave that at the posedge of clock, output q changes value based on the state of inputs j and k as given in the truth table.

Modulo-10 Counter
Modulus(MOD) counters simply count up to a certain number before rolling back to zero. A MOD-N counter will count from 0 to N-1 and then roll back to zero and start counting again. Such counters typically require ceil(log2N) flops to hold the count value. Shown below is the Verilog code for a MOD-10 counter that keeps counting up at every clock clk as long as reset rstn is deasserted.
Verilog parameters can be used to make a MOD-N counter which is more scalable.
// Modulo-10 Counter: Counts from 0 to 9, then rolls over
module mod10_counter (
input clk, // Clock input
input rstn, // Synchronous active-low reset
output reg [3:0] out // 4-bit output (0-9, needs 4 bits)
);
// Sequential logic: triggered on rising clock edge only
always @ (posedge clk) begin
if (!rstn) begin
// Synchronous reset: counter resets to 0 on next clock edge
// (Unlike asynchronous reset, this waits for clock edge)
out <= 0;
end else begin
// Counter logic
if (out == 9) // Check if reached maximum count
out <= 0; // Roll over to 0
else
out <= out + 1; // Increment counter
end
end
endmodule
Synchronous vs. Asynchronous Reset:
- Synchronous Reset: Sensitivity list is
@(posedge clk)only. Reset takes effect on next clock edge. Easier to meet timing, better for most designs. - Asynchronous Reset: Sensitivity list is
@(posedge clk or negedge rstn). Reset takes effect immediately. Used when instant reset is critical (e.g., safety systems).
Real-World Application
AMD Zen CPU Instruction Retirement Counter: AMD's Zen processor architecture uses performance counters--essentially sophisticated modulo counters--to track instruction execution statistics. These counters track retired instructions, cache misses, branch mispredictions, and hundreds of other performance metrics. The counters can be 48-bits wide, counting into the trillions before wrapping. Hardware profilers and performance analysis tools (AMD µProf, Intel VTune) read these counters to optimize code. The same modulo counter pattern you're learning here is scaled up to create these essential performance monitoring units in every modern CPU.
Testbench
The testbench first declares some variables that can be assigned some values and driven to the design inputs. The counter module is then instantiated and connected with the testbench signals which are later driven with some values in the stimulus. Since the counter also requires a clock, the testbench clock is modeled with an always block. The stimulus simply sets default values at time 0ns, then deasserts reset after 10ns and the design is allowed to run for some time.
module tb;
reg clk, rstn;
wire [3:0] out; // Counter output (wire, not reg)
// Instantiate the modulo-10 counter
mod10_counter u0 (
.clk(clk),
.rstn(rstn),
.out(out)
);
// Generate clock: 20ns period
always #10 clk = ~clk;
// Stimulus
initial begin
// Initialize signals
{clk, rstn} <= 0;
// Release reset after 10ns
#10 rstn <= 1;
// Let counter run for multiple cycles to observe rollover
#450 $finish;
end
// Display counter value at each change
initial begin
$monitor("Time=%0t | rstn=%b out=%0d", $time, rstn, out);
end
// Waveform dump
initial begin
$dumpfile("mod10_counter.vcd");
$dumpvars(0, tb);
end
endmodule
See that the counter module counts from zero to 9, rolls over to zero and starts counting again.

4-bit Left Shift Register
Shown below is a 4-bit left shift register that accepts an input d into LSB and all other bits will be shifted left by 1. For example, if d equals zero and the initial value of the register is 0011, it will become 0110 at the next edge of the clock clk.
// 4-bit Left Shift Register
module lshift_4b_reg (
input d, // Serial data input (enters at LSB)
input clk, // Clock signal
input rstn, // Synchronous active-low reset
output reg [3:0] out // 4-bit parallel output
);
// Sequential logic: shift operation on clock edge
always @ (posedge clk) begin
if (!rstn) begin
// Reset: clear all bits
out <= 0;
end else begin
// Left shift operation:
// - out[3] gets out[2] (MSB shifts out, lost)
// - out[2] gets out[1]
// - out[1] gets out[0]
// - out[0] gets d (new bit enters at LSB)
// Concatenation syntax: {out[2:0], d}
out <= {out[2:0], d};
end
end
endmodule
Shift Register Applications:
- Serial-to-Parallel Conversion: UART, SPI, I2C interfaces use shift registers to receive serial data
- Delay Lines: FIR filters use shift registers to create tapped delay lines for signal processing
- Pattern Detection: Communication systems shift in bits to detect sync patterns (e.g., Ethernet preamble)
- CRC Calculation: Linear Feedback Shift Registers (LFSRs) implement CRC error detection
FPGA Optimization: On Xilinx and Intel FPGAs, shift registers are recognized by the synthesizer and mapped to specialized SRL (Shift Register LUT) primitives instead of discrete flip-flops. A 32-bit shift register might use 32 flip-flops normally, but with SRL inference, it uses just 1-2 LUTs--a 90%+ resource savings! Write your shift registers in the standard pattern shown here, and modern synthesis tools will automatically optimize them.
Testbench
The testbench follows a similar template like the one shown before where some variables are declared, design module is instantiated and connected with the testbench signals. Then a clock is started and the stimulus is driven to the design using an initial block. In this testbench example, different values of d has to be exercised and hence a for loop is used to iterate 20 times and apply random values to the design.
module tb;
reg clk, rstn, d;
wire [3:0] out;
integer i;
// Instantiate the 4-bit left shift register
lshift_4b_reg u0 (
.d(d),
.clk(clk),
.rstn(rstn),
.out(out)
);
// Generate clock
always #10 clk = ~clk;
// Stimulus
initial begin
// Initialize all signals
{clk, rstn, d} <= 0;
// Release reset
#10 rstn <= 1;
// Shift in 20 random bits, synchronized to clock edge
for (i = 0; i < 20; i=i+1) begin
@(posedge clk) d <= $random; // Apply new bit at clock edge
end
// Wait a bit and finish
#10 $finish;
end
// Monitor shift register value
initial begin
$monitor("Time=%0t | d=%b out=%b", $time, d, out);
end
// Waveform dump
initial begin
$dumpfile("lshift_4b.vcd");
$dumpvars(0, tb);
end
endmodule
Note that each bit is shifted to the left by 1 and the new value of d is applied to LSB.

Common Beginner Mistakes
Mistake #1: Using blocking assignments (=) in sequential logic
Why it's problematic: Many beginners write sequential logic using blocking assignments:
always @(posedge clk) begin
q = d;
end
This creates race conditions in simulation and synthesis problems. Blocking assignments execute sequentially within the same time step, which can cause the wrong evaluation order when multiple always blocks interact. For example, if one block updates q and another reads q in the same clock cycle, the result is unpredictable. Worse, the synthesized flip-flops no longer match the simulation when other blocks read the same variable, which causes simulation/synthesis mismatches.
Best Practice: Always use non-blocking assignments (<=) for sequential logic:
always @(posedge clk) begin
q <= d;
end
Non-blocking assignments schedule updates to happen after all right-hand sides are evaluated, ensuring all flip-flops update "simultaneously" like real hardware. This is the industry-standard rule: use <= for sequential (clocked) logic, use = only for combinational logic in always @(*) blocks. Follow this rule religiously and you'll avoid entire categories of subtle timing bugs.
Mistake #2: Incomplete sensitivity lists for sequential logic
Why it's problematic: Students sometimes write always @(posedge clk) for logic that should have asynchronous reset, or forget to include reset in the sensitivity list:
always @(posedge clk)
if (!rstn) q <= 0;
else q <= d;
This creates a synchronous reset instead of the intended asynchronous reset--the reset won't take effect until the next clock edge, which can be problematic during power-up or critical fault conditions. The simulation might work, but the synthesized hardware behaves differently than expected.
Best Practice: Match your sensitivity list to your intent. For asynchronous reset:
always @(posedge clk or negedge rstn)
For synchronous reset:
always @(posedge clk)
In general, prefer synchronous reset for most designs--it's easier to meet timing constraints and results in more predictable behavior. Use asynchronous reset only when you specifically need instant reset response, such as in safety-critical systems or when initializing state before the first clock edge.
Mistake #3: Mixing combinational and sequential logic in one always block
Why it's problematic: Beginners sometimes write code like:
always @(posedge clk or a or b) begin
q <= a + b;
end
This tries to combine clock-triggered sequential logic (posedge clk) with combinational logic (sensitive to a and b). The result is confusing behavior: the block triggers both on clock edges AND when a or b change, creating neither pure combinational nor pure sequential logic. Synthesis tools will reject this or infer unexpected hardware (latches, multiple clock domains). The intent is unclear--do you want a registered sum or an unregistered sum?
Best Practice: Separate combinational and sequential logic into different always blocks. For combinational:
always @(*)
sum = a + b;
For sequential:
always @(posedge clk)
q <= sum;
This clearly shows data flow: combinational logic computes values, sequential logic registers them on clock edges. Modern synthesis tools can optimize this to the same hardware as a single block, but the code is far more readable and maintainable. This separation is critical for large designs and matches industry coding standards (IEEE RTL coding guidelines).
Mistake #4: Incorrect counter rollover conditions
Why it's problematic: Students often write counters with off-by-one errors in the rollover condition for a modulo-10 counter:
if (out == 10)
out <= 0;
This actually creates a modulo-11 counter! It counts 0,1,2,3,4,5,6,7,8,9,10, then rolls to 0--that's 11 states, not 10. The counter briefly reaches the value 10 for one clock cycle before resetting, which can cause glitches in downstream logic that expects only values 0-9. This bug is subtle because it "mostly works" but occasionally produces invalid values.
Best Practice: Use if (out == N-1) for a modulo-N counter, not if (out == N). For modulo-10:
if (out == 9)
out <= 0;
else
out <= out + 1;
This ensures the counter goes through exactly N states (0 to N-1). Alternatively, use a parameterized design:
parameter N = 10;
// other code
if (out == N-1)
out <= 0;
else
out <= out + 1;
Draw a state diagram to verify: for MOD-10, states should be {0,1,2,3,4,5,6,7,8,9}, never reaching 10.
By mastering sequential logic with always blocks, you now understand the fundamental building blocks of every digital system. The flip-flops, counters, and shift registers you've learned are not academic exercises--they're the exact patterns used in CPU cores, memory controllers, and communication interfaces in every modern chip. From Apple's M-series to NVIDIA's GPUs to Intel's processors, billions of flip-flops implement sequential logic using these exact principles.
Summary
In this tutorial, you learned:
- How sequential logic differs from combinational logic--sequential circuits have memory (flip-flops) and state, triggered by clock edges, while combinational circuits compute outputs instantly from inputs
- How to implement flip-flops, counters, and shift registers using
always @(posedge clk)blocks with proper non-blocking assignments (<=) - The critical difference between synchronous reset (in sensitivity list as
@(posedge clk)only) and asynchronous reset (@(posedge clk or negedge rstn)) and when to use each - Industry best practices: non-blocking assignments for sequential logic, separate always blocks for combinational vs. sequential, correct counter rollover conditions
- Common mistakes to avoid: blocking assignments in sequential logic, incomplete sensitivity lists, mixing logic types, off-by-one counter errors