SYSTEMVERILOG SERIES · SV-05A

SystemVerilog Series — SV-05a: Data Declarations & Constants — VLSI Trainers
SystemVerilog Series · SV-05a

Data Declarations & Constants

What data is in SystemVerilog, the declaration syntax, and the three kinds of constant — localparam, specparam, and the new const keyword — with worked examples covering every rule and edge case.

📈 Kinds of Data in SystemVerilog

SystemVerilog defines six categories of named data. Understanding the category determines what you can assign to it, when its storage is allocated, and how long it persists.

Literals
Numbers and strings written directly: 8'hFF, 3.14, "hello", 10ns
Parameters
Module-level constants overridable at instantiation: parameter WIDTH = 8;
Constants
Named values that never change: localparam, specparam, const — the focus of this article
Variables
Storage that procedures write to: int, logic, byte, class handles — next article
Nets
Hardware connections driven by continuous assignment: wire, tri, etc. — next article
Attributes
Tool hints attached to constructs: (* synthesis, keep *) — covered in the Attributes article
The big change from Verilog-2001: In Verilog-2001, variables could only be written by procedural statements (reg) and nets could only be driven by continuous assignment (wire). SystemVerilog blurs this line: a logic variable can now be driven by either a single continuous assignment or one or more procedural statements. The keyword logic replaces reg in most RTL code because it accurately describes the intent without the confusing “register” connotation.

📋 Declaration Syntax

A data declaration in SystemVerilog has an optional const qualifier, an optional lifetime qualifier, followed by the type and variable name. This is the full grammar for any data declaration:

data_declaration ::=
    [ const ] [ lifetime ] variable_declaration
    | type_declaration
    | package_import_declaration
    | virtual_interface_declaration

lifetime ::= static | automatic

The three optional qualifiers and what they do

QualifierWhat it doesDefault when omitted
const The variable is read-only after initialisation. Writing to it is a compile error. Mutable (can be written)
static Storage allocated once at elaboration, lives for the entire simulation. All instances share one copy (for tasks/functions). Default everywhere except inside automatic tasks/functions
automatic Storage allocated on each call/activation, freed on return. Each call gets its own copy. Required for recursion. Default inside automatic tasks, functions, programs, and class methods
// Examples of data declarations using each qualifier
const logic       flag    = 1;         // const, default lifetime (static)
static int        counter = 0;         // explicit static
automatic int     temp    = 0;         // automatic: new copy per call
const automatic logic once = 1'b1;   // const + automatic (set at entry, read-only)

// Type declaration (typedef) — also a data_declaration
typedef logic[7:0] byte_t;

// Package import declaration
import my_pkg::*;

// Note: automatic cannot appear outside a procedural context
// module m; automatic int x; endmodule  // ILLEGAL
Declare before use: SystemVerilog follows Verilog in requiring that all names be declared before they are referenced. The only exception is implicit nets: if an undeclared identifier is used where a 1-bit net is expected (inside a module instance port connection), an implicit wire is created — the same rule as Verilog-2001.

🔒 Three Kinds of Constant

SystemVerilog has three different constant mechanisms. They look similar but differ in when they are resolved, where they can be used, and what expressions are legal on the right-hand side.

localparam

  • Resolved at elaboration time
  • Cannot be overridden from outside the module
  • RHS: literals, other params, constant functions
  • No hierarchical names on RHS
  • Usable anywhere in the module

specparam

  • Resolved at elaboration time
  • Designed for specify blocks (timing)
  • RHS: literals, other specparams
  • Not usable outside specify/specparam context
  • Unchanged from Verilog-2001

const

  • Resolved at simulation time
  • New in SystemVerilog
  • Static: same as localparam (elaboration)
  • Automatic: evaluated on each entry to scope
  • Allows hierarchical names on RHS

🟢 localparam

A localparam is an elaboration-time constant that cannot be overridden from outside the module, unlike a plain parameter. It is typically used for derived constants that should not be changed by the instantiating parent.

Basic declaration

// Simple localparam
localparam byte     COLON    = ":";      // byte constant: ASCII 58
localparam int      MAX_LEN  = 256;
localparam logic    ACTIVE_H = 1'b1;

// Derived localparam — computed from parameters
module fifo #(
  parameter int DEPTH  = 16,
  parameter int WIDTH  = 8
);
  localparam int ADDR_W = $clog2(DEPTH);  // derived — cannot be overridden
  localparam int WORDS  = DEPTH * WIDTH;  // also derived

  logic [ADDR_W-1:0] rd_ptr, wr_ptr;
endmodule

Legal right-hand side expressions

parameter int  N = 4;
localparam int A = 8;                 // OK — literal
localparam int B = N * 2;              // OK — parameter expression
localparam int C = A + B;              // OK — other localparams
localparam int D = $clog2(N);        // OK — constant function of params

// genvar in generate context
generate
  genvar g;
  for(g=0; g<N; g++) begin
    localparam int IDX = g;           // OK — genvar
  end
endgenerate

// ILLEGAL right-hand sides for localparam:
// localparam int E = some_signal;       // ERROR — runtime value
// localparam int F = top.sub.param;     // ERROR — hierarchical name
parameter vs localparam — why it matters: A plain parameter can be overridden at instantiation: fifo #(.DEPTH(32)) u_fifo(...);. A localparam cannot — it is always computed from the module’s own parameters. This is important for derived values like ADDR_W = $clog2(DEPTH): if someone could override ADDR_W independently of DEPTH, the derived relationship would be broken. Use localparam for any constant derived from parameters.

localparam in packages

// In a package, localparams are accessible via scope resolution
package bus_pkg;
  localparam int DATA_W = 32;
  localparam int ADDR_W = 28;
  localparam int BYTES  = DATA_W / 8;
endpackage

module top;
  import bus_pkg::*;          // wildcard import
  logic [DATA_W-1:0] data;    // 32-bit vector

  // Or access explicitly without import:
  logic [bus_pkg::ADDR_W-1:0] addr;
endmodule

🟠 specparam

A specparam is a constant designed specifically for use in specify blocks — the Verilog timing specification mechanism. SystemVerilog does not change specparam behaviour from Verilog-2001.

module my_gate (
  input  logic a, b,
  output logic y
);
  specparam int tRise = 10;    // rise time in time units
  specparam int tFall = 8;     // fall time
  specparam int tHold = tRise + 2; // derived from another specparam — OK

  assign y = a & b;

  specify
    (a => y) = (tRise, tFall);  // use specparams in timing paths
    (b => y) = (tRise, tFall);
  endspecify
endmodule
specparam restrictions: A specparam can only appear in the body of a module (not in a package, interface, or class). Its right-hand side may only include literals and other specparam values — not regular parameter or localparam values. It cannot be used as an index, array size, or port connection width — those require a proper parameter or localparam. In practice, specparam is almost exclusively used in gate-level netlists generated by synthesis tools.

const — The New SystemVerilog Keyword

The const keyword declares a variable that is initialised once and then read-only. Unlike localparam, a const is calculated at simulation time rather than elaboration time — which means its initialiser can reference runtime values (in an automatic context) and hierarchical names (in a static context).

Static const — resolved after elaboration

// Static const: like localparam but can reference hierarchical names
const logic    flag     = 1;          // literal — same as localparam
const logic    option   = a.b.c;      // OK — hierarchical name allowed
const int      MAX      = top.u1.WIDTH; // OK — hierarchical param
const byte    COLON    = ":";        // typed const from a string literal

// Writing to a const after declaration is a compile error:
// flag = 0;   // ERROR — const cannot be written

Automatic const — evaluated on every scope entry

task automatic compute(int data);
  // This const is evaluated from data on each call to compute()
  // It acts like a read-only alias for the expression
  const int DOUBLED = data * 2;    // legal — automatic context

  $display("DOUBLED = %0d", DOUBLED);
  // DOUBLED = data;  // ERROR — cannot assign to const
endtask

compute(5);   // prints DOUBLED = 10
compute(3);   // prints DOUBLED = 6  (new const value on new call)
Static const vs localparam — the key difference: A localparam must be calculated entirely from other parameters, literals, and constant functions before simulation starts. A const is calculated after elaboration is complete, so it can reference hierarchical names (like another module’s parameter via top.sub.WIDTH) and simulation-computed values. The tradeoff is that tools cannot use a const to derive elaboration-time information (like array sizes); for that, you still need localparam.

const in different scope positions

module msl;
  // Module scope — static lifetime (even without 'static' keyword)
  const int MOD_CONST = 42;      // static, elaboration-time value

  initial begin
    // Unnamed block — const here is static by default
    const int BLK_C = 100;        // static
    automatic int auto1 = 0;       // automatic (explicitly)
  end

  task automatic t1();
    // Inside automatic task — variables are automatic by default
    int        auto2;              // automatic (task default)
    static int st3;               // explicitly static
    const int  auto_c = auto2+1;  // automatic const (evaluated at call)
  endtask
endmodule

What the const keyword prevents

const int LIMIT = 100;

// Reading: always legal
int x = LIMIT;            // OK
if(x > LIMIT) ...         // OK
logic[7:0] arr[LIMIT];    // OK — as array size if localparam-compatible

// Writing: compile error
// LIMIT = 200;               // ERROR
// LIMIT++;                   // ERROR
// LIMIT += 5;                // ERROR
// force LIMIT = 50;          // ERROR — force also illegal on const

📚 const with Class Objects

A class object handle can be declared const. This makes the handle constant — you cannot point it to a different object — but the members of the object can still be written (unless those members are themselves declared const).

class Config;
  int        width  = 8;
  const int  max_v  = 255;  // const member — cannot be written
  string     name   = "default";
endclass

// const class_name object = new(args);
// Arguments to new() must be constant expressions.
const Config cfg = new();

// Legal: reading the const handle
$display(cfg.width);          // OK

// Legal: modifying non-const MEMBERS of the object
cfg.width = 16;               // OK — width member is not const
cfg.name  = "custom";         // OK

// Illegal: writing to const MEMBERS
// cfg.max_v = 100;            // ERROR — max_v is const

// Illegal: reassigning the handle itself
// cfg = new();                // ERROR — cfg handle is const
// cfg = null;                 // ERROR
const object = new() requires constant constructor arguments. Because the const declaration must be calculable at a known point (elaboration for static, scope-entry for automatic), the arguments passed to new() must themselves be constant expressions — literals, localparam values, or other const values. You cannot pass a runtime variable to new() of a const object declaration.

🔄 Choosing the Right Constant

Here is a practical decision guide for choosing between the three constant kinds:

Use localparam when…

  • The value is computed from module parameters
  • It will be used as an array size, port width, or loop bound (elaboration-time needed)
  • You want synthesis tools to see it as a parameter value
  • Classic examples: ADDR_W = $clog2(DEPTH), BYTES = WIDTH/8

Use const when…

  • The value references a hierarchical name
  • You need a simulation-time computed value that should not change after that point
  • Inside an automatic task/function, where it should be a read-only alias for an argument expression
  • Declaring a class object that should not be re-pointed
Questionlocalparamspecparamconst
Computed at elaboration time?Static: ✅   Auto: ❌
Hierarchical name on RHS legal?
Runtime expression on RHS legal?Automatic only
Can be used as array size/port width?Only if static
Can be used as a class object?
Overridable at instantiation?
Where can it appear?Module, interface, packageModule onlyAnywhere data is legal

📋 Quick Reference

Constant declaration syntax cheatsheet

// localparam — elaboration constant, no override
localparam int    WIDTH   = 8;
localparam int    ADDR_W  = $clog2(256);
localparam logic  ACTIVE  = 1'b1;

// specparam — for timing specify blocks only
specparam int     tRise   = 10;
specparam int     tFall   = 8;
specparam int     tHold   = tRise + 2;

// const — simulation-time read-only variable
const logic       FLAG    = 1;          // static const (no keyword needed)
const logic       OPT     = a.b.c;      // hierarchical name allowed
const MyClass     obj     = new();     // const class object handle

// const in automatic context
task automatic f(int x);
  const int TWICE = x * 2;           // auto const: evaluated at call
endtask

Rules at a glance

  • A localparam is an unoverridable parameter — use it for values derived from parameters.
  • A specparam is only for timing specify blocks — don’t use it elsewhere.
  • A static const is calculated after elaboration and can see hierarchical names.
  • An automatic const is evaluated every time its enclosing scope is entered — useful for read-only aliases inside tasks and functions.
  • A const class object means the handle is fixed, not the object’s members.
  • Writing to any const, including force, is a compile error.
Coming next: SV-05b covers Variables, Nets, Logic, Scope & Lifetime — the rest of Section 5 including the difference between logic and wire, static vs automatic lifetime rules, signal aliasing, and type compatibility.

Leave a Comment

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

Scroll to Top