Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
    • SystemVerilog Data Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
      • Packages & Imports
      • Compilation Units & Compile Order
      • typedef, parameter & localparam
      • `include vs import
      • Namespace Hygiene at Scale
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹Packages, Typedefs & Compilation
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. Packages, Typedefs & Compilation
  5. typedef, parameter & localparam

Part 1 · Language Foundations · Intermediate

typedef, parameter & localparam

Typedef discipline, parameter versus localparam, and widths flowing from one package.

On this page

    Why this matters

    A width changed from 32 to 40 bits, and one corner of the bench kept a hard-coded 32. Nothing failed to compile; the top bits were silently cut off.

    After this lesson you can: name types with typedef, choose parameter or localparam, derive values safely, and size a module from one package.

    Names for types

    A typedef binds a name to a type so the meaning is written once and the structure follows it. Change addr_t from 32 to 40 bits in one package line and every port, variable and queue using it follows.

    The name also carries intent. addr_t tells a reader what a value is for, while logic [31:0] tells them only how wide it is. The discipline that follows: every type that crosses a file boundary gets a typedef, and the typedef lives in a package.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package mem_pkg;
      parameter int ADDR_W = 32;
      parameter int DATA_W = 64;
    
      typedef logic [ADDR_W-1:0] addr_t;
      typedef logic [DATA_W-1:0] data_t;
    
      typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } resp_e;
    
      typedef struct packed {
        addr_t addr;
        data_t data;
        resp_e resp;
      } mem_txn_t;
    endpackage
    
    // A width change edits ADDR_W once. Every addr_t in the
    // DUT ports and the testbench follows it. Hardcoding
    // 32 in one corner instead would truncate silently at 40.

    Contracts against derivations

    A parameter on a module header is part of its public contract: instantiations may override it with hash-parenthesis binding. A localparam is internal and cannot be overridden, which is where derived values belong.

    Inside a package the distinction collapses, because package parameters can never be overridden. The convention still records intent: tunable-in-spirit values stay parameters, computed values become localparams. Never use defparam for overrides; it is deprecated action at a distance.

    The fifo in the next section uses both: DEPTH and DATA_W are public parameters, and PTR_W is a localparam computed from DEPTH with $clog2, so no instantiation can make the two disagree.

    Predict first The fifo is instantiated with DEPTH 64. What is PTR_W, and how many bits wide are wr_ptr and rd_ptr?

    PTR_W is 6, because 2 to the 6th is 64. The pointers are 7 bits: [PTR_W:0] adds one extra bit, which is how the fifo tells full from empty.

    One source of truth, proved in simulation

    Widths defined once in a package flow into the interface, the DUT and the checker. A mismatch becomes impossible rather than merely unlikely, because nothing is written down twice.

    The testbench below proves it three ways. All-ones fills exactly the package widths, a register with package-typed ports carries an address, and a 64-deep fifo override fills, holds and drains in order.

    fifo.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    package mem_pkg;
      parameter int ADDR_W = 32;
      parameter int DATA_W = 64;
      typedef logic [ADDR_W-1:0] addr_t;
      typedef logic [DATA_W-1:0] data_t;
    endpackage
    
    // DEPTH is a power of two of at least 2, so PTR_W is at least 1.
    module fifo #(
      parameter int DEPTH  = 16,   // public: the user may override
      parameter int DATA_W = 32
    )(
      input  logic              clk,
      input  logic              rst_n,
      input  logic              wr_en,
      input  logic              rd_en,
      input  logic [DATA_W-1:0] wdata,
      output logic [DATA_W-1:0] rdata,
      output logic              full,
      output logic              empty
    );
      localparam int PTR_W = $clog2(DEPTH);   // derived: tracks DEPTH, cannot be overridden
      logic [DATA_W-1:0] mem [DEPTH];
      logic [PTR_W:0] wr_ptr, rd_ptr;          // extra bit tells full from empty
      assign empty = (wr_ptr == rd_ptr);
      assign full  = (wr_ptr[PTR_W] != rd_ptr[PTR_W]) &&
                     (wr_ptr[PTR_W-1:0] == rd_ptr[PTR_W-1:0]);
      assign rdata = mem[rd_ptr[PTR_W-1:0]];
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          wr_ptr <= '0;
          rd_ptr <= '0;
        end else begin
          if (wr_en && !full) begin
            mem[wr_ptr[PTR_W-1:0]] <= wdata;
            wr_ptr <= wr_ptr + 1;
          end
          if (rd_en && !empty) rd_ptr <= rd_ptr + 1;
        end
      end
    endmodule
    
    module addr_reg (
      input  logic            clk,
      input  mem_pkg::addr_t  din,
      output mem_pkg::addr_t  qout
    );
      always_ff @(posedge clk) qout <= din;
    endmodule
    
    Terminal
    PASS: one package sets widths and a 64-deep fifo fills, holds and drains in order

    Generic over types, sparingly

    Parameters can also carry types: parameter type T lets one body work with any payload the instantiator supplies. The sketch below shows the shape.

    compare_m.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: one body, any payload type, bound at elaboration
    module compare_m #(parameter type TXN_T = logic [31:0]) (
      input  TXN_T  expected,
      input  TXN_T  actual,
      output logic  match
    );
      assign match = (expected == actual);
    endmodule
    
    // instantiation picks the transaction type:
    // compare_m #(.TXN_T(mem_txn_t)) u_cmp (.expected(e), .actual(a), .match(m));

    Common mistakes

    Common pitfall

    Hardcoding a width in one corner of the bench while the package parameter moves on. The truncation is silent. Every width with two users belongs in the package.

    Interview

    Likely questions: “parameter or localparam”, “what is a type parameter”, and “how do you keep bench and DUT widths in sync”.

    • Parameter or localparam: overridable public contract against internal derived constant.

    • Type parameter: parameter type T makes a body generic over a type at elaboration.

    • Widths in sync: one package parameter flows into every port, never copied literals.

    Check yourself PTR_W is declared as a parameter and an instantiation overrides it to 3 while DEPTH stays 64. What breaks?

    The pointers no longer track the depth, so full and empty misbehave. A derived value must be a localparam so no override can desynchronize it.

    Check yourself Why does the fifo require DEPTH to be a power of two?

    The extra-bit full and empty comparison only wraps correctly when the pointer space is exactly twice the depth. Other depths need a different flag scheme.

    Project step

    PacketFIFO RTL v0 is this fifo with DEPTH 4 and DATA_W 9: bit 8 is last and bits 7 to 0 are the byte. Instantiate it, write the Part 0 packet, check that it is full after four bytes, and read the bytes back in order.

    One way to write it; add it after the fifo module above:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // snippet: add after the fifo module in the listing above
    module tb_pkt_fifo_v0;
      logic clk = 0;
      always #5 clk = ~clk;
      logic       rst_n = 0, wr_en = 0, rd_en = 0, full, empty;
      logic [8:0] wdata, rdata;                         // {last, byte}
      logic [8:0] pkt [0:3];
      int errors = 0;
      fifo #(.DEPTH(4), .DATA_W(9)) dut (.*);           // PacketFIFO RTL v0
    
      initial begin
        pkt[0] = 9'h022; pkt[1] = 9'h0A5; pkt[2] = 9'h03C; pkt[3] = 9'h1BB;
        @(negedge clk) rst_n = 1;
        foreach (pkt[i]) begin
          @(negedge clk);
          wr_en = 1;
          wdata = pkt[i];
        end
        @(negedge clk) wr_en = 0;
        if (!full) errors++;                            // four entries: full
        foreach (pkt[i]) begin
          if (rdata !== pkt[i]) errors++;
          $display("read %h  last=%b", rdata[7:0], rdata[8]);
          rd_en = 1;
          @(negedge clk);
        end
        rd_en = 0;
        if (!empty) errors++;
        if (errors == 0) $display("PASS: fifo #(.DEPTH(4), .DATA_W(9)) carried the packet in order, full after 4, empty after");
        else $fatal(1, "FAIL: %0d PacketFIFO v0 checks failed", errors);
        $finish;
      end
    endmodule

    Remember

    • A typedef names a type once so every user follows a change.

    • parameter is a public, overridable setting; localparam is derived and fixed.

    • Every width with two users comes from one package.

    Rule of thumb: write each number once; derive the rest.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.

    The PacketFIFO v0 step was run with the fifo module above it on both simulators and printed the PASS line fifo #(.DEPTH(4), .DATA_W(9)) carried the packet in order, full after 4, empty after.

    Related topics

    • Next: `include vs import

    Previous

    32 / 363

    Next: `include vs import

    Up next

    Up next`include vs import

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Packages & Imports
    • Namespace Hygiene at Scale