Module 4: Introduction to Verilog HDL ยท 4.2 Verilog Language Basics

Parameters

You write an 8-bit adder and it works. Then you need a 16-bit one, then 32, then a colleague wants 12 bits for an audio block. Each time you copy the file, rename the module and edit every [7:0]. Three months later you find a bug in the carry logic, fix it in one copy, and forget the other three. Parameters exist so that this never happens.

The problem

`timescale 1ns / 1ps
`default_nettype none

module adder_8bit (          // the width is in the name, which is already a bad sign
    input  wire [7:0] a,
    input  wire [7:0] b,
    input  wire       cin,
    output wire [7:0] sum,
    output wire       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

Nothing in the body depends on the width being 8. The copies differ only in a number.

Declaring a parameter

A parameter is a constant with a default, declared in #(...) between the module name and the port list, and usable anywhere a constant is, including port ranges:

`timescale 1ns / 1ps
`default_nettype none

module adder #(
    parameter WIDTH = 8
)(
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);

    assign {cout, sum} = a + b + cin;

endmodule

Overriding it

At instantiation, name the parameter and give it a value:

`timescale 1ns / 1ps
`default_nettype none

module top_level (
    input  wire [15:0] data_a,
    input  wire [15:0] data_b,
    input  wire        carry_in,
    output wire [15:0] result,
    output wire        carry_out
);

    adder #(
        .WIDTH(16)
    ) u_adder16 (
        .a   (data_a),
        .b   (data_b),
        .cin (carry_in),
        .sum (result),
        .cout(carry_out)
    );

endmodule

Use the named form, #(.WIDTH(16)), never positional. The same source can serve several widths in one design:

    adder u_byte_adder (            // default WIDTH = 8
        .a   (byte_a),
        .b   (byte_b),
        .cin (1'b0),
        .sum (byte_sum),
        .cout(byte_cout)
    );

    adder #(.WIDTH(32)) u_word_adder (
        .a   (word_a),
        .b   (word_b),
        .cin (1'b0),
        .sum (word_sum),
        .cout(word_cout)
    );

Parameterise every module from the start, even the ones you think will only ever have one width. It costs two lines now and a lot of editing later.

localparam

A localparam is a constant that cannot be overridden from outside. It is for things that are implementation details: state encodings, derived values, magic numbers.

`timescale 1ns / 1ps
`default_nettype none

module simple_fsm #(
    parameter NUM_STATES = 4
)(
    input  wire clk,
    input  wire rst_n,
    input  wire start,
    output reg  done
);

    localparam STATE_BITS = $clog2(NUM_STATES);   // derived, not overridable

    localparam IDLE = 2'd0;   // encodings are internal
    localparam RUN  = 2'd1;
    localparam WAIT = 2'd2;
    localparam DONE = 2'd3;

    reg [STATE_BITS-1:0] state, next_state;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= IDLE;
        else
            state <= next_state;
    end

    always @(*) begin
        next_state = state;
        done       = 1'b0;
        case (state)
            IDLE: if (start) next_state = RUN;
            RUN:             next_state = WAIT;
            WAIT:            next_state = DONE;
            DONE: begin
                done       = 1'b1;
                next_state = IDLE;
            end
            default: next_state = IDLE;
        endcase
    end

endmodule

If the encodings were parameters, an instantiation could set IDLE to the same value as RUN and break the machine without touching the file.

$clog2

$clog2(N) is the ceiling of $\log_2 N$: the number of bits needed to count N things. It is what turns a depth into an address width.

N $\lceil \log_2 N \rceil$
2 1
4 2
8 3
256 8
1000 10
1024 10

Here it sizes the pointers of a FIFO:

`timescale 1ns / 1ps
`default_nettype none

module sync_fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16
)(
    input  wire                  clk,
    input  wire                  rst_n,
    input  wire                  wr_en,
    input  wire                  rd_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    output reg  [DATA_WIDTH-1:0] rd_data,
    output wire                  full,
    output wire                  empty
);

    localparam ADDR_BITS = $clog2(DEPTH);   // 4 for a depth of 16

    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];

    // one extra bit so that full and empty can be told apart
    reg [ADDR_BITS:0] wr_ptr;
    reg [ADDR_BITS:0] rd_ptr;

    assign full  = (wr_ptr[ADDR_BITS] != rd_ptr[ADDR_BITS]) &&
                   (wr_ptr[ADDR_BITS-1:0] == rd_ptr[ADDR_BITS-1:0]);
    assign empty = (wr_ptr == rd_ptr);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr <= {(ADDR_BITS+1){1'b0}};
        end else if (wr_en && !full) begin
            mem[wr_ptr[ADDR_BITS-1:0]] <= wr_data;
            wr_ptr <= wr_ptr + 1'b1;
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rd_ptr  <= {(ADDR_BITS+1){1'b0}};
            rd_data <= {DATA_WIDTH{1'b0}};
        end else if (rd_en && !empty) begin
            rd_data <= mem[rd_ptr[ADDR_BITS-1:0]];
            rd_ptr  <= rd_ptr + 1'b1;
        end
    end

endmodule

Change the depth from 16 to 1024 and the pointers grow from 4 bits to 10, the memory scales, and the full and empty logic is still right. The full and empty test with the extra pointer bit assumes the depth is a power of two; for other depths the wrap has to be explicit.

One trap: $clog2(1) is 0. If a depth of 1 is legal, guard it: localparam ADDR_BITS = (DEPTH > 1) ? $clog2(DEPTH) : 1;.

Parameter types

module param_types #(
    parameter integer WIDTH  = 8,          // the usual case
    parameter         ENABLE = 1'b1,       // a flag
    parameter [3:0]   MASK   = 4'b1010,    // a sized vector
    parameter string  NAME   = "default"   // simulation only
)(/* ports */);
endmodule

For synthesis, stick to integers and sized vectors. Real and string parameters are for simulation.

defparam

Old code overrides parameters from outside the instantiation:

defparam u_adder.WIDTH = 16;   // legacy; do not write this

It separates the override from the instance, tools dislike it, and SystemVerilog dropped it. Always override at the instantiation.

A parameterised shifter

`timescale 1ns / 1ps
`default_nettype none

module barrel_shifter #(
    parameter DATA_WIDTH = 8
)(
    input  wire [DATA_WIDTH-1:0]         data_in,
    input  wire [$clog2(DATA_WIDTH)-1:0] shift_amt,   // 3 bits for 8, 5 bits for 32
    input  wire                          left_right,  // 1 left, 0 right
    output wire [DATA_WIDTH-1:0]         data_out
);

    assign data_out = left_right ? (data_in << shift_amt)
                                 : (data_in >> shift_amt);

endmodule

The shift-amount port sizes itself from the data width. That pattern, one parameter deriving the width of another port, is everywhere in parameterised arithmetic.

Practice Exercise

Write a synchronous up-counter with a MAX_COUNT parameter that counts 0 to MAX_COUNT-1 and wraps, with the output width derived by $clog2.

Solution
`timescale 1ns / 1ps
`default_nettype none

module up_counter #(
    parameter MAX_COUNT = 100
)(
    input  wire                         clk,
    input  wire                         rst_n,
    input  wire                         en,
    output reg  [$clog2(MAX_COUNT)-1:0] count   // 7 bits for 100
);

    always @(posedge clk) begin
        if (!rst_n) begin
            count <= {$clog2(MAX_COUNT){1'b0}};
        end else if (en) begin
            if (count == MAX_COUNT - 1)
                count <= {$clog2(MAX_COUNT){1'b0}};
            else
                count <= count + 1'b1;
        end
    end

endmodule

A seconds counter:

up_counter #(.MAX_COUNT(60)) u_seconds (
    .clk  (clk),
    .rst_n(rst_n),
    .en   (tick_1hz),
    .count(seconds)     // 6 bits, since $clog2(60) = 6
);

Parameters give a module its shape. The next article is about the constants inside it: how to write a number so that Verilog knows exactly how wide you mean it to be.

โ† Vectors and Arrays Number Literals โ†’