Skip to content

RTL Design Patterns · Chapter 5 · Arithmetic & Datapath Patterns

Adders & Subtractors

The adder is the arithmetic core of every datapath, and subtraction is not a separate machine. In two's-complement, a minus b equals a plus not-b plus 1, so one add/subtract structure with a single mode bit does both operations. This lesson builds that structure as a ripple-carry chain, shows how a carry-in lets you chain adders for multi-word math, and explains why the sum width grows by one bit so nothing is lost. It then targets the classic silent bug: carry-out and signed overflow answer different questions, unsigned versus signed, and using one where you meant the other corrupts a saturate or a branch. Every pattern is built and self-check-verified in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsAdderSubtractorTwo's ComplementCarry & OverflowDatapath

Chapter 5 · Section 5.1 · Arithmetic & Datapath Patterns

1. The Engineering Problem

You are building the arithmetic stage of a small processor's datapath, and everywhere you look, something has to add. The program counter advances: pc + 4. A load computes its effective address: base + offset. The ALU executes an ADD and, on the next instruction, a SUB. An accumulator sums a stream of signed samples. A loop counter counts down toward zero. Not one of these is arithmetic for decoration — each is the moment raw data becomes a new number that the rest of the machine depends on.

The block that produces those numbers is the adder, and its close twin the subtractor. At first they look like two separate machines you must build and maintain. They are not. In two's-complement — the representation every modern datapath uses for signed integers — subtraction is addition of the negation, and negation is invert-and-add-one: a - b = a + (~b + 1) = a + ~b + 1. So a single add/subtract structure, with one mode bit that chooses whether to add b or ~b and whether to inject a 1, computes both operations. That single insight is why this is one pattern, not two.

But the sum is only part of the story. Two more outputs come off that structure and they are where the bugs live. A carry-out tells you the unsigned result did not fit — it wrapped past the top of the unsigned range. A signed overflow (V) tells you the signed result did not fit — it wrapped past the top or bottom of the two's-complement range. These are different questions with different answers, and the number-one adder bug in real silicon is using one flag where you meant the other: reading the unsigned carry-out to decide whether a signed saturate or a signed branch should fire. It is correct on the positive half of the range your positive-only testbench exercised, and wrong the moment real signed data goes negative.

the-need.v — one structure, two operations, two DIFFERENT overflow flags
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Everywhere the datapath produces a new number, it is this structure:
   wire [31:0] next_pc  = pc   + 32'd4;        // increment           (+)
   wire [31:0] addr     = base + offset;        // effective address   (+)
   wire [31:0] diff     = a    - b;             // a - b = a + ~b + 1  (-)
 
   // ONE add/sub with a mode bit `sub` does both:  op = sub ? (a - b) : (a + b)
   // ...and it emits TWO overflow flags that answer DIFFERENT questions:
   //   cout : did the UNSIGNED result wrap?   (carry past the MSB)
   //   ovf  : did the SIGNED   result wrap?   (V = cin_msb XOR cout_msb)
   // Using `cout` where you meant `ovf` (or vice-versa) is the classic silent bug.

2. Mental Model

3. Pattern Anatomy

The add/subtract family is one structure — a chain of full-adders — read four ways.

The ripple-carry chain (the default synthesized structure). A full-adder takes three bits — a[i], b[i], and a carry-in c[i] — and produces a sum bit sum[i] = a[i] ^ b[i] ^ c[i] and a carry-out c[i+1] = (a[i] & b[i]) | (c[i] & (a[i] ^ b[i])). Chain N of them, wiring each stage's carry-out into the next stage's carry-in, and you have an N-bit ripple-carry adder — the structure a synthesis tool infers from a plain a + b. Its defining property is its delay: bit N-1's sum cannot settle until the carry has rippled through all N-1 stages below it, so the critical path grows linearly with width. That linear carry chain is exactly what fast/prefix adders (carry-lookahead, Brent-Kung, Kogge-Stone — a forward pattern) exist to break, trading area for a log2(N)-depth carry. For now the mental picture is: the carry is a wave that has to cross the whole word.

Add/subtract via XOR-invert and carry-in. To fold subtraction into the same chain, put an XOR gate on each b bit controlled by the sub mode bit, and drive the chain's bottom carry-in with sub too:

  • b_eff[i] = b[i] ^ sub — when sub=0 this is b (add); when sub=1 it is ~b (the invert half of two's-complement negate).
  • cin = sub — when sub=1 this injects the +1 half of the negate; when sub=0 it is the normal carry-in 0.

The result is a + (b ^ {N{sub}}) + sub, which is a + b for sub=0 and a + ~b + 1 = a - b for sub=1. One row of XORs and a carry-in turn the adder into an add/subtract unit — and the b-vs-~b selection is itself a 2:1 mux (§1.1) with sub as its select.

Carry-out vs signed-overflow detection — the two different alarms. From the top of the chain come two distinct flags, and this is the crux of the whole page:

  • Carry-out (cout) is simply c[N], the carry out of the most-significant stage. It is the unsigned overflow flag: for sub=0, cout=1 means a + b exceeded 2^N - 1; for sub=1, cout is the borrow-complement of the unsigned subtract. It answers "did the unsigned result wrap?"
  • Signed overflow (V) is V = c[N] XOR c[N-1] — the carry into the MSB XORed with the carry out of the MSB. It fires exactly when two same-signed operands produce a wrong-signed result (positive + positive = negative, or negative + negative = positive). It answers "did the signed result wrap?" Equivalently, V = (a[N-1] == b_eff[N-1]) && (sum[N-1] != a[N-1]).

The two flags disagree constantly. -1 + 1 (0xFF + 0x01 at 8 bits) produces 0x00 with cout=1 — the unsigned add wrapped — but there is no signed overflow, because -1 + 1 = 0 is perfectly representable. Conversely 0x7F + 0x01 (127 + 1) produces 0x80 with V=1 (signed overflow: +128 doesn't fit) — a signed disaster — while its unsigned interpretation 127 + 1 = 128 fits fine and sets no carry-out. Same machine, two alarms, and they are not interchangeable.

Carry-in and the width-grows-by-one rule. A carry-in on the bottom of the chain is not just for subtraction — it is how you chain adders for multi-word arithmetic. To add two 64-bit numbers on a 32-bit datapath, add the low words with cin=0, capture the cout, and feed it as the cin of the high-word add: the carry ripples across the word boundary exactly as it ripples across bits. That is why a general adder always exposes a carry-in. And the width rule closes the anatomy: adding two N-bit numbers yields an N+1-bit result — {cout, sum} is the full answer. If you declare sum as N bits you have chosen to discard the carry-out; that is fine only if you captured cout separately. Truncating the sum and dropping the carry is the width off-by-one bug of §6.

The add/subtract family — one ripple chain, an XOR row, and two different overflow flags

data flow
The add/subtract family — one ripple chain, an XOR row, and two different overflow flagsoperands a, b,sub, cinN bits each; sub picks add/subtractXOR row: b_eff =b ^ subsub=1 -> ~b (invert half of negate)ripple-carrychainN full-adders, delay ~ N (carry wave){cout, sum}N+1-bit result; sum=N, cout=extra bitcarry-out = c[N]UNSIGNED overflow: did a+b wrap 2^N?V = c[N] XORc[N-1]SIGNED overflow: wrong-signed result
One structure: an XOR row masks b with the sub bit (b_eff = b ^ sub), the sub bit also drives the bottom carry-in, and a ripple-carry chain of N full-adders produces the sum — delay linear in width because the carry has to ripple across the whole word (this is what fast/prefix adders exist to break). The full result is N+1 bits: sum plus the carry-out. From the top of the chain come TWO different alarms: carry-out = c[N] is the UNSIGNED overflow, signed overflow V = c[N] XOR c[N-1] is the SIGNED overflow. -1 + 1 sets carry-out but not V; 0x7F + 0x01 sets V but the unsigned add fits. They answer different questions — you must compute the one you mean. This is language-independent: the same structure and the same signed/unsigned decision exist in SystemVerilog, Verilog, and VHDL.

The whole anatomy reduces to four decisions you make on every adder: is it add or subtract (the sub bit and XOR row), do you need a carry-in (chaining, or the subtract +1), do you keep the carry-out (the N+1-th bit), and — the one that bites — which overflow do you mean, unsigned (cout) or signed (V). Get the last one wrong and the datapath is correct in simulation and wrong in the field.

4. Real RTL Implementation

This is the core of the page. We build two patterns — (a) a parameterized add/subtract unit with a sub mode bit, a carry-in, a carry-out, and a correct signed-overflow flag, WIDTH-generic and correct for both signed and unsigned interpretation; and (b) the carry-out-vs-overflow bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking testbench. The idea is identical across all three; only the syntax for declaring signedness and slicing the extra bit differs, and seeing them side by side is what fixes the carry-vs-overflow reasoning in your head.

One discipline runs through every block: the sum is computed one bit wider than the operands, so the carry-out is a real bit and not a dropped one; and the overflow flag is computed as V, never read off the carry-out. In SystemVerilog and Verilog that means an N+1-bit intermediate {cout, sum} and an explicit ovf = cin_to_msb ^ cout; in VHDL it means widening via numeric_std ('0' & a) and the same XOR of the two top carries.

4a. Parameterized add/subtract unit — one structure, both operations, correct flags

Start with the workhorse: one module that adds or subtracts under a sub bit, accepts a carry-in for chaining, and emits sum, cout (unsigned overflow), and ovf (signed overflow). The trick that makes the flags correct is to compute the sum one bit wider than the operands — extend a and b_eff with a leading 0, add, and the extra top bit is the carry-out. The signed-overflow flag is then V = (carry into MSB) XOR (carry out of MSB).

addsub.sv — parameterized add/subtract with carry-in, carry-out, and signed-overflow V
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub #(
       parameter int WIDTH = 8
   )(
       input  logic [WIDTH-1:0] a,
       input  logic [WIDTH-1:0] b,
       input  logic             sub,        // 0 = a + b,  1 = a - b
       input  logic             cin,        // carry-in for chaining multi-word adds
       output logic [WIDTH-1:0] sum,
       output logic             cout,       // UNSIGNED overflow (carry out of the MSB)
       output logic             ovf         // SIGNED overflow  (V = cin_msb ^ cout_msb)
   );
       // Two's-complement subtract = add the inverted b with an injected +1:
       //   a - b = a + ~b + 1.  XOR each b bit with sub (sub=1 => ~b), and fold the
       //   +1 into the bottom carry-in.  So one adder + an XOR row does BOTH ops.
       logic [WIDTH-1:0] b_eff  = b ^ {WIDTH{sub}};    // b (add) or ~b (subtract)
       logic             cin0   = cin ^ sub;           // subtract injects the +1
 
       // Compute the sum ONE BIT WIDER so the carry-out is a real bit, not dropped.
       // {1'b0, a} + {1'b0, b_eff} + cin0 is an (WIDTH+1)-bit ripple-carry add; the
       // top bit of the result IS the carry-out.
       logic [WIDTH:0] ext = {1'b0, a} + {1'b0, b_eff} + cin0;
 
       assign sum  = ext[WIDTH-1:0];
       assign cout = ext[WIDTH];                        // unsigned overflow / borrow-complement
 
       // Signed overflow V = carry INTO the MSB XOR carry OUT of the MSB. The carry
       // into the MSB is recoverable as sum[MSB] ^ a[MSB] ^ b_eff[MSB]. This is a
       // DIFFERENT question than cout and must be computed, never read off cout.
       logic cin_msb  = sum[WIDTH-1] ^ a[WIDTH-1] ^ b_eff[WIDTH-1];
       assign ovf = cin_msb ^ cout;
   endmodule

Two lines carry the whole lesson. cout = ext[WIDTH] is the unsigned overflow — the bit that would be lost if you truncated the sum to WIDTH. ovf = cin_msb ^ cout is the signed overflow V, a genuinely different computation. The testbench proves both against independent reference models and hammers the boundary operands where the two flags disagree — max+max, min-min, -1+1, 0x7F+0x01 — plus a random sweep across the full range.

addsub_tb.sv — self-checking: sum, cout, ovf vs reference; random + boundary corners
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub_tb;
       localparam int WIDTH = 8;
       logic [WIDTH-1:0] a, b, sum;
       logic             sub, cin, cout, ovf;
       int               errors = 0;
 
       addsub #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sub(sub), .cin(cin),
                                    .sum(sum), .cout(cout), .ovf(ovf));
 
       task check; begin
           #1;                                                  // settle combinationally
           // Reference: do the arithmetic in a WIDTH+1-bit space, then derive flags.
           logic [WIDTH:0]   ext_ref;
           logic [WIDTH-1:0] beff_ref;
           logic             cin0_ref, exp_cout, exp_ovf;
           logic signed [WIDTH:0] s_ref;                        // signed reference
           beff_ref = sub ? ~b : b;
           cin0_ref = cin ^ sub;
           ext_ref  = {1'b0, a} + {1'b0, beff_ref} + cin0_ref;
           exp_cout = ext_ref[WIDTH];                           // unsigned overflow
           // Signed reference: true signed result must fit in WIDTH bits or V=1.
           s_ref    = $signed(a) + (sub ? -$signed(b) : $signed(b)) + $signed({1'b0, cin});
           exp_ovf  = (s_ref > (2**(WIDTH-1) - 1)) || (s_ref < -(2**(WIDTH-1)));
           assert (sum  === ext_ref[WIDTH-1:0]) else begin $error("sum  a=%h b=%h sub=%b", a, b, sub); errors++; end
           assert (cout === exp_cout)           else begin $error("cout a=%h b=%h sub=%b", a, b, sub); errors++; end
           assert (ovf  === exp_ovf)            else begin $error("ovf  a=%h b=%h sub=%b got=%b exp=%b", a, b, sub, ovf, exp_ovf); errors++; end
       end endtask
 
       initial begin
           // Directed corners where cout and ovf DISAGREE (the whole lesson).
           sub=0; cin=0; a=8'hFF; b=8'h01; check();             // -1 + 1: cout=1 but NO signed ovf
           sub=0; cin=0; a=8'h7F; b=8'h01; check();             // 127 + 1: signed ovf=1, unsigned fits
           sub=0; cin=0; a=8'hFF; b=8'hFF; check();             // max+max unsigned (255+255)
           sub=1; cin=0; a=8'h80; b=8'h01; check();             // min - 1: signed underflow -> ovf
           sub=1; cin=0; a=8'h00; b=8'h80; check();             // 0 - min: signed ovf (+128 doesn't fit)
           sub=0; cin=1; a=8'hFF; b=8'h00; check();             // carry-in chaining path
           // Random sweep over both operations and carry-in.
           for (int t = 0; t < 400; t++) begin
               a = $urandom; b = $urandom; sub = t[0]; cin = t[1]; check();
           end
           if (errors == 0) $display("PASS: addsub sum/cout/ovf correct on all vectors");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent — only wire/reg typing, $random, and $signed system-function casts differ. The WIDTH+1-bit intermediate and the ovf = cin_msb ^ cout computation are line-for-line the same idea.

addsub.v — parameterized add/subtract in Verilog (WIDTH+1 intermediate, explicit V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub #(
       parameter WIDTH = 8
   )(
       input  wire [WIDTH-1:0] a,
       input  wire [WIDTH-1:0] b,
       input  wire             sub,          // 0 = add, 1 = subtract
       input  wire             cin,
       output wire [WIDTH-1:0] sum,
       output wire             cout,         // UNSIGNED overflow
       output wire             ovf           // SIGNED overflow (V)
   );
       // a - b = a + ~b + 1: invert b with the sub bit, inject the +1 via the carry-in.
       wire [WIDTH-1:0] b_eff = b ^ {WIDTH{sub}};
       wire             cin0  = cin ^ sub;
 
       // One-bit-wider add so the carry-out is a real bit and never truncated away.
       wire [WIDTH:0]   ext   = {1'b0, a} + {1'b0, b_eff} + cin0;
 
       assign sum  = ext[WIDTH-1:0];
       assign cout = ext[WIDTH];                           // unsigned overflow
 
       // Carry INTO the MSB, recovered from the sum bit; V = cin_msb ^ cout.
       wire cin_msb = sum[WIDTH-1] ^ a[WIDTH-1] ^ b_eff[WIDTH-1];
       assign ovf = cin_msb ^ cout;                        // signed overflow, NOT cout
   endmodule
addsub_tb.v — self-checking; random + max/min/-1/0x7F corners, checks sum/cout/ovf
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub_tb;
       parameter WIDTH = 8;
       reg  [WIDTH-1:0] a, b;
       reg              sub, cin;
       wire [WIDTH-1:0] sum;
       wire             cout, ovf;
       integer          t, errors;
 
       addsub #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sub(sub), .cin(cin),
                                    .sum(sum), .cout(cout), .ovf(ovf));
 
       task check;
           reg [WIDTH:0]         ext_ref;
           reg [WIDTH-1:0]       beff_ref;
           reg                   cin0_ref, exp_cout, exp_ovf;
           reg signed [WIDTH+1:0] s_ref;                   // wide signed reference
           begin
               #1;
               beff_ref = sub ? ~b : b;
               cin0_ref = cin ^ sub;
               ext_ref  = {1'b0, a} + {1'b0, beff_ref} + cin0_ref;
               exp_cout = ext_ref[WIDTH];                  // unsigned overflow
               s_ref    = $signed(a) + (sub ? -$signed(b) : $signed(b)) + cin;
               exp_ovf  = (s_ref > (2**(WIDTH-1) - 1)) || (s_ref < -(2**(WIDTH-1)));
               if (sum  !== ext_ref[WIDTH-1:0]) begin $display("FAIL sum  a=%h b=%h sub=%b", a, b, sub); errors=errors+1; end
               if (cout !== exp_cout)           begin $display("FAIL cout a=%h b=%h sub=%b", a, b, sub); errors=errors+1; end
               if (ovf  !== exp_ovf)            begin $display("FAIL ovf  a=%h b=%h sub=%b got=%b exp=%b", a, b, sub, ovf, exp_ovf); errors=errors+1; end
           end
       endtask
 
       initial begin
           errors = 0;
           sub=0; cin=0; a=8'hFF; b=8'h01; check;          // -1 + 1: cout=1, no signed ovf
           sub=0; cin=0; a=8'h7F; b=8'h01; check;          // 127 + 1: signed ovf, unsigned fits
           sub=0; cin=0; a=8'hFF; b=8'hFF; check;          // max + max (unsigned wrap)
           sub=1; cin=0; a=8'h80; b=8'h01; check;          // min - 1: signed underflow
           sub=1; cin=0; a=8'h00; b=8'h80; check;          // 0 - min: signed overflow
           sub=0; cin=1; a=8'hFF; b=8'h00; check;          // carry-in chaining
           for (t = 0; t < 400; t = t + 1) begin
               a = $random; b = $random; sub = t[0]; cin = t[1]; check;
           end
           if (errors == 0) $display("PASS: addsub sum/cout/ovf correct on all vectors");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

VHDL makes the widening and the signedness the most explicit of the three, because numeric_std gives you distinct unsigned and signed types and forces you to name the interpretation. The WIDTH+1-bit unsigned add produces sum and cout; the signed-overflow flag is the same V = cin_msb XOR cout computation, built from std_logic bits.

addsub.vhd — parameterized add/subtract in VHDL (numeric_std widen, explicit V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity addsub is
       generic ( WIDTH : positive := 8 );
       port (
           a    : in  std_logic_vector(WIDTH-1 downto 0);
           b    : in  std_logic_vector(WIDTH-1 downto 0);
           sub  : in  std_logic;                              -- '0' add, '1' subtract
           cin  : in  std_logic;                              -- carry-in for chaining
           sum  : out std_logic_vector(WIDTH-1 downto 0);
           cout : out std_logic;                              -- UNSIGNED overflow
           ovf  : out std_logic                               -- SIGNED overflow (V)
       );
   end entity;
 
   architecture rtl of addsub is
       signal b_eff : std_logic_vector(WIDTH-1 downto 0);
       signal cin0  : std_logic;
       signal ext   : unsigned(WIDTH downto 0);               -- one bit wider
   begin
       -- a - b = a + ~b + 1: XOR-invert b under sub, fold +1 into the carry-in.
       b_eff <= b xor (WIDTH-1 downto 0 => sub);
       cin0  <= cin xor sub;
 
       -- Widen both operands by a leading '0' and add as UNSIGNED; the extra top
       -- bit is the carry-out. numeric_std makes the interpretation explicit.
       ext <= ('0' & unsigned(a)) + ('0' & unsigned(b_eff))
              + ("" & unsigned'("" & cin0));                  -- add cin0 as a 1-bit unsigned
 
       sum  <= std_logic_vector(ext(WIDTH-1 downto 0));
       cout <= ext(WIDTH);                                    -- unsigned overflow
 
       -- Signed overflow: carry INTO the MSB XOR carry OUT of the MSB. The carry into
       -- the MSB is sum(MSB) XOR a(MSB) XOR b_eff(MSB); V is that XOR cout.
       ovf  <= (ext(WIDTH-1) xor a(WIDTH-1) xor b_eff(WIDTH-1)) xor ext(WIDTH);
   end architecture;
addsub_tb.vhd — self-checking; max/min/-1 corners + sweep (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity addsub_tb is
   end entity;
 
   architecture sim of addsub_tb is
       constant WIDTH : positive := 8;
       signal a, b, sum : std_logic_vector(WIDTH-1 downto 0);
       signal sub, cin, cout, ovf : std_logic;
 
       procedure check(signal av, bv, sv : in std_logic_vector;
                       signal sb, ci, co, ov : in std_logic;
                       signal subm, cinm : in std_logic) is
           variable beff  : unsigned(WIDTH-1 downto 0);
           variable extr  : unsigned(WIDTH downto 0);
           variable sref  : integer;
           variable eco    : std_logic;
           variable eov    : std_logic;
       begin
           wait for 1 ns;
           -- Unsigned reference in a WIDTH+1-bit space.
           if subm = '1' then beff := not unsigned(bv); else beff := unsigned(bv); end if;
           extr := ('0' & unsigned(av)) + ('0' & beff)
                   + ("" & (cinm xor subm));
           if extr(WIDTH) = '1' then eco := '1'; else eco := '0'; end if;
           -- Signed reference: true value must fit in WIDTH bits or V='1'.
           if subm = '1' then
               sref := to_integer(signed(av)) - to_integer(signed(bv));
           else
               sref := to_integer(signed(av)) + to_integer(signed(bv));
           end if;
           sref := sref + (character'pos('0') - character'pos('0'));   -- (cin omitted from signed ref)
           if sref > (2**(WIDTH-1) - 1) or sref < -(2**(WIDTH-1)) then eov := '1'; else eov := '0'; end if;
           assert sv = std_logic_vector(extr(WIDTH-1 downto 0)) report "sum mismatch"  severity error;
           assert co = eco                                       report "cout mismatch" severity error;
           assert ov = eov                                       report "ovf mismatch"  severity error;
       end procedure;
   begin
       dut : entity work.addsub generic map (WIDTH => WIDTH)
           port map (a => a, b => b, sub => sub, cin => cin, sum => sum, cout => cout, ovf => ovf);
 
       stim : process
       begin
           cin <= '0';
           sub <= '0'; a <= x"FF"; b <= x"01"; check(a,b,sum,sub,cin,cout,ovf,sub,cin);  -- -1 + 1
           sub <= '0'; a <= x"7F"; b <= x"01"; check(a,b,sum,sub,cin,cout,ovf,sub,cin);  -- 127 + 1: V
           sub <= '0'; a <= x"FF"; b <= x"FF"; check(a,b,sum,sub,cin,cout,ovf,sub,cin);  -- max+max
           sub <= '1'; a <= x"80"; b <= x"01"; check(a,b,sum,sub,cin,cout,ovf,sub,cin);  -- min - 1
           sub <= '1'; a <= x"00"; b <= x"80"; check(a,b,sum,sub,cin,cout,ovf,sub,cin);  -- 0 - min
           report "addsub self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The carry-out-vs-overflow bug — buggy vs fixed, in all three HDLs

Now the signature failure, shown as buggy vs fixed RTL in each language, then dramatized narratively in §7. The setting is a signed saturating add: two signed operands are summed, and the result must clamp to the signed max/min if it overflows the range. The correct overflow condition is the signed flag V. The bug uses the adder's carry-out as the overflow indicator instead — a plausible mistake, because on unsigned data carry-out is the overflow. On signed data it is the wrong question: -1 + 1 sets carry-out (so the buggy block wrongly saturates a perfectly-valid 0), while some genuine signed overflows do not set the carry-out the way the code assumed — so the block saturates when it must not and passes through when it must clamp.

sat_add.sv — BUGGY (saturate on carry-out) vs FIXED (saturate on signed V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: signed saturating add that uses the UNSIGNED carry-out as its overflow
   //        flag. For -1 + 1 (0xFF + 0x01) carry-out is 1, so it clamps a valid 0.
   //        Silent in positive-only sims; wrong the moment operands go negative.
   module sat_add_bad #(parameter int WIDTH = 8) (
       input  logic signed [WIDTH-1:0] a,
       input  logic signed [WIDTH-1:0] b,
       output logic signed [WIDTH-1:0] y
   );
       logic [WIDTH:0] ext = {a[WIDTH-1], a} + {b[WIDTH-1], b};   // sign-extended add
       wire            cout = ext[WIDTH];                          // UNSIGNED carry — WRONG flag
       // Clamps on carry-out, which is the unsigned question, not the signed one.
       assign y = cout ? (a[WIDTH-1] ? {1'b1, {(WIDTH-1){1'b0}}}   // clamp to signed MIN
                                     : {1'b0, {(WIDTH-1){1'b1}}})  // clamp to signed MAX
                       : ext[WIDTH-1:0];
   endmodule
 
   // FIXED: saturate on the SIGNED overflow flag V = carry-in-MSB XOR carry-out-MSB.
   //        Now -1 + 1 = 0 passes through, and only true signed overflows clamp.
   module sat_add_good #(parameter int WIDTH = 8) (
       input  logic signed [WIDTH-1:0] a,
       input  logic signed [WIDTH-1:0] b,
       output logic signed [WIDTH-1:0] y
   );
       logic [WIDTH-1:0] sum   = a + b;
       // V = the two same-signed operands produced a wrong-signed result.
       wire             ovf   = (a[WIDTH-1] == b[WIDTH-1]) && (sum[WIDTH-1] != a[WIDTH-1]);
       // On overflow, clamp toward the sign of the operands; else pass the sum.
       assign y = ovf ? (a[WIDTH-1] ? {1'b1, {(WIDTH-1){1'b0}}}    // signed MIN
                                    : {1'b0, {(WIDTH-1){1'b1}}})   // signed MAX
                      : sum;
   endmodule
sat_add_tb.sv — signed corners expose the carry-vs-overflow confusion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sat_add_tb;
       localparam int WIDTH = 8;
       logic signed [WIDTH-1:0] a, b, y, exp;
       int                      errors = 0;
 
       // Bind to sat_add_good to PASS; to _bad to watch -1 + 1 get wrongly clamped.
       sat_add_good #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .y(y));
 
       task check;
           input signed [WIDTH+1:0] ia, ib;
           logic signed [WIDTH+1:0] s;
           begin
               a = ia[WIDTH-1:0]; b = ib[WIDTH-1:0];
               #1;
               // Correct saturating reference: clamp the TRUE signed sum to the range.
               s   = $signed(a) + $signed(b);
               if      (s >  (2**(WIDTH-1) - 1)) exp =  (2**(WIDTH-1) - 1);   // signed MAX
               else if (s < -(2**(WIDTH-1)))     exp = -(2**(WIDTH-1));       // signed MIN
               else                              exp = s[WIDTH-1:0];
               assert (y === exp)
                   else begin $error("a=%0d b=%0d y=%0d exp=%0d", a, b, y, exp); errors++; end
           end
       endtask
 
       initial begin
           check(0, 0);   check(10, 20);                       // no overflow
           check(-1, 1);  check(-1, -1);                       // NEGATIVE: buggy clamps -1+1 wrongly
           check(127, 1); check(100, 100);                     // signed positive overflow -> MAX
           check(-128,-1);check(-100,-100);                    // signed negative overflow -> MIN
           if (errors == 0) $display("PASS: sat_add clamps on SIGNED overflow only");
           else             $display("FAIL: %0d mismatches (carry-out used as overflow?)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story. The bug is clamping on the sign-extended add's carry-out; the fix is clamping on the signed-overflow condition (a[MSB]==b[MSB]) && (sum[MSB]!=a[MSB]). Driving signed operands whose sum crosses the range — and especially the -1 + 1 case the carry-out mishandles — is what surfaces it.

sat_add.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: saturating add clamps on the UNSIGNED carry-out of a sign-extended add.
   //        -1 + 1 sets carry-out and gets wrongly clamped; a signed corruption.
   module sat_add_bad #(parameter WIDTH = 8) (
       input  wire signed [WIDTH-1:0] a,
       input  wire signed [WIDTH-1:0] b,
       output wire signed [WIDTH-1:0] y
   );
       wire [WIDTH:0] ext  = {a[WIDTH-1], a} + {b[WIDTH-1], b};
       wire           cout = ext[WIDTH];                        // WRONG flag for signed
       assign y = cout ? (a[WIDTH-1] ? {1'b1, {(WIDTH-1){1'b0}}}    // MIN
                                     : {1'b0, {(WIDTH-1){1'b1}}})   // MAX
                       : ext[WIDTH-1:0];
   endmodule
 
   // FIXED: clamp on the SIGNED overflow flag V, not the carry-out.
   module sat_add_good #(parameter WIDTH = 8) (
       input  wire signed [WIDTH-1:0] a,
       input  wire signed [WIDTH-1:0] b,
       output wire signed [WIDTH-1:0] y
   );
       wire [WIDTH-1:0] sum = a + b;
       wire            ovf = (a[WIDTH-1] == b[WIDTH-1]) && (sum[WIDTH-1] != a[WIDTH-1]);
       assign y = ovf ? (a[WIDTH-1] ? {1'b1, {(WIDTH-1){1'b0}}}     // MIN
                                    : {1'b0, {(WIDTH-1){1'b1}}})    // MAX
                      : sum;
   endmodule
sat_add_tb.v — self-checking; signed corners incl. -1+1 catch the carry-vs-ovf bug
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sat_add_tb;
       parameter WIDTH = 8;
       reg  signed [WIDTH-1:0] a, b;
       wire signed [WIDTH-1:0] y;
       reg  signed [WIDTH-1:0] exp;
       reg  signed [WIDTH+1:0] s;
       integer                 errors;
 
       // Bind to _good to PASS; to _bad to observe -1 + 1 being wrongly clamped.
       sat_add_good #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .y(y));
 
       task check;
           input signed [WIDTH+1:0] ia, ib;
           begin
               a = ia[WIDTH-1:0]; b = ib[WIDTH-1:0];
               #1;
               s = $signed(a) + $signed(b);                    // true signed sum
               if      (s >  (2**(WIDTH-1) - 1)) exp =  (2**(WIDTH-1) - 1);
               else if (s < -(2**(WIDTH-1)))     exp = -(2**(WIDTH-1));
               else                              exp = s[WIDTH-1:0];
               if (y !== exp) begin
                   $display("FAIL: a=%0d b=%0d y=%0d exp=%0d", a, b, y, exp);
                   errors = errors + 1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           check(0, 0);    check(10, 20);                      // no overflow
           check(-1, 1);   check(-1, -1);                      // NEGATIVE: exposes the bug
           check(127, 1);  check(100, 100);                    // positive overflow -> MAX
           check(-128,-1); check(-100,-100);                   // negative overflow -> MIN
           if (errors == 0) $display("PASS: sat_add clamps on signed overflow only");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the mistake wears its cause on its sleeve: the buggy version reads the carry-out of an unsigned widen, the fixed version computes V from the sign bits. Because numeric_std forces you to name signed vs unsigned, the wrong-flag bug looks deliberate in review — which is exactly why it is caught faster in VHDL.

sat_add.vhd — BUGGY (clamp on carry-out) vs FIXED (clamp on signed V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: signed saturating add that clamps on the UNSIGNED carry-out. For
   --        -1 + 1 the carry-out is '1' and a valid 0 is wrongly saturated.
   entity sat_add_bad is
       generic ( WIDTH : positive := 8 );
       port ( a, b : in  std_logic_vector(WIDTH-1 downto 0);
              y    : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of sat_add_bad is
       signal ext : unsigned(WIDTH downto 0);
   begin
       ext <= ('0' & unsigned(a)) + ('0' & unsigned(b));       -- unsigned widen
       process (ext, a)
       begin
           if ext(WIDTH) = '1' then                            -- carry-out = WRONG flag
               if a(WIDTH-1) = '1' then
                   y <= '1' & (WIDTH-2 downto 0 => '0');        -- signed MIN
               else
                   y <= '0' & (WIDTH-2 downto 0 => '1');        -- signed MAX
               end if;
           else
               y <= std_logic_vector(ext(WIDTH-1 downto 0));
           end if;
       end process;
   end architecture;
 
   -- FIXED: clamp on the SIGNED overflow flag V (same-signed operands, wrong-signed
   --        result). -1 + 1 = 0 now passes; only true signed overflow clamps.
   entity sat_add_good is
       generic ( WIDTH : positive := 8 );
       port ( a, b : in  std_logic_vector(WIDTH-1 downto 0);
              y    : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of sat_add_good is
       signal sum : signed(WIDTH-1 downto 0);
       signal ovf : std_logic;
   begin
       sum <= signed(a) + signed(b);
       ovf <= '1' when (a(WIDTH-1) = b(WIDTH-1)) and (sum(WIDTH-1) /= a(WIDTH-1))
                  else '0';                                    -- V, the SIGNED question
       process (ovf, a, sum)
       begin
           if ovf = '1' then
               if a(WIDTH-1) = '1' then
                   y <= '1' & (WIDTH-2 downto 0 => '0');        -- signed MIN
               else
                   y <= '0' & (WIDTH-2 downto 0 => '1');        -- signed MAX
               end if;
           else
               y <= std_logic_vector(sum);
           end if;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: carry-out and overflow are different alarms — carry-out is the unsigned question, V = c[N] XOR c[N-1] is the signed question — so on signed data you must saturate and branch on V, never on the carry-out.

5. Verification Strategy

An add/subtract unit is combinational, so its correctness is a function you can check against a reference model — the great advantage being that the reference (integer arithmetic in the testbench, done in a wider space) is trivially correct, so you can hammer it with volume. The single invariant that captures a correct unit is:

In an N+1-bit space, {cout, sum} == a + (sub ? ~b : b) + (cin ^ sub); the signed-overflow flag ovf is true exactly when the true signed result does not fit in N bits; and cout is true exactly when the unsigned result does not fit — two separate checks, never conflated.

Everything below is that invariant made checkable, and it maps directly onto the §4 testbenches.

  • Random + boundary self-check against a reference (self-checking TB). Drive random a, b, sub, cin and compute the golden result in a wider integer space (WIDTH+1 bits or a signed integer), then assert sum, cout, and ovf each against their own reference. Because the reference arithmetic is independent of the DUT's structure, a match is real evidence. The three §4a testbenches do exactly this: SV assert (sum === ...) / assert (cout === ...) / assert (ovf === ...), Verilog if (... !== ...) $display("FAIL"), VHDL assert ... report ... severity error.
  • Verify carry-out (unsigned) AND overflow (signed) SEPARATELY. This is the point of the whole page, so it is the point of verification: never derive one flag's expectation from the other. Compute the expected cout from an unsigned WIDTH+1-bit add and the expected ovf from a signed range check (true_signed > MAX || true_signed < MIN). A testbench that only checks the sum, or that checks ovf by reading cout, cannot catch the §7 bug — it would contain the bug in its own model.
  • Boundary operands are mandatory, including the disagreement cases. Exercise max + max, min - min, min - 1, 0 - min, -1 + 1, 0x7F + 1, and both max and min of the signed range. The -1 + 1 and 0x7F + 1 cases are the crown jewels: -1 + 1 sets cout but not ovf, and 0x7F + 1 sets ovf but the unsigned add fits — any TB that passes both has genuinely separated the two flags.
  • The eq-of-flags invariant is a free checker. For a single operation, cout and ovf are independent bits; four combinations {cout, ovf} are all reachable, and a correct model reproduces each. If your reference ever ties them together (ovf := cout), that is a bug in the checker, and the directed corners above will not expose the DUT bug — a reminder to keep the two reference computations structurally separate.
  • Parameter-generic verification (WIDTH = 4, 8, 16, 32). A parameterized unit must be verified generic, not assumed generic. Re-run the random + boundary suite at several WIDTHs; the boundary values (max, min, -1) are recomputed from WIDTH, so the same testbench code covers every width. A unit that passes at WIDTH=8 can still be wrong at WIDTH=32 if a slice or a sign-extension was hardcoded — width-genericity is a property you prove.
  • Expected waveform. Because the unit is combinational, "correct" on a waveform is immediate: as a/b/sub/cin change, sum, cout, and ovf settle within the propagation delay to the reference values. The visual signature of the §7 bug is a saturate/branch output that fires on the wrong operands — clamping on -1 + 1 (a valid 0) or failing to clamp on 0x7F + 1 — a mismatch you see the instant a negative operand appears.

6. Common Mistakes

Confusing carry-out with signed overflow. The signature bug of this page. Carry-out answers "did the unsigned result wrap?"; signed overflow V answers "did the signed result wrap?" They are different bits (V = carry-into-MSB XOR carry-out-of-MSB) and they disagree constantly — -1 + 1 sets carry-out but not V; 0x7F + 1 sets V but the unsigned add fits. Using the unsigned carry-out to drive a signed saturate or a signed branch (or vice-versa) produces a circuit that is correct in a positive-only sim and wrong on negative data. This is the exact signedness trap of the comparator (§1.5) reappearing on the adder: compute the overflow you mean. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4b.)

Truncating the sum and dropping the carry (the width off-by-one). Adding two N-bit numbers yields an N+1-bit result — {cout, sum}. Assigning that into an N-bit wire discards the top bit, which is the carry-out. If you did not capture cout separately, you have silently lost information: 0xFF + 0xFF becomes 0xFE with the carry gone. The fix is to compute the sum one bit wider ({1'b0, a} + {1'b0, b}) and take cout from the extra bit, or to declare the destination N+1 bits. Width in arithmetic is never accidental — decide whether you keep the carry.

Wrong two's-complement subtract (forgetting the +1 / carry-in). Subtraction is a + ~b + 1, and the +1 is not optional — it is what completes the two's-complement negate. Inverting b but injecting cin=0 computes a + ~b, which is a - b - 1 (off by one on every subtract). The correct add/subtract folds the +1 into the bottom carry-in (cin0 = cin ^ sub), so sub=1 supplies it automatically. Any subtractor whose results are uniformly one too small has dropped the negate's +1.

Sign-extension mistakes on mixed widths. When you add operands of different widths, or a narrow signed value into a wider accumulator, the narrow operand must be sign-extended (replicate its MSB), not zero-extended — zero-extending a negative number turns it into a large positive one, exactly the -1-becomes-255 error from the comparator. Symmetrically, an unsigned narrow operand must be zero-extended. The rule follows the operand's declared signedness: sign-extend signed, zero-extend unsigned. Mixing them silently corrupts the arithmetic on the top half of the narrow operand's range.

7. DebugLab

The saturating adder that clamped valid data — carry-out used where signed overflow was meant

The engineering lesson: carry-out and signed overflow are two different alarms on the same adder — one asks whether the unsigned result wrapped, the other whether the signed result wrapped — and they disagree on exactly the operands (negatives, sign boundaries) a positive-only testbench never drives. The tell is a saturate or a branch that fires on the wrong data in a sign-dependent way: if a signed decision behaves correctly on positive operands and wrongly on negative ones, suspect that an unsigned flag (carry-out) is steering a signed choice. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: compute V = c[N] XOR c[N-1] (or (a[MSB]==b[MSB]) && (sum[MSB]!=a[MSB])) for the signed overflow and route that into signed saturates/branches, and in verification, check carry-out and overflow against separate reference models and drive the corners where they disagree (-1 + 1, 0x7F + 1, min - 1, 0 - min).

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "which overflow do I mean?" and "one structure, both operations" habits.

Exercise 1 — Predict the two flags

For an 8-bit add/subtract unit, fill in sum, cout, and ovf (signed V) for each operation and explain which flag matters: (a) 0xFF + 0x01 (add); (b) 0x7F + 0x01 (add); (c) 0x80 - 0x01 (subtract, i.e. min - 1); (d) 0x00 - 0x80 (subtract, i.e. 0 - min); (e) 0x40 + 0x40 (add). For each, state whether an unsigned user and a signed user would each see an overflow, and why the two can differ. (Hint: compute {cout, sum} in a 9-bit space, then apply V = c[8] XOR c[7].)

Exercise 2 — Turn the adder into a subtractor on paper

You are given only an N-bit adder with a carry-in (no subtract mode). Show precisely how to compute a - b using it: what you feed to the b input, what you set the carry-in to, and why the combination equals a - b in two's-complement. Then explain what the adder's carry-out means for the subtract (borrow vs no-borrow) and how it differs from the carry-out's meaning for an add. Finally, describe the one row of gates you would add to fold add and subtract into a single mode-bit-controlled unit.

Exercise 3 — Design the saturating unit correctly

Design (in words + RTL sketch) a signed saturating adder that clamps to signed MAX/MIN on overflow, for parameter WIDTH. State the exact overflow condition you gate the clamp on and why carry-out is the wrong condition. Then list the four boundary vectors your self-checking testbench must include to prove the clamp fires on — and only on — genuine signed overflow, and name for each vector which of cout/ovf is set. Explain why -1 + 1 and 0x7F + 1 are the two most important vectors.

Exercise 4 — Chain and extend

(a) Using two N-bit add/subtract units, show how to build a 2N-bit adder: what connects between them, and what the low unit's carry-out becomes. (b) Your datapath must add a 12-bit signed sensor reading into a 32-bit signed accumulator every cycle. Describe the extension you apply to the 12-bit value before the add, why zero-extension would be wrong, and what the accumulator's own overflow flag should be computed from. (c) State how the answer to (b) would change if the 12-bit reading were unsigned.

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • Multipliers — Chapter 5.2; the next arithmetic primitive — a multiply is a tree of these adders (partial-product summation), and its width grows by 2N, not N+1.
  • Barrel Shifters — Chapter 5; the third datapath primitive, built from muxes rather than adders — shift/rotate by any amount in one level.
  • Saturation & Rounding — Chapter 5; the correct signed-overflow flag from this page is exactly what a saturating datapath clamps on — the §7 bug, generalized.
  • Fast / Prefix Adders — Chapter 5; the carry-lookahead and parallel-prefix structures that break the ripple-carry chain's width-linear delay.
  • ALU Construction — Chapter 5; this add/subtract unit is the arithmetic heart of the ALU, sharing its flags (carry, overflow, zero, negative) with the logic and shift units.

In-track dependencies (patterns this one builds on):

  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the b/~b mode-bit selection is a 2:1 mux with sub as its select — selection meets arithmetic.
  • Comparators — Chapter 1.5; a magnitude comparator is a subtract-and-inspect, and it carries the same signedness trap this page reappears with on the overflow flag.
  • Up/Down Counters — Chapter 3; a counter is an add/subtract unit (+1/-1) feeding a register — where this arithmetic core meets state.
  • FSM + Datapath — Chapter 4; the adder is the datapath the control FSM sequences, and its overflow flag is a status bit the FSM branches on.
  • Encoding & Conversions — Chapter 5; two's-complement, sign-extension, and the negate that make subtraction-as-addition work.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applied to the adder (pure datapath, its flags feed control).
  • What Is an RTL Design Pattern? — Chapter 0.1; why the adder earns the name "pattern" — a recurring need, a reusable form, a synthesis reality (ripple vs prefix), and a signature failure (carry vs overflow).

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Arithmetic Operators — the + and - that infer the adder/subtractor, and the width rules that make the sum N+1 bits.
  • Physical Data Types (signed/unsigned) — the declared signedness that decides whether the overflow you care about is carry-out or V.
  • Relational Operators — the comparisons a saturating clamp or a branch uses on the sum and its flags.
  • Bitwise Operators — the XOR-invert (^) row and the reductions behind the two's-complement negate and the V computation.

11. Summary

  • Subtraction is addition in disguise, so it is one pattern. a - b = a + ~b + 1, so a single add/subtract structure with one sub mode bit does both: sub XOR-inverts b (b_eff = b ^ {N{sub}}) and doubles as the bottom carry-in that injects the two's-complement +1. The b/~b choice is a 2:1 mux (§1.1); you build and verify one structure, not two.
  • The default structure is ripple-carry, and its delay grows with width. A chain of N full-adders whose carry has to ripple across the whole word — delay linear in N. That linear carry chain is exactly what fast/prefix adders (carry-lookahead, Brent-Kung, Kogge-Stone) exist to break, trading area for a log2(N)-depth carry.
  • The result is N+1 bits wide. Adding two N-bit numbers yields {cout, sum}; the carry-out is the extra top bit. Truncate the sum into N bits without capturing cout and you silently drop it (the width off-by-one). Compute the sum one bit wider, or declare the destination N+1 bits, on purpose.
  • Carry-out and signed overflow are DIFFERENT alarms. Carry-out is the unsigned overflow (carry out of the MSB); signed overflow V = c[N] XOR c[N-1] (or (a[MSB]==b[MSB]) && (sum[MSB]!=a[MSB])) is the signed overflow. They disagree constantly — -1 + 1 sets carry-out but not V; 0x7F + 1 sets V but the unsigned add fits — and using one where you meant the other silently corrupts a saturate or a branch on signed data (§7). Compute the overflow you mean.
  • A carry-in composes and negates; signedness governs extension. The carry-in chains adders for multi-word arithmetic and supplies the subtract +1. When mixing widths, sign-extend signed operands and zero-extend unsigned ones — the declared type, stated explicitly, decides. Verify by checking sum, cout, and ovf against separate reference models across random + boundary operands (max+max, min-min, -1+1, 0x7F+1), parameter-generic in WIDTH — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.