SystemVerilog · Module 6
Function Declarations & Return Values
Return types, return statement vs named variable, early return, arguments, RTL usage, recursion, packages, synthesis deep dive, 7 debugging labs, and interview Q&A.
Module 6 · Page 6.2
Functions are the backbone of synthesizable RTL reuse — the same function body gets inlined into gates at every call site. This page covers every part of the declaration, both return styles, synthesis implications, guard-clause patterns, verification helpers, seven debugging labs pulled from real project post-mortems, and thirteen interview-ready questions.
Anatomy of a Function — Every Part Labelled
A SystemVerilog function has six distinct parts. Read the annotated diagram first, then the code below it.
// ① function + automatic — sets variable lifetime
// ② return type — what the function hands back
// ③ name — snake_case convention
// ④ argument list — input ports only (by default)
// ⑤ local variable — private to this call
// ⑥ return — exits and sends the value to the caller
function automatic logic [7:0] calc_crc8 (
input logic [63:0] payload,
input logic [7:0] poly
);
logic [7:0] crc = 8'hFF;
foreach (payload[i])
crc = crc ^ payload[i];
return crc;
endfunction : calc_crc8 // optional labelReturn Types — What a Function Can Return
The return type is declared immediately after function [automatic] and before the function name. It tells the caller and the tool exactly what kind of value to expect.
// ── Single-bit logic ─────────────────────────────────────────────
function automatic logic is_zero (input logic [31:0] val);
return (val == 32'h0);
endfunction
// ── Multi-bit logic vector ───────────────────────────────────────
function automatic logic [7:0] calc_parity (input logic [63:0] d);
return ^d[7:0] ^ ^d[15:8] ^ ^d[23:16] ^ ^d[31:24]
^ ^d[39:32] ^ ^d[47:40] ^ ^d[55:48] ^ ^d[63:56];
endfunction
// ── Signed integer ───────────────────────────────────────────────
function automatic int abs_val (input int x);
return (x < 0) ? -x : x;
endfunction
// ── Unsigned integer ─────────────────────────────────────────────
function automatic int unsigned bit_count (input logic [31:0] val);
int unsigned cnt = 0;
foreach (val[i]) cnt += val[i];
return cnt;
endfunction
// ── String (testbench / display only) ────────────────────────────
function automatic string state_name (input logic [2:0] s);
case (s)
3'd0: return "IDLE";
3'd1: return "BUSY";
default: return "UNKNOWN";
endcase
endfunction
// ── Real (modeling / timing — not synthesisable) ─────────────────
function automatic real cycles_to_ns (input int cycles, input real period_ns);
return cycles * period_ns;
endfunction
// ── void (no return value — covered fully in 6.7) ────────────────
function automatic void log_msg (input string msg);
$display("[%0t] %s", $time, msg);
endfunction| Return type | Synthesisable? | Typical use |
|---|---|---|
logic | Yes | Single-bit result, flags, comparisons |
logic [N-1:0] | Yes | Bus computation, encoders, ALU results |
int | Limited | Loop counters, indices in testbenches |
int unsigned | Limited | Counts, sizes, bit population |
string | No | Debug labels, FSM state names in $display |
real | No | Timing models, floating-point simulation |
void | Yes (body must be) | Logging, side-effect functions with no return |
Two Ways to Return a Value
SystemVerilog inherited both return styles from Verilog history. The return statement is the modern approach; the named return variable is the legacy Verilog style. Both are valid — know both so you can read any codebase.
// ════ STYLE 1: return statement — PREFERRED ═══════════════════════
function automatic logic [7:0] gray2bin (input logic [7:0] gray);
logic [7:0] bin;
bin[7] = gray[7];
for (int i = 6; i >= 0; i--)
bin[i] = bin[i+1] ^ gray[i];
return bin; // explicit: hand this value to the caller
endfunction
// ════ STYLE 2: named return variable (legacy Verilog) ═════════════
function logic [7:0] gray2bin_v (input logic [7:0] gray);
// The function name itself acts as the return variable
gray2bin_v[7] = gray[7];
for (int i = 6; i >= 0; i--)
gray2bin_v[i] = gray2bin_v[i+1] ^ gray[i];
// falls through to endfunction — gray2bin_v holds the result
endfunction
// ── Both called identically ───────────────────────────────────────
logic [7:0] binary_val;
binary_val = gray2bin(8'hA3); // style 1
binary_val = gray2bin_v(8'hA3); // style 2 — same resultEarly Return — Exiting Before endfunction
A function can have multiple return statements. As soon as any return is reached, execution leaves the function immediately — no further lines run. This is called an early return and it is the cleanest way to handle guard clauses and special cases without deep nesting.
// ── Early return: guard clause pattern ───────────────────────────
function automatic logic [3:0] count_ones (input logic [7:0] data);
// Guard: if all zeros, return immediately — no iteration needed
if (data == 8'h00) return 4'd0;
// Guard: if all ones, return immediately
if (data == 8'hFF) return 4'd8;
// General case: count the set bits
automatic logic [3:0] cnt = 0;
foreach (data[i]) cnt += data[i];
return cnt;
endfunction
// ── Multiple returns inside case (very common pattern) ──────────
function automatic string opcode_name (input logic [5:0] op);
case (op)
6'b000000: return "ADD";
6'b000001: return "SUB";
6'b000010: return "AND";
6'b000011: return "OR";
6'b000100: return "XOR";
6'b000101: return "SHL";
6'b000110: return "SHR";
default : return "UNKNOWN";
endcase
endfunction
// Usage in display:
$display("Executing: %s", opcode_name(instr[5:0]));Function Arguments — Input Only (by Default)
Function arguments are declared just like module ports — with a direction and a type. Unlike tasks, functions only accept input arguments by default. Using output or inout is technically legal but strongly discouraged — use the return value instead. ref arguments (pass by reference) are covered in page 6.5.
// ── No arguments — function uses only module-level signals ───────
function automatic logic is_idle ();
return (state == IDLE); // 'state' is a module-level signal
endfunction
// ── Single argument ──────────────────────────────────────────────
function automatic logic is_one_hot (input logic [7:0] val);
return (val != 0) && ((val & (val - 1)) == 0);
endfunction
// ── Multiple arguments with types ────────────────────────────────
function automatic logic [15:0] saturate_add (
input logic [15:0] a,
input logic [15:0] b,
input logic signed_mode // 1=signed, 0=unsigned saturation
);
logic [16:0] full = a + b;
if (signed_mode)
return full[16] ? 16'h7FFF : full[15:0];
else
return full[16] ? 16'hFFFF : full[15:0];
endfunction
// ── Direction can be omitted — defaults to 'input' ───────────────
function automatic int max3 (int a, int b, int c);
// 'int a' = 'input int a' — direction is implicit
return (a >= b && a >= c) ? a :
(b >= a && b >= c) ? b : c;
endfunction
// ── Packed struct or enum as argument ────────────────────────────
typedef struct packed {
logic [7:0] addr;
logic [31:0] data;
logic write;
} apb_pkt_t;
function automatic logic pkt_valid (input apb_pkt_t pkt);
return (pkt.addr[1:0] == 2'b00); // address must be word-aligned
endfunctionFunctions in RTL — always_comb and assign
Functions that contain only combinational logic (no timing controls, no static state) are fully synthesisable and encouraged in RTL. They allow you to give a name to a complex combinational expression — making the RTL significantly more readable and testable.
// ── Functions defined in the module (or imported from a package) ──
function automatic logic [2:0] priority_enc (input logic [7:0] req);
priority_enc = 3'b0;
for (int i = 7; i >= 0; i--)
if (req[i]) priority_enc = i[2:0];
endfunction
function automatic logic is_one_hot (input logic [7:0] val);
return (val != 0) && ((val & (val-1)) == 0);
endfunction
// ── Use in continuous assign — legal, synthesisable ──────────────
assign parity_out = ^data_in; // inline
assign encoded = priority_enc(req_bus); // function call
// ── Use in always_comb — legal, recommended ──────────────────────
always_comb begin
next_state = IDLE; // default
if (is_one_hot(req_bus)) begin
grant = priority_enc(req_bus);
next_state = GRANT;
end
end
// ── Use in always_ff — function used in combinational expression ──
always_ff @(posedge clk) begin
// Function call computes the value; flip-flop registers it
parity_r <= calc_parity(data_bus);
end
// ── Use in port connection ───────────────────────────────────────
adder_n u_add (.a(gray2bin(raw_a)), .b(b), .sum(sum));Recursive Functions — Must Use automatic
A function can call itself — this is recursion. Recursive functions must be declared automatic — each call needs its own private stack frame with independent copies of local variables. Without automatic, all calls share the same static storage and the result is wrong.
// ── Factorial — classic recursion example ────────────────────────
function automatic int unsigned factorial (input int unsigned n);
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive call
endfunction
// Each call gets its own copy of 'n' because of 'automatic'
$display("5! = %0d", factorial(5)); // prints: 5! = 120
// ── Fibonacci — useful for constraint generation in testbenches ──
function automatic int fib (input int n);
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
endfunction
// ── Recursive bit-reversal — practical hardware utility ──────────
function automatic logic [7:0] bit_reverse8 (input logic [7:0] data, input int n);
if (n == 0) return data;
return bit_reverse8({data[6:0], data[7]}, n-1); // rotate and recurse
endfunction
// ⚠️ WRONG — missing automatic: recursion with static storage = corruption
// function int bad_factorial(input int n);
// if (n <= 1) return 1;
// return n * bad_factorial(n-1); // ← shares ONE copy of n — broken!
// endfunctionFunctions in Packages — Share Across Modules
Functions defined inside a package are available to any module that imports the package. This is the right place for utility functions used across multiple RTL files — CRC calculators, encoders, type checkers. You never duplicate them; every module imports the single canonical version.
// ── file: utils_pkg.sv ───────────────────────────────────────────
package utils_pkg;
function automatic logic is_one_hot (input logic [7:0] v);
return (v != 0) && ((v & (v-1)) == 0);
endfunction
function automatic logic [7:0] gray2bin (input logic [7:0] g);
logic [7:0] b;
b[7] = g[7];
for (int i = 6; i >= 0; i--) b[i] = b[i+1] ^ g[i];
return b;
endfunction
function automatic int unsigned clog2 (input int unsigned n);
int unsigned result = 0;
while ((1 << result) < n) result++;
return result;
endfunction
endpackage
// ── file: arbiter.sv — imports and uses package functions ────────
import utils_pkg::*; // wildcard import — all functions available
module arbiter (
input logic [7:0] req,
output logic [7:0] grant
);
always_comb begin
grant = 8'h0;
if (is_one_hot(req)) // package function — no prefix needed
grant = req;
end
endmodule
// Alternatively, use explicit scope resolution:
// if (utils_pkg::is_one_hot(req))Common Mistakes
// ════ MISTAKE 1: Missing return type ══════════════════════════════
function automatic bad_fn (input logic a); // ❌ no return type
return a;
endfunction
// ✅ FIX: always specify the return type
function automatic logic good_fn (input logic a);
return a;
endfunction
// ════ MISTAKE 2: Recursive without automatic ══════════════════════
function int bad_fact (input int n); // ❌ shared static storage
if (n <= 1) return 1;
return n * bad_fact(n-1);
endfunction
// ✅ FIX: add automatic
function automatic int good_fact (input int n);
if (n <= 1) return 1;
return n * good_fact(n-1); // ✅ each call has its own 'n'
endfunction
// ════ MISTAKE 3: Not all paths return a value ═════════════════════
function automatic int bad_max (input int a, b);
if (a > b) return a;
// ❌ What if a == b? No return — simulator returns X
endfunction
// ✅ FIX: every path must return
function automatic int good_max (input int a, b);
if (a > b) return a;
return b; // ✅ covers a<=b
endfunction
// ════ MISTAKE 4: Non-synthesisable code in RTL function ═══════════
function automatic real bad_rtl_fn (input int n);
return $sqrt(1.0 * n); // ❌ real type and $sqrt not synthesisable
endfunction
assign result = bad_rtl_fn(count); // ❌ synthesis errors here
// ✅ FIX: use integer math or a lookup table for synthesisable RTLQuick Reference — Function Declaration Cheat Sheet
// ── Full syntax ──────────────────────────────────────────────────
function automatic <return_type> fn_name (
input <type> arg1,
input <type> arg2
);
// local variables, logic
return <value>;
endfunction [: fn_name] // optional label
// ── Common return types ──────────────────────────────────────────
function automatic logic f1 (...); // single bit
function automatic logic [N-1:0] f2 (...); // N-bit vector
function automatic int f3 (...); // signed 32-bit
function automatic string f4 (...); // string (testbench)
function automatic void f5 (...); // no return value
// ── Named return variable (legacy — avoid in new code) ───────────
function logic [7:0] my_fn (...);
my_fn = <value>; // assign to function name instead of return
endfunction
// ── Early return (multiple paths) ────────────────────────────────
if (guard_condition) return default_val;
// ... main logic ...
return computed_val;
// ── Where you can call a function ────────────────────────────────
assign x = fn(a); // continuous assign
always_comb x = fn(a); // combinational block
always_ff q <= fn(d); // sequential block RHS
y = fn(a) + fn(b); // expression
assert (fn(x)); // assertion condition
// ── Package function ─────────────────────────────────────────────
import my_pkg::*; // wildcard import
// or: my_pkg::fn_name(args) // explicit scopeSynthesis Deep Dive — What Your Function Becomes in Silicon
A function that simulates perfectly can still produce surprises at synthesis: unexpected area growth, combinational loops, elaboration errors from unsupported constructs, or sim/synth mismatches caused by a stray $display. This section dissects exactly what a synthesis tool does to a function — from the call site through elaboration to the final gate netlist.
Function inlining — one body, N hardware copies
Synthesis does not compile a function into a sub-module and instantiate it. It inlines the function body at every call site, generating an independent set of gates for each call. Call a 32-gate XOR parity function three times in parallel — you get 96 gates in the netlist. There is no shared "parity module." Each call site is its own hardware.
assign p1 = calc_par8(bus_a);
assign p2 = calc_par8(bus_b);
assign p3 = calc_par8(bus_c);
// All three evaluate simultaneously,
// all three need independent hardware.// bus_a → XOR tree (8 gates) → p1
// bus_b → XOR tree (8 gates) → p2
// bus_c → XOR tree (8 gates) → p3
//
// Total: 24 XOR gates — no sharing.
// 3 independent timing paths.| Call count | Gates generated | STA timing paths | Engineering implication |
|---|---|---|---|
| 1 call | 1× function area | 1 path | Normal. Baseline expectation. |
| 2 calls, different inputs | 2× function area | 2 independent paths | Correct — you want parallel hardware. |
| N calls | N× function area | N independent paths | Area concern. Verify all N operate simultaneously. |
Constant inputs (localparam) | 0 gates | None (constant wire) | Synthesis folds to a constant — free computation. |
Mutually exclusive calls inside case | N× area (all built) | N paths (all active) | Synthesis still generates all N copies — use a shared module if area matters. |
Constant propagation — free computation at elaboration time
When a function is called with elaboration-time constant arguments — localparam values, integer literals, or parameter expressions — synthesis evaluates the function at elaboration and replaces the entire call with a constant wire. Zero gates are generated. This is the basis of the industry-standard clog2 pattern for sizing address buses.
// ── Package: define once, import everywhere ──────────────────────
package math_pkg;
function automatic int unsigned clog2 (input int unsigned n);
int unsigned r = 0;
if (n <= 1) return 1;
while ((1 << r) < n) r++;
return r;
endfunction
endpackage
import math_pkg::*;
module sync_fifo #(parameter int DEPTH = 64) (
input logic clk, rst_n,
output logic [clog2(DEPTH)-1:0] count // 6 bits for DEPTH=64 — zero runtime logic
);
// clog2(64)=6 and clog2(65)=7 both resolved at elaboration time
localparam int PTR_W = clog2(DEPTH) + 1; // 7-bit pointer (extra bit = full/empty flag)
logic [PTR_W-1:0] wr_ptr, rd_ptr;
// No gates from clog2 — synthesis folds everything to constants during elaboration
endmoduleCombinational-loop hazard
A combinational loop occurs when a function output feeds — directly or through intermediate assign statements — back into one of its own inputs, with no register in the path. Synthesis tools flag this. Static timing analysis cannot close timing on a looping arc. In simulation the signal may oscillate or lock to X within a single timestep.
logic [7:0] result;
// Output re-enters input — zero-delay loop
assign result = process(result, ctrl);
// Synthesis: ERROR — combinational loop
// Sim: oscillation or X-lock at time 0
// STA: timing arc cycles — unsolvablelogic [7:0] result_r;
always_ff @(posedge clk)
result_r <= process(result_r, ctrl);
// D-FF separates output from input
// Clean path: result_r → comb → D → FF
// STA: setup/hold analysis on one cycleSynthesisable vs non-synthesisable constructs inside functions
| Construct | Synthesisable? | What synthesis does |
|---|---|---|
if / case / casex / casez | Yes | Maps to MUX or priority logic tree |
for with static bounds | Yes | Loop unrolled at elaboration into parallel logic |
logic [N:0] local variables | Yes | Become combinational nodes (wires) in inlined logic |
int / int unsigned | Yes (with care) | Treated as 32-bit signed/unsigned vector; tool may warn on width |
| Packed struct / enum return | Yes | Struct treated as flat bit vector; fields map to bit ranges |
while with runtime condition | No | Synthesis cannot determine iteration count — elaboration error |
real / shortreal | No | No floating-point gates — elaboration error |
string | No | Simulation-only type — elaboration error |
$display / $write / $time | No (silently dropped) | Warning issued; statement removed from netlist |
#delay or @(event) | Illegal | Compile error — timing controls forbidden in functions |
| Recursive calls | No | Synthesis cannot bound recursion depth |
automatic local vars | Yes | Becomes a wire in the inlined combinational logic |
Verification Helper Functions — Building the TB Utility Layer
In a well-engineered verification environment, functions do most of the protocol intelligence: validating transaction fields, computing expected outputs for scoreboards, extracting human-readable state names for logs, and encoding stimulus data. The golden pattern is always the same — define once in a package, import into every UVM component that needs it.
Protocol packet validator — used in monitor and scoreboard
// ── file: apb_pkg.sv ─────────────────────────────────────────────
package apb_pkg;
typedef struct packed {
logic [31:0] paddr;
logic [31:0] pwdata;
logic pwrite;
logic psel;
logic penable;
} apb_xact_t;
// Structural validator — returns 1 if transfer is protocol-legal
function automatic logic apb_xact_valid (input apb_xact_t x);
// APB rule 1: address must be 32-bit word-aligned
if (x.paddr[1:0] != 2'b00) return 1'b0;
// APB rule 2: penable must follow psel by one cycle
if (x.penable && !x.psel) return 1'b0;
return 1'b1;
endfunction
// Human-readable decode — scoreboard logs, waveform labels
function automatic string apb_xact_str (input apb_xact_t x);
return $sformatf("APB %s @0x%08h data=0x%08h",
x.pwrite ? "WR" : "RD", x.paddr, x.pwdata);
endfunction
endpackage
// ── RTL: same function used in a concurrent assertion ────────────
import apb_pkg::*;
module apb_slave ( input apb_xact_t apb );
// Assertion fires on any illegal APB transaction
assert property (@(posedge clk) apb.penable |-> apb_xact_valid(apb))
else $error("Illegal APB: %s", apb_xact_str(apb));
endmodule
// ── UVM Scoreboard: identical check, zero code duplication ───────
import apb_pkg::*;
class apb_scoreboard extends uvm_scoreboard;
function void check_xact (apb_xact_t xact);
if (!apb_xact_valid(xact))
`uvm_error("APB_SCB", apb_xact_str(xact))
endfunction
endclassScoreboard predictor functions — compute expected output
package predict_pkg;
// Compute expected CRC8 of a byte payload (functional reference model)
function automatic logic [7:0] crc8_expected (
input logic [7:0] payload [], // dynamic array of bytes
input logic [7:0] poly = 8'h07 // CRC-8/SMBUS polynomial default
);
logic [7:0] crc = 8'hFF;
foreach (payload[i]) begin
crc ^= payload[i];
for (int b = 0; b < 8; b++)
crc = crc[7] ? (crc << 1) ^ poly : crc << 1;
end
return crc;
endfunction
// ALU expected output — used in scoreboard write_phase
function automatic logic [31:0] alu_expected (
input logic [31:0] a, b,
input logic [3:0] opcode
);
case (opcode)
4'h0: return a + b;
4'h1: return a - b;
4'h2: return a & b;
4'h3: return a | b;
4'h4: return a ^ b;
4'h5: return a << b[4:0];
4'h6: return a >> b[4:0];
default: return 32'hXXXX_XXXX; // unknown opcode → X
endcase
endfunction
endpackageCoverage helpers — bin selection logic
package cov_pkg;
// Classify a burst length for coverage bins
function automatic string burst_class (input int len);
if (len == 1) return "SINGLE";
else if (len inside {2,4}) return "SHORT";
else if (len inside {[5:16]}) return "MEDIUM";
else return "LONG";
endfunction
// FSM state name for display and assertion messages
function automatic string fsm_state_str (input logic [2:0] s);
case (s)
3'd0: return "IDLE";
3'd1: return "SETUP";
3'd2: return "ACCESS";
3'd3: return "WAIT";
3'd4: return "ERROR";
default: return $sformatf("UNKNOWN(%0d)", s);
endcase
endfunction
endpackage
// ── Monitor uses fsm_state_str in every log line — readable transcripts
$display("[%0t] State transition: %s → %s", $time,
fsm_state_str(prev_state), fsm_state_str(curr_state));
// ── Coverage group samples burst_class — named bins in report
covergroup burst_cg;
burst_len_cp: coverpoint burst_class(xact.len) {
bins single = {"SINGLE"};
bins short_b = {"SHORT"};
bins medium = {"MEDIUM"};
bins long_b = {"LONG"};
}
endgroupAdvanced Patterns — How Real Projects Use Functions
Once you have the syntax down, the real engineering begins. These patterns appear constantly in production RTL and enterprise verification: struct-returning functions, dual-use RTL/TB utilities, functions inside assertions, and the gold-standard address-decode pattern.
Pattern 1 — Functions returning packed structs
A function can return a packed struct. Synthesis treats the struct as a flat bit vector — which is exactly right. This pattern is far cleaner than returning an integer and manually slicing fields at every call site.
typedef struct packed {
logic valid;
logic [3:0] grant_id;
logic [1:0] priority;
} arb_result_t; // 7-bit packed — synthesis sees it as logic [6:0]
function automatic arb_result_t arbitrate (
input logic [7:0] req,
input logic [1:0] priority_mode
);
arb_result_t r;
r.valid = |req; // any request present?
r.priority = priority_mode;
r.grant_id = 4'hF; // default: no grant
if (r.valid) begin
for (int i = 7; i >= 0; i--)
if (req[i]) r.grant_id = i[3:0]; // highest-index wins
end
return r;
endfunction
// ── Call site — field extraction is automatic ────────────────────
arb_result_t result;
always_comb result = arbitrate(req_bus, priority_cfg);
// ── Downstream logic reads individual fields — clean, named access
assign grant_valid = result.valid;
assign grant_id = result.grant_id;
assign grant_pri = result.priority;Pattern 2 — Dual-use RTL/TB address decode
The most powerful use of package functions is writing a register address decoder once and using it identically in the RTL (inside always_comb) and in the testbench (inside the UVM register model and scoreboard). No divergence. No separately maintained decode tables.
package reg_map_pkg;
typedef enum logic [3:0] {
REG_CTRL = 4'h0,
REG_STATUS = 4'h1,
REG_DATA = 4'h2,
REG_IRQ = 4'h3,
REG_NONE = 4'hF
} reg_sel_t;
function automatic reg_sel_t decode_addr (input logic [11:0] addr);
case (addr[11:2]) // word index (byte offset ignored)
10'h000: return REG_CTRL;
10'h001: return REG_STATUS;
10'h002: return REG_DATA;
10'h003: return REG_IRQ;
default: return REG_NONE;
endcase
endfunction
function automatic string reg_name (input logic [11:0] addr);
case (decode_addr(addr)) // call the decode function
REG_CTRL: return "CTRL";
REG_STATUS: return "STATUS";
REG_DATA: return "DATA";
REG_IRQ: return "IRQ";
default: return $sformatf("UNKNOWN@0x%03h", addr);
endcase
endfunction
endpackage
// ── RTL slave: uses decode_addr in always_comb ───────────────────
import reg_map_pkg::*;
module reg_block (input apb_xact_t apb, output logic [31:0] rd_data);
always_comb begin
rd_data = 32'h0;
case (decode_addr(apb.paddr[11:0]))
REG_STATUS: rd_data = status_r;
REG_DATA: rd_data = data_r;
default: rd_data = 32'h0;
endcase
end
endmodule
// ── Scoreboard: identical decode — single source of truth ────────
import reg_map_pkg::*;
class reg_scoreboard extends uvm_scoreboard;
function void check_write(input logic [11:0] addr, input logic [31:0] data);
`uvm_info("SCB",
$sformatf("Write %s = 0x%08h", reg_name(addr), data),
UVM_MEDIUM)
endfunction
endclassPattern 3 — Functions inside concurrent assertions
function automatic logic one_hot_or_zero (input logic [7:0] v);
return (v == 8'h00) || ((v & (v-1)) == 8'h00);
endfunction
function automatic logic addr_aligned (input logic [31:0] addr, input int bytes);
return (addr % bytes) == 0;
endfunction
// ── Concurrent assertion using functions ─────────────────────────
// "Whenever the arbiter grants, exactly one or zero grants are active"
property p_grant_one_hot;
@(posedge clk) disable iff (!rst_n)
one_hot_or_zero(grant_bus);
endproperty
assert property (p_grant_one_hot)
else $error("[ARB] Multiple simultaneous grants: %08b", grant_bus);
// "AHB write address must be 4-byte aligned"
property p_ahb_align;
@(posedge clk)
(hwrite && htrans == 2'b10) |-> addr_aligned(haddr, 4);
endproperty
assert property (p_ahb_align)
else $error("[AHB] Unaligned write to 0x%08h", haddr);Pattern 4 — Chained function calls in complex RTL
function automatic logic [2:0] priority_enc8 (input logic [7:0] req);
for (int i = 7; i >= 0; i--)
if (req[i]) return i[2:0];
return 3'd0;
endfunction
function automatic logic odd_parity (input logic [2:0] val);
return ^val; // 1-bit XOR reduction
endfunction
function automatic logic [3:0] encode_with_parity (input logic [7:0] req);
logic [2:0] enc = priority_enc8(req); // inner call
return {odd_parity(enc), enc}; // {parity_bit, 3-bit encoding}
endfunction
// ── RTL usage: single clean expression, three layers of logic ────
always_comb
grant_with_par = encode_with_parity(req_bus);
// Synthesis inlines all three functions into one combinational chain:
// req_bus → priority MUX → 3-bit encoded → XOR parity → 4-bit output
// Total gate depth: ~6 levels. STA sees it as one continuous path.Debugging Academy — 7 Real Function Bugs Dissected
These are bugs pulled from actual project post-mortems and code reviews. Each one has a signature that makes it recognisable once you've seen it. Read these, understand the symptoms, and you'll catch them in the first five minutes of debug — not after three days.
Named Return Variable Read Before It Is Written
X-PROPAGATIONfunction logic [7:0] gray2bin_bad (input logic [7:0] g);
// BUG: gray2bin_bad[6:0] computed BEFORE gray2bin_bad[7] is set
for (int i = 6; i >= 0; i--)
gray2bin_bad[i] = gray2bin_bad[i+1] ^ g[i]; // ← reads uninitialised bit 7
gray2bin_bad[7] = g[7]; // ← assigned AFTER the loop
endfunctionFunction output is X for the most-significant bits. gray2bin_bad(8'hA3) returns 8'hXX rather than the expected binary value. Waveform shows upper bits as undefined even with fully valid inputs.
The named return variable (gray2bin_bad) is initialised to X at function entry. The loop reads gray2bin_bad[7] on its first iteration (when i=6) before it has been assigned. X propagates through the XOR cascade, poisoning all lower bits.
function automatic logic [7:0] gray2bin_ok (input logic [7:0] g);
logic [7:0] b;
b[7] = g[7]; // ← MSB first
for (int i = 6; i >= 0; i--)
b[i] = b[i+1] ^ g[i]; // b[i+1] is already set
return b; // explicit return, no ordering trap
endfunctionAlways write MSB before the loop. Or use the return style with a local variable — the data flow is unambiguous.
Missing Default in Case — Unhandled Inputs Return X
INCOMPLETE COVERAGEfunction automatic logic [3:0] opcode_decode (input logic [5:0] op);
case (op)
6'b000000: return 4'h1; // ADD
6'b000001: return 4'h2; // SUB
6'b000010: return 4'h4; // AND
// 60 other opcodes have no case arm
// No default — what does the function return for op=6'h3F?
endcase
endfunction // ← falls off end with no return for unmatched opThe function returns X (or 0 — tool-dependent) for any opcode not in the case arms. In simulation, downstream logic receives X for legitimate opcodes not yet handled, causing cascading X-propagation through the entire datapath. A waveform shows the decode output blinking X on valid instruction fetch cycles.
The SystemVerilog LRM states that if execution reaches endfunction without a return, the named return variable holds its last assigned value — which is X for an automatic function with no initialisation. Some tools return 0 in 2-state mode, masking the bug in regression and exposing it only in 4-state tests.
function automatic logic [3:0] opcode_decode (input logic [5:0] op);
case (op)
6'b000000: return 4'h1;
6'b000001: return 4'h2;
6'b000010: return 4'h4;
default: return 4'h0; // ← always provide a default
endcase
endfunctionRule: every function must have a return on every possible code path. Lint tools check for this — enable the "missing return" lint rule in your flow.
Static Function Called Recursively — Wrong Results
STORAGE CORRUPTION// Missing 'automatic' — all calls share one copy of 'n'
function int factorial_bad (input int n);
if (n <= 1) return 1;
return n * factorial_bad(n - 1); // inner call overwrites shared 'n'
endfunction
// Test:
$display("5! = %0d", factorial_bad(5)); // expected 120, may print garbageThe function returns an incorrect value — often 1 or an intermediate result. The behaviour may be simulator-dependent: some simulators return 1 (the base case value), others return something unpredictable based on call ordering. It never returns the mathematically correct result.
Without automatic, the function has static storage — one shared copy of every local variable, including its arguments. When the inner recursive call starts, it overwrites the shared copy of n with the smaller value. When the outer call resumes, n has been clobbered. The multiplication computes with the wrong value.
function automatic int factorial (input int n);
if (n <= 1) return 1;
return n * factorial(n - 1); // each call has its own 'n'
endfunctionAny function that calls itself — or that is called from multiple concurrent processes — must be automatic. A safe default: declare all testbench functions automatic.
Return Value Width Mismatch — Silent Truncation
WIDTH TRUNCATION// Return type is 4 bits, but computed result can be up to 9 bits
function automatic logic [3:0] add_bytes (input logic [7:0] a, b);
return a + b; // a+b can be up to 9 bits; truncated silently to 4 bits!
endfunction
logic [3:0] result;
always_comb result = add_bytes(8'hF0, 8'h20);
// Expected: 0x110 → but 4-bit truncation gives 0x0 — WRONG silentlyNo compile error. No simulation error. The function quietly returns a truncated result. The downstream logic receives a value that is bitwise-correct for a narrower bus but arithmetically wrong. This typically surfaces as a hard-to-explain functional failure — the datapath produces correct results for small inputs but fails for large values near the overflow boundary.
The return type is narrower than the computed expression. SystemVerilog truncates the result to fit the declared return type — no warning by default. The truncation happens at the assignment of the return value, silently discarding the upper bits.
// Option A — widen the return type to accommodate the full result
function automatic logic [8:0] add_bytes_wide (input logic [7:0] a, b);
return {1'b0, a} + {1'b0, b}; // explicit 9-bit addition with carry
endfunction
// Option B — if saturation is intended, make it explicit
function automatic logic [7:0] saturate_add (input logic [7:0] a, b);
logic [8:0] full = {1'b0, a} + {1'b0, b};
return full[8] ? 8'hFF : full[7:0]; // saturate on overflow
endfunctionEnable the truncation-in-return-expression lint rule (VCS: +lint=TRNC; Questa: -pedanticerrors; Xcelium: -warn TRNA).
Non-Synthesisable Construct in RTL Function — Sim/Synth Mismatch
SIM/SYNTH MISMATCH// RTL function — but uses $display which synthesis silently drops
function automatic logic [7:0] crc_compute (input logic [63:0] data);
logic [7:0] crc = 8'hFF;
foreach (data[i]) crc ^= data[i];
$display("DBG: crc mid = %0h", crc); // ← synthesis DROPS this silently
return crc;
endfunction
always_comb crc_out = crc_compute(data_bus);Simulation floods the transcript with debug prints — one per combinational re-evaluation. Synthesis completes with only a buried warning ("ignoring system task") in 50,000 lines of log. Gate-sim (running the gate netlist) produces no prints and behaviourally matches. Functional behaviour is identical only if the $display was truly side-effect-free — but if it was masking an X that simulation happened to print around, the gate-sim will diverge.
Synthesis ignores all system tasks ($display, $write, $time) inside RTL functions. The simulation and synthesis netlists are structurally different — the gate netlist has no display logic. This is a mild case; the dangerous variant is $stop or simulation-time computation that affects the return value — those introduce genuine sim/synth divergence.
function automatic logic [7:0] crc_compute (input logic [63:0] data);
logic [7:0] crc = 8'hFF;
foreach (data[i]) crc ^= data[i];
`ifdef SIMULATION
$display("DBG: crc mid = %0h", crc); // ← guarded — synthesis never sees it
`endif
return crc;
endfunctionEvery $display in a synthesisable function must be guarded with `ifdef SIMULATION. Add this as a lint rule and code-review checklist item.
Function Reads Module-Level Signal — Caller Uses Plain always
SENSITIVITY MISSlogic [7:0] threshold; // module-level signal — changes independently
function automatic logic alarm_check (input logic [7:0] sensor);
return sensor > threshold; // reads module-level 'threshold'
endfunction
// BUG: plain always with manual sensitivity — threshold is NOT in the list
always @(sensor_in) begin // ← only triggers on sensor_in changes
alarm = alarm_check(sensor_in);
end
// If threshold changes, 'alarm' is NOT updated — stale output in simulationThe alarm signal shows stale values in simulation after threshold changes. Waveform: threshold changes at T=500ns, but alarm holds its previous value. It only updates when sensor_in changes again — which could be much later, or never in a corner case. This bug is invisible in synthesis (where sensitivity lists are irrelevant) — pure simulation mismatch.
A plain always block with a manual sensitivity list only re-evaluates when signals in @(...) change. The function internally reads threshold, but since threshold is not in the sensitivity list, the block does not re-evaluate when threshold changes. The output is stale.
// Fix A (BEST): always_comb — automatically includes all signals read by the block,
// including those read inside called functions
always_comb begin
alarm = alarm_check(sensor_in); // threshold is auto-added to sensitivity
end
// Fix B: if you must use plain always, add threshold manually
always @(sensor_in or threshold) begin
alarm = alarm_check(sensor_in);
endThis is the strongest argument for always using always_comb for combinational logic. It captures every signal read — including those hidden inside function bodies — so sensitivity gaps are impossible.
Output Argument in Function Instead of Return Value
BAD CODING STYLE// Using 'output' argument instead of return value — legal but wrong style
function automatic void compute_parity_bad (
input logic [7:0] data,
output logic parity // ← output arg instead of return
);
parity = ^data;
endfunction
// Caller must pass a variable to receive output — cannot use inline
logic p;
compute_parity_bad(data_bus, p); // extra variable required at every call site
assign parity_out = p; // two-step where one step sufficesNo functional bug in simulation, but the code is harder to use, harder to read, and cannot be used inline in expressions: assign x = compute_parity_bad(data) is illegal because the function is void. Synthesis tools warn on output arguments in functions. Some tools that enforce strict RTL style flag this as a coding violation. It also prevents using the function inside assertions.
The engineer is writing the function like a task (which is designed for multi-output, time-consuming operations). Functions are designed specifically for single-return-value computation. Using output arguments in a function defeats the entire purpose of having a function instead of a task.
function automatic logic compute_parity (input logic [7:0] data);
return ^data;
endfunction
// Now usable everywhere:
assign parity_out = compute_parity(data_bus); // inline assign
always_comb data_valid = compute_parity(rx_data) ^ parity_in; // inline expression
assert property (@(posedge clk) compute_parity(tx_data)); // inline assertionRule: if a function produces one result, use the return value. If it produces two or more results, use a struct return or consider whether it should be a task. Never use output arguments in functions for single-value computation.
Interview Q&A — 13 Questions Engineers Actually Ask
These questions are drawn from real technical interviews at semiconductor companies and EDA firms. Read each question before the answer — test yourself first.
Functions execute in zero simulation time, accept only input arguments by default, and return exactly one value. They are used for pure combinational computation. Tasks can consume simulation time (via #delay or @(event)), can have output and inout arguments, and return no value. Use a function when you need to compute a value inline in an expression. Use a task when the operation takes time — driving a bus, waiting for a handshake, or consuming a clock edge.