L15: Adders
Faster ways to do addition.
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.4 (Fast Adders)
Optional Supplemental Instruction:
- x
Verilog Examples
The example code is for reference only. We’ll cover Verilog implementations in more detail in L17.
Recall Basic Adder Building Blocks
To add two $N$-bit binary numbers, we must first understand how to add single bits.
The Half Adder (HA)
A Half Adder takes two 1-bit inputs ($A$ and $B$) and produces two 1-bit outputs: a Sum ($S$) and a Carry-out ($C$). It does not account for a carry-in from a previous stage.
| $A$ | $B$ | $C$ | $S$ |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 0 |
Boolean Equations:
$$S = A \oplus B$$
$$C = AB$$
Verilog Implementation:
module half_adder (
input wire A,
input wire B,
output wire S,
output wire C
);
assign S = A ^ B;
assign C = A & B;
endmoduleThe Full Adder (FA)
A Full Adder takes three 1-bit inputs ($A$, $B$, and $C_{in}$) and produces a Sum ($S$) and a Carry-out ($C_{out}$). This allows for cascading multiple stages.
| $A$ | $B$ | $C_{in}$ | $C_{out}$ | $S$ |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 0 | 0 | 1 |
| 1 | 0 | 0 | 0 | 1 |
| 1 | 1 | 0 | 1 | 0 |
| 0 | 0 | 1 | 0 | 1 |
| 0 | 1 | 1 | 1 | 0 |
| 1 | 0 | 1 | 1 | 0 |
| 1 | 1 | 1 | 1 | 1 |
Boolean Equations:
$$S = A \oplus B \oplus C_{in}$$
$$C_{out} = AB + C_{in}(A \oplus B) = AB + AC_{in} + BC_{in}$$
Verilog Implementation:
module full_adder (
input wire A,
input wire B,
input wire C_in,
output wire S,
output wire C_out
);
assign S = A ^ B ^ C_in;
assign C_out = (A & B) | (C_in & (A ^ B));
endmoduleLinear-Time Adder: Ripple Carry Adder (RCA)
To add two $N$-bit numbers, we can chain $N$ Full Adders together by connecting the $C_{out}$ of bit stage $i$ directly to the $C_{in}$ of bit stage $i+1$.
Critical Path Analysis
- The Problem: The carry signal must “ripple” all the way from the least significant bit ($LSB$) to the most significant bit ($MSB$).
- Delay: If each Full Adder takes $t_{carry}$ to produce its carry output, the total carry propagation delay for an $N$-bit adder is:
$$t_{RCA} \approx N \cdot t_{carry}$$
- Time Complexity: $\mathcal{O}(N)$. This is painfully slow for 32-bit or 64-bit architectures!
Verilog Implementation (Parameterized RCA):
module ripple_carry_adder #(
parameter N = 8
) (
input wire [N-1:0] A,
input wire [N-1:0] B,
input wire C_in,
output wire [N-1:0] S,
output wire C_out
);
wire [N:0] C;
assign C[0] = C_in;
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : FA_STAGE
full_adder fa (
.A (A[i]),
.B (B[i]),
.C_in (C[i]),
.S (S[i]),
.C_out(C[i+1])
);
end
endgenerate
assign C_out = C[N];
endmoduleAdvanced Designs for Fast Adders
To break the linear delay bottleneck, we must compute carries faster. Here are the prominent architectures used to achieve sub-linear time complexity.
A. Carry Lookahead Adder (CLA)
Instead of waiting for the carry to ripple through every stage, the CLA precomputes the carry signals using two intermediate concepts:
- Generate ($G_i$): Bit stage $i$ will definitely generate a carry-out if both inputs are 1, regardless of the carry-in.
$$G_i = A_i B_i$$
- Propagate ($P_i$): Bit stage $i$ will propagate an incoming carry to the next stage if at least one input is 1.
$$P_i = A_i \oplus B_i \quad \text{(or } A_i + B_i\text{)}$$
Using these, the carry-out for any stage can be written without waiting for sequential evaluation:
$$C_1 = G_0 + P_0 C_0$$
$$C_2 = G_1 + P_1 C_1 = G_1 + P_1 G_0 + P_1 P_0 C_0$$
$$C_3 = G_2 + P_2 G_2 = G_2 + P_2 G_1 + P_2 P_1 G_0 + P_2 P_1 P_0 C_0$$
Trade-off: It reduces execution time drastically ($\mathcal{O}(\log N)$), but requires massive amounts of irregular wiring and large-input logic gates as $N$ grows. Practically, CLAs are usually grouped into 4-bit blocks.
Verilog Implementation:
module cla_4bit (
input wire [3:0] A,
input wire [3:0] B,
input wire C_in,
output wire [3:0] S,
output wire C_out
);
wire [3:0] G, P;
wire [4:0] C;
assign G = A & B;
assign P = A ^ B;
assign C[0] = C_in;
assign C[1] = G[0] | (P[0] & C[0]);
assign C[2] = G[1] | (P[1] & G[0]) | (P[1] & P[0] & C[0]);
assign C[3] = G[2] | (P[2] & G[1]) | (P[2] & P[1] & G[0]) | (P[2] & P[1] & P[0] & C[0]);
assign C[4] = G[3] | (P[3] & G[2]) | (P[3] & P[2] & G[1]) | (P[3] & P[2] & P[1] & G[0]) | (P[3] & P[2] & P[1] & P[0] & C[0]);
assign S = P ^ C[3:0];
assign C_out = C[4];
endmoduleB. Carry-Select Adder
The Carry-Select Adder avoids waiting for the true carry-in by anticipating both possibilities.
- How it works: It duplicates the hardware for the upper bits of an adder block. One sub-adder calculates the sum assuming the incoming carry is
0, while the other sub-adder calculates the sum assuming the incoming carry is1. - When the real carry-in finally arrives from the lower bits, a multiplexer (MUX) instantly selects the correct precomputed sum.
- Time Complexity: $\mathcal{O}(\sqrt{N})$ when block sizes are optimized.
- Trade-off: High area overhead (nearly double the hardware cost for the duplicated sections).
Verilog Implementation (8-bit, 4+4 split):
module carry_select_adder_8bit (
input wire [7:0] A,
input wire [7:0] B,
input wire C_in,
output wire [7:0] S,
output wire C_out
);
// Lower 4 bits (true carry path)
wire [3:0] s_lo;
wire c4;
cla_4bit lo (
.A(A[3:0]), .B(B[3:0]), .C_in(C_in),
.S(s_lo), .C_out(c4)
);
// Upper 4 bits precomputed for Cin=0 and Cin=1
wire [3:0] s_hi0, s_hi1;
wire c8_0, c8_1;
cla_4bit hi0 (
.A(A[7:4]), .B(B[7:4]), .C_in(1'b0),
.S(s_hi0), .C_out(c8_0)
);
cla_4bit hi1 (
.A(A[7:4]), .B(B[7:4]), .C_in(1'b1),
.S(s_hi1), .C_out(c8_1)
);
assign S[3:0] = s_lo;
assign S[7:4] = c4 ? s_hi1 : s_hi0;
assign C_out = c4 ? c8_1 : c8_0;
endmoduleC. Carry-Skip (Bypass) Adder
Instead of precomputing every possible sum like the Carry-Select Adder, the Carry-Skip Adder looks for opportunities to fast-forward the carry signal across groups of bits.
- How it works: Bits are divided into blocks. For a 4-bit block, a block-level propagate signal is calculated:
$$P_{block} = P_3 P_2 P_1 P_0$$
- If $P_{block} = 1$, it means a carry entering this block will definitely exit it. A MUX uses $P_{block}$ to skip the carry completely past the 4-bit ripple chain directly to the next block.
- Time Complexity: $\mathcal{O}(\sqrt{N})$.
- Trade-off: Highly efficient alternative that costs far less area than a Carry-Select adder while still beating raw RCA speeds.
Verilog Implementation (8-bit, 4-bit skip blocks):
module carry_skip_adder_8bit (
input wire [7:0] A,
input wire [7:0] B,
input wire C_in,
output wire [7:0] S,
output wire C_out
);
// Block 0 ripple (bits 3:0)
wire [4:0] c0;
wire [3:0] p0;
assign c0[0] = C_in;
assign p0 = A[3:0] ^ B[3:0];
genvar i;
generate
for (i = 0; i < 4; i = i + 1) begin : B0
assign S[i] = p0[i] ^ c0[i];
assign c0[i+1] = (A[i] & B[i]) | (p0[i] & c0[i]);
end
endgenerate
// Skip logic after block 0
wire p_block0;
wire c4_ripple, c4_skip;
assign p_block0 = &p0;
assign c4_ripple = c0[4];
assign c4_skip = p_block0 ? C_in : c4_ripple;
// Block 1 ripple (bits 7:4)
wire [4:0] c1;
wire [3:0] p1;
assign c1[0] = c4_skip;
assign p1 = A[7:4] ^ B[7:4];
generate
for (i = 0; i < 4; i = i + 1) begin : B1
assign S[i+4] = p1[i] ^ c1[i];
assign c1[i+1] = (A[i+4] & B[i+4]) | (p1[i] & c1[i]);
end
endgenerate
assign C_out = c1[4];
endmoduleSummary Matrix of Adder Performance
| Adder Architecture | Delay Complexity | Area Overhead | Best Suited For… |
|---|---|---|---|
| Ripple Carry (RCA) | $\mathcal{O}(N)$ | Minimal ($\mathcal{O}(N)$) | Low-power, low-speed, narrow bit-widths |
| Carry-Skip | $\mathcal{O}(\sqrt{N})$ | Low-Moderate | Budget-constrained intermediate systems |
| Carry-Select | $\mathcal{O}(\sqrt{N})$ | High | High-speed paths where area isn’t a bottleneck |
| Carry Lookahead (CLA) | $\mathcal{O}(\log N)$ | Heavy ($\mathcal{O}(N \log N)$) | High-performance ALU architectures |