Skip to content

Combinational and Sequential Logic in Verilog

Introduction

Digital systems are built using two types of logic: combinational and sequential. When writing Verilog code, it’s essential to model these correctly so that simulation and synthesized hardware behave as intended.

This article explains how to write combinational and sequential logic in Verilog, the differences between them, and how to avoid common pitfalls in RTL design.

Understanding Logic Types

Combinational Logic

Combinational logic produces outputs that depend only on the current inputs — with no memory or feedback. Examples: adders, multiplexers, encoders, decoders, and logic gates.

Mathematically:

Output = f(Inputs)

Sequential Logic

Sequential logic, in contrast, depends on current inputs and past states. It uses storage elements (flip-flops, latches) to hold information.

Output = f(Inputs, Previous State)

Examples: counters, shift registers, finite state machines.

Difference Between Combinational and Sequential Logic

Feature Combinational Block Sequential Block
Sensitivity list @(*) @(posedge clk) / @(negedge clk)
Assignment type Blocking = Non-blocking <=
Timing Instantaneous On clock edge
Used for Logic equations, multiplexers Registers, counters, FSMs
Memory elements None Flip-flops
Latch risk Yes (if incomplete) No (if properly clocked)

Writing Combinational Logic in Verilog

Combinational logic is written using continuous assignments (assign) or procedural blocks with sensitivity lists (always @(*)).

Using Continuous Assignment (assign)

The simplest way to describe logic equations:

assign y = (a & b) | (~a & c);

This is ideal for simple logic gates, multiplexers, or arithmetic operations.

Using Procedural Block (always @(*))

For more complex logic, especially involving conditionals or case statements, use:

always @(*) begin
  y = 0;  // default assignment to avoid latches
  if (sel)
    y = a;
  else
    y = b;
end

Key points:

  • Use blocking (=) assignments inside always @(*).
  • Always provide default values for all outputs.
  • Use the wildcard @(*) so the sensitivity list automatically includes all inputs.

Example 1: 4-to-1 Multiplexer

module mux4to1 (
  input [3:0] d,
  input [1:0] sel,
  output reg y
);
  always @(*) begin
    case (sel)
      2'b00: y = d[0];
      2'b01: y = d[1];
      2'b10: y = d[2];
      2'b11: y = d[3];
      default: y = 1'b0;
    endcase
  end
endmodule

This is purely combinational — output y changes immediately with inputs.

Example 2: 1-Bit Full Adder

module full_adder (
  input a, b, cin,
  output sum, cout
);
  assign {cout, sum} = a + b + cin;
endmodule

No memory, no clock — just logic equations.

Writing Sequential Logic in Verilog

Sequential logic requires a clock signal (and often a reset). Outputs update only on clock edges, representing real flip-flop behavior.

Syntax

always @(posedge clk or posedge reset)

or

always @(negedge clk)

Key Rules

  • Use non-blocking (<=) assignments.
  • Include reset logic to initialize registers.
  • Each always block models a group of flip-flops.

Example 1: D Flip-Flop

module dff (
  input clk, reset, d,
  output reg q
);
  always @(posedge clk or posedge reset) begin
    if (reset)
      q <= 1'b0;
    else
      q <= d;
  end
endmodule

Explanation:

  • Triggered on clock’s rising edge.
  • Reset clears the output.
  • <= ensures correct register timing.

Example 2: 4-Bit Register

module register4 (
  input clk, reset,
  input [3:0] d,
  output reg [3:0] q
);
  always @(posedge clk or posedge reset) begin
    if (reset)
      q <= 4'b0000;
    else
      q <= d;
  end
endmodule

Every bit in q represents a flip-flop that captures d on each clock edge.

Example 3: Counter (Sequential Logic)

module counter (
  input clk, reset,
  output reg [3:0] count
);
  always @(posedge clk or posedge reset) begin
    if (reset)
      count <= 0;
    else
      count <= count + 1;
  end
endmodule

This models a simple binary up-counter — updated once per clock edge.

Combining Combinational and Sequential Logic in same module

Real designs mix both types. For instance, a finite state machine (FSM) has:

  • Sequential logic for state memory.
  • Combinational logic for next-state and output decoding.

Example: Simple FSM (Moore)

module fsm (
  input clk, reset, x,
  output reg y
);
  reg [1:0] state, next_state;

  // Sequential block – state update
  always @(posedge clk or posedge reset) begin
    if (reset)
      state <= 2'b00;
    else
      state <= next_state;
  end

  // Combinational block – next-state logic
  always @(*) begin
    next_state = state;
    y = 0;
    case (state)
      2'b00: begin
        if (x) next_state = 2'b01;
      end
      2'b01: begin
        y = 1;
        next_state = 2'b10;
      end
      2'b10: begin
        if (!x) next_state = 2'b00;
      end
    endcase
  end
endmodule

Key takeaway:

  • One always @(posedge clk) for flip-flops.
  • One always @(*) for combinational logic.

Avoiding Latches in Combinational Logic

Latches are unintended storage elements that appear when outputs are not assigned in all conditions.

Example: Problematic Code

always @(*) begin
  if (enable)
    y = a;
  // Missing else
end

If enable is 0, y retains its previous value → latch inferred!

Example: Fixed code

always @(*) begin
  y = 0;        // default value
  if (enable)
    y = a;
end

Always ensure outputs are assigned in every possible path.

Difference Between Combinational and Sequential always Blocks

Feature Combinational Block Sequential Block Sensitivity list @(*) @(posedge clk) / @(negedge clk) Assignment type Blocking = Non-blocking <= Timing Instantaneous On clock edge Used for Logic equations, multiplexers Registers, counters, FSMs Memory elements None Flip-flops Latch risk Yes (if incomplete) No (if properly clocked)

Writing Clean RTL: The Two-Process Method

Professional RTL designers often follow a two-process coding style:

  • Combinational process — computes next state and outputs.
  • Sequential process — updates registers.

Template

reg [3:0] state, next_state;
reg [7:0] out;

// combinational logic start
always @(*) begin
  next_state = state;
  out = 0;
  case (state)
    4'b0000: begin
      next_state = 4'b0001;
      out = 8'hAA;
    end
    4'b0001: begin
      next_state = 4'b0010;
      out = 8'hBB;
    end
  endcase
end
// combinational logic ends

// Sequential logic starts
always @(posedge clk or posedge reset) begin
  if (reset)
    state <= 4'b0000;
  else
    state <= next_state;
end
// Sequential logic ends

This ensures clean separation which improves readability, avoids race conditions, and leads to predictable synthesis.

Simulation vs Synthesis Behavior

Sometimes simulation seems fine, but the synthesized hardware behaves differently. This happens if you misuse blocking/non-blocking assignments or forget sensitivity lists.

Simulation-only construct (avoid in synthesizable RTL)

#5 a = b;   // delay control — not synthesizable

Synthesizable style

always @(posedge clk)
  a <= b;

Simulation should match hardware timing — use proper sequential modeling.

Common Mistakes

Mistake Description Consequence
Using = inside clocked block Sequential logic simulated as combinational Wrong timing
Missing @(*) Incomplete sensitivity list Incorrect simulation
Forgetting default values Latch inferred Undesired memory element
Mixing = and <= in one block Race condition Unpredictable behavior
Missing reset Registers start undefined X propagation in simulation
Using blocking in FSM state register Overwritten transitions Incorrect state changes

Example — Complete RTL Module

Here’s a simple design combining both types.

4-bit Up/Down Counter

module updown_counter (
  input clk, reset, up_down,
  output reg [3:0] count
);
  reg [3:0] next_count;

  // Combinational: next state
  always @(*) begin
    if (up_down)
      next_count = count + 1;
    else
      next_count = count - 1;
  end

  // Sequential: update state
  always @(posedge clk or posedge reset) begin
    if (reset)
      count <= 4'b0000;
    else
      count <= next_count;
  end
endmodule

This is a classic RTL pattern — safe, clean, and synthesizable.

Practical Design Tips

Best-practices

  • Always separate combinational and sequential logic.
  • Initialize outputs in combinational blocks.
  • Use @(*) for automatic sensitivity.
  • Use non-blocking assignments in clocked blocks.
  • Reset registers properly.

Avoid

  • Mixing assignment types.
  • Using # delays in RTL.
  • Implicit latches.
  • Shared variables between unrelated blocks.

Hardware Perspective

Concept Hardware Equivalent

Concept Hardware Equivalent
always @(*) Logic gates / combinational network
always @(posedge clk) Flip-flops / registers
Blocking = Wire connection
Non-blocking <= Parallel register update
Default assignments Pull-down / reset logic

Understanding this mapping helps you visualize real circuits from Verilog code.

Quick Check — Identify the Type

Code Logic Type
assign y = a & b; Combinational
always @(posedge clk) q <= d; Sequential
always @(*) begin if(sel) y=a; else y=b; end Combinational
always @(posedge clk) begin count <= count+1; end Sequential

Summary

Concept Combinational Logic Sequential Logic
Timing Instantaneous Edge-triggered
Memory None Flip-flops
Assignment = <=
Block Type always @(*) always @(posedge clk)
Example ALU, MUX Register, Counter, FSM
Common issue Latch inference Missed reset

Key Takeaways

  • Combinational logic → depends only on current inputs, uses always @(*), blocking =.
  • Sequential logic → depends on clock and previous state, uses always @(posedge clk), non-blocking <=.
  • Separate the two for clarity, maintainability, and correct synthesis.
  • Always include default values and resets to prevent hidden latches and undefined states.