SYSTEMVERILOG SERIES · SV-21

SystemVerilog Series — SV-21: Parameters — VLSI Trainers
SystemVerilog Series · SV-21

Parameters

Everything SV adds to Verilog-2001 parameters: typed value parameters, type parameters (parameter type), omitting the parameter keyword in port lists, localparam in generate/package/compilation-unit scope, the $ unbounded token, $isunbounded(), and dependent parameter chains — with real-world parameterised checker and class examples.

📋 What Verilog-2001 Provided

Verilog-2001 provided three compile-time constant constructs and four methods for setting them:

  • parameter — overridable per-instance constant with a default value.
  • localparam — read-only constant; cannot be overridden at instantiation.
  • specparam — timing constants inside specify blocks.

Override methods: implicit positional (#(val1, val2)), explicit named (#(.p1(v1))), or defparam statements. Verilog parameters had no data type — they were size and sign inferred from their assigned value. SV adds explicit types, type parameters, and a cleaner set of scopes for localparam.

defparam is deprecated. The defparam statement may be removed from a future version of SV. Use explicit named overrides (#(.name(value))) instead. defparam was problematic because it could silently override a parameter across compilation boundaries, making designs harder to verify and port.

💡 What SV Adds

Typed parameters
Declare a parameter with an explicit data type — int, bit[N:0], real, struct, enum, etc. Type-checks overrides at elaboration.
Type parameters
parameter type T = int — the parameter is a data type. Allows modules and classes to be generic over types, similar to C++ templates.
localparam scopes
localparam now valid inside generate blocks, packages, and the compilation-unit scope. parameter acts as a synonym for localparam in those contexts.

📋 Typed Value Parameters

Adding an explicit data type to a parameter declaration constrains what values can be used to override it. The type is checked at elaboration time — a type mismatch produces an error rather than a silent truncation or sign change.

// Verilog-2001: no type — p1 infers type from its value
module old #(parameter p1 = 8) (...); endmodule

// SV: explicit types — p1 is int, p2 is shortint (type parameter)
module ma
  #(parameter int       p1 = 1,
    parameter type      p2 = shortint)
  (input logic  [p1:0] i,
   output logic [p1:0] o);

  p2 j = 0;               // j has type shortint unless p2 is overridden
  always @(i) begin
    o = i;
    j++;
  end
endmodule

// Override at instantiation — p2 becomes int instead of shortint
module mb;
  logic [3:0] i, o;
  ma #(.p1(3), .p2(int)) u1(i, o);
  // In u1: p1=3 → ports are [3:0]; p2=int → j is int
endmodule

// Other typed parameter examples
parameter bit [7:0]  MASK    = 8'hFF;
parameter real        FREQ    = 100.0;
parameter string      TAG     = "default";
parameter int unsigned DEPTH  = 64;
Always declare types on parameters. Without a type, a parameter defaults to logic of arbitrary size (for backward compatibility). An integer parameter declared without a type can silently expand to more bits than intended when overridden. Declaring parameter int DEPTH = 64 ensures the override is always checked against the int type.

📋 No-Type Default Behaviour

For compatibility with Verilog-2001, if no data type is given, a parameter defaults to logic of arbitrary size. The actual width and signing are inferred from the assigned value — either the default or the override.

// No type: inferred as logic, width from default value 8 → logic[3:0]
parameter N = 8;

// Override with 16 → now treated as 4-bit logic containing 4'b10000? No —
// the override VALUE determines the width at that instance.
// This is exactly why typing parameters prevents surprises.

// In a parameter port list, the 'parameter' keyword on the first entry
// sets the type for subsequent entries that only have the data type:
#(parameter int N = 5, M = N*16)
// N is int, M is also int (inherits type from previous)

📋 Type Parameters — parameter type

A type parameter is a parameter whose value is a data type. This makes modules, interfaces, and classes generic over types — like C++ templates. The actual type is substituted at elaboration, and all variables and ports using that type get the substituted type.

// module ma: p2 is a type parameter, default = shortint
// j takes the type of p2 at every instance
module ma #(parameter int p1 = 1, parameter type p2 = shortint) (...);
  p2 j = 0;   // shortint by default
endmodule

// Override p2 to int at instantiation
ma #(.p2(int)) u1(...);          // j is int in u1
ma #(.p2(real)) u2(...);         // j is real in u2
ma #(.p2(my_struct_t)) u3(...);  // j is my_struct_t in u3

// Type parameter with a user-defined type as default
typedef struct { int x; int y; } point_t;
module geom #(parameter type T = point_t);
  T vertices[3];   // array of 3 T — type set per instance
endmodule

📋 Uses of Type Parameters

Type parameters make design code reusable across different data widths, signedness, and even struct types — all without preprocessor macros or code duplication.

// Generic FIFO: data type configurable per instance
module generic_fifo #(parameter int DEPTH = 16,
                      parameter type T     = logic[7:0])
                     (input  T wr_data,
                      output T rd_data, ...);
  T mem[0:DEPTH-1];   // memory uses the same type T
endmodule

// Three different instantiations with different data types
generic_fifo #(.T(byte))              byte_fifo (...);
generic_fifo #(.T(logic[31:0]))       word_fifo (...);
generic_fifo #(.T(packet_t))           pkt_fifo  (...);  // struct type

// Type parameter used in a task prototype inside a module
module processor #(parameter type OP = int);
  task automatic execute(input OP operand);
    // ... operand type changes with OP
  endtask
endmodule
Type parameters and $typeof. The $typeof(expr) system function (Section 23) returns the type of an expression as an elaboration-time constant. This lets you use a signal’s actual type as a type parameter override: module m #(parameter type T = $typeof(some_signal)). This creates a tighter coupling between types that must match.

📋 localparam Enhancements

SV extends the places where localparam can appear and adds a rule that parameter acts as a synonym for localparam in certain scopes.

// localparam in a generate block — new in SV
generate
  for(genvar i=0; i<4; i++) begin
    localparam int LANE_W = 8 * (i+1);  // computed per iteration
    // LANE_W is different for each generated block
  end
endgenerate

// localparam (or parameter) in a package — acts as a package constant
package bus_pkg;
  parameter int BUS_W  = 32;   // parameter as synonym for localparam in packages
  localparam    MASK  = 32'hFF;
endpackage

// localparam (or parameter) in compilation-unit scope
parameter int CLK_PERIOD = 10;  // outside all modules — unit-local constant
parameter = localparam in packages and compilation-unit scope. In these contexts, a parameter cannot be overridden (there is no instantiation), so parameter and localparam are identical. Using parameter in a package is the conventional style for shared constants.

📋 Omitting the parameter Keyword in Port Lists

In a parameter port list, the first entry must use the parameter keyword. Subsequent entries can omit it — they inherit the parameter nature from context. Only the data type (or type) needs to be written.

// Full form — every parameter written explicitly
module full #(parameter int N=5, parameter int M=10, parameter type T=int) (...);

// Shorthand — keyword omitted after the first entry
module short #(parameter int N=5, int M=10, type T=int) (...);
// M and T still 'parameter'-qualified — just shorter to write

// Class with shortened parameter list
class vector #(size = 1);    // 'parameter' omitted — inferred as parameter
  logic [size-1:0] v;
endclass
typedef vector #(16) word;    // specialise to 16-bit word

// Interface with type parameter and omitted keyword
interface simple_bus #(AWIDTH = 64, type T = word) (input bit clk);
endinterface

$ The $ Unbounded Token

$ can be assigned to integer-type parameters to mean unbounded. A parameter holding $ may only be used where $ is legal as a literal constant — primarily as the upper bound of a range.

// Parameter holding $: means "no upper bound"
parameter r2 = $;

// Use in a property range: r1 to r2 clock ticks
// With r2=$, the range is [3:$] — unbounded upper end
property inq1(r1, r2);
  @(posedge clk) a ##[r1:r2] b ##1 c |=> d;
endproperty
assert inq1(3);    // r1=3 (explicit), r2=$ (default) → ##[3:$]

// $ in assertion sequences: 'true ##[1:$] b  →  b eventually holds
// $ in coverage bins: bins d = {[1000:$]}  →  1000 to max value of the variable
// $ in array dimensions: logic [N:0] arr[0:$] — NOT valid; $ is for ranges, not declarations
$ is not a general wildcard. You cannot use $ in a type declaration (e.g. bit [N:$] is not legal as a variable declaration). It is only valid where an unbounded range literal is allowed: in assertion sequence delay ranges (##[1:$]), in property range bounds, and in coverage bin value ranges ([low:$]).

📋 $isunbounded()

$isunbounded(const_expression) returns true if the constant expression evaluates to $. It is used in generate blocks to choose the correct property or logic based on whether a parameter is bounded or unbounded.

// $isunbounded used in a generate condition
parameter int foo = $;
// $isunbounded(foo)  → true
// $isunbounded(42)   → false

// Pattern: generate different logic for bounded vs unbounded parameter
generate
  if ($isunbounded(max_val)) begin
    // property that assumes no upper limit
  end else begin
    // property that checks upper limit
  end
endgenerate

Parameterised Quiet-Time Checker

This interface uses $isunbounded to write a parameterised protocol checker that works for both finite and infinite quiet-time windows — without any preprocessor tricks.

interface quiet_time_checker
  #(parameter min_quiet = 0,
    parameter max_quiet = 0)
  (input logic clk, reset_n, [1:0] en);

  generate
    if (max_quiet == 0) begin
      // No quiet window at all: exactly one enable must always be active
      property quiet_time;
        @(posedge clk) reset_n |-> ($countones(en) == 1);
      endproperty
      a1: assert property (quiet_time);

    end else begin
      // Bounded or unbounded quiet window: en transitions to 0, stays quiet
      // for [min_quiet:max_quiet] cycles, then exactly one enable returns
      property quiet_time;
        @(posedge clk)
        (reset_n && ($past(en) != 0) && en == 0)
        |-> (en == 0)[*min_quiet:max_quiet]
            ##1 ($countones(en) == 1);
      endproperty
      a1: assert property (quiet_time);
    end

    // Warn if parameters are inconsistent
    if ((min_quiet == 0) && $isunbounded(max_quiet))
      $display("warning: quiet window is unbounded");
  endgenerate
endinterface

// Three instantiations with completely different behaviour
quiet_time_checker #(0, 0) quiet_never     (clk, 1, enables); // no quiet time allowed
quiet_time_checker #(2, 4) quiet_in_window (clk, 1, enables); // quiet 2–4 cycles
quiet_time_checker #(0, $) quiet_any       (clk, 1, enables); // unbounded quiet OK

Parameterised Width Checker

A second example shows how $isunbounded makes it possible to write a single interface that correctly handles both exact-width and minimum-width specifications without duplicating assertion code.

interface width_checker
  #(parameter min_cks = 1,
    parameter max_cks = 1)
  (input logic clk, reset_n, expr);

  generate begin
    if ($isunbounded(max_cks)) begin
      // Minimum-width only: expr must stay high for AT LEAST min_cks cycles
      property width;
        @(posedge clk)
        (reset_n && $rose(expr)) |-> (expr [* min_cks]);
      endproperty
      a2: assert property (width);

    end else begin
      // Exact-width window: expr must stay high for min_cks to max_cks cycles,
      // then must go low
      property assert_width_p;
        @(posedge clk)
        (reset_n && $rose(expr))
        |-> (expr[* min_cks:max_cks]) ##1 (!expr);
      endproperty
      a2: assert property (assert_width_p);
    end
  end endgenerate
endinterface

width_checker #(3, $) max_width_unspecified (clk, 1, enables); // ≥3 cycles
width_checker #(2, 4) width_specified       (clk, 1, enables); // exactly 2–4 cycles
The pattern: generate + $isunbounded. Anywhere you would use `ifdef to choose between two property implementations, you can instead use generate if ($isunbounded(param)). This is cleaner because it uses the actual parameter value rather than a separate compile-time define, keeping the checker self-contained.

🔁 Dependent Parameter Chains

A parameter’s default value can reference earlier parameters in the same port list. This creates a dependency chain resolved at elaboration. The chain can mix value parameters and type parameters.

// Four-parameter chain:
// M depends on N; x depends on type T
module mc #(int N = 5,
            M     = N*16,   // M = 80 by default (N*16)
            type T = int,    // T defaults to int
            T x   = 0)       // x has type T (= int) and default 0
  (...);
endmodule

// Overriding N changes M automatically (if M not also overridden)
mc #(.N(8)) u1(...);          // N=8, M=128, T=int, x=0

// Override T changes the type of x
mc #(.T(real)) u2(...);       // N=5, M=80, T=real, x=0.0 (real 0)

// Override all four
mc #(.N(4), .M(64), .T(bit[7:0]), .x(8'hAA)) u3(...);
Dependency order matters. A parameter can only reference parameters declared before it in the list. Forward references (a later parameter referencing an earlier one that itself depends on a later one) are not allowed.

📋 Parameters in Classes and Interfaces

Classes and interfaces support the same parameter syntax as modules, including type parameters and dependent chains.

// Parameterised class with a size parameter
class vector #(size = 1);
  logic [size-1:0] v;
endclass

typedef vector #(16) word;   // specialize to 16-bit word

// Parameterised interface with type parameter T defaulting to word
interface simple_bus #(AWIDTH = 64, type T = word) (input bit clk);
  T            data;                      // uses type T (= word by default)
  logic[AWIDTH-1:0] addr;
endinterface

// Default instance: T=word (16-bit), AWIDTH=64
simple_bus bus_dflt(clk);

// Override data to int (32-bit)
simple_bus #(.T(int)) bus_wide(clk);

📋 Quick Reference

Parameter forms

SyntaxWhat it declaresNotes
parameter p = valUntyped value parameterType inferred from value (logic, arbitrary size)
parameter int p = valTyped value parameterType-checked at override; preferred style
parameter type T = intType parameterOverride with any data type
localparam int K = exprNon-overridable constantIn module, generate, package, compilation-unit
parameter $ Unbounded integer parameterUse only where $ is legal (range bounds)

Key rules

  • Modules, interfaces, programs, and classes all support parameters with the same syntax.
  • In packages and compilation-unit scope, parameter is a synonym for localparam — cannot be overridden.
  • localparam is now legal inside generate blocks.
  • In a parameter port list, the parameter keyword on the first entry covers subsequent entries — can be omitted after the first.
  • A parameter can depend on earlier parameters in the same list.
  • $ assigned to a parameter means unbounded; only valid as a range bound.
  • $isunbounded(param) returns true if param === $; use in generate if to select different logic.
  • defparam is deprecated — use named parameter overrides instead.
Section 21 complete. The PDF section closes here. The SV-21 article covers all content from §21.1 and §21.2 as requested.

Leave a Comment

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

Scroll to Top