Verification with Verilog
Table of Contents
-
4-bit Counter DUT Verification
- wire vs. reg
- Blocking vs. Non-blocking
- Synchronous vs. Asynchronous Resets
- Verification Plan
- Manual Verification
- Test Bench -
256-bit Counter DUT Verification
- Verification Plan
- Manual Verification
- Test Bench
- Tasks and Functions
- System Tasks and Functions
Why are we doing verification with Verilog?
The RTL is in Verilog. Generally the flow of learning is Digitals -> RTL intro through Verilog -> Verifying the RTL using a TB. So, it is only logical to try to verify using what's already known until we hit a point where what we know is not enough (or probably not efficient at all) anymore!
4-bit Counter DUT Verification
module dut_4_bit_counter(
input clk,
input rstn,
output reg[3:0] c_out //understand reg vs wire
);
always @ (posedge clk) begin
if(!rstn)
c_out <= 0; //synchronous active low reset. It's important to understand what this means.
else
c_out <= c_out + 1; //understand blocking vs non-blocking
end
A bit of revision on important verilog topics before proceeding with the verification of above DUT (Device Under Test)-
- wire vs reg
- blocking vs non-blocking
- synchronous vs asynchronous reset
HDLs operate in concurrancy. Languages like C that operate in procedural manner handle multiple assignments to a variable in a straight forward manner- the variable holds the last assigned value.
In case of Verilog, the value a variable holds at any instant is determined by:
- whether multiple drivers driving this variable. In which case, the final value will depend on the resolution functions.
- how the variable is synced to the clock
wire vs. reg
- wire and reg are two different data types defined in Verilog.
- wire represents a connection between two concurrent processes (physically, it'll manifest as a net between two components). It should be continously driven (in verilog, it is done using 'assign' keyword), and it can't store values. It can be driven by multiple drivers simultaneously and the value on the wire at any instant is determined by the underlying resolution function(verilog provides option to decide strength of the driver).
- reg is a 1-bit 4-state variable. It is generally used to store data.
- It's not allowed to drive a reg type variable using 'assign' keyword. Instead, reg variables are assigned values using = or <= within procedural blocks (always, initial, task, function).
- A reg type will synthesize to a register only if it's linked to edge of clock. If it is linked to ALL the input signals and not clock, it'll synthesize to a combinational circuit. However, if all paths are not defined, it can inadvertently infer a latch, which is often undesirable.
This code would be interpreted at synthesis level (without any optimizations), refer below block diagram:
module reg_wire_example( input clk, input a, input b, output c, output d ); wire temp_wire; reg i; // 1. Continuous assignment to wire 'temp_wire' assign temp_wire = a & b; // 2. Procedural block (sequential logic) always @(posedge clk) begin i <= a | b; end // 3. Assign 'c' (output) from 'temp_wire' (wire) assign c = temp_wire; // 4. Assign 'd' (output) from 'i' (reg) assign d = i; endmodule
The below code shows how reg might cause inadvertent inferred latch:
module combinational_using_reg (
input a,
input b,
input sel,
output reg out_assign_all_paths, // Declared as reg as it's assigned in an always block
output reg out_default_assign,
output reg latch_out_1,
output reg latch_out_2
);
// --- Method 1: Assigning in all if/else branches ---
always @(*) begin // Sensitive to all inputs (a, b, sel). 'always_comb' in SystemVerilog.
if (sel == 1'b1) begin
out_assign_all_paths = a & b; // Assignment for sel = 1
end else begin
out_assign_all_paths = a | b; // Assignment for sel = 0 (covers all paths)
end
end
// --- Method 2: Providing a default assignment ---
always @(*) begin // Sensitive to all inputs (a, b, sel). 'always_comb' in SystemVerilog.
out_default_assign = a ^ b; // Default assignment (e.g., for the 'else' case or unhandled cases)
if (sel == 1'b1) begin
out_default_assign = a + b; // Overrides default if sel is 1
end
// No 'else' needed here because 'out_default_assign' already has a value.
// If sel is 0, the default assignment (a ^ b) will be the final value.
end
//Inferred latch case 1
always @(*) begin // Sensitive to all inputs (a, b, enable)
if (sel == 1'b1) begin
latch_out_1 = a & b; // latch_out_1 is assigned only when enable is high
end
// IMPLICIT ELSE: What happens to 'latch_out_1' when 'sel' is 0?
// It retains its old value. This behavior implies a latch.
end
//Inferred latch case 2
always @(a) begin // Sensitive to only one input
if (sel == 1'b1) begin
latch_out_2 = a & b; // Assignment for sel = 1
end else begin
latch_out_2 = a | b; // Assignment for sel = 0 (covers all paths)
end
/*Since the block only re-executes when a changes, if b or sel change without a changing,
the current value of latch_out_2 will hold its previous value.
This "holding" behavior is precisely what defines a latch.*/
end
endmodule
Blocking vs. Non-blocking
- Blocking and Non-blocking are type of assignments in verilog (or Systemverilog).
- Blocking assignment (=) is also called procedural assignment and behaves similar to how assignment is done in languages like C. RHS is evaluated, assigned to LHS and then the code moves to the next line. This has less relevance in hardware design as this could lead to race conditions: unwanted sequential dependency of variables which might not reflect in the actual hardware (or might not be the intent to begin with). For example:
module blockingexample ( input in_data, input clk, output reg out1, output reg out2 ); always @(posedge clk) begin // Blocking assignments out1 = in_data; // out1 gets in_data's value immediately out2 = out1; // out2 immediately gets the *new* value of out1 // (which just got in_data's value) end endmodule /*If this were synthesizable sequential logic, it would effectively model out2 always being the same as in_data in the same clock cycle, making out1 redundant or effectively out2 = in_data. This is not how two cascaded flip-flops work.*/
- Blocking assignments are typically used to model combinational logic within an always @(*) or always_comb block (read about always_comb an SV construct), or for calculating intermediate values in test benches.
- When a non-blocking assignment is encountered, the RHS is evaluated immediately, but the actual assignment (update) to the LHS variable is scheduled to occur at the end of the current time step, after all other blocking assignments and RHS evaluations in all active always blocks for that time step have completed.
- Non-blocking assignments are mandatorily used for modeling sequential logic (flip-flops, latches) in always @(posedge clk) or always_ff blocks.
module nonblockingexample ( input logic in_data, input logic clk, output logic out1, output logic out2 ); always @(posedge clk) begin // Non-blocking assignments out1 <= in_data; // out1 is scheduled to get in_data's value out2 <= out1; // out2 is scheduled to get out1's *current* value // (which is from the *previous* clock cycle, not the new scheduled one) end /*This code correctly models two cascaded flip-flops: * out1 will get the value of in_data from the current clock cycle. * out2 will get the value of out1 from the previous clock cycle. This is exactly how two flip-flops chained together behave.*/ endmodule
- The following examples cover how variou delays affect blocking and non-blocking assignment:
module zerodelayexample; reg a, b, c, d; initial begin $display("--- Initial State ---"); a = 0; b = 0; c = 0; d = 0; $monitor("Time %0t: a=%b, b=%b, c=%b, d=%b", $time, a, b, c, d); #1; // Advance to time 1 $display("\n--- Time 1: Blocking then Non-Blocking within same time slot ---"); a = 1; // Blocking: 'a' updates immediately to 1 b <= a; // Non-blocking: 'b' scheduled to get 'a's *new* value (1) c = b; // Blocking: 'c' gets 'b's *old* value (0), as 'b' hasn't updated yet from NBA d <= c; // Non-blocking: 'd' scheduled to get 'c's *new* value (0) // At this point (still time 1), the $monitor will display current values. // Non-blocking updates (b, d) haven't occurred yet. #0; // Explicitly consume a delta cycle to see immediate effects $display("\n--- Time 1 (after 1st set of updates) ---"); // Non-blocking updates occur now, at the end of time 1's events end endmodule /\EXPECTED OUTPUT/\ --- Initial State --- Time 0: a=0, b=0, c=0, d=0 --- Time 1: Blocking then Non-Blocking within same time slot --- Time 1: a=1, b=0, c=0, d=0 <-- This is the display from $monitor at the start of Time 1. a becomes 1 immediately. b is scheduled to become 1. c becomes 0 (from b's old value). d is scheduled to become 0. --- Time 1 (after 1st set of updates) --- Time 1: a=1, b=1, c=0, d=0 <-- This display happens after non-blocking assignments (b, d) for Time 1 have completed. b now shows 1. d now shows 0. /\OUTPUT ENDS/\ module delaysexample; reg data_in; reg out_bb, out_bn, out_nb, out_nn; // Blocking-Blocking, Blocking-NonBlocking, etc. initial begin data_in = 0; out_bb = 0; out_bn = 0; out_nb = 0; out_nn = 0; $monitor("Time %0t: data_in=%b, out_bb=%b, out_bn=%b, out_nb=%b, out_nn=%b", $time, data_in, out_bb, out_bn, out_nb, out_nn); #10; // Advance to time 10 $display("\n--- Time 10: data_in changes to 1 ---"); data_in = 1; // 1. Blocking with RHS delay, then Blocking (BB) // Execution pauses for #5 for out_bb. out_bb = #5 data_in; // out_bb assigned at time 15 out_bn = out_bb; // out_bn assigned at time 15, takes value of out_bb at time 15 // 2. Non-blocking with RHS delay, then Blocking (NB) // RHS evaluated at time 10. out_nb update scheduled for time 15. // Execution of block *does not* pause. out_nb <= #5 data_in; // out_nb becomes 1 at time 15 out_nn <= out_nb; // out_nn scheduled for time 10, takes out_nb's OLD value (0) $display("\n--- Check values immediately at time 10 ---"); // Only blocking assignments without delays will show immediate changes. // Non-blocking updates are still scheduled. #1; // Advance time by 1 unit // No change yet, non-blocking updates are still scheduled for time 15. #4; // Advance to time 15 // At time 15, all scheduled updates for #5 delay happen. $display("\n--- Time 15: Delays complete ---"); #5; // Advance to time 20 $display("\n--- Time 20: Stable ---"); end endmodule /\EXPECTED OUTPUT/\ Time 0: data_in=0, out_bb=0, out_bn=0, out_nb=0, out_nn=0 --- Time 10: data_in changes to 1 --- Time 10: data_in=1, out_bb=0, out_bn=0, out_nb=0, out_nn=0 <-- data_in changed, others still old values. --- Check values immediately at time 10 --- Time 10: data_in=1, out_bb=0, out_bn=0, out_nb=0, out_nn=0 <-- Still time 10. No immediate effect from scheduled assignments. --- Time 15: Delays complete --- Time 15: data_in=1, out_bb=1, out_bn=1, out_nb=1, out_nn=0 <-- out_bb, out_bn, out_nb update to 1. out_nn remains 0 as it got out_nb's old value. --- Time 20: Stable --- Time 20: data_in=1, out_bb=1, out_bn=1, out_nb=1, out_nn=0 /\OUTPUT ENDS/\ module inertialdelayfilter; wire a; reg a_driver; wire #5 out_inertial; // 5 units inertial delay assign out_inertial = a; initial begin $display("--- Inertial Delay Filtering ---"); a_driver = 0; $monitor("Time %0t: a=%b, out_inertial=%b", $time, a, out_inertial); #10; // Advance to time 10 $display("\n--- Time 10: Driving a short pulse (width < delay) ---"); a_driver = 1; // 'a' becomes 1 at time 10 #3; // 'a' stays 1 for 3 units a_driver = 0; // 'a' becomes 0 at time 13. Pulse width = 3ns. // Since 3ns < 5ns (delay), out_inertial should NOT change. #10; // Observe after the pulse has passed and delay period $display("\n--- Time 23: After short pulse ---"); #10; // Advance to time 33 $display("\n--- Time 33: Driving a long pulse (width > delay) ---"); a_driver = 1; // 'a' becomes 1 at time 33 #7; // 'a' stays 1 for 7 units a_driver = 0; // 'a' becomes 0 at time 40. Pulse width = 7ns. // Since 7ns > 5ns (delay), out_inertial SHOULD change. #10; // Observe after the pulse has passed and delay period $display("\n--- Time 50: After long pulse ---"); end // Connect reg driver to wire assign a = a_driver; endmodule /\EXPECTED OUTPUT/\ --- Inertial Delay Filtering --- Time 0: a=0, out_inertial=0 Time 10: a=1, out_inertial=0 <-- 'a' goes high. out_inertial is still 0 (delay). Time 13: a=0, out_inertial=0 <-- 'a' goes low again. Pulse (10-13) was 3ns. Since 3ns < 5ns delay, out_inertial is NOT affected. --- Time 23: After short pulse --- Time 23: a=0, out_inertial=0 <-- Confirms out_inertial never changed. --- Time 33: Driving a long pulse (width > delay) --- Time 33: a=1, out_inertial=0 <-- 'a' goes high. out_inertial still 0. Time 38: a=1, out_inertial=1 <-- After 5ns delay, out_inertial becomes 1. Time 40: a=0, out_inertial=1 <-- 'a' goes low. out_inertial still 1. Time 45: a=0, out_inertial=0 <-- After another 5ns delay, out_inertial becomes 0. --- Time 50: After long pulse --- Time 50: a=0, out_inertial=0 <-- Confirms out_inertial correctly followed the long pulse. /\OUTPUT ENDS/\
Synchronous vs. Asynchronous Resets
Q: First of all, why do we need reset? A: Reset signals are essential in digital circuits to bring flip-flops and registers to a known, initial state.
The way this reset operates relative to the clock signal defines whether it's synchronous or asynchronous. Usage depends on application.
- Synchronous resets are just like any other inputs to the circuit. If its value changes, this is sampled at the next clk edge. Therefore, they are easy for STA and are hence predictable, glitch immune, and preferred for design.
- Howerver, they can't be used when the clock is not up and running yet. To account for this, Asynchronous resets are used for reset assertion and Synchronous resets are used for reset de-assertion. Going full async could lead to timing issues or unwanted glitches.
- Asynchronous resets themselves don't directly cause STA issues (setup time and hold time violations) or glitches on the reset line itself. They cause problems in how the design responds to them, particularly during deassertion, and how they interact with synchronous logic.
- Setup Time Violation: If a data input changes too close to (before) the active clock edge, the flip-flop might not have enough time to correctly capture the new data.
- Hold Time Violation: If a data input changes too close to (after) the active clock edge, the flip-flop might not be able to hold the previous data and could change unexpectedly.
module simpleff (
input clk,
input rst_n_async, // Asynchronous active-low reset
input d,
output q
);
always_ff @(posedge clk or negedge rst_n_async) begin
if (!rst_n_async) begin
q <= 0;
end else begin
q <= d;
end
end
endmodule
Suppose rst_n_async is currently '0' (asserted), so q is '0'.
At Time T0, rst_n_async goes '1' (deasserted).
Simultaneously, or very close to T0, a posedge clk occurs.
The Problem: The flip-flop is trying to do two things at once:
Release from asynchronous reset: Transition its internal state from '0' (forced by reset) to whatever d is,
immediately because the reset condition is no longer met.
Sample d synchronously: Capture d on the posedge clk
If the deassertion edge of rst_n_async falls within the setup and hold window of the posedge clk, the flip-flop enters metastability.
Result: q might not settle to a definite '0' or '1'. It could oscillate for a period,
settle to a random value, or settle very slowly.
STA Impact: STA tools cannot reliably analyze paths that go through a metastable state.
They will flag these as setup/hold violations, which indicates potential functional failures in hardware.
You can't guarantee the circuit will work reliably.
- Glitches and Spurious Resets:
If the asynchronous reset signal is generated by combinational logic, and that logic produces a glitch (a very short, unintended pulse) while it's normally deasserted, the asynchronous reset can immediately and unintentionally trigger all connected flip-flops to reset.
wire rst_logic;
assign rst_logic = (some_condition_a & some_condition_b); // Imagine glitch here
// And this rst_logic feeds rst_n_async
module hybridreset (
input logic clk, // System clock
input logic rst_n_ext, // External active-low asynchronous reset
output logic some_q
);
// --- Reset Synchronizer ---
// Creates an internal synchronous reset signal from the external asynchronous one.
// This is the core of the hybrid approach.
reg rst_n_sync1; // First stage of synchronizer
reg rst_n_sync2; // Second stage of synchronizer (output used by design)
// The synchronizer operates on the deassertion edge of rst_n_ext
always_ff @(posedge clk or negedge rst_n_ext) begin
if (!rst_n_ext) begin // Asynchronous assertion
rst_n_sync1 <= 1'b0;
rst_n_sync2 <= 1'b0;
end else begin // Synchronous deassertion
rst_n_sync1 <= 1'b1; // Pass '1' through a chain of FFs
rst_n_sync2 <= rst_n_sync1; // Output of first FF feeds second
end
end
// --- Main Design Logic ---
// All synchronous flip-flops in the design use rst_n_sync2
// which is now guaranteed to be synchronous on its deassertion.
reg internal_counter; // Example of a register in the design
always_ff @(posedge clk) begin
if (!rst_n_sync2) begin // Using the synchronized reset
internal_counter <= 4'b0000;
some_q <= 1'b0;
end else begin
internal_counter <= internal_counter + 1;
some_q <= internal_counter[0]; // Example logic
end
end
endmodule
Second synchronizer FF: - The 2nd FF is timed with same clk as the first one. - This provides an additional clk cycle for rst_n_sync1 to resolve from any metastability and settle down. rst_n_sync2 is basically a stable version of rst_n_sync1.
Verification Plan (4-bit)
The verification plan typically involves 3 activities: 1. Test Plan: - Check if the count resets to 0 on reset assertion. - Check if the counter rolls back to 0 after max count. - Check if the counter can count all counts. 2. Verification Strategy - Random or Manual? Here easier manual as limited cases. - Are there scenarios that need directed testing? Yep. All are directed. - Do we need an automated checking mechanism? Nope. Manually verifiable. 3. Test Bench development - Clock and reset generation