Topic 8 · Hierarchy & Reuse

Parameters & Parameterization

Video 2 of 4 · ~9 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

HierarchyParametersGenerateReuse

🌍 Where This Lives

Where it shows up

An ARM Cortex CPU isn't one chip — it's a generator. Same source tree, you can license it with 16KB of cache or 64KB, with two cores or eight, with or without a floating-point unit, at any of half a dozen bus widths. The processor in your phone, the processor in a smart thermostat, and the processor in a Mars rover are siblings: same Verilog, different numbers tweaked at the top of one file.

When it goes wrong

The Boeing 787 had a bug where leaving the flight computer powered for 248 days froze the flight controls — a width assumption the original engineer thought would never matter. Y2K. The 2038 Unix timestamp. Mars Climate Orbiter's units. Every “we'll never go that big / that small / that fast” assumption that gets welded into code at compile time eventually meets the customer who breaks it.

⚠️ Parameters Are Compile-Time, Not Runtime

❌ Wrong Model

“Parameters are inputs I can change while the chip is running. I set the width to 8, then change it to 16 at runtime.”

✓ Right Model

Parameters are compile-time constants. Each instance of a module fixes its parameter values when it's instantiated, and the synthesis tool builds that specific instance with those specific widths. Different instances of the same module can have different parameter values — but within a single instance, the value is soldered in.

The receipt: A parameterized 8-bit counter instance and a parameterized 16-bit counter instance produce different hardware. They happen to share source code.

👁️ I Do — Parameterized Counter

module counter #(
    parameter WIDTH    = 8,           // default
    parameter MAX_VAL  = 2**WIDTH-1  // derived
) (
    input  wire                i_clk, i_reset,
    output reg  [WIDTH-1:0]  o_count
);
    always @(posedge i_clk) begin
        if      (i_reset)              o_count <= 0;
        else if (o_count==MAX_VAL) o_count <= 0;
        else                          o_count <= o_count+1'b1;
    end
endmodule

// Instantiation with override
counter #(.WIDTH(12)) u_ticker (
    .i_clk(clk), .i_reset(rst),
    .o_count(tick));
RTL block diagram of a parameterized counter. A WIDTH-bit register (blue) feeds a +1 adder, a comparator checks o_count==MAX_VAL (orange) and steers a MUX to wrap to 0; on match the comparator drives the MUX select. Signals are colored: i_clk blue, i_reset red, o_count purple, WIDTH and MAX_VAL annotated in gold.
Three idioms: (1) Default WIDTH=8 — instantiation without override still works. (2) Derived MAX_VAL=2**WIDTH-1 auto-updates. (3) Named override #(.WIDTH(12)). The register, adder, and comparator widths all scale with the gold parameter — that's why the diagram uses one box for any size.

🤝 We Do — localparam and $clog2

module debounce #(
    parameter CLKS_STABLE = 500_000   // user knob
) (
    input  wire i_clk, i_reset, i_noisy,
    output reg  o_clean
);
    // localparam = internal, NOT overridable
    localparam COUNT_WIDTH =
                  $clog2(CLKS_STABLE);

    reg [COUNT_WIDTH-1:0] r_count;
    // counter increments while
    // i_noisy matches r_clean;
    // latches new o_clean at threshold.
endmodule
RTL block diagram of parameterized debounce. Top-right pill shows parameter CLKS_STABLE and derived localparam COUNT_WIDTH=$clog2(CLKS_STABLE). The body shows an equality compare between i_noisy (orange) and r_clean (green, feedback), feeding a COUNT_WIDTH-sized r_count register (blue/purple). A threshold comparator (orange) detects r_count == CLKS_STABLE-1 and gates the r_clean register to latch the new value. Signals colored: i_clk blue, i_reset red, i_noisy orange, o_clean green, r_count purple.
Together: parameter exposes a knob. localparam is internal — derived, not overridable. $clog2 picks the smallest width that fits. Users set CLKS_STABLE; COUNT_WIDTH and the r_count register auto-size to match.

🧪 You Do — Design a Parameterized FIFO

Hard-coded 16-deep × 8-wide FIFO. Find every number that would need to change for a different size — then parameterize.

module fifo (
    input  wire        i_clk, i_reset,
    input  wire        i_wr, i_rd,
    input  wire [7:0]  i_wdata,    // 8-bit
    output reg  [7:0]  o_rdata,    // 8-bit
    output wire        o_full, o_empty
);
    reg [7:0] memory [0:15];      // 16 × 8
    reg [3:0] r_wptr, r_rptr;   // 4-bit
    reg [4:0] r_count;            // 0..16 = 5 bits
    // ... read/write/full/empty logic ...
endmodule
RTL block diagram of FIFO. Center: memory[0..DEPTH-1] with DEPTH rows × WIDTH bits (blue). Left: r_wptr write pointer (pink), r_rptr read pointer (brown), r_count occupancy counter (purple). Right: o_rdata (cyan), full/empty comparator (orange) producing o_full and o_empty (gold). i_wdata (indigo) enters memory; i_wr (pink), i_rd (brown), i_clk (blue), i_reset (red) on the left. The parameter pill shows DEPTH/WIDTH and the derived ADDR_W and COUNT_W localparams.
Reveal — parameterized version:
module fifo #(parameter DEPTH=16, parameter WIDTH=8) (
    input  wire              i_clk, i_reset, i_wr, i_rd,
    input  wire [WIDTH-1:0]  i_wdata,
    output reg  [WIDTH-1:0]  o_rdata,
    output wire              o_full, o_empty);
    localparam ADDR_W  = $clog2(DEPTH);     // pointer width
    localparam COUNT_W = $clog2(DEPTH+1);   // count needs DEPTH+1 values
    reg [WIDTH-1:0]   memory [0:DEPTH-1];
    reg [ADDR_W-1:0]  r_wptr, r_rptr;
    reg [COUNT_W-1:0] r_count;
    // ... same logic, size-agnostic ...
endmodule
Same logic, same RTL picture — only the gold annotations change. Works for 8×8, 1024×64, or anything else.
▶ LIVE DEMO

One Module, Three Instances, Three Sizes

~5 minutes

▸ COMMANDS

cd lecture_examples/week2_day08/d08_s2_ex2/
cat top_with_three_counters.v
# 4-bit, 8-bit, 16-bit counters
# from one day08_ex02_counter.v
make sim
make stat

▸ EXPECTED STDOUT

=== top ===
  u_cnt_4bit:  4 DFF, 3 CARRY
  u_cnt_8bit:  8 DFF, 7 CARRY
  u_cnt_16b:  16 DFF, 15 CARRY
  Total cells: ~40
=== 15 passed, 0 failed ===

▸ KEY OBSERVATION

One counter.v file produced three different hardware blocks, each correctly sized. If you needed a 32-bit version, you'd add one more line — not a new file.

🔧 Each Instance Is Its Own Hardware

$ yosys -p "read_verilog top_with_three_counters.v day08_ex02_counter.v; \
           hierarchy -top top; synth_ice40; stat"

=== counter ===
   multiple instances — each synthesized separately
     (instance u_cnt_4bit : WIDTH=4  → 4 DFF + 3 CARRY)
     (instance u_cnt_8bit : WIDTH=8  → 8 DFF + 7 CARRY)
     (instance u_cnt_16b  : WIDTH=16 → 16 DFF + 15 CARRY)

=== top ===
   Number of cells:   40   (sum of all three instances)
What to notice: Parameters make each instance a distinct circuit. The synthesis tool instantiates the module separately for each unique parameter set. This is why parameterization is “free” — no runtime overhead, no multiplexing, just the right-sized hardware everywhere.

🤖 Check the Machine

Ask AI: “Convert this hard-coded 8-bit counter to a parameterized counter with WIDTH and MAX_VAL parameters. Use $clog2 where appropriate.”

TASK

AI parameterizes a fixed-width module.

BEFORE

Predict: parameter block, WIDTH in port widths, $clog2 for derived.

AFTER

Strong AI uses default values + localparam for derived. Weak AI only changes port widths.

TAKEAWAY

Full parameterization = every hardcoded width replaced by parameter arithmetic.

Key Takeaways

 Parameters are compile-time constants, resolved per-instance.

 Always provide default values. Always use named overrides.

localparam for derived constants; $clog2 for auto-sizing.

 A well-parameterized module becomes a reusable IP block.

Every hardcoded number in your RTL is a parameter waiting to happen.

🔗 Transfer

Generate Blocks

Video 3 of 4 · ~9 minutes

▸ WHY THIS MATTERS NEXT

Parameters change one instance. What if you need N instances — like 8 identical debouncers for 8 buttons? Video 3 introduces generate blocks: compile-time replication of hardware. One block of code, N physical instances, scaled by a parameter. This is how processor lane counts, cache ways, and bus widths work.