Digital System Design
Combinational Logic Modeling with Verilog
Nitin Chandrachoodan
IIT Madras
Logic Modeling in
Verilog ●
●
Logic values in Verilog
First module
● Value Propagation
Logic values
True / False ⇔0/1
● Boolean values: algebra defined around this
● Reality:
○ unknown values: X => “don’t care”
○ electrical isolation => “high impedance” or “floating” values
● Verilog uses 4-valued logic: (0, 1, x, z)
Verilog module
// Example Verilog module ● keywords: module, assign etc.
○ module name
module basic_gates ( ● ports vs function arguments
○ order matters
input wire a, b, // Input ports
○ use named instantiation
output wire y1, y2 // Output ports
● comments
);
● wire
// Basic continuous assignments ○ internal connections
assign y1 = a & b; // AND gate ○ implicit declaration - BE CAREFUL!
assign y2 = a | b; // OR gate ■ typos
endmodule
Value Propagation
module prop_example(
input wire a, b, c,
output wire y
);
wire temp1, temp2; // Internal connections
assign temp1 = a & b; // First level
assign temp2 = temp1 | c; // Second level
assign y = ~temp2; // Output level
endmodule
X- propagation
// Example showing X propagation
module x_propagation(
input wire sel, a, b,
output wire y
);
// If sel is x, output will be x
assign y = (sel) ? a : b;
endmodule
Why X?
● Unconnected ports / signals
● Power-up initialization of registers
These are common “unknown” states.
● Simulation can propagate these.
● Can help to debug problems with uninitialized values
Some simulators stick to 2-state (0, 1) simulation: eg. Verilator
High impedance (Z)
module tristate( ● Tri-state
input wire data, enable, ○ 0, 1, z
○ Implies unconnected
output wire out
● Bus connections
); ○ Can short outputs safely if they are Z
// Output is 'z' when enable is 0 ○ Enable only one
○ Common scenario in practice
assign out = enable ? data : 1'bz;
● Actual high impedance
endmodule
○ Electrical model
● inout ports:
○ generally avoid unless you know why
Simulation
● “Interpret” the Verilog code
○ Semantics
● Similar to programming languages
○ Concurrency, Time etc. different
○ Compiler / Interpreter
● Implements notion of time
Testbench
Purpose
Design Under Test (DUT)
● Provide stimulus to DUT
● Verify correct operation
● No inputs, no outputs
Basic Structure
● DUT instantiation
● Stimulus generation
● Response monitoring
Basic Testbench
module testbench;
// 1. Declare signals for DUT connection
reg a, b; // Inputs are reg
wire y; // Outputs are wire
// 2. Instantiate the DUT
my_and dut(
.a(a),
.b(b),
.y(y)
);
// 3. Apply stimulus and monitor output
initial begin
// Test code here
end
endmodule
Modern Testbench template (SystemVerilog)
module testbench;
// 1. Declare signals for DUT connection
logic a, b; // Use logic for all signals
logic y; // Output is also logic
// 2. Instantiate the DUT
my_and dut(
.a(a),
.b(b),
.y(y)
);
// 3. Apply stimulus
initial begin
// Test code here
end
endmodule
reg/wire vs logic
● Verilog: reg/wire
○ wire for connectivity
○ reg for procedural updates
■ not necessarily physical
register
■ potential source of confusion
● SystemVerilog: logic
○ use for either input or output
○ automatically infers direction of
signal flow
reg/wire vs logic
● Verilog: reg/wire Compatibility notes:
○ wire for connectivity
● logic requires SystemVerilog support
○ reg for procedural updates
(available in EDA Playground)
■ not necessarily physical
● Legacy code still uses reg/wire
register
extensively
■ potential source of confusion
● Both styles are valid and widely used
● SystemVerilog: logic
○ use for either input or output
○ automatically infers direction of
signal flow
Stimulus generation
initial begin ● initial block
// Initialize inputs
● reg vs wire in Verilog
a = 0; b = 0;
● Delays:
#10; // Wait 10 time units
○ Simulation construct
○ Nothing to do with physical gates
// Change inputs
● $finish : system task
a = 1;
#10;
b = 1;
#10;
// End simulation
$finish;
end
Display and Monitor
// Display - prints once ● Tracing and Debugging
$display("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
● Limited compared to C etc
● $time
// Monitor - prints when values change
● Automatic $monitor
initial
$monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
// Format specifiers:
// %b - binary, %h - hex, %d - decimal
// %t - time, %0t - time without padding
// Testbench
Putting things together
module testbench;
reg a, b;
wire y;
// DUT: 2-input AND gate // Instantiate DUT
module my_and( my_and dut(.a(a), .b(b), .y(y));
input wire a, b,
// Monitor changes
output wire y
initial
);
$monitor("Time=%0t a=%b b=%b y=%b", $time, a, b, y);
assign y = a & b;
endmodule // Stimulus
initial begin
// Test all combinations
a = 0; b = 0; #10;
a = 0; b = 1; #10;
a = 1; b = 0; #10;
a = 1; b = 1; #10;
$display("Test complete");
$finish;
end
endmodule
Key points to remember
● Inputs to DUT are 'reg' in testbench
● Outputs from DUT are 'wire' in testbench
● Use $monitor for continuous value tracking
● Use $display for specific message printing
● Always include $finish to end simulation
● Use meaningful time delays between stimuli
Time ● Simulation Time
● Basic delays
Simulation Time Basics
● Simulation operates on discrete time steps
● Time units: #10 means "wait 10 time units"
● Default time unit is 1ns unless specified
● $time system function returns current simulation time
Specifying time units
// At start of module or file:
`timescale 1ns/100ps // time unit / time precision
module time_example;
logic a, b, y;
// #1 means 1ns
// #0.1 means 100ps
initial begin
a = 0;
#1 a = 1; // wait 1ns
#0.1 a = 0; // wait 100ps
end
endmodule
Timing diagrams
module timing_demo;
logic in1, in2, out;
// Simple gate with delay
assign #2 out = in1 & in2;
initial begin
in1 = 0; in2 = 0;
#5 in1 = 1;
#5 in2 = 1;
#5 in1 = 0;
#5 $finish;
end
// Monitor changes
initial
$monitor("Time=%0t in1=%b in2=%b out=%b",
$time, in1, in2, out);
endmodule
Timing diagrams
● Good way to visualize transitions
● Convention to show change “after” time
● [Link] online editor
Understanding gate delays
module delay_example;
logic a, b, temp, y;
// Gates with different delays
assign #2 temp = a & b; // 2 unit delay
assign #3 y = ~temp; // 3 unit delay
initial begin
a = 0; b = 0;
#6 a = 1; b = 1;
// Output changes after 2+3=5 units
#10 $finish;
end
initial
$monitor("Time=%0t a=%b b=%b temp=%b y=%b",
$time, a, b, temp, y);
endmodule
Understanding gate delays
module delay_example;
logic a, b, temp, y;
// Gates with different delays
assign #2 temp = a & b; // 2 unit delay
assign #3 y = ~temp; // 3 unit delay
initial begin
a = 0; b = 0;
#6 a = 1; b = 1;
// Output changes after 2+3=5 units
#10 $finish;
end
initial
$monitor("Time=%0t a=%b b=%b temp=%b y=%b",
$time, a, b, temp, y);
endmodule
Multi-path delays
module multi_path;
logic a, b, c, y;
// intermediate signals
logic p1, p2;
// Two paths to output
assign #2 p1 = a & b;
assign #3 p2 = b & c;
assign #1 y = p1 ^ p2;
initial begin
a = 0; b = 1; c = 0;
#6 a = 1; // y changes after 3ns
#6 c = 1; // y changes after 4ns
#10 $finish;
end
endmodule
Multi-path delays
module multi_path;
logic a, b, c, y;
// intermediate signals
logic p1, p2;
// Two paths to output
assign #2 p1 = a & b;
assign #3 p2 = b & c;
assign #1 y = p1 ^ p2;
initial begin
a = 0; b = 1; c = 0;
#6 a = 1; // y changes after 3ns
#6 c = 1; // y changes after 4ns
#10 $finish;
end
endmodule
Key points
● Simulation time is discrete
● Delays accumulate through gates
● Multiple paths can have different delays
● Timing diagrams help visualize behavior
● Monitor statements help track changes
● Use appropriate timescale for your design
Building Complex
Circuits
assign statements
assign a = b;
● Continuous assignment
● Respond to any change in RHS
● Suitable for “simple” logic
○ Express on single line
○ simple logic combinations
Procedural blocks: always and always_comb
always @(a or b or c) begin
s = a ^ b ^ c;
co = a & b | b & c | a & c;
end
● Potential pitfall: missing elements in
sensitivity list
● simulation issues:
○ slow (multiple iterations)
○ missed updates
● synthesis issues:
○ latches
Procedural blocks: always and always_comb
always @(a or b or c) begin always_comb begin
s = a ^ b ^ c; s = a ^ b ^ c;
co = a & b | b & c | a & c; co = a & b | b & c | a & c;
end end
● Potential pitfall: missing elements in ● Experience: separate out combinational and
sensitivity list sequential blocks
● simulation issues: ● Still possibility of latches
○ slow (multiple iterations) ○ less error prone
○ missed updates
● Clear logic
● synthesis issues:
○ latches
Procedural blocks: always and always_comb
always @(a or b or c) begin always_comb begin
s = a ^ b ^ c; s = a ^ b ^ c;
co = a & b | b & c | a & c; co = a & b | b & c | a & c;
end end
● Potential pitfall: missing elements in ● Experience: separate out combinational and
sensitivity list sequential blocks
● simulation issues: ● Still possibility of latches
○ slow (multiple iterations) ○ less error prone
○ missed updates
● Clear logic
● synthesis issues:
○ latches
Use SystemVerilog: always_comb
Combinational modeling
module mux4_to_1(
input logic [1:0] sel,
input logic [3:0] data,
output logic y
);
// Proper combinational block
always_comb begin
case(sel)
2'b00: y = data[0];
2'b01: y = data[1];
2'b10: y = data[2];
2'b11: y = data[3];
endcase
end
endmodule
Common Pitfalls: 1. Incomplete case statements
module incomplete_case(
input logic [1:0] sel,
input logic data,
output logic y
);
always_comb begin
case(sel)
2'b00: y = data;
2'b01: y = ~data;
// Missing cases! Creates latches!
endcase
end
endmodule
Common Pitfalls 2: Missing output assignments in branches
module partial_assign(
input logic [1:0] sel,
input logic data,
output logic y1, y2
);
always_comb begin
if (sel[1]) begin
y1 = data; // y2 not assigned in this branch!
end else begin
y1 = ~data;
y2 = data;
end
end
endmodule
Common Pitfalls 3: Combinational loops
module comb_loop(
input logic a, b,
output logic y
);
logic temp;
always_comb begin
temp = y & b; // temp depends on y
y = a | temp; // y depends on temp - loop!
end
endmodule
Common Pitfalls 4: Mixed blocking/non-blocking assignments
module mixed_assigns(
input logic a, b,
output logic y
);
logic temp;
always_comb begin
temp <= a & b; // Non-blocking in comb logic!
y = temp | a;
end
endmodule
Common Pitfalls 5: Implicit latches from if without else
module implicit_latch(
input logic a, en,
output logic y
);
always_comb begin
if (en)
y = a; // What happens when en is 0?
end
endmodule
Preferred Coding Styles
module good_design1(
input logic [1:0] sel,
input logic data,
output logic y1, y2
);
always_comb begin
// Default values for ALL outputs
y1 = '0;
y2 = '0;
if (sel[1]) begin
y1 = data;
y2 = ~data; // Explicit assignment
end else begin
y1 = ~data;
y2 = data;
end
end
endmodule
Preferred Coding Styles
module good_design2(
input logic a, b,
output logic y
);
logic temp;
always_comb begin
temp = a & b; // No loop
y = a | temp; // Use blocking assignments
end
endmodule
Synthesis Issues
● Incomplete cases -> Latches
● Missing assignments -> Latches
● Combinational loops -> Synthesis errors
● Mixed assignments -> Simulation/synthesis mismatch
● Implicit latches -> Unwanted state elements
Multiple Outputs and Dependencies
module alu_flags(
input logic [7:0] a, b,
input logic [1:0] op,
output logic [7:0] result,
output logic zero,
output logic carry
);
always_comb begin
{carry, result} = 9'b0; // Default values
case(op)
2'b00: {carry, result} = a + b;
2'b01: result = a & b;
2'b10: result = a | b;
2'b11: result = ~a;
endcase
zero = (result == 8'b0); // Dependent output
end
endmodule
Priority Encoders and If-Else
module priority_encoder(
input logic [3:0] req,
output logic [1:0] grant,
output logic valid
);
always_comb begin
valid = 1'b1;
if (req[3]) grant = 2'b11;
else if (req[2]) grant = 2'b10;
else if (req[1]) grant = 2'b01;
else if (req[0]) grant = 2'b00;
else begin
grant = 2'b00;
valid = 1'b0;
end
end
endmodule
Good Coding Practices
1. Always specify defaults for all outputs
2. Use explicit case labels (no wildcards)
3. Include default case
4. Avoid blocking assignments in combinational logic
5. Keep logic depth reasonable
More on
Testbenches
VCD and signal traces
module counter_tb;
● Visualize signal output
reg clk;
● Selectively dump traces to files reg rst;
for later analysis wire [3:0] count;
● Interactive viewers counter DUT(.clk(clk), .rst(rst), .count(count));
initial begin
$dumpfile("[Link]");
$dumpvars(0, counter_tb);
// Rest of testbench...
end
endmodule
VCD options
initial begin
$dumpfile("[Link]");
Demo on
// This won't recurse into the DUT
$dumpvars(1, counter_tb);
edaplayground
// Present module and one sub-level
$dumpvars(2, counter_tb);
// Dump specific signals
$dumpvars(0, counter_tb.clk);
$dumpvars(0, counter_tb.rst);
$dumpvars(0, counter_tb.enable);
$dumpvars(0, counter_tb.count);
end
Complex stimulus generation
initial begin ● Use verilog constructs like for loops
for(int i = 0; i < 16; i++) begin ○ Normally not synthesizable
a = i[3:0]; ■ with caveats
#10; ○ Ideal for test benches
end ● Integers -> bits and vice versa
end
● Include delays inside loop
Verilog Tasks and Functions
task apply_stimulus; ● Combine repetitive steps
input [3:0] data;
● Can include delays, display etc.
begin
● Programming construct - no physical
a = data;
meaning
#10;
$display("Input=%h, Output=%h", data, y);
end
endtask
initial begin
apply_stimulus(4'h5);
apply_stimulus(4'hA);
end
Verilog Tasks and Functions
Functions: Tasks:
● Must execute in zero simulation time (no delays) ● Can include timing controls (#, @)
● Must return exactly one value ● Can have zero or multiple outputs
● Can only have input arguments ● Can have input, output, and inout arguments
● Used for combinational logic (calc_parity, ● Used for sequential operations (send_packet,
count_ones) process_array)
● Can be used in continuous assignments ● Cannot be used in continuous assignments
Use functions for operations like: Use tasks for operations like:
● Calculating checksums ● Bus protocols
● Data transformations ● Test sequences
● Arithmetic operations ● Complex verification scenarios
● Boolean logic ● Multi-step operations with timing
Demo Tasks vs Functions
Self-checking testbenches
module adder_tb; // … continued from left
reg [3:0] a, b;
wire [4:0] sum;
initial begin
adder DUT(.a(a), .b(b), .sum(sum)); check_sum(4'h5, 4'h3, 5'h8);
check_sum(4'hF, 4'h1, 5'h10);
task check_sum;
end
input [3:0] in1, in2;
input [4:0] expected; endmodule
begin
a = in1; b = in2;
#1;
if(sum !== expected) begin
$display("Error: %h + %h = %h (Expected: %h)",
in1, in2, sum, expected);
end
end
endtask
Self-checking testbenches
● Automated results verification
● Does not require manual verification
● Ideal for use in scripted workflows
○ Essential for larger and more complex designs
● Regression testing
○ Catch “regressions” caused by changes in code easily
Good practice for organizing testbench code
module tb_top; Example of common design pattern:
// Clock generation
● module
task clock_gen;
// Stimulus generation ● process (always directly in TB)
task apply_stimulus; ● task
// Result checking
task verify_output;
// Test scenarios
module clock_gen(output reg clk);
task run_test;
initial clk = 0;
endmodule
always #5 clk = ~clk;
endmodule
Key takeaways
1. VCD files enable detailed waveform analysis
2. Tasks make stimulus generation modular and reusable
3. Self-checking code reduces manual verification
4. Organized code structure improves maintainability
Understanding
Delay Models
Types of Delays in Digital Circuits
● Gate Delays:
○ implicit filtering: short pulses get filtered out by internal switching mechanisms
● Wire Delays
○ change at input propagates to output - no filtering
● Two delay models in Verilog:
○ Inertial delay (default)
○ Transport delay (explicit)
Inertial Delay
// Inertial delay filters out pulses shorter than delay
module inertial_example (
input logic a,
output logic y
);
// Pulses shorter than 5 time units are filtered
assign #5 y = a;
endmodule
Transport Delay
// Transport delay preserves all pulses
module transport_example (
input logic a,
output logic y1, y2
);
// Inertial delay - filters short pulses
assign #5 y1 = a;
// Transport delay - preserves all pulses
always @(a)
y_transport <= #5 a;
endmodule
Transport Delay
// Transport delay preserves all pulses
module transport_example (
input logic a,
output logic y1, y2
);
// Inertial delay - filters short pulses
assign #5 y1 = a;
// Transport delay - preserves all pulses
always @(a)
y_transport <= #5 a;
endmodule
Demo
specify blocks
module path_delays( More advanced timing modeling
input logic a, b,
output logic y
);
Separate timing from functionality
// Functional description
assign y = a & b; Generally used inside standard cell definitions etc.
// Timing specification Avoid unless absolutely necessary
specify
// Path delay
(a => y) = 5;
(b => y) = 5;
// Setup/hold times
$setup(a, posedge b, 2);
$hold(posedge b, a, 1);
endspecify
endmodule
min/typ/max delays
module min_typ_max( Especially useful inside cell definitions
input logic a, b,
output logic y Not for typical use
);
// Min:3 Typ:5 Max:7
assign #(3:5:7) y = a & b;
specify
// Alternative specify block notation
specparam tmin = 3, ttyp = 5, tmax = 7;
(a => y) = (tmin:ttyp:tmax);
endspecify
endmodule
Standard Delay Format
module test_delays;
● Final stage modeling
logic a, b, y;
● Timing simulation: can catch issues with
// Device under test
min_typ_max dut(.*);
timing
● Clock frequency depends on min/max
initial begin
// Set simulation timescale
delays
`timescale 1ns/100ps ● Process corners, environmental
// Control simulation mode variations
`ifdef TIMING_MIN
$sdf_annotate("min_delays.sdf", dut);
`elsif TIMING_MAX
$sdf_annotate("max_delays.sdf", dut);
`else // typical
$sdf_annotate("typ_delays.sdf", dut);
`endif
end
endmodule
Key takeaways
● Delay modeling can get quite complicated
● Good reference: C. Cummings, “Correct Methods For Adding Delays To
Verilog Behavioral Models”, HDLCon 1999
○ Delay added to statements inside always block: does not model physical hardware.
■ Can be used for modeling transport delay - at cost of simulator performance
○ delays in assign, or taking output of always block and then assign: model inertial delays
● Generally:
Use delays in testbenches. Avoid using them in synthesizable code
Simulation
Internals
Verilog Information
Ultimate sources of truth:
● IEEE Std 1364™-2005: Verilog Language Reference Manual
● IEEE Std 1800™-2023: SystemVerilog— Unified Hardware Design,
Specification, and Verification Language
Simulator Operation
● Simulation proceeds in steps (time + delta cycles)
● Events are processed in an ordered queue
● Two types of simulation time:
○ Real time (#delays)
○ Delta time (instantaneous but ordered)
Event Simulation
● Any change in value of a net: Update event
● Processes are sensitive to updates:
○ All processes sensitive to an update event are executed
○ Order of execution is arbitrary (may be non-deterministic)
○ Results in evaluation events
● Time: monotonically increasing: maintained by simulator to order events
● Events are placed on event queue, ordered by simulation time
● Putting an event on the queue is called scheduling an event
Event Types in Verilog
Stratified Event Queue
● Active events: first priority to evaluate in time step
● Inactive events: at this time, but after all active events
● Non-blocking assign updates: after active and inactive
● Monitor events: debug and trace
● Future events: to be scheduled at a future time
Event Types in Verilog
Stratified Event Queue
● Active events: first priority to evaluate in time step
● Inactive events: at this time, but after all active events
● Non-blocking assign updates: after active and inactive
● Monitor events: debug and trace
● Future events: to be scheduled at a future time
Just getting started…
Systemverilog has many more sub-divisions of these event types…
“Delta Cycles”
Primarily VHDL concept, but useful to understand
module delta_cycle_demo(
input logic a, b,
output logic n1, n2, y
);
// All these happen in delta cycles
// at the same simulation time
assign n1 = a & b; // Delta cycle 1
assign n2 = n1 | b; // Delta cycle 2
assign y = n1 & n2; // Delta cycle 3
endmodule
Blocking vs Non-blocking
module blocking_example(
input logic clk, d,
output logic q1, q2
);
// BAD: Blocking in sequential logic
always_ff @(posedge clk) begin
q1 = d; // Executes first
q2 = q1; // Uses new value of q1
end
endmodule
Blocking vs Non-blocking
module blocking_example( module nonblocking_example(
input logic clk, d, input logic clk, d,
output logic q1, q2 output logic q1, q2
); );
// BAD: Blocking in sequential logic // GOOD: Non-blocking for sequential logic
always_ff @(posedge clk) begin always_ff @(posedge clk) begin
q1 = d; // Executes first q1 <= d; // Schedule update
q2 = q1; // Uses new value of q1 q2 <= q1; // Uses old value of q1
end end
endmodule endmodule
Key points
● Simulation time advances in discrete steps
● Delta cycles order events within same time
● Use blocking assignments for combinational logic
● Use non-blocking for sequential logic
● Monitor statements execute after all other events
Understanding event ordering helps debug race conditions
Keep in mind these are models, not reality: fixing a problem by using some aspect
of the event algorithm may not imply hardware correctness