Every answer in this bank follows one formula: direct answer first, evidence second, the edge third, and the testbench payoff last.
Direct answer: one sentence that answers exactly what was asked.
Evidence: the smallest code that proves it.
The edge: the follow-up, raised before the interviewer does.
So what: where it bites in a real testbench.
logic is a 4-state data type. As a variable it may have one continuous driver or any number of procedural writers, but not both kinds; always_ff and always_comb additionally forbid other writers to their outputs. wire is a net with resolution across many drivers, for tristate buses. reg is the legacy name that logic replaces; it never implied a register.
// snippet
// one continuous driver is legal on logic
logic b;
assign b = x & y;
// a second continuous driver on b would be a compile error;
// a many-driver bus stays a net:
wire w;
assign w = en1 ? d1 : 1'bz;
assign w = en2 ? d2 : 1'bz;A reg becomes a flop only when assigned in a clocked block. In always_comb it is plain combinational logic.
Four-state (logic, integer) belongs everywhere a signal touches the DUT. X propagation is a debugging signal, not noise.
Two-state (bit, int) is smaller and faster and belongs in testbench-internal bookkeeping. Assigning X to a two-state variable silently converts it to 0.
The rule is 4-state at the DUT boundary and 2-state inside the testbench, with $isunknown guarding the crossing.
The listing below is not run in this course's checks. The course runner tries Verilator first, and Verilator is 2-state, so the X constant becomes a plain value and the checks fail. It passes under Icarus (iverilog -g2012), which models four states.
// not-run: needs 4-state X; Verilator 5.048 (tried first by the runner) is 2-state and fails the X checks; passes under Icarus
module x_guard (
input logic [3:0] sampled,
output logic flagged,
output bit [3:0] safe
);
always_comb begin
flagged = $isunknown(sampled);
if (flagged) safe = 4'b0;
else safe = sampled;
end
endmodule
module tb_x_guard;
timeunit 1ns; timeprecision 1ps;
logic [3:0] sampled;
logic flagged;
bit [3:0] safe;
x_guard dut (.*);
int errors = 0;
initial begin
sampled = 4'b10x1; #1;
if (flagged !== 1'b1) errors++;
if (safe !== 4'b0000) errors++; // guarded: flagged sample replaced by 0
sampled = 4'b1011; #1;
if (flagged !== 1'b0) errors++;
if (safe !== 4'b1011) errors++;
if (errors == 0) $display("PASS: X flagged before conversion and clean values pass through");
else $fatal(1, "FAIL: x-guard output wrong");
$finish;
end
endmoduleThe same boundary discipline runs here with a parity check. The testbench proves corrupt samples are flagged and replaced while clean samples pass through.
module parity_guard (
input logic [3:0] sampled,
input logic sampled_parity,
output logic flagged,
output logic [3:0] safe
);
always_comb begin
flagged = (^sampled !== sampled_parity);
if (flagged) safe = 4'b0;
else safe = sampled;
end
endmodule
module tb_parity_guard;
timeunit 1ns; timeprecision 1ps;
logic [3:0] sampled;
logic sampled_parity;
logic flagged;
logic [3:0] safe;
parity_guard dut (.*);
int errors = 0;
initial begin
sampled = 4'b1011; sampled_parity = 1'b1; #1; // reduction xor is 1: clean
if (flagged !== 1'b0) errors++;
if (safe !== 4'b1011) errors++;
sampled = 4'b1010; sampled_parity = 1'b1; #1; // reduction xor is 0: corrupted
if (flagged !== 1'b1) errors++;
if (safe !== 4'b0000) errors++;
sampled = 4'b0000; sampled_parity = 1'b0; #1; // reduction xor is 0: clean zero
if (flagged !== 1'b0) errors++;
if (safe !== 4'b0000) errors++;
if (errors == 0) $display("PASS: parity guard flags corruption and passes clean data");
else $fatal(1, "FAIL: parity guard output wrong");
$finish;
end
endmoduleBlocking (=) executes in statement order. Nonblocking (<=) samples its right-hand side now and assigns in the NBA region, after all blocking code in the step.
The rule is nonblocking in clocked logic and blocking in combinational logic. Cross-block blocking races are scheduler-undefined; NBA deferral makes every block read pre-edge values, so results are deterministic.
module tb_swap;
timeunit 1ns; timeprecision 1ps;
logic clk = 0;
always #5 clk = ~clk;
logic a_nb = 1'b0, b_nb = 1'b1, a_b = 1'b0, b_b = 1'b1;
int errors = 0;
always_ff @(posedge clk) begin a_nb <= b_nb; b_nb <= a_nb; end
always @(posedge clk) begin a_b = b_b; b_b = a_b; end
initial begin
@(posedge clk); #1;
if (a_nb !== 1'b1 || b_nb !== 1'b0) errors++; // clean swap
if (a_b !== 1'b1 || b_b !== 1'b1) errors++; // blocking kept itself
@(posedge clk); #1;
if (a_nb !== 1'b0 || b_nb !== 1'b1) errors++; // swapped back
if (errors == 0) $display("PASS: nonblocking swap exchanges values and blocking version collapses");
else $fatal(1, "FAIL: swap behavior wrong");
$finish;
end
endmoduleThe waveform shows the exchange repeating on every clock edge.
Slot 0: the first rising edge exchanges the seeds, so a becomes 1 and b becomes 0.
Slot 1: the second edge swaps them back, a 0 and b 1.
Slot 2: the third edge, a 1 and b 0 again.
Slot 3: the fourth edge, a 0 and b 1 again.
Slot 4: the fifth edge, a 1 and b 0. Every edge exchanges the two bits.
No. always_comb runs once at time zero, sees signals read inside called functions, and forbids other processes from writing its outputs. always @* waits for an input change and can miss function-internal reads.
A latch inferred inside always_comb is flagged as a warning. It becomes an error only where the lint flow escalates it.
// snippet
function logic f(input logic x);
return x ^ mode; // hidden input, not an argument
endfunction
// always @* misses mode changes; always_comb stays sensitive:
// always_comb begin y2 = f(a); end// bad-example
always_comb begin
if (sel) z = a; // no else: z holds state, a latch is inferred
endThe testbench below proves the positive form: defaults assigned before the branch leave no path unassigned. Every select gives a defined one-hot output, and disable gives all-zero.
module dec24 (
input logic [1:0] sel,
input logic en,
output logic [3:0] y
);
always_comb begin
y = 4'b0;
if (en) y[sel] = 1'b1;
end
endmodule
module tb_dec24;
timeunit 1ns; timeprecision 1ps;
logic [1:0] sel;
logic en;
logic [3:0] y;
dec24 dut (.*);
int errors = 0;
initial begin
en = 1;
for (int s = 0; s < 4; s++) begin
sel = s[1:0]; #1;
if (y !== (4'b0001 << s)) errors++;
end
en = 0; sel = 2'd2; #1;
if (y !== 4'b0) errors++;
if (errors == 0) $display("PASS: decoder is one-hot for every select and silent when disabled");
else $fatal(1, "FAIL: decoder output wrong");
$finish;
end
endmoduleA function runs in zero time and returns a value. A task may consume time and returns results through output arguments.
Anything touching the clock is a task; pure computation is a function. Both are automatic in testbenches, since static locals corrupt each other across concurrent calls.
Packed dimensions sit before the name and form one contiguous vector. Unpacked dimensions sit after the name and form separate elements, the natural model for memories.
Whole-vector operations work on packed data only. The testbench below drives a byte memory through a task, computes expectations in a function, and checks all 16 locations.
module byte_mem (
input logic clk,
input logic wr,
input logic [3:0] addr,
input logic [7:0] wdata,
output logic [7:0] rdata
);
logic [7:0] mem [16];
always_ff @(posedge clk) begin
if (wr) mem[addr] <= wdata;
rdata <= mem[addr];
end
endmodule
Construct | Use it for | Never for |
|---|---|---|
| Driving pins and waiting for responses | Pure computation |
| CRC, compare, format, expected-value math | Anything with |
Packed vector | Buses, slices, whole-word arithmetic | Sparse storage |
Unpacked array | Memories and lookup tables | Whole-array assignment from a scalar |
a == b?The first line updates a immediately, so the second line reads the new a instead of the old one. Both end up holding the old b.
always_comb stays correct because its sensitivity includes function-internal reads. always @* misses them and goes stale.
When it consumes time: driving pins across clock edges or waiting for a response. Computation with no time advance belongs in a function.