VERILOG SERIES · MODULE 18

Compiler Directives, Hierarchical Access & UDPs — VLSI Trainers
Verilog Series · Module 18

Compiler Directives, Hierarchical Access & UDPs

Complete coverage of all Verilog compiler directives, hierarchical naming and access techniques, and User-Defined Primitives (UDPs) — combinational and sequential — with full truth table notation.

🔧 Compiler Directives — Introduction

Compiler directives are instructions to the Verilog preprocessor — they are processed before compilation begins, transforming the source text before the compiler sees it. All directives start with a backtick (`) character.

Unlike regular Verilog statements, directives are not enclosed in modules and their effect is file-global — a directive declared in one file can affect all subsequently compiled files in the same compilation run.

`
Backtick Prefix
All directives start with ` (grave accent). This is distinct from the single-quote ' used in number literals.
🌐
Global Scope
Directives persist from the point of declaration across all subsequently compiled files in the same compile unit — unlike module-scoped parameters.
Preprocessed First
The preprocessor runs before the Verilog parser. Directives expand macros and include files before any syntax checking occurs.
🚫
Not Synthesized
The selected code after conditional compilation is synthesized, but the directives themselves are invisible to synthesis tools.

`timescale

The `timescale directive defines the time unit (what #1 means) and time precision (the finest resolvable time step) for all modules that follow it in the compiled file.

Fig 1 — `timescale: syntax, valid values, and practical impact
// Syntax: `timescale <unit> / <precision>
// Valid unit multipliers: 1, 10, 100
// Valid suffixes: fs, ps, ns, us, ms, s

`timescale 1ns/1ps      // most common RTL: #1 = 1ns, resolution 1ps
`timescale 10ns/1ns    // #1 = 10ns, resolution 1ns
`timescale 1us/100ns   // microcontroller / slow design
`timescale 1ps/1fs     // switch-level / analog-mixed signal

// Effect on delay values in code below this directive:
`timescale 1ns/100ps
always #5     clk = ~clk;       // 5ns period
assign #(2.3) y   = a & b;      // 2.3ns (rounded to 2.3 per 100ps prec)

// ── Rules ────────────────────────────────────────────────────
// ✅ precision ≤ time_unit (1ps ≤ 1ns — legal)
// ❌ precision > time_unit (`timescale 1ps/1ns — ILLEGAL)
// The simulator uses the smallest precision of ALL modules

// ── Common pairing for different use cases ────────────────────
// RTL simulation:        `timescale 1ns/1ps
// Gate-level simulation: `timescale 1ns/100ps  (SDF precision)
// Standard cell models:  `timescale 1ns/1ps
// Testbench w/ real arith: `timescale 1ns/1ps with $timeformat
Mixed timescales — simulator uses the smallest precision. If module A uses 1ns/1ps and module B uses 1ns/100ps, the simulator’s global precision is 1ps. This can significantly slow simulation for large designs. Standardise to one timescale across all files using a shared include file.

🏷️ `define, `undef, `ifdef Family

Macros and conditional compilation are the most powerful compiler directive features — enabling a single source to produce different designs for different targets, process nodes, and verification modes.

Fig 2 — `define, `undef, and conditional compilation directives
// ── `define: create a text macro ──────────────────────────────
`define BUS_WIDTH    32              // constant macro
`define CLK_PERIOD   10              // used as #(`CLK_PERIOD/2)
`define RESET_VAL    8'h00           // multi-character value

// Using a macro — prefix with backtick
wire [`BUS_WIDTH-1:0] data;           // expands to wire [31:0] data
always #(`CLK_PERIOD/2) clk=~clk; // expands to #(10/2) = #5

// ── Function-like macros (with arguments) ─────────────────────
`define MIN(a,b)  ((a) < (b) ? (a) : (b))
`define MAX(a,b)  ((a) > (b) ? (a) : (b))
`define ABS(x)   ((x) >= 0 ? (x) : -(x))

assign y = `MIN(a, b);             // expands inline

// ── `undef: remove a macro definition ─────────────────────────
`define DEBUG                       // defined (no value — just a flag)
// ... use `DEBUG in `ifdef blocks ...
`undef  DEBUG                       // undefine — no longer exists

// ── `ifdef / `ifndef / `elsif / `else / `endif ────────────────
`ifdef SYNTHESIS
  // code compiled for synthesis only
  assign y = a & b;
`elsif FPGA_TARGET
  // code compiled for FPGA targets
  fpga_and u1(y, a, b);
`else
  // code compiled otherwise (simulation default)
  and (y, a, b);
`endif

// ── `ifndef: compile if NOT defined ───────────────────────────
`ifndef SYNTHESIS
  initial $monitor("%t y=%b", $time, y); // sim-only
`endif
Macro vs parameter — when to use which: Use `define for global compile-time constants shared across many files (bus widths, feature flags, target selectors). Use parameter for per-instance configurability within a module hierarchy. Never use `define where parameter would work — macros have no scope and no type checking.

📁 `include

The `include directive textually inserts the contents of another file at the current point. It is the primary mechanism for sharing macro definitions, interface definitions, and global constants across all files in a project.

Fig 3 — `include: shared headers with include guards
// ── global_pkg.vh — shared project header ─────────────────────
`ifndef GLOBAL_PKG_VH
`define GLOBAL_PKG_VH                 // include guard

  `timescale 1ns/1ps
  `define DATA_W     32
  `define ADDR_W     10
  `define CLK_PERIOD 10
  `define RESET_ADDR 32'h0000_0000

`endif  // GLOBAL_PKG_VH

// ── axi_master.v — module file using shared header ────────────
`include "global_pkg.vh"   // paste header contents here
`include "axi_defs.vh"     // AXI-specific macros

module axi_master (
  input  [`ADDR_W-1:0] addr,
  input  [`DATA_W-1:0] wdata,
  output [`DATA_W-1:0] rdata
);
  // ... uses macros from included header ...
endmodule

// ── axi_slave.v — same header, guaranteed consistent ──────────
`include "global_pkg.vh"   // guard prevents redefinition error

module axi_slave ( ... );
  // macros identical to axi_master — no inconsistency
endmodule

⚙️ Other Compiler Directives

Verilog defines several additional directives beyond `timescale, `define, and `include. Each serves a specific purpose in controlling compilation behaviour:

`default_nettype
Changes the default type for undeclared nets. Always set to none — forces explicit declarations and catches typos at compile time.
`default_nettype none
// Undeclared net = compile error ✅
`default_nettype wire
// Undeclared net = wire (dangerous)
`celldefine / `endcelldefine
Marks a module as a standard cell. Simulator may treat the cell’s internals as opaque (no sub-module debug). Used in cell library models.
`celldefine
module INV_X1 (input a, output y);
  not (y, a);
endmodule
`endcelldefine
`unconnected_drive / `nounconnected_drive
Sets the default value for unconnected input ports of modules. Useful for detecting floating inputs.
`unconnected_drive pull1
// unconnected inputs float high
`nounconnected_drive
// restore default (z)
`resetall
Resets all compiler directives to their defaults: removes all `define macros, restores `default_nettype wire, etc. Useful at the start of library files to ensure a clean state.
`resetall
// All `define macros removed
// `default_nettype restored to wire
// `timescale cleared
`line
Overrides the reported filename and line number in compiler messages. Used by tools that generate Verilog code to point errors back to the original source file.
`line 42 "original_source.v" 0
// errors reported as line 42
// of original_source.v
`pragma
Tool-specific directives that the preprocessor passes through unchanged. Used for vendor extensions like synthesis constraints, power intent, and analysis hints.
`pragma synthesis_on
`pragma translate_off
// synthesis tool ignores this block
`pragma translate_on

Complete Directive Reference

DirectivePurposePersists
`timescale u/p Set time unit and precision Until next `timescale or end of file
`define NAME val Define a text macro Until `undef or end of compilation
`undef NAME Remove a macro definition Immediate
`ifdef / `ifndef Conditional compilation start Until matching `endif
`elsif / `else Conditional branches Within `ifdef block
`endif End conditional block Closes nearest open `ifdef
`include “file” Insert file contents here One-time at point of include
`default_nettype t Set default net type for undeclared nets Until next `default_nettype
`resetall Reset all directives to defaults Immediate — clears all macros
`celldefine Mark module as a cell (library model) Until `endcelldefine
`endcelldefine End cell definition Closes nearest `celldefine
`unconnected_drive Drive unconnected inputs high or low Until `nounconnected_drive
`nounconnected_driveRestore default for unconnected inputs Immediate
`line n “file” lvl Override error reporting line/file For following source lines
`pragma … Tool-specific pass-through directives Tool-dependent

🌳 Hierarchical Naming — Introduction

Verilog designs are built from nested module instantiations forming a tree — the design hierarchy. Every instance, module, net, register, task, and function in the hierarchy has a unique hierarchical name that identifies its exact position in the tree.

Hierarchical naming allows any part of the design to be referenced from anywhere else — either for debugging (reading internal signals from a testbench), or for configuration (defparam across hierarchy boundaries).

🌲
Tree Structure
The top module is the root. Each instantiation adds a level. Instance names (not module names) form the path.
🔵
Instance Names
The path is built from instance names, not module names. Two instances of the same module have different hierarchical paths.
🔗
Dot Separator
Hierarchy levels are separated by dots: top.u_cpu.u_alu.carry_out
🧪
Testbench Access
Testbenches use hierarchical names to read internal DUT signals, force values, and set parameters — without modifying DUT source.

📍 Hierarchical Path Names

A hierarchical path name is a sequence of instance names separated by dots, starting from the top module (or any upward reference point), ending at the target signal or element.

Fig 4 — Design hierarchy tree and corresponding path names
tb  ← testbench top
└─ u_dut  ← DUT instance (module: cpu)
├─ u_alu  ← ALU instance (module: alu)
│   ├─ result[31:0]
│   ├─ carry_out
│   └─ u_adder  (module: adder)
│       └─ sum[31:0]
└─ u_regfile  ← Register file instance
    ├─ mem[0:31]
    └─ rd_data[31:0]
// ── Hierarchical path names for above design ─────────────────
tb.u_dut                      // DUT module instance
tb.u_dut.u_alu               // ALU inside DUT
tb.u_dut.u_alu.result        // signal inside ALU
tb.u_dut.u_alu.carry_out     // another signal in ALU
tb.u_dut.u_alu.u_adder.sum  // signal 4 levels deep
tb.u_dut.u_regfile.mem[5]   // array element in register file

// ── Things that can have hierarchical names ───────────────────
top.u1.my_wire           // wire / reg / net
top.u1.my_task           // task
top.u1.my_func           // function
top.u1.PARAM_N           // parameter
top.u1.BLK_NAME          // named block

🔭 Hierarchical Access

Hierarchical access means using a full hierarchical path name to read or drive a signal that exists at a different level in the module hierarchy. This is fundamental to testbench-based verification — the testbench can probe any internal node of the DUT without adding ports to the DUT itself.

Fig 5 — Hierarchical access: reading, monitoring, and driving internal signals
// ── Testbench: read internal DUT signals ─────────────────────
module tb;
  reg  clk, rst_n;
  wire [7:0] out;

  my_dut u_dut(.clk(clk), .rst_n(rst_n), .out(out));

  initial begin
    // Read internal state register
    $display("state = %b", tb.u_dut.state);  // hierarchical read

    // Monitor internal counter
    $monitor("%t cnt=%d", $time, tb.u_dut.u_counter.cnt);

    // Force internal signal (hierarchical drive)
    force   tb.u_dut.state = 3'b101;  // override internal state
    @(posedge clk);
    release tb.u_dut.state;          // release override

    // Read internal memory array element
    $display("mem[0] = %h", tb.u_dut.u_mem.data[0]);
  end
endmodule

// ── defparam: set parameters via hierarchy ────────────────────
my_module u1(...);
defparam tb.u1.WIDTH    = 16;      // override parameter via hier path
defparam tb.u1.u_sub.DEPTH = 64;  // override in sub-instance
Hierarchical access is simulation-only. Synthesis tools cannot cross module boundaries to access internal signals. Any hierarchical reference in testbench code is fine — but if hierarchical names appear inside synthesizable RTL modules, those references will not work after synthesis. Keep hierarchical access in testbench files only.

⬆️ Upward Hierarchical References

Verilog also allows upward references — a module referencing signals or parameters from a higher level in the hierarchy. While supported by the language, upward references are considered very bad practice and should be avoided.

❌ Upward reference — Avoid!

module sub_module;
  // References a signal in parent!
  always @(posedge top.clk)
    q <= d;
endmodule
// sub_module cannot be reused
// anywhere else — breaks if moved

✅ Port-based — Always preferred

module sub_module(
  input clk, d,
  output reg q
);
  always @(posedge clk)
    q <= d;
endmodule
// self-contained — reusable ✅

💡 Hierarchical Access Examples

Fig 6 — White-box testbench: monitor internal FSM state

Testbench reading internal state and triggering on state changes
module fsm_whitebox_tb;
  reg  clk=0, rst_n, en;
  wire done;

  my_fsm dut(.clk(clk), .rst_n(rst_n), .en(en), .done(done));
  always #5 clk = ~clk;

  // Decode internal state for readability
  reg [2:0] last_state;
  always @(posedge clk) begin
    if (fsm_whitebox_tb.dut.state !== last_state) begin
      $display("%0t: FSM state changed %0d → %0d",
        $time, last_state, fsm_whitebox_tb.dut.state);
      last_state = fsm_whitebox_tb.dut.state;
    end
  end

  initial begin
    // Inject corner case: force specific state
    rst_n=0; #20; rst_n=1;
    repeat(5) @(posedge clk);

    force   fsm_whitebox_tb.dut.state = 3'd6; // FAULT state
    @(posedge clk);
    release fsm_whitebox_tb.dut.state;
    repeat(10) @(posedge clk);
    $finish;
  end
endmodule

Fig 7 — Hierarchical $readmemh: pre-load internal memory

Load ROM/RAM content via hierarchical path without modifying DUT
initial begin
  // Pre-load instruction memory inside the DUT CPU
  $readmemh("program.hex", tb.u_cpu.u_imem.mem);

  // Pre-load data RAM with test data
  $readmemh("data.hex",    tb.u_cpu.u_dmem.mem);

  // Verify load was successful by reading back first word
  $display("PC reset: imem[0] = %h", tb.u_cpu.u_imem.mem[0]);

  // Read internal register file value after test
  repeat(100) @(posedge clk);
  $display("R0 = %h", tb.u_cpu.u_regfile.regs[0]);
  $display("R1 = %h", tb.u_cpu.u_regfile.regs[1]);
end

🧬 User-Defined Primitives (UDP)

A User-Defined Primitive (UDP) is a custom logic element — similar to a gate primitive (and, or) — but with behaviour defined by a truth table rather than a logic equation. UDPs can model any combinational or sequential logic function and are instantiated exactly like built-in primitives.

📊
Truth Table Based
Behaviour is defined in a table...endtable block. Each row maps input combinations to output values.
🔧
Two Types
Combinational: output depends only on current inputs. Sequential: output depends on inputs and current state (like a flip-flop or latch).
Efficient Simulation
Simulators can evaluate UDP truth tables faster than procedural code — UDPs are used in standard cell library models for speed.
🚫
One Output Only
UDPs can have only one output port, which must be declared first. Any number of input ports are allowed.
UDP key constraints: The output must be the first port. The output type is wire for combinational UDPs and reg for sequential UDPs. UDPs cannot have inout ports, cannot be parameterised, and the table may only use values 0, 1, x (and edge descriptors for sequential).

🔷 Combinational UDPs

A combinational UDP defines a purely combinational function — its output depends only on its current input values. The truth table lists one row per input combination. The wildcard ? matches any input value (0, 1, or x).

primitive udp_name (
  output,      // output must be FIRST, declared as output
  input_1,
  input_2,
  input_n
);

  table
    // input_1  input_2  ...  input_n : output
    0        0             0   : 0;
    1        1             1   : 1;
    ?        ?             ?   : x;  // ? = any value (0,1,x)
  endtable

endprimitive

Fig 8 — Combinational UDP: 2-input multiplexer

UDP MUX: out = sel ? b : a — defined by truth table entries
2:1 MUX Truth Table
abselout
0?00
1?01
?010
?111
00x0
11x1
??xx
primitive udp_mux2 (
  out, a, b, sel
);
  output out;
  input  a, b, sel;

  table
  // a  b  sel : out
     0  ?  0  : 0;
     1  ?  0  : 1;
     ?  0  1  : 0;
     ?  1  1  : 1;
     0  0  x  : 0;
     1  1  x  : 1;
     ?  ?  x  : x;
  endtable
endprimitive

// Instantiation — like any primitive
udp_mux2 m1(y, in0, in1, sel);

Fig 9 — Combinational UDP: majority voter (3-input)

UDP majority: output = 1 when at least 2 of 3 inputs are 1
primitive udp_majority (out, a, b, c);
  output out;
  input  a, b, c;

  table
  // a  b  c  : out
     0  0  ?  : 0;   // a=0, b=0 → out=0 regardless of c
     0  ?  0  : 0;   // a=0, c=0 → out=0 regardless of b
     ?  0  0  : 0;   // b=0, c=0 → out=0 regardless of a
     1  1  ?  : 1;   // a=1, b=1 → out=1 regardless of c
     1  ?  1  : 1;   // a=1, c=1 → out=1 regardless of b
     ?  1  1  : 1;   // b=1, c=1 → out=1 regardless of a
  endtable
endprimitive

Combinational UDP Table Symbols

SymbolMeaningWhere used
0Logic 0Input or output
1Logic 1Input or output
xUnknownInput or output
?Any of 0, 1, or x (don’t care)Input only
No change (hold current value)Output only (sequential)

🔄 Sequential UDPs — Level-Sensitive (Latches)

A sequential UDP has an internal state — its output depends on both the current inputs and the current state. The output must be declared as reg. The table has an additional column: the current state. An optional initial statement sets the power-on state.

Sequential UDPs with level-sensitive entries model latches — the output responds to input levels, not edges.

Fig 10 — Level-sensitive UDP: SR latch
SR Latch (level-sensitive)
srstateout
10?1
01?0
000
001
11?x
x011
0x00
primitive udp_sr_latch(
  q, s, r
);
  output reg q;     // reg for sequential
  input s, r;
  initial q = 1'b0; // power-on state

  table
  // s  r  state : next
     1  0    ?  :  1;  // Set
     0  1    ?  :  0;  // Reset
     0  0    0  :  -;  // Hold 0
     0  0    1  :  -;  // Hold 1
     1  1    ?  :  x;  // Forbidden
     x  0    1  :  1;
     0  x    0  :  0;
  endtable
endprimitive

In a sequential UDP table, the colon separates the current state from the next state: // s r current_state : next_state. The dash - in the output column means “no change — retain current state”.

Edge-Sensitive Sequential UDPs — Flip-Flops

Edge-sensitive UDPs model flip-flops and registers — the output updates only on a specified edge of the clock, not continuously in response to levels. Edge descriptors replace simple input values in the clock column of the truth table.

(01)rising edge (0→1) (10)falling edge (1→0) rrising (shorthand for 01) ffalling (shorthand for 10) bboth edges (either transition) ppositive edge (01 or 0x or x1) nnegative edge (10 or 1x or x0) (*)any transition on that input
Fig 11 — Edge-sensitive UDP: D flip-flop with async reset
D Flip-Flop (posedge clk)
dclkrststateout
??1?0
0(01)0?0
1(01)0?1
?(0x)00
?(0x)01
?(x1)00
?n0?
????
primitive udp_dff(
  q, d, clk, rst
);
  output reg q;
  input d, clk, rst;
  initial q = 1'bx;

  table
  // d   clk    rst  state : next
     ?   ?      1    ?   :  0; // async reset
     0   (01)   0    ?   :  0; // capture 0
     1   (01)   0    ?   :  1; // capture 1
     ?   (0x)   0    0   :  -; // clk edge unclear
     ?   (0x)   0    1   :  -;
     ?   (x1)   0    0   :  -;
     ?    n     0    ?   :  -; // falling edge: hold
     ?    ?     ?    ?   :  -; // all others: hold
  endtable
endprimitive
Reading the edge-sensitive table: The column header comment format is // input1 clk_input ... state : next_output. An edge descriptor like (01) in the clock column means “this row applies only when clock transitions 0→1”. All non-edge inputs in that row must also match for the row to fire.

💡 UDP Examples

Fig 12 — Level-sensitive D Latch UDP

D latch: transparent when en=1, latches when en=0
primitive udp_d_latch (q, d, en);
  output reg q;
  input d, en;
  initial q = 1'b0;

  table
  // d  en  state : next
     0  1    ?  :  0;  // en=1, d=0 → q=0 (transparent)
     1  1    ?  :  1;  // en=1, d=1 → q=1 (transparent)
     ?  0    0  :  -;  // en=0, state=0 → hold 0
     ?  0    1  :  -;  // en=0, state=1 → hold 1
     0  x    0  :  -;  // en=x but d=0 and state=0 → hold
     1  x    1  :  -;  // en=x but d=1 and state=1 → hold
     x  1    ?  :  x;  // d=x when transparent → x
  endtable
endprimitive

Fig 13 — JK Flip-Flop UDP

JK flip-flop: clocked on posedge, toggle when J=K=1
primitive udp_jk_ff (q, j, k, clk);
  output reg q;
  input j, k, clk;
  initial q = 1'b0;

  table
  // j  k   clk   state : next
     0  0  (01)   ?   :  -;  // J=0,K=0 → hold
     0  1  (01)   ?   :  0;  // J=0,K=1 → reset
     1  0  (01)   ?   :  1;  // J=1,K=0 → set
     1  1  (01)   0   :  1;  // J=1,K=1, state=0 → toggle → 1
     1  1  (01)   1   :  0;  // J=1,K=1, state=1 → toggle → 0
     ?  ?   n     ?   :  -;  // falling edge → hold
     ?  ?  (0x)   ?   :  -;  // ambiguous edge → hold
     ?  ?  (x1)   ?   :  -;  // ambiguous edge → hold
  endtable
endprimitive

Fig 14 — Using UDPs with delays

UDPs support the same delay and strength syntax as built-in primitives
// ── Instantiation with delay ──────────────────────────────────
udp_mux2  #(2, 3)    m1(y, a, b, sel);  // rise=2, fall=3
udp_dff   #(3, 4, 1) ff1(q, d, clk, rst); // rise=3, fall=4, off=1

// ── Instantiation with drive strength ─────────────────────────
udp_mux2  (strong1, strong0)
           #(2, 3) m2(y, a, b, sel);

// ── Array of UDP instances ────────────────────────────────────
udp_dff   ff[7:0](q, d, {8{clk}}, {8{rst}});
// Creates 8 D flip-flop instances — one per bit

📋 Summary

Compiler Directives Quick Reference

DirectivePurposeKey Rule
`timescale Set time unit and precision Precision must be ≤ unit; affects all following modules
`define Create text macro Global — use for project-wide constants and feature flags
`undef Remove macro Essential for local/scoped macro use
`ifdef / `ifndef Conditional compilation Primary mechanism for multi-target designs
`include Insert file Always use include guards (`ifndef)
`default_nettype noneForce explicit net declarationsAlways set this — catches typos at compile time
`resetall Reset all directives Use at start of library files for clean state
`celldefine Mark module as a cell Standard cell library models only

Hierarchical Access Summary

Access typeSyntaxUseSynthesizable?
Read internal signal$display(tb.u_dut.sig) Debug — observe without ports❌ Testbench only
Force internal signalforce tb.u_dut.sig = val Corner case injection ❌ Testbench only
Load memory $readmemh("f", tb.dut.mem)Pre-load ROM/RAM content ❌ Testbench only
defparam override defparam tb.u1.N = 16 Parameter override (legacy) ✅ Synthesizable
Upward reference top.parent.signal Avoid — breaks reusability ❌ Do not use

UDP Summary

FeatureCombinational UDPSequential UDP
Output declarationoutput output reg
Table column formatinputs : output inputs state : next_output
Edge descriptors Not applicable (01), (10), r, f, b, p, n, (*)
Initial statement Not supported Optional: sets power-on state
Wildcard ? Matches 0, 1, or x (inputs) Same — inputs only
Hold output (-) Not applicable Output column: retain current value
Instantiation Same as built-in primitives Same — supports delay and strength
UDP best practice: Use UDPs for standard cell library models and custom logic elements that are used repeatedly and need fast simulation. For one-off designs in RTL, behavioral always blocks are clearer and more maintainable. UDPs shine when you need simulation-speed optimisation for heavily instantiated primitives — exactly the use case they were designed for.

Leave a Comment

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

Scroll to Top