VERILOG SERIES · MODULE 16

System Tasks, Functions & Compiler Directives — VLSI Trainers
Verilog Series · Module 16

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:

⚙️
Parameters
Named compile-time constants that make modules configurable and reusable without source code modification.
Path Delays
Timing delays from input to output through a module — specified in the specify block for accurate timing simulation.
🔧
Compiler Directives
Backtick commands (`timescale, `define, `include, `ifdef) that control the compilation process itself.
🔬
Timing Checks
Built-in system tasks ($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.

📏
Compile-Time Constant
Set once during elaboration. Cannot change at runtime. Synthesis tools see them as literals.
♻️
Enables Reuse
One module definition → many different configurations. 8-bit adder becomes 32-bit just by changing N.
🔗
Derived Parameters
Parameters can reference other parameters in their expressions. ADDR_W = $clog2(DEPTH) is legal.
🧩
Type Optional
Parameters are untyped by default. Optional type keywords (integer, real, string) enforce the value type.

📐 parameter Syntax

parameter
Keyword
always lowercase
integer
Type
optional: integer, real, string
NAME
Identifier
UPPER_CASE convention
= expr
Default Value
required — used if not overridden
;
Terminate
Fig 1 — All parameter declaration forms
// ── 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
Fig 2 — parameter + localparam in a parameterised FIFO
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(...);
Fig 3 — All override methods: #(), defparam, and generate
// ── 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 is deprecated. The 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.

📍
Pin-to-Pin Paths
Describes delay from each input pin to each output pin — models the actual silicon propagation path inside the cell.
📐
specparam
Parameters declared inside the specify block — used for timing constants like setup time, hold time, clock period.
Timing Checks
Built-in system tasks ($setup, $hold, $period, $width) verify that timing constraints are met during simulation.
🔬
Simulation Only
The specify block is completely ignored by synthesis — it only affects timing simulation and SDF back-annotation.
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;
Fig 4 — Path delay types and syntax forms
Simple path: (a => y) — input a to output y
a (input)
──
delay = (3, 5)
──→
y (output)
Edge path: (posedge clk => q) — clock to data out
clk ↑
──
clk→q delay
──→
q (output)
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.

Fig 5 — specparam: declaration, use, and SDF override
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 vs parameter: Both declare constants, but 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.

TaskPurposeSyntax
$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);
Fig 6 — Complete D flip-flop specify block with all timing checks
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:

Fig 7 — Best-practice parameterised module template
// ── 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

ConventionExampleWhen to use
UPPER_SNAKE_CASEDATA_WIDTHAll parameters and localparams
_W suffixADDR_W, DATA_WBit-width parameters
_DEPTH suffixFIFO_DEPTHMemory depth / entry count
_EN suffixPARITY_ENEnable/disable feature flags
$clog2() derivedlocalparam 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
Sets the time unit and precision for simulation timing.
`timescale 1ns/1ps
`define / `undef
Creates or removes a text macro — replaces all occurrences of the name with its value.
`define WIDTH 8
wire [`WIDTH-1:0] d;
`include
Inserts the contents of another file at this location during compilation.
`include "my_defs.vh"
`ifdef / `ifndef
Conditional compilation — include or exclude code blocks based on whether a macro is defined.
`ifdef SIMULATION
  // sim-only code
`endif
`default_nettype
Changes the default net type for undeclared nets. Setting to none forces explicit declarations.
`default_nettype none
// Now undeclared nets = error
`celldefine / `endcelldefine
Marks a module as a cell — used in standard cell library models to indicate the module represents a single physical cell.
`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.

`timescale
Directive
backtick prefix
1ns
Time Unit
what #1 means in code
/
Separator
1ps
Precision
finest resolvable time
Fig 8 — `timescale: formats, scope, and common values
// ── 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.

Macro vs parameter: A `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.
Fig 9 — `define usage: constants, macros, feature flags, and `undef
// ── 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.

Fig 10 — `include: shared definitions across multiple files
// ── 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
Always use include guards. If a file is included more than once in a compilation (which easily happens with complex hierarchies), without guards the macros get redefined causing compiler warnings or errors. The `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).

Fig 11 — All conditional compilation directives
// ── 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

FeaturePurposeScopeOverridable?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
Fig 12 — Complete module combining parameters, specify, and directives
`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
Professional design tip: Use `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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top