Part 8 · Senior & Interview Prep · Intermediate
Q&A: Language Core
logic vs wire vs reg, 4-state vs 2-state, blocking vs nonblocking with the swap example, always_comb, task vs function, packed vs unpacked.
Q: What is the difference between logic, wire, and reg?
logic is a 4-state data type that can be driven by exactly one driver — procedural or continuous. wire is a net that supports multiple drivers with resolution (needed for tristate buses). reg is the legacy Verilog type that logic replaces — despite the name it never implied a register. Modern rule: use logic everywhere except true multi-driver nets, which must stay wire.
logic a; // single driver: procedural OR continuous assign
wire w; // net: multiple drivers resolved (tristate, pullups)
assign w = en1 ? d1 : 1'bz;
assign w = en2 ? d2 : 1'bz; // legal on wire, illegal on logic
logic b;
assign b = x & y; // fine: one continuous driver
// initial b = 0; // adding this too = compile error (two drivers)Follow-up: "Does reg synthesize to a flip-flop?" — No. reg/logic becomes a flop only when assigned in a clocked always block; in always_comb it is pure combinational logic. The name is a historical accident.
Junior vs senior: a junior says "logic is the new reg." A senior adds the single-driver rule, why wire still exists (resolution), and that the compiler enforcing one driver on logic catches real wiring bugs.
Q: 4-state vs 2-state types — when do you use which?
4-state (logic, integer) represent 0/1/X/Z and belong everywhere a signal touches the DUT — X propagation is a debugging signal, not noise. 2-state (bit, int) are faster and smaller, and belong in testbench-internal bookkeeping: counters, scoreboards, transaction fields that are generated rather than sampled.
logic [31:0] dut_rdata; // sampled from DUT: X means "bug here"
bit [31:0] exp_data; // scoreboard expectation: never legitimately X
int unsigned match_count; // 2-state counter: fast, can't go X
// THE TRAP: 2-state silently converts X to 0
bit b = dut_rdata[0]; // if rdata[0] is X, b becomes 0 — bug hidden!
if ($isunknown(dut_rdata)) $error("X on rdata"); // check BEFORE convertingFollow-up: "What happens when you assign X to a bit?" — It silently becomes 0. That is precisely the danger: sampling DUT outputs into 2-state types erases the X that would have exposed an uninitialized register.
Junior vs senior: a junior cites the speed benefit of 2-state. A senior leads with the X-masking hazard and gives the rule: 4-state at the DUT boundary, 2-state inside the testbench, $isunknown at the crossing.
Q: Blocking vs nonblocking — and why does the swap example matter?
Blocking (=) executes immediately in statement order. Nonblocking (<=) evaluates the right-hand side now but assigns in the NBA region, after all blocking code in the time step. The rule: nonblocking in clocked logic, blocking in combinational logic — mixing them creates simulation/synthesis mismatches and race conditions.
// THE SWAP — the canonical demonstration
always_ff @(posedge clk) begin // NONBLOCKING: works
a <= b; // both RHS sampled first,
b <= a; // then both assigned → clean swap
end
always @(posedge clk) begin // BLOCKING: broken
a = b; // a updated immediately...
b = a; // ...so b gets b's own old value back
end // result: a == b, swap lostFollow-up: "Why does blocking in clocked logic create races between two always blocks?" — If block A blocking-writes a signal that block B reads on the same edge, the result depends on scheduler ordering, which the LRM leaves undefined between blocks. Nonblocking defers all updates to the NBA region, so every block reads pre-edge values — deterministic.
Junior vs senior: a junior recites "<= for sequential, = for combinational." A senior demonstrates the swap, explains RHS-sampling versus NBA-region assignment, and names the cross-block race as the real reason the rule exists.
Q: always_comb vs always @* — aren't they the same?
No — always_comb is stricter and better in four ways: it executes once at time zero (always @* waits for an input change, so outputs can start stale); its sensitivity includes signals inside called functions (always @* misses them); it forbids other processes from writing its outputs; and it declares design intent that tools verify — a latch inferred inside always_comb is a lint/compile error, not a silent surprise.
function logic f(input logic x);
return x ^ mode; // reads 'mode' — hidden input!
endfunction
always @* begin y1 = f(a); end // NOT retriggered when 'mode' changes
always_comb begin y2 = f(a); end // correctly sensitive to 'mode' too
always_comb begin
if (sel) z = a; // no else → latch → tool ERROR (good!)
endFollow-up: "When would always @* and always_comb behave differently in simulation?" — At time zero (always @* may never trigger if inputs hold steady) and whenever a called function reads a signal not passed as an argument.
Junior vs senior: a junior says "always_comb is the SystemVerilog version." A senior lists the time-zero execution, function-internal sensitivity, single-writer enforcement, and latch checking — four concrete differences.
Q: Task vs function — what are the real differences?
A function executes in zero simulation time — no #, @, or wait allowed — and returns a value. A task may consume time and block, but returns results only through output/ref arguments. The testbench consequence: anything that touches the clock (driving pins, waiting for a response) must be a task; pure computation (CRC, compare, format) should be a function.
function automatic logic [7:0] crc8(input logic [7:0] d);
return d ^ 8'h07; // zero-time computation
endfunction
task automatic drive_byte(input logic [7:0] d);
@(posedge clk); // consumes time — must be a task
data <= d;
valid <= 1;
@(posedge clk);
valid <= 0;
endtaskFollow-up: "Why mark them automatic in a testbench?" — Static (the default in modules) means one shared copy of arguments and locals; two concurrent calls — fork branches, or two agents — corrupt each other's variables. automatic gives each call its own stack frame.
Junior vs senior: a junior says "tasks can have delays." A senior adds the automatic-vs-static trap and notes that class methods are automatic by default but module tasks are not — the bug appears exactly when you move code between them.
Q: Packed vs unpacked arrays — what is the difference?
Packed dimensions (before the name, logic [7:0][3:0] x) form one contiguous vector — bit-addressable, sliceable, usable in arithmetic, and synthesizable as a single bus. Unpacked dimensions (after the name, logic [7:0] mem [256]) are a collection of separate elements — the natural model for memories and lookup tables. Packed maps to wires; unpacked maps to storage.
logic [3:0][7:0] word; // packed: 32 contiguous bits
logic [7:0] mem [0:255]; // unpacked: 256 separate bytes
word = 32'hAABBCCDD; // whole-vector ops legal
byte2 = word[2]; // packed select: 8'hBB
// mem = 32'h0; // ILLEGAL: no whole-array assign from scalar
mem[5] = word[0]; // element access fine
// memory implication:
// packed → one vector, contiguous simulator storage
// unpacked → per-element storage; can't slice across elementsFollow-up: "Can you mix them?" — Yes: logic [7:0] mem [256] is an unpacked array of packed bytes — the standard memory model. Packed dimensions index right-to-left innermost; unpacked left-to-right.
Junior vs senior: a junior gives the before/after-the-name rule. A senior adds what each is for (vectors vs storage), which operations each permits, and the mixed-declaration memory idiom.
Key takeaways
logic = single-driver 4-state everywhere; wire only for true multi-driver nets.
4-state at the DUT boundary, 2-state inside the TB, $isunknown at the crossing.
Nonblocking defers assignment to the NBA region — that is what makes the swap and cross-block determinism work.
always_comb beats always @* on time-zero, function sensitivity, single-writer, and latch checks.
Time-consuming code is a task; computation is a function; both automatic in testbenches.
Common pitfalls
Sampling DUT outputs into bit/int — X silently becomes 0 and the bug hides.
Blocking assignments in clocked blocks — order-dependent races between always blocks.
Static tasks called concurrently from fork branches — shared locals corrupt each other.