Module 4: Introduction to Verilog HDL ยท 4.4 Structural Modeling

Hierarchy and Generate

Nobody writes a processor as a flat list of gates. A design is a tree: a top module instantiates a few large blocks, each of those instantiates smaller ones, and at the bottom are gates. Each level can be tested alone and each hides everything below it. This article is about doing that on purpose, and about the generate construct that lets one description stand for any number of instances.

Why

A 16-bit ALU as one flat module is some hundreds of gate instantiations, tested as a whole, with every bug traced through the whole thing. As a hierarchy it is a half adder of two gates, a full adder of two half adders and an OR, a 4-bit adder of four full adders, and a 16-bit adder of four of those. Each piece is small enough to see at once and to test alone, and a bug in the full adder is fixed in one place.

The 4-bit adder

`timescale 1ns / 1ps
`default_nettype none

module rca_4bit (
    input  wire [3:0] a,
    input  wire [3:0] b,
    input  wire       cin,
    output wire [3:0] sum,
    output wire       cout
);
    wire c1, c2, c3;

    full_adder u_fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c1));
    full_adder u_fa1 (.a(a[1]), .b(b[1]), .cin(c1),  .sum(sum[1]), .cout(c2));
    full_adder u_fa2 (.a(a[2]), .b(b[2]), .cin(c2),  .sum(sum[2]), .cout(c3));
    full_adder u_fa3 (.a(a[3]), .b(b[3]), .cin(c3),  .sum(sum[3]), .cout(cout));

endmodule

That is the schematic of section 2.1 as text: each box is an instance, each wire between boxes is a named wire, and the module boundary is the port list.

Generate

Four instantiations is fine. Sixty-four is not. A generate loop makes the instances at elaboration, before simulation starts:

`timescale 1ns / 1ps
`default_nettype none

module rca #(
    parameter WIDTH = 4
)(
    input  wire [WIDTH-1:0] a,
    input  wire [WIDTH-1:0] b,
    input  wire             cin,
    output wire [WIDTH-1:0] sum,
    output wire             cout
);
    wire [WIDTH:0] carries;          // carries[0] is cin, carries[WIDTH] is cout

    assign carries[0] = cin;
    assign cout       = carries[WIDTH];

    genvar i;
    generate
        for (i = 0; i < WIDTH; i = i + 1) begin : gen_fa
            full_adder u_fa (
                .a   (a[i]),
                .b   (b[i]),
                .cin (carries[i]),
                .sum (sum[i]),
                .cout(carries[i+1])
            );
        end
    endgenerate

endmodule

genvar i is a loop variable that exists only while the tool is building the design; it is not a signal and cannot be used in an always block or an assign. The loop is not something that runs; it is unrolled once, producing one copy of its body per value of i. The block label gen_fa is required when the body instantiates modules, and it gives each copy a name: gen_fa[0].u_fa, gen_fa[1].u_fa, and so on. After elaboration, rca #(.WIDTH(4)) is the same hardware as the four-instance version; only the source is shorter.

generate also takes if and case, for choosing between structures by parameter:

generate
    if (FAST_ADDER) begin : gen_cla
        cla_adder #(.WIDTH(WIDTH)) u_adder (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
    end else begin : gen_rca
        rca #(.WIDTH(WIDTH)) u_adder (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
    end
endgenerate

Hierarchical names

Every instance has a path from the top of the design, instance names joined with dots, and a testbench can use that path to look at any signal at any depth:

`timescale 1ns / 1ps
`default_nettype none

module tb_rca;
    reg  [3:0] a, b;
    reg        cin;
    wire [3:0] sum;
    wire       cout;

    rca #(.WIDTH(4)) u_dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

    initial begin
        $dumpfile("rca_waves.vcd");
        $dumpvars(0, tb_rca);

        a = 4'd5; b = 4'd3; cin = 1'b0; #20;

        $display("carry into bit 2 = %b", tb_rca.u_dut.carries[2]);
        $display("a input of bit-2 adder = %b", tb_rca.u_dut.gen_fa[2].u_fa.a);

        $finish;
    end
endmodule

This is for testbenches and debugging only. A synthesisable module must never refer to a signal by hierarchical path: the tools reject it, and a module that knows the name of its parent is wrong in principle.

Habits

One job per module: an adder, a UART receiver, a FIFO controller. Keep modules under a few hundred lines; a longer one is usually two. Instance names that say what the instance is for, u_tx_fifo rather than u1. And keep the top level thin: the module that connects to the pins should instantiate the big blocks and wire them, with no always blocks and no logic of its own, because logic in the top level cannot be tested at module level.

module chip_top (
    input  wire       clk, rst_n,
    input  wire       rx_pin,
    output wire       tx_pin,
    output wire [7:0] led
);
    wire [7:0] rx_data;
    wire       rx_valid;

    uart_rx  u_rx  (.clk(clk), .rst_n(rst_n), .rx(rx_pin), .data(rx_data), .valid(rx_valid));
    led_ctrl u_led (.clk(clk), .rst_n(rst_n), .data(rx_data), .valid(rx_valid), .leds(led));
    uart_tx  u_tx  (.clk(clk), .rst_n(rst_n), .data(rx_data), .start(rx_valid), .tx(tx_pin));

endmodule

A production SoC is hundreds or thousands of modules, ten or more levels deep, and it is manageable for exactly the reasons the 4-bit adder is.

Practice Exercise

Write rca_16bit as a wrapper around rca #(.WIDTH(16)), then a 32-bit adder from two of them with the carry chained.

Solution
`timescale 1ns / 1ps
`default_nettype none

module rca_16bit (
    input  wire [15:0] a, b,
    input  wire        cin,
    output wire [15:0] sum,
    output wire        cout
);
    rca #(.WIDTH(16)) u_rca16 (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
endmodule
`timescale 1ns / 1ps
`default_nettype none

module rca_32bit (
    input  wire [31:0] a, b,
    input  wire        cin,
    output wire [31:0] sum,
    output wire        cout
);
    wire mid_carry;

    rca_16bit u_low  (.a(a[15:0]),  .b(b[15:0]),  .cin(cin),       .sum(sum[15:0]),  .cout(mid_carry));
    rca_16bit u_high (.a(a[31:16]), .b(b[31:16]), .cin(mid_carry), .sum(sum[31:16]), .cout(cout));
endmodule

mid_carry is the whole point: the carry out of the low half is the carry into the high half.

The last article of this section puts the whole of 4.4 to work: a 4-bit ALU built from gates upward, with a testbench.

โ† Port Connections in Detail A 4-bit ALU from the Gates Up โ†’