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.
` (grave accent). This is distinct from the single-quote ' used in number literals.⏱ `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.
// 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
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.
// ── `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
`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.
// ── 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:
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 module INV_X1 (input a, output y); not (y, a); endmodule `endcelldefine
`unconnected_drive pull1 // unconnected inputs float high `nounconnected_drive // restore default (z)
`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 42 "original_source.v" 0 // errors reported as line 42 // of original_source.v
`pragma synthesis_on `pragma translate_off // synthesis tool ignores this block `pragma translate_on
Complete Directive Reference
| Directive | Purpose | Persists |
|---|---|---|
| `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_drive | Restore 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).
top.u_cpu.u_alu.carry_out📍 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.
// ── 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.
// ── 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
⬆️ 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
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
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.
table...endtable block. Each row maps input combinations to output values.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
| 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 |
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)
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
| Symbol | Meaning | Where used |
|---|---|---|
| 0 | Logic 0 | Input or output |
| 1 | Logic 1 | Input or output |
| x | Unknown | Input 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.
| s | r | state | out |
|---|---|---|---|
| 1 | 0 | ? | 1 |
| 0 | 1 | ? | 0 |
| 0 | 0 | 0 | – |
| 0 | 0 | 1 | – |
| 1 | 1 | ? | x |
| x | 0 | 1 | 1 |
| 0 | x | 0 | 0 |
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.
| d | clk | rst | state | out |
|---|---|---|---|---|
| ? | ? | 1 | ? | 0 |
| 0 | (01) | 0 | ? | 0 |
| 1 | (01) | 0 | ? | 1 |
| ? | (0x) | 0 | 0 | – |
| ? | (0x) | 0 | 1 | – |
| ? | (x1) | 0 | 0 | – |
| ? | n | 0 | ? | – |
| ? | ? | ? | ? | – |
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
// 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
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
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
// ── 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
| Directive | Purpose | Key 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 none | Force explicit net declarations | Always 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 type | Syntax | Use | Synthesizable? |
|---|---|---|---|
| Read internal signal | $display(tb.u_dut.sig) | Debug — observe without ports | ❌ Testbench only |
| Force internal signal | force 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
| Feature | Combinational UDP | Sequential UDP |
|---|---|---|
| Output declaration | output | output reg |
| Table column format | inputs : 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 |
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.
