System Tasks, Functions & Compiler Directives
Complete coverage of Verilog parameters (module and specify), path delays in the specify block, and the full set of compiler directives — with annotated diagrams and practical examples throughout.
📖 Introduction
This module covers three closely related features of Verilog that control how designs are parameterised, timed, and compiled:
specify block for accurate timing simulation.`timescale, `define, `include, `ifdef) that control the compilation process itself.$setup, $hold, $period) inside the specify block to verify timing constraints at simulation time.⚙️ Parameters — Introduction
A parameter is a named constant whose value is fixed at compile/elaboration time. Unlike runtime variables, parameters never change during simulation — they are evaluated once when the design is elaborated and then treated as constants. This allows a single module definition to be reused with different configurations simply by overriding parameters at instantiation.
ADDR_W = $clog2(DEPTH) is legal.integer, real, string) enforce the value type.📐 parameter Syntax
// ── Basic parameter ─────────────────────────────────────────── parameter WIDTH = 8; parameter DEPTH = 16; // ── Typed parameters ────────────────────────────────────────── parameter integer COUNT = 100; parameter real PERIOD = 10.0; // 10.0 time units parameter string NAME = "adder"; // string parameter // ── Vector parameter ────────────────────────────────────────── parameter [3:0] MODE = 4'b0001; // 4-bit parameter // ── Multiple parameters, one declaration ────────────────────── parameter A = 8, B = 16, C = A + B; // C = 24 (derived) // ── Common derived parameters ───────────────────────────────── parameter DEPTH = 256; parameter ADDR_W = $clog2(DEPTH); // = 8 (log2 ceiling) parameter MAX_VAL = (1 << ADDR_W) - 1; // = 255 // ── Header style (inline port list — Verilog-2001) ──────────── module adder #( parameter N = 8, parameter SIGNED = 0 ) ( input [N-1:0] a, b, output [N :0] sum ); assign sum = a + b; endmodule
🔒 localparam — Local Constants
localparam declares a constant that is local to the module and cannot be overridden from outside at instantiation. Use it for constants derived from parameters (like address widths from depths) or for internal implementation constants that must not be changed.
🔵 parameter — Overridable from outside
parameter DEPTH = 256; // Can be changed at instantiation: // adder #(.DEPTH(512)) u1(...);
🟣 localparam — Internal only, cannot override
localparam ADDR_W = $clog2(DEPTH); // Derived — cannot override from outside // Prevents accidental misconfiguration
module fifo_param #( parameter WIDTH = 8, // data width — user configurable parameter DEPTH = 16 // depth — user configurable ) ( input clk, rst_n, input wr_en, rd_en, input [WIDTH-1:0] wdata, output [WIDTH-1:0] rdata, output full, empty ); // ── Derived constants — not user-configurable ──────────────── localparam ADDR_W = $clog2(DEPTH); // address width localparam MAX_PTR = DEPTH - 1; // max pointer value localparam FULL_TH = DEPTH; // full threshold // ── Internal storage using derived widths ──────────────────── reg [WIDTH-1:0] mem [0:DEPTH-1]; reg [ADDR_W-1:0] wr_ptr, rd_ptr; reg [ADDR_W :0] count; // one extra bit for full detect // ... logic ... endmodule
🔄 Overriding Parameters at Instantiation
Parameters are overridden when instantiating a module. Verilog provides two methods — the modern #() syntax and the legacy defparam statement:
Module Definition (defaults)
module adder_n #( parameter N = 8, parameter SIGNED = 0 ) ( ... );
Instantiation (override)
// Named style (preferred) adder_n #(.N(32), .SIGNED(1)) u1(...); // Positional style adder_n #(32, 1) u2(...);
// ── Method 1: Named parameter override (preferred) ─────────── adder_n #(.N(16), .SIGNED(1)) u_add16(...); adder_n #(.N(32)) u_add32(...); // SIGNED stays 0 adder_n u_add8(...); // all defaults (N=8) // ── Method 2: Positional parameter override ─────────────────── adder_n #(16, 1) u_add16b(...); // order must match definition // ── Method 3: defparam (legacy — avoid in new code) ────────── adder_n u_add64(...); defparam u_add64.N = 64; defparam u_add64.SIGNED = 1; // ── Method 4: generate loop with parameters ─────────────────── genvar k; generate for (k=8; k<=64; k=k*2) begin : adder_bank adder_n #(.N(k)) u(...); // creates 8-bit, 16-bit, 32-bit, 64-bit end endgenerate
defparam statement can override parameters deep in the hierarchy, which makes designs hard to read and understand. Always use the #() named parameter override style. Many modern linters flag defparam as a warning.
⏱ Path Delays — The specify Block
The specify block is a special section inside a module that describes pin-to-pin timing paths — the propagation delay from each input to each output of the module. This information is used by the simulator for accurate timing analysis and by STA tools for path delay annotation.
The specify block is primarily used in standard cell library models to characterise cell timing. It is separate from the functional description and applies only to simulation timing, not synthesis.
$setup, $hold, $period, $width) verify that timing constraints are met during simulation.module and2_lib (input a, b, output y); // Functional description and (y, a, b); specify // specparam — timing constants specparam t_rise = 0.5, t_fall = 0.3; // Path delays: input → output (a => y) = (t_rise, t_fall); // a to y (b => y) = (t_rise, t_fall); // b to y endspecify endmodule
🛤 Path Delay Types
Verilog specify blocks support two kinds of path delay statements:
🔵 Simple Path ( => )
The delay applies regardless of the polarity of the input transition. All edge types use the same delay values.
// a to y — any transition (a => y) = 5; (a => y) = (3, 5); // rise, fall (a => y) = (2, 4, 1); // rise, fall, off
🟣 Edge-Sensitive Path ( *> )
Delay applies only when specified edge conditions on the input occur. Used for clocked paths.
// posedge clk to q (posedge clk => q) = 3; // any input of bus to output (a, b *> y) = 4;
specify specparam tRise_a_y = 0.5, tFall_a_y = 0.3, tOff_a_y = 0.1; // ── Simple paths ────────────────────────────────────────── (a => y) = 5; // single value (a => y) = (3, 5); // rise=3, fall=5 (a => y) = (2, 4, 1); // rise, fall, off (a => y) = (tRise_a_y, tFall_a_y); // using specparam // ── Parallel connection — all listed inputs to all outputs ─ (a, b => y, z) = (3, 5); // a→y, a→z, b→y, b→z // ── Full connection — every input to every output ────────── (a, b *> y, z) = (3, 5); // same as above // ── Edge-sensitive paths ────────────────────────────────── (posedge clk => q) = 3; // clock to Q delay (tCQ) (negedge clk => qn) = 2; // neg-edge to Qbar // ── State-dependent paths (conditional) ────────────────── if (sel) (a => y) = (2, 3); // path when sel=1 if (!sel) (b => y) = (3, 4); // path when sel=0 // ── Min:Typ:Max delays ──────────────────────────────────── (a => y) = (1:2:3, 2:3:5); // rise: 1-2-3, fall: 2-3-5 endspecify
📌 specparam — Specify Block Parameters
specparam declares constants that are local to the specify block and are specifically intended for timing values. Unlike regular parameter, specparam values can be overridden by SDF (Standard Delay Format) back-annotation during gate-level simulation — making them the correct choice for any timing constant in the specify block.
module dff_lib ( input clk, d, rst_n, output reg q ); always @(posedge clk) q <= d; specify // Timing parameters — can be overridden by SDF specparam tCQ_r = 0.35, // clk→Q rise delay tCQ_f = 0.30, // clk→Q fall delay tSetup = 0.20, // D setup time before clk tHold = 0.05, // D hold time after clk tPeriod = 1.00; // minimum clock period // Path delays using specparam values (posedge clk => q) = (tCQ_r, tCQ_f); // Timing checks $setup(d, posedge clk, tSetup); $hold (posedge clk, d, tHold); $period(posedge clk, tPeriod); endspecify endmodule
specparam is specifically designed for specify block timing values. Only specparam values can be overridden by SDF files during gate-level simulation. Using regular parameter inside a specify block is legal but the values cannot be back-annotated from SDF.
✅ Timing Checks
Verilog provides built-in system tasks specifically for verifying timing constraints inside the specify block. These check that setup times, hold times, clock periods, and pulse widths are satisfied during simulation — and print violations as warnings.
| Task | Purpose | Syntax |
|---|---|---|
| $setup | Checks data arrives before active clock edge by at least t_setup | $setup(data, clk_edge, limit); |
| $hold | Checks data is stable after active clock edge for at least t_hold | $hold(clk_edge, data, limit); |
| $setuphold | Combined setup and hold check in one statement | $setuphold(clk_edge, data, t_setup, t_hold); |
| $period | Checks clock period meets minimum requirement | $period(clk_edge, min_period); |
| $width | Checks pulse width meets minimum for high or low | $width(clk_edge, min_width); |
| $skew | Checks the skew between two events is within limit | $skew(event1, event2, limit); |
| $recovery | Checks recovery time (async reset release to next clock) | $recovery(clk_edge, reset, limit); |
specify specparam tCQ = 0.4, // clock-to-Q delay (ns) tSU = 0.25, // setup time tHLD = 0.08, // hold time tPWH = 0.5, // min pulse width high tPWL = 0.5, // min pulse width low tPERIOD = 1.0, // minimum clock period tREC = 0.3; // async reset recovery time // ── Path delays ─────────────────────────────────────────── (posedge clk => q) = (tCQ, tCQ); (negedge rst_n => q) = (0.1, 0.1); // async clear path // ── Timing checks ───────────────────────────────────────── $setup(d, posedge clk, tSU); // D must settle tSU before clk↑ $hold (posedge clk, d, tHLD); // D must hold tHLD after clk↑ $period(posedge clk, tPERIOD); // clock must not be too fast $width (posedge clk, tPWH); // high pulse must be ≥ tPWH $width (negedge clk, tPWL); // low pulse must be ≥ tPWL $recovery(posedge clk, rst_n, tREC); // rst_n must release tREC before clk↑ endspecify
🏗 Module Parameters — Design Best Practices
Well-structured parameter use is one of the hallmarks of professional RTL design. These patterns make designs portable, readable, and reusable:
// ── Professional parameterised module structure ─────────────── module axi_fifo #( // ── User-configurable parameters (public interface) ────── parameter integer DATA_WIDTH = 32, parameter integer FIFO_DEPTH = 16, parameter integer ALMOST_FULL = FIFO_DEPTH - 2 ) ( input clk, rst_n, input [DATA_WIDTH-1:0] s_data, input s_valid, output s_ready, output [DATA_WIDTH-1:0] m_data, output m_valid, input m_ready, output full, empty, almost_full ); // ── Derived constants (private — cannot be overridden) ─────── localparam ADDR_W = $clog2(FIFO_DEPTH); localparam CNT_W = $clog2(FIFO_DEPTH + 1); localparam PTR_WRAP = FIFO_DEPTH; // ── Internal registers using derived widths ─────────────────── reg [DATA_WIDTH-1:0] mem[0:FIFO_DEPTH-1]; reg [ADDR_W-1:0] wr_ptr, rd_ptr; reg [CNT_W-1:0] count; // ── Status flags ───────────────────────────────────────────── assign full = (count == FIFO_DEPTH); assign empty = (count == 0); assign almost_full = (count >= ALMOST_FULL); // ... remaining logic ... endmodule
Parameter Naming Conventions
| Convention | Example | When to use |
|---|---|---|
| UPPER_SNAKE_CASE | DATA_WIDTH | All parameters and localparams |
| _W suffix | ADDR_W, DATA_W | Bit-width parameters |
| _DEPTH suffix | FIFO_DEPTH | Memory depth / entry count |
| _EN suffix | PARITY_EN | Enable/disable feature flags |
| $clog2() derived | localparam ADDR_W = $clog2(DEPTH) | Address widths from depths |
🔧 Compiler Directives
Compiler directives are instructions to the Verilog preprocessor — they are processed before compilation begins. All directives start with a backtick (`) and are in scope from the point of declaration to the end of the compilation unit (or until explicitly cancelled).
`timescale 1ns/1ps
`define WIDTH 8 wire [`WIDTH-1:0] d;
`include "my_defs.vh"
`ifdef SIMULATION // sim-only code `endif
none forces explicit declarations.
`default_nettype none // Now undeclared nets = error
`celldefine module AND2 (...); `endcelldefine
⏱ `timescale
The `timescale directive tells the simulator what each time unit means and how finely time should be measured. It must appear before any module definition that uses delays.
// ── Valid time unit values: 1, 10, 100 ─────────────────────── `timescale 1ns/1ps // unit=1ns, precision=1ps (most common) `timescale 10ns/1ns // unit=10ns, precision=1ns `timescale 1us/100ns // microsecond-level designs `timescale 1ps/1fs // ultra-fine — RF / analog-adjacent // ── Effect on delays ────────────────────────────────────────── `timescale 1ns/1ps nmos #(0.35, 0.28) n1(out, in, g); // rise=0.35ns, fall=0.28ns always #5 clk = ~clk; // toggle every 5ns // ── scope: timescale applies from declaration to next timescale // or end of file — each file can have its own timescale // ── Precision must be ≤ time unit ───────────────────────────── `timescale 1ns/1ps // ✅ precision 1ps ≤ unit 1ns // `timescale 1ps/1ns ❌ precision > unit — illegal // ── Common combinations ─────────────────────────────────────── // 1ns/1ps → RTL simulation, post-synthesis GLS // 1ns/100ps → STA, common for cell library models // 1ps/1fs → switch-level, analog-mixed signal
🏷️ `define and `undef
The `define directive creates a text macro — a named substitution that the preprocessor replaces throughout the code before compilation. Macros are referenced with a backtick: `MACRO_NAME. The `undef directive removes a previously defined macro.
`define macro is a text substitution — it has no type, no scope (it’s global to the compilation), and cannot be overridden per-instance. A parameter is a typed, scoped, per-instance constant. Use parameter for design configuration; use `define for global compile-time switches and constants shared across files.
// ── Constant macros ─────────────────────────────────────────── `define DATA_WIDTH 32 `define FIFO_DEPTH 256 `define CLK_PERIOD 10 // 10 time units // Referencing macros — backtick prefix wire [`DATA_WIDTH-1:0] bus; // expands to wire [31:0] bus always #(`CLK_PERIOD/2) clk=~clk; // expands to #(10/2) = #5 // ── Function-like macros (with arguments) ───────────────────── `define MAX(a,b) ((a) > (b) ? (a) : (b)) `define CLOG2(x) ($clog2(x)) wire [7:0] result = `MAX(a, b); // expands inline // ── Feature flags for conditional compilation ───────────────── `define SYNTHESIS // define to enable synthesis-specific code `define ENABLE_PARITY // define to include parity logic // ── undef — remove a macro definition ──────────────────────── `define DEBUG // defined here // ... some debug code ... `undef DEBUG // undefine — macro no longer exists // `DEBUG here would be a compile error // ── Checking if a macro is defined ──────────────────────────── `ifdef ENABLE_PARITY wire parity = ^data; // only compiled if ENABLE_PARITY defined `endif
📁 `include
The `include directive inserts the entire contents of a specified file at the current position during preprocessing — before compilation. It is used to share common definitions, macros, and interfaces across multiple design files without duplication.
// ── File: global_defs.vh (shared header file) ───────────────── `define DATA_WIDTH 32 `define ADDR_WIDTH 10 `define CLK_PERIOD 10 `timescale 1ns/1ps // ── File: axi_master.v — includes shared header ─────────────── `include "global_defs.vh" // paste global_defs.vh here `include "axi_if.vh" // AXI interface definitions module axi_master ( ... ); wire [`DATA_WIDTH-1:0] wdata; // uses macro from include endmodule // ── File: axi_slave.v — uses the same shared header ─────────── `include "global_defs.vh" // same file, consistent definitions module axi_slave ( ... ); wire [`ADDR_WIDTH-1:0] addr; // same macro, guaranteed consistent endmodule // ── Include guard — prevent double-inclusion ────────────────── `ifndef GLOBAL_DEFS_VH `define GLOBAL_DEFS_VH `define DATA_WIDTH 32 // ... all definitions ... `endif // GLOBAL_DEFS_VH — safe to include multiple times
`ifndef`/`define`/`endif pattern prevents this.
🔀 `ifdef / `ifndef / `else / `elsif / `endif
Conditional compilation directives allow you to include or exclude entire blocks of Verilog code based on whether a macro is defined. This enables a single source file to produce different designs for different targets (FPGA vs ASIC, simulation vs synthesis, debug vs production).
// ── Basic ifdef ─────────────────────────────────────────────── `ifdef SIMULATION // code only compiled when SIMULATION is defined initial $dumpvars(0, tb); `endif // ── ifdef with else ─────────────────────────────────────────── `ifdef FPGA_TARGET assign out = fpga_specific_logic; `else assign out = asic_specific_logic; `endif // ── ifdef with elsif chain ──────────────────────────────────── `ifdef TARGET_28NM `define CLK_PERIOD 2 // 500 MHz `elsif TARGET_65NM `define CLK_PERIOD 5 // 200 MHz `elsif TARGET_180NM `define CLK_PERIOD 20 // 50 MHz `else `define CLK_PERIOD 10 // default `endif // ── ifndef — compile if NOT defined ─────────────────────────── `ifndef SYNTHESIS // testbench code — excluded from synthesis initial begin $monitor("%0t: q=%b", $time, q); end `endif // ── Practical: enable optional parity logic ─────────────────── module uart_tx ( input [7:0] data, output tx `ifdef ENABLE_PARITY , output parity_out // extra port only if parity enabled `endif ); `ifdef ENABLE_PARITY assign parity_out = ^data; `endif endmodule
📋 Summary
| Feature | Purpose | Scope | Overridable? | Synthesizable? |
|---|---|---|---|---|
| parameter | Named module constant with default | Module scope | ✅ Yes — at instantiation | ✅ Yes |
| localparam | Internal constant, not overridable | Module scope | ❌ No | ✅ Yes |
| defparam | Override parameter hierarchically | Any level | ✅ Yes (legacy) | ✅ Yes |
| specparam | Timing constant in specify block | specify block | ✅ Via SDF | ❌ No |
| specify block | Pin-to-pin path delays and timing checks | Module | ✅ Via SDF | ❌ No |
| `timescale | Sets simulation time unit and precision | File/compilation | N/A | ❌ Ignored |
| `define | Global text macro substitution | Global (compilation) | Via `undef + redefine | ✅ Preprocessed first |
| `include | Insert file contents inline | Preprocessor | N/A | ✅ Preprocessed first |
| `ifdef / `ifndef | Conditional compilation | Preprocessor | N/A | ✅ Selected code compiled |
| `default_nettype none | Force explicit net declarations | File-wide | N/A | ✅ Compile-time check |
`timescale 1ns/1ps `default_nettype none `ifndef AND2_LIB_VH `define AND2_LIB_VH `celldefine module AND2 #( parameter real SCALE = 1.0 // drive strength scale factor ) ( input a, b, output y ); and (y, a, b); // functional model specify specparam tRise_ay = 0.50, // a→y rise (SDF-overridable) tFall_ay = 0.35, tRise_by = 0.48, // b→y rise tFall_by = 0.33; (a => y) = (tRise_ay, tFall_ay); (b => y) = (tRise_by, tFall_by); endspecify endmodule `endcelldefine `endif // AND2_LIB_VH
`default_nettype none at the top of every RTL file. Without it, any typo in a net name silently creates a new 1-bit wire — a bug that can be nearly impossible to find. With `default_nettype none, the compiler immediately flags undeclared identifiers as errors, catching mistakes at compile time instead of in simulation.
