Video 2 of 4 · ~9 minutes
Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF
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.
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 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
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));
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.
localparam and $clog2module 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
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.
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
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.
~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.
$ 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)
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.