Video 2 of 4 · ~9 minutes
Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF
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.
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 inputs I can change while the chip is running. I set the width to 8, then change it to 16 at runtime.”
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.
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
WIDTH=8 means instantiating without override still works. (2) Derived parameter — MAX_VAL = 2**WIDTH-1 auto-updates. (3) Named override — #(.WIDTH(12)). Never use positional overrides; same reasoning as named ports.
localparam and $clog2module 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
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.
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.
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
~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.
$ 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)
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.
① 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.
🔗 Transfer
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.