L17: Verilog Arithmetic

Let’s practice Boolean algebra!

Objective
Practice usage of theorems 5-17 of Boolean algebra.

Additional Materials & Formats
Check Box for the most up-to-date versions of this lecture’s materials.


Before the Lecture

Required Textbook Reading:

  • 3.5 (Design of Arithmetic Circuits Using CAD Tools)

Optional Supplemental Instruction:


Design of Arithmetic Circuits Using Verilog

In the previous section, we saw that an $n$-bit adder can be built by connecting together $n$ full-adders in a ripple-carry chain. Verilog gives us a clean way to describe that same structure: first write a module for the basic building block, then build a higher-level module by instantiating that block repeatedly.

This is an example of hierarchical design. The idea is simple:

  • Design the smallest useful circuit first, such as a full-adder.
  • Put that circuit into its own Verilog module.
  • Use several copies of that module inside a larger module.

For a full-adder, the inputs are $C_{in}$, $x$, and $y$, and the outputs are $s$ and $C_{out}$. Verilog can describe this circuit in more than one way. One option is to use gate-level primitives, where the logic is written directly with AND, OR, and XOR gates. Another option is to use continuous assignments, where the Boolean equations are written as expressions.

module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output s, Cout;

    xor (s, x, y, Cin);
    and (z1, x, y);
    and (z2, x, Cin);
    and (z3, y, Cin);
    or (Cout, z1, z2, z3);
endmodule
module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output s, Cout;

    xor (s, x, y, Cin);
    and (z1, x, y),
        (z2, x, Cin),
        (z3, y, Cin);
    or (Cout, z1, z2, z3);
endmodule
module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output s, Cout;

    assign s = x ^ y ^ Cin;
    assign Cout = (x & y) | (x & Cin) | (y & Cin);
endmodule
module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output s, Cout;

    assign s = x ^ y ^ Cin,
        Cout = (x & y) | (x & Cin) | (y & Cin);
endmodule

All styles describe the same hardware. The choice is mostly a matter of readability and design preference. Gate-level code looks close to a schematic, while assignment-based code looks closer to the Boolean equations for the circuit.

Once the full-adder module is available, we can build a 4-bit ripple-carry adder by instantiating that module four times. Each instance represents one stage of the adder. The least-significant stage takes the external carry-in, and each stage passes its carry-out to the next stage’s carry-in. This is why the design is called a ripple-carry adder: the carry “ripples” from one bit position to the next.

module adder4 (carryin, x3, x2, x1, x0, y3, y2, y1, y0, s3, s2, s1, s0, carryout);
    input carryin, x3, x2, x1, x0, y3, y2, y1, y0;
    output s3, s2, s1, s0, carryout;

    fulladd stage0 (carryin, x0, y0, s0, c1);
    fulladd stage1 (c1, x1, y1, s1, c2);
    fulladd stage2 (c2, x2, y2, s2, c3);
    fulladd stage3 (c3, x3, y3, s3, carryout);
endmodule

module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output s, Cout;

    assign s = x ^ y ^ Cin;
    assign Cout = (x & y) | (x & Cin) | (y & Cin);
endmodule

The port list in each instantiation must match the order of the full-adder module’s ports. In this case, the signals are connected in the order $C_{in}$, $x$, $y$, $s$, $C_{out}$. The instance names, such as stage0 and stage3, must be unique so that each copy of the module can be identified separately.

This modular approach has two major advantages. First, it makes the design easier to understand because the circuit is broken into smaller parts. Second, it makes the code reusable: the same full-adder module can be used in many different adder designs, not just the 4-bit version.

When the Verilog code is synthesized, the resulting circuit has the same structure as the hand-drawn ripple-carry adder. In other words, hierarchical Verilog lets us describe the hardware at a higher level without losing the underlying circuit structure.

Using Vectored Signals

A cleaner approach is to use vectors, which are multibit signals in Verilog. Vectors let us group related wires together and refer to them by bit position.

Declaring a Vector

input [3:0] X;

This declaration makes X a four-bit vector.

  • X[3] is the most-significant bit (MSB)
  • X[0] is the least-significant bit (LSB)
  • X[2:1] refers to the two middle bits
  • X refers to the full vector

Four-Bit Adder with Vectors

Using vectors, the ripple-carry adder can be written more compactly:

module adder4 (carryin, X, Y, S, carryout);
    input carryin;
    input [3:0] X, Y;
    output [3:0] S;
    output carryout;

    wire [3:1] C;

    fulladd stage0 (carryin, X[0], Y[0], S[0], C[1]);
    fulladd stage1 (C[1], X[1], Y[1], S[1], C[2]);
    fulladd stage2 (C[2], X[2], Y[2], S[2], C[3]);
    fulladd stage3 (C[3], X[3], Y[3], S[3], carryout);
endmodule

Carry Connections

The internal carry signals are stored in the vector C[3:1].

  • carryin feeds the first full-adder stage
  • C[1] connects stage 0 to stage 1
  • C[2] connects stage 1 to stage 2
  • C[3] connects stage 2 to stage 3
  • carryout is the final carry from stage 3

Bit Ordering

The bit range is written with the MSB first and the LSB last, as in X[3:0].

  • X[3] is the MSB
  • X[0] is the LSB
  • Reverse ordering is also allowed, such as Z[0:3]

When vectors represent numbers, MSB and LSB describe significance. When vectors represent general signals, the indices simply identify bits.

Using a Generic Specification

Why a generic specification is useful

  • A four-bit ripple-carry adder is fixed in size.
  • A scalable design can be reused for 4-bit, 32-bit, or n-bit adders.
  • Verilog parameters make the module width adjustable.

Parameterized vectors

  • An n-bit vector can be written as X[n−1:0].

  • Example: if parameter n = 4;, then X has range [3:0].

  • The ripple-carry adder can be described by:

    s_k = x_k ⊕ y_k ⊕ c_k

    c_{k+1} = x_k y_k + x_k c_k + y_k c_k

  • These equations can replace repeated full-adder instantiations.

Generic n-bit adder

  • X, Y, and S are n-bit vectors.
  • C is an (n+1)-bit carry vector.
  • C[0] is the carry into the LSB.
  • C[n] is the carry out of the MSB.
  • So C[0] = carryin and carryout = C[n].

Verilog for loops in always blocks

  • The for statement is procedural and belongs inside an always block.
  • Signals assigned inside always must be declared reg.
  • In Figure 3.24, S, C, and carryout are reg signals.
  • The sensitivity list is X, Y, and carryin.

Loop behavior

  • The loop variable k runs from 0 to n−1.
  • Each iteration computes one adder stage.
  • Verilog does not use ++ or --; use k = k + 1 or k = k - 1.
  • k is an integer control variable, not a circuit wire.
  • For k = 2, the loop expands into the corresponding explicit assignments for bit 2.

Example module outline

module addern (carryin, X, Y, S, carryout);
parameter n = 32;
input carryin;
input [n-1:0] X, Y;
output reg [n-1:0] S;
output reg carryout;
reg [n:0] C;
integer k;

always @(X, Y, carryin)
begin
    C[0] = carryin;
    for (k = 0; k < n; k = k + 1)
    begin
        S[k] = X[k] ^ Y[k] ^ C[k];
        C[k+1] = (X[k] & Y[k]) | (X[k] & C[k]) | (Y[k] & C[k]);
    end
    carryout = C[n];
end
endmodule
  • With n = 32, this implements a 32-bit adder.

Using the Generate Capability

Main idea

  • The generate construct can instantiate repeated hardware.
  • It is useful when the design contains many identical subcircuits.
  • A for loop inside generate can create n copies of fulladd.

genvar

  • The loop index in a generate block must be declared genvar.
  • genvar is used only in generate blocks.
  • It is similar to integer, but it only takes positive values.

Generated instances

  • Each generated instance gets a unique name.
  • Example names include addbit[0].stage, addbit[1].stage, …, addbit[n−1].stage.
  • The generated circuit matches the behavior of the procedural version.

Example generate-based adder

module addern (carryin, X, Y, S, carryout);
parameter n = 32;
input carryin;
input [n-1:0] X, Y;
output [n-1:0] S;
output carryout;
wire [n:0] C;
genvar i;

assign C[0] = carryin;
assign carryout = C[n];

generate
    for (i = 0; i <= n-1; i = i+1)
    begin : addbit
        fulladd stage (C[i], X[i], Y[i], S[i], C[i+1]);
    end
endgenerate
endmodule

module fulladd (Cin, x, y, s, Cout);
input Cin, x, y;
output s, Cout;
assign s = x ^ y ^ Cin;
assign Cout = (x & y) | (x & Cin) | (y & Cin);
endmodule

Nets and Variables in Verilog

Modeling in Verilog

  • A logic circuit can be described by interconnected logic elements.
  • It can also be described with procedural statements.
  • Connections between logic elements are called nets.
  • Signals produced by procedural statements are called variables.

Nets

  • A net is a node in a circuit.
  • The main synthesis net type is wire.
  • A wire connects one logic element output to another element input.
  • A wire may be scalar or vector.
  • In Figure 3.22, c3, c2, and c1 are scalar nets.
  • In Figure 3.23, the carries are collected into a vector C.
  • Nets are assumed by default, but vectors must be declared when needed.

Variables

  • Variables describe behavior in procedural code.
  • A variable keeps its value until it is reassigned.
  • Verilog variable types include reg and integer.
  • Signals assigned in procedural statements must be declared as variables.
  • In Figure 3.24, carryout, S, and C are reg variables.
  • The loop variable k is an integer.
  • These variables usually do not map directly to physical circuit wires.

Arithmetic assignment statements

Verilog supports arithmetic assignment on vectors (e.g., S = X + Y;) to represent combinational arithmetic hardware such as n-bit adders. Vectors participating in an arithmetic expression are automatically zero-extended on the left if the result requires more bits.

Example: n-bit adder (behavioral)

  • input [n-1:0] X, Y;
  • output [n-1:0] S;
  • the statement S = X + Y; implements an n-bit adder producing S.

Carry-out and overflow:

  • For unsigned operands, carry-out from the MSB indicates overflow of n bits.
  • For signed operands, arithmetic overflow must be generated (see Section 3.3.5).

Behavioral Verilog example (simple adder):

module addern (carryin, X, Y, S);
    parameter n = 32;
    input carryin;
    input [n-1:0] X, Y;
    output reg [n-1:0] S;

    always @(X, Y, carryin)
        S = X + Y + carryin;
endmodule

Adding carry-out and overflow signals:

  • Carry-out from MSB (bit n-1) can be computed as: carryout = X[n-1] & Y[n-1] | X[n-1] & S[n-1] | Y[n-1] & S[n-1];
  • Overflow (for signed numbers) can be derived from MSB bits; one common form is overflow = (X[n-1] & Y[n-1] & ~S[n-1]) | (~X[n-1] & ~Y[n-1] & S[n-1]);

Behavioral Verilog with carry-out and overflow (illustrative):

module addern (carryin, X, Y, S, carryout, overflow);
    parameter n = 32;
    input carryin;
    input [n-1:0] X, Y;
    output reg [n-1:0] S;
    output reg carryout, overflow;

    always @(X, Y, carryin) begin
        S = X + Y + carryin;
        carryout = (X[n-1] & Y[n-1]) | (X[n-1] & S[n-1]) | (Y[n-1] & S[n-1]);
        overflow = (X[n-1] & Y[n-1] & ~S[n-1]) | (~X[n-1] & ~Y[n-1] & S[n-1]);
    end
endmodule

Alternative: use an (n+1)-bit intermediate to capture carry-out directly. Create reg [n:0] Sum and compute Sum = X + Y + carryin; then S = Sum[n-1:0]; carryout = Sum[n];

Concatenation shortcut:

  • You can avoid an intermediate vector by concatenating carryout to S on the left side of the assignment: {carryout, S} = X + Y + carryin;

Full-adder (behavioral) example:

module fulladd (Cin, x, y, s, Cout);
    input Cin, x, y;
    output reg s, Cout;

    always @(x, y, Cin)
        {Cout, s} = x + y + Cin;
endmodule

Notes:

  • These behavioral forms describe arithmetic intent; synthesis tools infer the appropriate adder implementation for the target technology.