Day 8 · Hierarchy & Reuse

Parameters & Parameterization

Video 2 of 4 · ~9 minutes

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

HierarchyParametersGenerateReuse

🌍 Where This Lives

In Industry

Every reusable IP block is parameterized. Xilinx's MicroBlaze CPU has ~40 parameters (cache size, multiplier presence, FPU presence). ARM's AMBA bus IP is parameterized in data width, burst length, ID width. Cadence, Synopsys, and ARM sell IP that's essentially just Verilog modules with parameters — the value is the generality. A well-parameterized module becomes a product.

In This Course

Your counter, shift register, debouncer, and UART modules all become parameterized today. Your Day 11 UART will support 9600/115200 baud via one parameter. Your capstone wins judges by showing parameterization — it's what distinguishes “built a thing” from “built a reusable tool.”

⚠️ 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 value
    parameter MAX_VAL  = 2**WIDTH-1  // derived from another parameter
) (
    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));
//       ^^^^^^^^^^^^  overrides default WIDTH=8 → WIDTH=12
My thinking: Three idioms. (1) Default valueWIDTH=8 means instantiating without override still works. (2) Derived parameterMAX_VAL = 2**WIDTH-1 auto-updates. (3) Named override#(.WIDTH(12)). Never use positional overrides; same reasoning as named ports.

🤝 We Do — localparam and $clog2

module debounce #(
    parameter CLKS_STABLE = 500_000   // user-configurable
) (
    input  wire i_clk, i_reset, i_noisy,
    output reg  o_clean
);
    // localparam = internal-only constant, derived from parameter
    // Caller CANNOT override it
    localparam COUNT_WIDTH = $clog2(CLKS_STABLE);

    reg [COUNT_WIDTH-1:0] r_count;
    // ... counter logic ...
endmodule
Together: parameter exposes a knob to the user. localparam is for internal derivations — “this constant is computed from the parameter, don't touch it.” $clog2 computes the ceiling of log2 at compile time. Combined: users set CLKS_STABLE, the width auto-sizes, no manual calculation ever.

🧪 You Do — Design a Parameterized FIFO

Sketch the parameter declaration for a FIFO module. You need depth (how many entries) and width (bits per entry) as user-settable. Internal derived constants: address width, flags width.

Sketch:
module fifo #(
    parameter DEPTH = 16,         // user: how many entries
    parameter WIDTH = 8           // user: bits per entry
) (
    // ... ports ...
);
    localparam ADDR_W = $clog2(DEPTH);
    localparam MAX_CNT = DEPTH;

    reg [WIDTH-1:0]  memory [0:DEPTH-1];
    reg [ADDR_W-1:0] r_wptr, r_rptr;
    // ... logic ...
endmodule
▶ LIVE DEMO

One Module, Three Instances, Three Sizes

~5 minutes

▸ COMMANDS

cd labs/week2_day08/ex2_params/
cat top_with_three_counters.v
# 4-bit, 8-bit, 16-bit counters
# from one counter.v file
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
=== 18 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 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.