Skip to content

4.2 Adders and parameters in SystemVerilog

Why this matters

In SystemVerilog an addition is one character, and that character is where students lose carries. The language decides how wide a sum is from its surroundings, and a sum that is too narrow keeps only its low bits without any warning. This section shows exactly when the carry survives, builds a full adder and a two-bit adder from it, and introduces parameters, which let one module describe an adder of any width.

  • Predict the width at which an addition is carried out from the operands and the left side of the assignment.
  • Keep the carry of a sum by putting a concatenation on the left side.
  • Write a full adder and connect two of them into a two-bit adder, using named ports.
  • Declare a parameter, give it a default, and set it when a module is instantiated.
  • Spot a sum that quietly drops its carry, including one hidden inside a shift.

Think of a pocket calculator with a four-digit display. Add 9999 and 1 and the display shows 0000: the calculator did the sum correctly and then kept the four digits it has room for. A signal in a circuit is that display. It has a fixed number of wires, and a result that needs more is cut to fit without any complaint.

If you are used to a language with fixed-size integers, the same thing happens in C when you store the sum of two uint32_t values in a uint32_t. The analogy breaks in one important way. In C the operand types alone fix how the sum is computed: storing the sum of two uint32_t values in a uint64_t still loses the carry. (Types narrower than int, such as uint8_t, are first widened to int, which hides the problem.) In SystemVerilog the width of the variable on the left is part of the context, and it changes how wide the addition is. That is why a carry can be saved by what you write on the left of the equals sign.

Definition 4.2.1(context-determined width).

In SystemVerilog the operands of an addition are context-determined. The addition is carried out at the width of the widest operand or of the left side of the assignment it belongs to, whichever is wider, and every narrower operand is first extended to that width, with zeros for unsigned operands such as logic vectors. The result is then stored in the left side, and any bits that do not fit are discarded.

The context includes the left side. So the same expression a + b is a different sum in different places, and two lines show it.

Example 4.2.1(a one-bit adder that loses its carry).
sad_add.sv
// Adds two one-bit numbers into a one-bit result. The carry has no signal to go to.
module sad_add(
input logic a,
input logic b,
output logic s
);
assign s = a + b;
endmodule

Both operands are one bit and the target s is one bit, so the addition is carried out in one bit. The true sum of 1 and 1 is two, which is 10 in binary, and only the low bit is kept. A testbench tries all four input pairs:

sad_add_tb.sv
// Tries all four input pairs on the one-bit adder that has no carry output.
module sad_add_tb();
logic a, b, s;
sad_add dut(.*);
initial begin
a = 0; b = 0; #10; // expect: s=0
a = 0; b = 1; #10; // expect: s=1
a = 1; b = 0; #10; // expect: s=1
a = 1; b = 1; #10; // expect: s=0
$display("DD one_plus_one=%0d", s);
$stop;
end
endmodule

For a and b both 1, the simulation gives s = 0. Nothing is computed wrongly; the carry has no signal to go to. This module is a legal description of a one-bit adder with no carry output, and that is why no tool reports a problem. The book's tools accept this file without a warning, and the same will be true of every module in this section that drops a carry. Finding the lost carry is the designer's job.

A concatenation on the left side changes the context, because it makes the left side wider.

Example 4.2.2(the half adder with the carry kept).
half_add.sv
// A half adder: c and s together hold the two-bit sum of a and b.
module half_add(
input logic a,
input logic b,
output logic c,
output logic s
);
assign {c, s} = a + b;
endmodule

The left side {c, s} is two bits wide, so the addition is carried out in two bits: each operand is extended to two bits and the sum of 1 and 1 is 10. The concatenation then splits it, the top bit going to c and the low bit to s. A program applied all four input pairs to this module and compared c and s with the half-adder table of Section 4.1, and every row agreed.

The same idea gives the full adder of Section 4.1 in one line. With three one-bit operands the largest possible sum is three, which fits in two bits, so a two-bit left side is enough.

fulladd.sv
// A full adder: cout and s together hold the two-bit sum of a, b and the carry-in bit cin.
module fulladd(
input logic a,
input logic b,
input logic cin,
output logic s,
output logic cout
);
assign {cout, s} = cin + a + b;
endmodule

The module is checked against the eight rows of the full-adder table in the same way. Its ports use the names a, b and cin for inputs and s and cout for outputs, as the table does.

A two-bit adder uses one full adder for each bit. The carry out of bit 0 is a wire inside the module, here called c1, and it goes to the carry in of bit 1.

add2.sv
// A two-bit adder made of two full adders. The carry out of bit 0 is the signal c1, and it is the carry in of bit 1.
module add2(
input logic [1:0] a,
input logic [1:0] b,
input logic cin,
output logic [1:0] s,
output logic cout
);
logic c1;
fulladd bit0(.a(a[0]), .b(b[0]), .cin(cin), .s(s[0]), .cout(c1));
fulladd bit1(.a(a[1]), .b(b[1]), .cin(c1), .s(s[1]), .cout(cout));
endmodule

Each fulladd line is an instance: bit0 is one full adder and bit1 is another. The ports are connected by name, .a(a[0]) meaning that the instance's port a is wired to bit 0 of the module's a. The module's cin goes into bit 0, c1 joins the two, and bit 1 gives the module's cout.

Ports can also be connected by position, listing signals in the order of the instance's port list. Lecture 4 writes the two fulladd instances of its two-bit adder that way, with the outputs first: cout, s, cin, a, b. (fact ch04/positional-ports) The hazard is that the order is defined in another file. A change to the port list of fulladd silently re-wires every instance that is connected by position, and if two ports have the same width the compiler cannot notice. Named connections do not depend on the order, so this book uses them.

A testbench for the two-bit adder tries four additions. In the last three, the carry out of bit 0 is 1.

add2_tb.sv
// Four additions on the two-bit adder. In the last three the carry out of bit 0 is 1, and it ripples into bit 1.
module add2_tb();
logic [1:0] a, b, s;
logic cin, cout;
add2 dut(.*);
initial begin
a = 2'b00; b = 2'b00; cin = 0; #10; // expect: s=2'b00, cout=0, dut.c1=0
a = 2'b01; b = 2'b01; cin = 0; #10; // expect: s=2'b10, cout=0, dut.c1=1
a = 2'b11; b = 2'b00; cin = 1; #10; // expect: s=2'b00, cout=1, dut.c1=1
a = 2'b11; b = 2'b11; cin = 0; #10; // expect: s=2'b10, cout=1, dut.c1=1
$stop;
end
endmodule

The // expect: comments read the module's internal wire as dut.c1, the name c1 inside the instance dut. Here is the run.

Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout020time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout020time (ps)

Time in ps; one column is 10 ps.

Timing diagram from simulation, ch04/add2-trace: time in ps from 0 to 40, 4 columns of 10 ps.

a (2 bits, bin): 00 at 0, then 01 at 10, 11 at 20.

b (2 bits, bin): 00 at 0, then 01 at 10, 00 at 20, 11 at 30.

cin: 0 at 0, then 1 at 20, 0 at 30.

dut.c1: 0 at 0, then 1 at 10.

s (2 bits, bin): 00 at 0, then 10 at 10, 00 at 20, 10 at 30.

cout: 0 at 0, then 1 at 20.

Description: A timing diagram of a, b, cin, the internal signal dut.c1, s and cout over four steps. In the first step a and b are 00, c1 is 0, s is 00 and cout is 0. In the second step a and b are 01, c1 rises to 1 and s is 10. In the third step a is 11, b is 00 and cin is 1, c1 stays 1, s is 00 and cout rises to 1. In the fourth step a and b are 11 and cin is 0, c1 is 1, s is 10 and cout is 1.

Figure 4.2.1. Four additions on the two-bit adder. a and b are shown in binary; c1 is the carry from bit 0 into bit 1.

Read Figure 4.2.1 one step at a time. In the second step both inputs are 01, so bit 0 adds 1 and 1, writes 0 and carries: c1 rises to 1 and s becomes 10. In the third step a is 11, b is 00 and cin is 1. Bit 0 adds 1, 0 and the carry in and carries, so c1 stays at 1. Bit 1 adds 1, 0 and that carry and carries out, so cout rises to 1 and s is 00. This is a carry that travels through the whole adder: it was made by the carry in of bit 0, passed through bit 1, and ended up as cout. In the fourth step a and b are both 11, bit 0 adds 1 and 1, bit 1 adds 1, 1 and the carry, s is 10 and cout is 1.

The simulation has no gate delays, so every signal in the figure changes at the same instant as the inputs. The ripple is an order of dependence, not a delay you can see here. A separate program applied all 32 combinations of a, b and cin and compared {cout, s} with the whole-number sum of the three.

An adder of one width is rarely the only one you need. A parameter lets one module describe all of them.

Definition 4.2.2(parameter).

A parameter is a constant of a module whose value can be chosen where the module is instantiated. #(parameter N = 4) in the module header declares a parameter N with the default value 4, and the module can then use N in its declarations, such as logic [N-1:0]. An instance sets it with #(.N(8)) before its name. A parameter is fixed before the simulation starts and never changes while the circuit runs.

add_n.sv
// An adder whose width is a parameter. The sum is N bits and the carry out of the top bit is a separate output.
module add_n #(parameter N = 4) (
input logic [N-1:0] a,
input logic [N-1:0] b,
output logic [N-1:0] sum,
output logic carry
);
assign {carry, sum} = a + b;
endmodule

The module is the half adder of this section with the operands and the sum widened to N bits. The sum is N bits and the carry is a separate one-bit output, and the left side {carry, sum} is N + 1 bits wide, so the addition is carried out in N + 1 bits. Compare it with a module that stores the sum of two 4-bit numbers in a 4-bit target:

add4_lossy.sv
// Four-bit numbers added into a four-bit result: the carry out of bit 3 is lost.
module add4_lossy(
input logic [3:0] a,
input logic [3:0] b,
output logic [3:0] s
);
assign s = a + b;
endmodule

For 15 and 1 the lossy module gives 0 and no carry. A program applied all 256 pairs of 4-bit inputs and found that its output is always the true sum modulo 16, so it is the true sum exactly when that sum is below 16. The testbench of add_n at its default width of 4 repeats the same additions.

add_n_tb.sv
// The adder at a width of 4 bits: 15 + 1 now gives a sum of 0 and a carry of 1.
module add_n_tb();
logic [3:0] a, b, sum;
logic carry;
add_n #(.N(4)) dut(.*);
initial begin
a = 4'd3; b = 4'd4; #10; // expect: sum=4'd7, carry=0
a = 4'd15; b = 4'd1; #10; // expect: sum=4'd0, carry=1
$display("DD sum=%0d", sum);
$display("DD carry=%0d", carry);
a = 4'd9; b = 4'd9; #10; // expect: sum=4'd2, carry=1
$stop;
end
endmodule

For 15 and 1, add_n gives a sum of 0 and a carry of 1: the same low bits as the lossy module, plus the carry it threw away. All 256 pairs were also compared with the whole-number sum of the two inputs, in 5 bits.

The width is chosen where the module is used. This testbench sets N to 8 for its one instance, and the buses in the testbench are 8 bits to match:

add_n_wide_tb.sv
// The same module with N set to 8: the carry stays 0 until the sum passes 255.
module add_n_wide_tb();
logic [7:0] a, b, sum;
logic carry;
add_n #(.N(8)) dut(.*);
initial begin
a = 8'd15; b = 8'd1; #10; // expect: sum=8'd16, carry=0
a = 8'd200; b = 8'd100; #10; // expect: sum=8'd44, carry=1
$stop;
end
endmodule

The first line adds 15 and 1 and the carry stays 0, because the sum fits in 8 bits. The second adds 200 and 100, which does not fit, and the carry is 1. Each instance has its own value of N, so one design can use an 8-bit adder in one place and a 4-bit adder in another. In the rest of this chapter, wider adders are written with +, as add_n is.

How CSE 369 writes this

In CSE 369 code, a parameter is declared in the module header as #(parameter N=...) and set at an instance with #(.N(4)), as above. The course writes a bus width as [N-1:0], and names bits of a number from 0 at the right. The book's own code connects ports by name, and declares each signal with logic.

The book also has a convention of its own for its figures: the time axis of a simulated waveform counts the simulator's own time units, and the figures label it ps. Those numbers are arbitrary: a label of ps does not describe a real clock that fast, or any real hardware speed. This note is made once for the chapter.

Common pitfall

  • Storing a sum in a target as wide as its operands and expecting the carry. The sum is cut to the target's width, and nothing warns you. Put {carry, sum} on the left, or a wider target.
  • Shifting or dividing a sum that may carry. In (a + b) >> 1 with an 8-bit target, the addition is still carried out in 8 bits, so the carry is gone before the shift. Make the operands wider first.
  • Connecting ports by position. It is correct only while the port order never changes, and a swap of two same-width ports compiles without complaint.
  • Treating a parameter as a signal. It is a constant set when the module is instantiated. It cannot be changed while the circuit runs, and a module with a parameter needs the right width on every bus that connects to it.
  • Trusting a tool to find a dropped carry. The tools in this book accept every lossy module in this section, because each one is a legal description of something.
Take the quiz (5 questions)

In sad_add, both inputs are 1. What does s come out as?

In half_add, the left side of the assignment is {c, s}. In how many bits is the addition a + b carried out?

add4_lossy is given 15 and 1. What value does s hold?

A module has #(parameter N = 4). Which instantiation gives an 8-bit adder?

In add2, the carry in of bit 1 is connected to which signal?

Exercise 4.2.1

The module add4_lossy is given a = 1001 and b = 0111. What does s hold, and what is the true sum? Is the carry lost?

Hint
Add the two 4-bit patterns as unsigned numbers and see whether the result needs a fifth bit.
Solution

The addition is carried out in 4 bits, because the operands and the target are 4 bits wide, and the target keeps the low 4 bits of the result.

4-bit A + B: A is 1001, B is 0111; result 0000, 0 unsigned, 0 signed; carry 1
4-bit patternunsigned
A10019
+ B01117
=00000

carry = 1

The result row is what s holds. The carry out is 1, which means the true sum needs a fifth bit, and a 4-bit target has no place for it. So the carry is lost, and s is 16 less than the true sum.

Tags: Warm-upArithmeticSystemVerilog

Exercise 4.2.2

The two-bit adder of this section is driven by the four steps of the figure. For each step, work out the internal carry c1, the sum s and the carry out cout.

Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)cout020time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)cout020time (ps)

Time in ps; one column is 10 ps.

Timing diagram from simulation, ch04/add2-trace: time in ps from 0 to 40, 4 columns of 10 ps.

a (2 bits, bin): 00 at 0, then 01 at 10, 11 at 20.

b (2 bits, bin): 00 at 0, then 01 at 10, 00 at 20, 11 at 30.

cin: 0 at 0, then 1 at 20, 0 at 30.

Empty lanes for the reader to complete: dut.c1, s, cout.

Description: A timing diagram of a, b and cin over four steps with empty lanes for dut.c1, s and cout.

Figure 4.2.2. The question: only the inputs. The lanes for c1, s and cout are empty.
Hint
In each step, bit 0 adds a[0], b[0] and cin. Its carry is c1. Bit 1 adds a[1], b[1] and c1, and its carry is cout.
Solution

Take the steps one at a time, bit 0 first.

  • Step 1: a and b are 00 and cin is 0. Nothing is added, so c1 is 0, s is 00 and cout is 0.
  • Step 2: a and b are 01. Bit 0 adds 1, 1 and 0, so its sum bit is 0 and c1 is 1. Bit 1 adds 0, 0 and the carry, so its sum bit is 1 and cout is 0. s is 10.
  • Step 3: a is 11, b is 00 and cin is 1. Bit 0 adds 1, 0 and 1: sum bit 0, c1 is 1. Bit 1 adds 1, 0 and 1: sum bit 0, cout is 1. s is 00.
  • Step 4: a and b are 11 and cin is 0. Bit 0 adds 1, 1 and 0: sum bit 0, c1 is 1. Bit 1 adds 1, 1 and 1: sum bit 1, cout is 1. s is 10.
Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout0102030time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout020time (ps)Waveform ch04/add2-trace: a, b, cin, dut.c1, s, couta (bin)000111b (bin)00010011cindut.c1s (bin)00100010cout020time (ps)

Time in ps; one column is 10 ps.

Timing diagram from simulation, ch04/add2-trace: time in ps from 0 to 40, 4 columns of 10 ps.

a (2 bits, bin): 00 at 0, then 01 at 10, 11 at 20.

b (2 bits, bin): 00 at 0, then 01 at 10, 00 at 20, 11 at 30.

cin: 0 at 0, then 1 at 20, 0 at 30.

dut.c1: 0 at 0, then 1 at 10.

s (2 bits, bin): 00 at 0, then 10 at 10, 00 at 20, 10 at 30.

cout: 0 at 0, then 1 at 20.

Description: The full timing diagram of a, b, cin, dut.c1, s and cout over the four steps.

Figure 4.2.3. The answer: every signal, from the simulation.

Tags: StandardArithmeticWaveforms

Exercise 4.2.3

Add a carry in to the parameterized adder. The new module add_cin has the inputs a and b of N bits and a one-bit cin, and outputs sum of N bits and a one-bit carry. It should compute the sum of the three inputs.

Hint
The left side is {carry, sum}. The one-bit cin is added to two N-bit buses, and the tools in this book look closely at an addition that mixes a bus with a single bit.
Solution

The structure is the same as add_n, with cin as a third operand. If the line is written with cin added directly, one of the book's tools objects to the mixed widths of the operands:

add_cin_bad.sv
// The same adder with the one-bit cin added directly to two buses.
module add_cin_bad #(parameter N = 4) (
input logic [N-1:0] a,
input logic [N-1:0] b,
input logic cin,
output logic [N-1:0] sum,
output logic carry
);
assign {carry, sum} = a + b + cin;
endmodule
Compiles, but is rejected. This file compiles and slang only prints a warning (arith-op-mismatch). The book treats a warning as a failure, so the checks reject it; the warning is shown below the listing.
hdl/ch04/s02/ex/add_cin_bad.sv:9:31: warning: arithmetic between operands of different types ('logic[3:0]' and 'logic') [-Warith-op-mismatch]
  assign {carry, sum} = a + b + cin;
                        ~~~~~ ^ ~~~
Output of slang for add_cin_bad

This is a warning, not an error: the file compiles and simulates, and the book's checks treat every warning as a failure. The message says that the arithmetic is between operands of different types. The sum would still come out right, since cin is extended to the context width, but the warning is fair: a reader may not see that a one-bit value is being added to buses. The clear form states the widening, with a size cast N'(cin), which makes cin an N-bit number whose low bit is cin. The cast does not change the width of the addition, which the left side {carry, sum} already sets to N+1N+1 bits, and cin was going to be extended with zeros to that width anyway, so the sum is the same bit for bit. The cast only shows the widening to the reader and the tools. It is the book's choice: Section 4.4 shows the lecture's adder/subtractor, which adds a one-bit value the same way without a cast.

add_cin.sv
// The parameterized adder with a carry in. The one-bit cin is widened to N bits before it is added.
module add_cin #(parameter N = 4) (
input logic [N-1:0] a,
input logic [N-1:0] b,
input logic cin,
output logic [N-1:0] sum,
output logic carry
);
assign {carry, sum} = a + b + N'(cin);
endmodule

A program applied all 512 combinations of a, b and cin at width 4 and compared {carry, sum} with the sum of the three as whole numbers, and every one agreed.

Tags: StandardArithmeticSystemVerilog

Exercise 4.2.4

A module average_lossy computes the average of two 8-bit numbers, rounded down, as (a + b) >> 1 with an 8-bit result. It works for 100 and 50 but gives the wrong answer for 200 and 100. Find the bug and write a correct module average.

average_lossy.sv
// The average of two 8-bit numbers, written as the sum shifted right by one place.
module average_lossy(
input logic [7:0] a,
input logic [7:0] b,
output logic [7:0] avg
);
assign avg = (a + b) >> 1;
endmodule
Hint
Ask how many bits the addition is carried out in, when the result and both operands are 8 bits. The shift comes after the addition.
Solution

The context of the addition is the 8-bit result, so the addition is carried out in 8 bits. The sum of 200 and 100 is 300, which needs 9 bits, and the carry is dropped before the shift ever sees it. A simulation of the lossy module gives an average of 22 for 200 and 100, where the true average is 150.

The sum has to be formed in 9 bits. Putting a zero bit in front of each operand makes each of them 9 bits wide, which makes the addition 9 bits wide, and the shift then moves the carry into the top of the 8-bit answer:

average.sv
// The average of two 8-bit numbers, rounded down. The sum is formed in 9 bits, so its carry is kept before the shift.
module average(
input logic [7:0] a,
input logic [7:0] b,
output logic [7:0] avg
);
assign avg = 8'(({1'b0, a} + {1'b0, b}) >> 1);
endmodule

The cast 8'(…) keeps the low 8 bits of the shifted 9-bit sum. After the shift the top bit is 0, so nothing is lost: the 9-bit sum is already formed inside the cast, and the cast changes none of its bits, only the width of what is assigned to the 8-bit avg. It says in the code that cutting 9 bits to 8 is meant, which the tools would otherwise report. This is the book's way of saying it; the lecture's adder code, shown in Section 4.4, has no size cast.

The corrected module gives 150 for the same inputs. A program compared both modules with 256 pairs that span the whole range of 8-bit values. The lossy module matched the sum modulo 256 shifted right, and the corrected one matched the true sum shifted right.

Tags: ChallengeArithmeticSystemVerilog