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

A 4-bit ALU from the Gates Up

This is the section’s worked example. Primitives, instantiation, port rules and hierarchy all go into one design: the 4-bit ALU of section 2.2, built the way a designer would build it, from the smallest block up, each block tested and then used as a part in the next (Figure 1).

ALU hierarchy
Figure 1: The ALU as a tree of modules

Half adder

$$sum = a \oplus b \qquad c_{out} = ab$$

`timescale 1ns / 1ps
`default_nettype none

module half_adder (
    input  wire a, b,
    output wire sum,
    output wire cout
);
    xor g_xor (sum,  a, b);
    and g_and (cout, a, b);
endmodule

Two gates, four test vectors, and after that it is a black box.

Full adder

Two half adders and an OR: add $a$ and $b$, then add the carry-in to the result.

`timescale 1ns / 1ps
`default_nettype none

module full_adder (
    input  wire a, b, cin,
    output wire sum,
    output wire cout
);
    wire s1;    // a xor b
    wire c1;    // a and b
    wire c2;    // s1 and cin

    half_adder u_ha1 (.a(a),  .b(b),   .sum(s1),  .cout(c1));
    half_adder u_ha2 (.a(s1), .b(cin), .sum(sum), .cout(c2));

    or g_or (cout, c1, c2);
endmodule

The OR is enough because c1 and c2 cannot both be 1: c1 needs $a = b = 1$, which makes s1 0, which makes c2 0. Three bits added can produce at most one carry.

Adder-subtractor

Subtraction is inversion of b plus a carry-in of 1, as in section 2.2. An XOR on each bit of b with the sub control does the conditional inversion:

`timescale 1ns / 1ps
`default_nettype none

module add_sub_4bit (
    input  wire [3:0] a, b,
    input  wire       sub,       // 0 add, 1 subtract
    output wire [3:0] result,
    output wire       cout,
    output wire       overflow   // signed overflow
);
    wire [3:0] b_xored;
    wire       c0, c1, c2, c3, c4;

    assign c0 = sub;             // the +1 of two's complement

    xor g_xor0 (b_xored[0], b[0], sub);
    xor g_xor1 (b_xored[1], b[1], sub);
    xor g_xor2 (b_xored[2], b[2], sub);
    xor g_xor3 (b_xored[3], b[3], sub);

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

    assign cout = c4;

    xor g_ov (overflow, c3, c4);   // carry into the top bit differs from carry out of it
endmodule

Four-bit signed values run from $-8$ to $+7$. $7 + 1$ is 8, which does not fit and wraps to $-8$; the sign of that is the carry into bit 3 disagreeing with the carry out of it, and one XOR catches it.

Logic unit

Four bitwise operations at once. This introduces the gate array: a range after the instance name makes one gate per bit and distributes the vectors across them.

`timescale 1ns / 1ps
`default_nettype none

module logic_unit_4bit (
    input  wire [3:0] a, b,
    output wire [3:0] out_and,
    output wire [3:0] out_or,
    output wire [3:0] out_xor,
    output wire [3:0] out_not
);
    and g_and [3:0] (out_and, a, b);   // four AND gates, bit for bit
    or  g_or  [3:0] (out_or,  a, b);
    xor g_xor [3:0] (out_xor, a, b);
    not g_not [3:0] (out_not, a);
endmodule

and g_and [3:0] (out_and, a, b) is g_and[3] computing out_and[3] = a[3] & b[3] down to g_and[0]. Four lines in place of sixteen, same hardware.

The ALU

The top module instantiates one adder-subtractor for addition, another for subtraction, and the logic unit, and selects among the results:

`timescale 1ns / 1ps
`default_nettype none

module alu_4bit (
    input  wire [3:0] a, b,
    input  wire [2:0] op,
    output reg  [3:0] result,
    output wire       zero,
    output wire       cout_arith,
    output wire       overflow
);
    wire [3:0] add_result, sub_result;
    wire       cout_add, overflow_add;
    wire       cout_sub, overflow_sub;
    wire [3:0] and_result, or_result, xor_result, not_result;

    add_sub_4bit u_adder (
        .a       (a),
        .b       (b),
        .sub     (1'b0),
        .result  (add_result),
        .cout    (cout_add),
        .overflow(overflow_add)
    );

    add_sub_4bit u_subber (
        .a       (a),
        .b       (b),
        .sub     (1'b1),
        .result  (sub_result),
        .cout    (cout_sub),
        .overflow(overflow_sub)
    );

    logic_unit_4bit u_logic (
        .a      (a),
        .b      (b),
        .out_and(and_result),
        .out_or (or_result),
        .out_xor(xor_result),
        .out_not(not_result)
    );

    // the result mux; behavioural inside a structural module, which is normal
    always @(*) begin
        case (op)
            3'b000:  result = add_result;
            3'b001:  result = sub_result;
            3'b010:  result = and_result;
            3'b011:  result = or_result;
            3'b100:  result = xor_result;
            3'b101:  result = not_result;
            default: result = 4'h0;
        endcase
    end

    assign zero       = ~|result;
    assign cout_arith = op[0] ? cout_sub     : cout_add;
    assign overflow   = op[0] ? overflow_sub : overflow_add;

endmodule
op Operation
000 add
001 subtract
010 AND
011 OR
100 XOR
101 NOT a

Both adder-subtractors compute all the time, whatever op is; the mux only chooses which answer to show. You cannot turn a combinational circuit off. That is the “compute everything, select one” structure of every real ALU.

Tracing 5 + 3

a = 0101, b = 0011, op = 000.

In u_adder, sub is 0, so b_xored is 0011 and c0 is 0.

Bit 0: a = 1, b = 1, cin = 0. The first half adder gives s1 = 0, c1 = 1; the second gives sum = 0, c2 = 0; cout = 1. So result[0] = 0, carry 1.

Bit 1: a = 0, b = 1, cin = 1. s1 = 1, c1 = 0; sum = 0, c2 = 1; cout = 1. result[1] = 0, carry 1.

Bit 2: a = 1, b = 0, cin = 1. s1 = 1, c1 = 0; sum = 0, c2 = 1; cout = 1. result[2] = 0, carry 1.

Bit 3: a = 0, b = 0, cin = 1. s1 = 0, c1 = 0; sum = 1, c2 = 0; cout = 0. result[3] = 1, carry out 0.

add_result is 1000, which is 8. zero is 0, cout_arith is 0, and overflow is c3 ^ c4 = 1 ^ 0 = 1, which is right: in signed 4-bit arithmetic $5 + 3 = 8$ is out of range.

Testbench

`timescale 1ns / 1ps
`default_nettype none

module tb_alu_4bit;
    reg  [3:0] a, b;
    reg  [2:0] op;
    wire [3:0] result;
    wire       zero, cout_arith, overflow;

    alu_4bit u_dut (
        .a          (a),
        .b          (b),
        .op         (op),
        .result     (result),
        .zero       (zero),
        .cout_arith (cout_arith),
        .overflow   (overflow)
    );

    initial begin
        $dumpfile("alu_waves.vcd");
        $dumpvars(0, tb_alu_4bit);
    end

    initial begin
        a = 4'd5;  b = 4'd3; op = 3'b000; #10;
        $display("ADD: %0d + %0d = %0d  (zero=%b, cout=%b, ov=%b)", a, b, result, zero, cout_arith, overflow);

        a = 4'd15; b = 4'd1; op = 3'b000; #10;
        $display("ADD: %0d + %0d = %0d  (zero=%b, cout=%b, ov=%b)", a, b, result, zero, cout_arith, overflow);

        a = 4'd5;  b = 4'd3; op = 3'b001; #10;
        $display("SUB: %0d - %0d = %0d  (zero=%b, cout=%b, ov=%b)", a, b, result, zero, cout_arith, overflow);

        a = 4'd4;  b = 4'd4; op = 3'b001; #10;
        $display("SUB: %0d - %0d = %0d  (zero=%b, cout=%b, ov=%b)", a, b, result, zero, cout_arith, overflow);

        a = 4'hA;  b = 4'hC; op = 3'b010; #10;
        $display("AND: 0x%h & 0x%h = 0x%h  (zero=%b)", a, b, result, zero);

        a = 4'hA;  b = 4'hC; op = 3'b011; #10;
        $display("OR:  0x%h | 0x%h = 0x%h  (zero=%b)", a, b, result, zero);

        a = 4'hA;  b = 4'hC; op = 3'b100; #10;
        $display("XOR: 0x%h ^ 0x%h = 0x%h  (zero=%b)", a, b, result, zero);

        a = 4'hA;  b = 4'h0; op = 3'b101; #10;
        $display("NOT: ~0x%h = 0x%h  (zero=%b)", a, result, zero);

        #5;
        $display("carry c2 inside u_adder: %b", tb_alu_4bit.u_dut.u_adder.c2);

        $finish;
    end
endmodule
iverilog -o alu_sim half_adder.v full_adder.v add_sub_4bit.v logic_unit_4bit.v alu_4bit.v tb_alu_4bit.v
vvp alu_sim
gtkwave alu_waves.vcd
ADD: 5 + 3 = 8    (zero=0, cout=0, ov=1)
ADD: 15 + 1 = 0   (zero=1, cout=1, ov=0)
SUB: 5 - 3 = 2    (zero=0, cout=1, ov=0)
SUB: 4 - 4 = 0    (zero=1, cout=1, ov=0)
AND: 0xa & 0xc = 0x8  (zero=0)
OR:  0xa | 0xc = 0xe  (zero=0)
XOR: 0xa ^ 0xc = 0x6  (zero=0)
NOT: ~0xa = 0x5   (zero=0)

Look at the two additions. $5 + 3$ sets the overflow flag and not the carry: as unsigned numbers the sum is fine, as signed numbers it is not. $15 + 1$ sets the carry and not the overflow: as unsigned it wrapped, as signed it is $-1 + 1 = 0$, which is correct. The two flags answer two different questions, as section 2.2 said. And the subtractions give cout = 1, meaning no borrow, which is the carry convention for subtraction.

The whole design is five modules, five levels deep, about sixty gates, and the top module has no gate-level logic of its own. A synthesis netlist for a small processor looks just like this, only with a few hundred thousand instances.

Practice Exercise

Add NAND (op = 110) and NOR (op = 111) to the ALU.

Solution

In the logic unit:

    output wire [3:0] out_nand,
    output wire [3:0] out_nor
    ...
    nand g_nand [3:0] (out_nand, a, b);
    nor  g_nor  [3:0] (out_nor,  a, b);

In the ALU, two more wires, two more connections on u_logic, and two more arms:

            3'b110:  result = nand_result;
            3'b111:  result = nor_result;

That is structural modelling. The next section describes the same circuits without naming a single gate: assign sum = a + b; and the synthesiser builds the adder you have just built by hand.

โ† Hierarchy and Generate Continuous Assignments โ†’