Part 7 · Advanced & Integration · Intermediate

`define & Text Macros

Object-like and function-like macros, argument substitution, multi-line macros, stringification, and macros vs parameters.

Text substitution, not variables

A `define creates a text macro : every later use of the macro name (prefixed with a backtick) is replaced by the macro body, character for character, before compilation. There is no type, no scope, no evaluation — just substitution. An object-like macro has no arguments; a function-like macro takes arguments that are pasted into the body.

systemverilog
// Object-like macro — simple text replacement
`define DATA_WIDTH 32
`define RESET_VECTOR 32'h0000_1000

logic [`DATA_WIDTH-1:0] data_bus;   // becomes: logic [32-1:0] data_bus;

// Function-like macro — arguments substituted into body
// NOTE: no space allowed between macro name and (
`define MIN(a, b) (((a) < (b)) ? (a) : (b))

int smaller = `MIN(x + 1, y);
// expands to: (((x + 1) < (y)) ? (x + 1) : (y))
// parentheses around each argument prevent precedence bugs

// Default argument values (SystemVerilog extension)
`define CLK_GEN(period = 10) \
  always #((period)/2) clk = ~clk;

`CLK_GEN()      // uses period = 10
`CLK_GEN(4)     // period = 4

Why the parentheses matter

Without parentheses around arguments, `MIN(a & 1, b) would expand the bitwise-and unparenthesized into the comparison, and operator precedence silently changes the meaning. Defensive parenthesization — around each argument and around the whole body — is the first habit of macro writing.


Multi-line macros and stringification

A macro body ends at the newline unless the line ends with a backslash line continuation . Multi-line macros are how UVM builds entire functions from one macro call. Stringification (`") turns macro text into a string literal with argument substitution still applied — which a normal quoted string would not do.

systemverilog
// Multi-line macro: backslash continues the body onto the next line
`define REPORT_FIELD(obj, field) \
  $display("%s.%s = %0d at %0t",  \
           obj.get_name(),         \
           `"field`",             \
           obj.field, $time);

// `"field`" stringifies the ARGUMENT: `REPORT_FIELD(txn, addr)
// prints  "txn.addr = 256 at 100" — the literal text "addr" appears
// because `" substitutes macro args inside the string, unlike plain ""

// Token pasting with `` joins macro text into one identifier
`define MAKE_GETTER(name) \
  function int get_``name(); \
    return name; \
  endfunction

`MAKE_GETTER(count)   // declares: function int get_count();
diagram
MACRO EXPANSION PIPELINE

  source text          `REPORT_FIELD(txn, addr)
       │
       ▼  preprocessor finds macro, binds args: obj=txn, field=addr
  body lookup          $display("%s.%s = %0d at %0t",
                                obj.get_name(), `"field`", obj.field, $time);
       │
       ▼  substitute args, apply `" stringification
  expanded text        $display("%s.%s = %0d at %0t",
                                txn.get_name(), "addr", txn.addr, $time);
       │
       ▼
  COMPILER sees only the expanded text — never the macro

Macros vs parameters

When each wins

  • Parameters are typed, scoped, and overridable per instance — always prefer them for sizing module ports and arrays.

  • Macros are global across the whole compilation — one `define DATA_WIDTH affects every file compiled after it, which is both their power and their danger.

  • Macros can generate code (functions, declarations, whole classes) — parameters cannot; this is why UVM utils macros exist.

  • Macros work in places parameters cannot reach: inside string literals via stringification, in `ifdef conditions, and across module boundaries without plumbing.

  • Parameters participate in elaboration and show up in hierarchy debug; macros vanish after preprocessing and leave no trace in the elaborated design.

systemverilog
// GOOD: parameter for per-instance configuration
module fifo #(parameter int DEPTH = 16, parameter int WIDTH = 32) (
  input  logic [WIDTH-1:0] wdata,
  output logic [WIDTH-1:0] rdata
);
endmodule

fifo #(.DEPTH(64), .WIDTH(8)) small_fifo (.*);  // overridable per instance

// GOOD: macro for cross-cutting code generation
`define CHECK_FATAL(cond, msg) \
  if (!(cond)) $fatal(1, "CHECK failed: %s", msg);

// BAD: macro where a parameter belongs — every fifo in the
// design is now forced to the same width, silently
`define FIFO_WIDTH 32

Key takeaways

  • Macros are preprocessor text substitution — no types, no scope, global effect from definition point onward.

  • Parenthesize every macro argument and the full body to avoid precedence surprises.

  • Use `" for stringification with argument substitution and `` for token pasting.

  • Prefer parameters for sizing and per-instance config; reserve macros for code generation and cross-cutting concerns.

Common pitfalls

  • Space between macro name and ( in the definition — turns a function-like macro into an object-like macro whose body starts with (.

  • Unparenthesized arguments — `MIN(a | b, c) silently miscompares due to operator precedence.

  • Trailing semicolon inside the macro body plus one at the call site — double semicolon breaks if/else chains.

  • Redefining a macro mid-codebase — later files see the new body, earlier files saw the old one, and compile order changes behavior.

Interview angle

Classic questions: difference between a macro and a parameter (text substitution vs typed elaboration-time constant); why UVM uses macros for factory registration (code generation that parameters cannot do); what `" does that a normal quote does not (argument substitution inside the string). Be ready to write a safe multi-line macro with line continuations on a whiteboard.