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.
Learning goals
Section titled “Learning goals”- 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.
Intuition first
Section titled “Intuition first”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.
How wide is a sum?
Section titled “How wide is a sum?”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.
In SystemVerilog
Section titled “In SystemVerilog”// 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;endmoduleBoth 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:
// 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; endendmoduleFor 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.
// 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;endmoduleThe 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.
// 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;endmoduleThe 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.
Chaining two full adders
Section titled “Chaining two full adders”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.
// 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));endmoduleEach 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.
// 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; endendmoduleThe // expect: comments read the module's internal wire as dut.c1, the name c1 inside the instance dut. Here is the run.
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.
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.
Parameters
Section titled “Parameters”An adder of one width is rarely the only one you need. A parameter lets one module describe all of them.
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.
// 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;endmoduleThe 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:
// 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;endmoduleFor 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.
// 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; endendmoduleFor 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:
// 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; endendmoduleThe 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.
Course conventions
Section titled “Course conventions”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.
Pitfalls
Section titled “Pitfalls”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) >> 1with 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.
Check your understanding
Section titled “Check your understanding”Take the quiz (5 questions)
In sad_add, both inputs are 1. What does s come out as?
10, but s has room for only the low bit. A one-bit signal cannot hold 2.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?
cin goes into bit 0 only, and cout leaves bit 1 only.Exercises
Section titled “Exercises”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
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 pattern | unsigned | |
|---|---|---|
| A | 1001 | 9 |
| + B | 0111 | 7 |
| = | 0000 | 0 |
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.
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.
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.
Hint
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:
aandbare00andcinis 0. Nothing is added, soc1is 0,sis00andcoutis 0. - Step 2:
aandbare01. Bit 0 adds 1, 1 and 0, so its sum bit is 0 andc1is 1. Bit 1 adds 0, 0 and the carry, so its sum bit is 1 andcoutis 0.sis10. - Step 3:
ais11,bis00andcinis 1. Bit 0 adds 1, 0 and 1: sum bit 0,c1is 1. Bit 1 adds 1, 0 and 1: sum bit 0,coutis 1.sis00. - Step 4:
aandbare11andcinis 0. Bit 0 adds 1, 1 and 0: sum bit 0,c1is 1. Bit 1 adds 1, 1 and 1: sum bit 1,coutis 1.sis10.
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.
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
{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:
// 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;endmodulehdl/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;
~~~~~ ^ ~~~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 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.
// 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);endmoduleA 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.
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.
// 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;endmoduleHint
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:
// 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);endmoduleThe 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.