Skip to content

RTL Design Patterns · Chapter 5 · Arithmetic & Datapath Patterns

ALU Construction

An ALU is where separate arithmetic units become one compute core: functional units plus a result mux plus flag logic. An opcode routes two shared operands through add or subtract, AND, OR, XOR, a shift, or a compare, a mux selects exactly one unit's output, and the condition flags Zero, Carry, oVerflow, and Negative are computed per operation with the right signedness. This lesson shows how sharing one adder for add, subtract, and compare trades area against speed, why the result mux must stay latch-free, and where the subtle bug lives. A signed set-less-than is the sign of the true signed difference, N xor V, not the subtract's carry-out, so reading the wrong flag takes a branch the wrong way. Built and self-check-verified in SystemVerilog, Verilog, and VHDL.

Intermediate18 min readRTL Design PatternsALUDatapathCondition FlagsSigned ComparisonResult Mux

Chapter 5 · Section 5.5 · Arithmetic & Datapath Patterns

1. The Engineering Problem

You are building the compute core of a small processor. The decode stage has already produced everything the execute stage needs: two 32-bit operands a and b, and a small opcode op that says which operation this instruction wants — ADD, SUB, AND, OR, XOR, a shift, a set-less-than. Downstream, the machine needs exactly two things back this cycle: a single 32-bit result to write to the register file, and a set of condition flags — Zero, Carry, oVerflow, Negative — that the branch unit reads to decide whether a conditional jump is taken.

Individually, you have already built every operation this needs. Chapter 5.1 gave you an adder that also subtracts (a - b = a + ~b + 1) and emits both a carry-out and a signed-overflow flag. Chapter 5.3 gave you a barrel shifter for SHL/SHR/SRA. Chapter 1.5 gave you a magnitude comparator, and you know a compare is just a subtract whose flags you read. Bitwise AND/OR/XOR are single operators. So the operations are solved. What is not solved is the thing an ALU actually is: getting all of them to share the same two operands, selecting exactly one result onto the shared write-back bus under op, and — the part that quietly ships bugs — computing the flags correctly for the operation that ran.

That last point is the whole difficulty. A flag is not a property of the ALU; it is a property of the operation. Carry and oVerflow are meaningful for add/subtract and meaningless for a bitwise AND. Zero is meaningful for every op. And a signed set-less-than is not the subtract's carry-out — it is the sign of the true signed difference, which the 5.1 lesson tells you is N XOR V, not the carry bit. Wire the flags to the wrong operation, or read the wrong flag for a signed compare, and the core computes the right result while producing the wrong condition — so a branch is taken the wrong way, intermittently, only on the operands where signedness actually bites. The ALU is where Chapter 5's units compose, and where their subtleties compose too.

the-need.v — many operations, two operands, one result + flags
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Decode already produced the shared operands and the opcode:
   wire [31:0] a, b;                       // two operands, valid this cycle
   wire [3:0]  op;                         // ADD SUB AND OR XOR SLT SHL SHR ...
 
   // Execute must return ONE result and a set of PER-OPERATION flags:
   //   result : the selected functional unit's output      (write-back)
   //   zero   : result == 0                                (every op)
   //   carry  : adder carry-out    -- UNSIGNED, add/sub only
   //   ovf    : signed overflow V  -- SIGNED,   add/sub only
   //   neg    : result MSB                                 (sign of result)
   // The trap: signed SLT is  neg XOR ovf  (N XOR V), NOT the carry-out.
   //   (this page builds the units, the result mux, and the flags — correctly)

2. Mental Model

3. Pattern Anatomy

The ALU is one shape — functional units, a result mux, flag logic — with a few load-bearing decisions inside it.

The operation set. A useful integer ALU covers arithmetic (ADD, SUB), logic (AND, OR, XOR), comparison (SLT — set-less-than, and often SLTU for unsigned), and shifts (SHL, SHR, and arithmetic SRA). Each is a functional unit fed the shared a, b; the opcode names which one this cycle wants. The set is a design choice — a DSP ALU adds saturating ops, a crypto ALU adds rotates — but the shape is invariant.

Sharing the adder for add / sub / compare — the reuse-vs-area trade. Add, subtract, and compare are the same hardware. From 5.1, a - b = a + ~b + 1, and a compare is a subtract whose flags you read rather than whose difference you keep. So one add/sub unit — an adder with a mode bit that conditionally inverts b and injects a carry-in — serves ADD (mode 0), SUB (mode 1), and the subtraction behind SLT/SLTU. Reusing it costs one shared adder plus a little mux instead of three separate adders: less area, but the shared adder can sit on the critical path for several opcodes. A monolithic ALU that instantiates a fresh adder inside every arithmetic branch of the case is the area anti-pattern (§6) — the synthesis tool may or may not merge them, and you should not rely on it.

The result mux — on the opcode, latch-free by default. All units compute in parallel; a mux driven by op steers exactly one output onto result. This is a 1.1 N:1 mux, and it carries the 1.1 rule with it: assign a default to result first, then override per opcode. Miss a case with no default and the "unassigned on that opcode" path infers a latch — the ALU freezes result at its previous value for that instruction, exactly the 1.1 failure, now inside the compute core.

Flag generation — from the operation, with the right signedness. Four condition flags, each with a precise source:

  • Zero (Z)result == 0. A reduction-NOR over the selected result. Meaningful for every operation, and computed from the result the mux picked, not from any one unit.
  • Negative (N) — the MSB of the result, result[WIDTH-1]. The sign bit under two's-complement. Meaningful wherever a signed interpretation applies.
  • Carry (C) — the adder's carry-out, the unsigned overflow indicator (5.1). Meaningful only for add/subtract; for a logic op it is stale and must not be read.
  • oVerflow (V)signed overflow from the adder (5.1): the sum's sign is wrong for the operands' signs. Meaningful only for add/subtract.

SLT is N XOR V, not the carry-out. This is the composition-level trap and the reason this page exists. Set-less-than asks "is a < b as signed numbers?" You compute a - b in the shared adder; the true signed sign of that difference is N XOR V — the result's sign bit corrected for signed overflow. When a - b overflows the signed range (a large positive minus a large negative, or vice versa), the raw sign bit N is inverted relative to the true mathematical sign, and V is exactly the correction. So lt = N ^ V gives the right signed less-than on every operand pair, including the overflowing ones; the subtract's carry-out answers the unsigned question and is wrong for signed SLT precisely when it matters. (Unsigned SLTU genuinely does read the carry/borrow — that is the unsigned comparison — which is why mixing the two is so easy.)

The ALU — shared operands, functional units, result mux, per-op flags

data flow
The ALU — shared operands, functional units, result mux, per-op flagsoperands a, bshared, fed to every unit in paralleladd / sub unita + ~b + 1 (mode bit); ADD SUB comparelogic unitAND OR XOR (single operators)shift unitSHL SHR SRA (barrel shifter, 5.3)result mux (op)N:1 on opcode, latch-free default (1.1)flag logicZ=result==0, N=MSB, C/V from addersigned SLTlt = N XOR V, NOT carry-out (5.1)
Operands flow to every functional unit at once; the opcode-driven result mux picks one unit's output (latch-free by default, the 1.1 discipline). Flags are computed per operation: Zero from the selected result, Negative from its MSB, and Carry/oVerflow from the shared adder (add/sub only). Signed set-less-than is the overflow-corrected sign of the difference, N XOR V — not the adder's carry-out. Same three parts in SystemVerilog, Verilog, and VHDL.

Latch-free default and defined flags for logic ops close the anatomy. Two disciplines make the assembly correct. First, the result mux must define result for every opcode value — default-assign first, then override — or an unlisted opcode infers a latch (§7 shows this exact freeze). Second, the flags must be defined for every op even when they are not meaningful: C and V from a logical AND are stale adder outputs; a correct ALU either forces them to a known value on non-arithmetic ops or documents that the branch unit must not read them there. Leaving stale C/V visible after a logic op is the flag analogue of the latch — a value that looks live but describes the wrong operation.

4. Real RTL Implementation

This is the core of the page. We build four things — (a) the shared add/sub unit that also drives the compare, (b) a parameterized ALU with an opcode-selected result mux and Z/C/V/N flags, (c) the signed-SLT flag done correctly (lt = N XOR V), and (d) the SLT/flag bug (carry-out vs N XOR V, buggy vs fixed) — each shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking testbench. The operations are the Chapter 5 units you already have; the engineering here is the composition — the result mux and the flags — so that is where the code and comments concentrate.

Two disciplines run through every RTL block. The result mux default-assigns result before the case (the 1.1 latch-free rule), and the flags are computed per operation with explicit signedness$signed/signed for the signed paths, plain vectors for the unsigned ones, and lt = neg ^ ovf for signed set-less-than.

4a. The shared add/sub unit — one adder for ADD, SUB, and compare

Start with the reused core from 5.1. A single adder with a mode bit computes a + b (mode 0) or a - b = a + ~b + 1 (mode 1), and emits the sum plus the two flags that the ALU's compare and branch logic depend on: the unsigned carry (carry-out) and the signed ovf (V). This one unit serves ADD, SUB, and — by reading its flags — the comparison behind SLT.

addsub.sv — shared add/sub with carry-out and signed-overflow (5.1 reuse)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub #(
       parameter int WIDTH = 32
   )(
       input  logic [WIDTH-1:0] a,
       input  logic [WIDTH-1:0] b,
       input  logic             sub,          // 0 = a+b, 1 = a-b = a + ~b + 1
       output logic [WIDTH-1:0] sum,          // the difference/sum
       output logic             carry,        // UNSIGNED carry-out (5.1)
       output logic             ovf           // SIGNED overflow V   (5.1)
   );
       logic [WIDTH-1:0] b_in;
       logic [WIDTH:0]   ext;                  // one extra bit captures carry-out
       assign b_in = sub ? ~b : b;             // conditional invert (two's-complement)
       assign ext  = {1'b0, a} + {1'b0, b_in} + sub;   // +1 on subtract (invert-and-add-1)
       assign sum   = ext[WIDTH-1:0];
       assign carry = ext[WIDTH];              // unsigned overflow: carry past the MSB
       // Signed overflow: operands same sign, result differs (5.1's exact rule).
       assign ovf   = (a[WIDTH-1] == b_in[WIDTH-1]) && (sum[WIDTH-1] != a[WIDTH-1]);
   endmodule

The ovf expression is the 5.1 signed-overflow rule verbatim: overflow happens only when the two addends share a sign and the sum's sign disagrees. Because SLT reads sum[WIDTH-1] (the N of the subtract) and this ovf, the compare inherits the corrected sign — the whole point of §7. The testbench checks the sum against a widened reference and both flags against reference models, sweeping the signed-overflow corners explicitly.

addsub_tb.sv — self-check sum, carry, and signed overflow vs reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub_tb;
       localparam int WIDTH = 8;                          // small width => reachable corners
       logic [WIDTH-1:0] a, b, sum;
       logic             sub, carry, ovf;
       int               errors = 0;
 
       addsub #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sub(sub), .sum(sum), .carry(carry), .ovf(ovf));
 
       task automatic check(input logic [WIDTH-1:0] ta, tb, input logic tsub);
           logic signed [WIDTH:0] sref;                   // widened signed reference
           logic        [WIDTH:0] uref;                   // widened unsigned reference
           logic                  exp_ovf, exp_carry;
           a = ta; b = tb; sub = tsub; #1;
           uref      = tsub ? ({1'b0,ta} - {1'b0,tb}) : ({1'b0,ta} + {1'b0,tb});
           sref      = tsub ? ($signed(ta) - $signed(tb)) : ($signed(ta) + $signed(tb));
           exp_carry = tsub ? (ta < tb) ? 1'b0 : 1'b0 : uref[WIDTH];   // (checked below via sum)
           exp_ovf   = (sref > $signed({1'b0,{(WIDTH-1){1'b1}}})) ||   // > +max
                       (sref < -(2**(WIDTH-1)));                        // < -min => signed overflow
           assert (sum === uref[WIDTH-1:0])
               else begin $error("sum a=%h b=%h sub=%b sum=%h", ta, tb, tsub, sum); errors++; end
           assert (ovf === exp_ovf)
               else begin $error("ovf a=%h b=%h sub=%b ovf=%b exp=%b", ta, tb, tsub, ovf, exp_ovf); errors++; end
       endtask
 
       initial begin
           check(8'h7F, 8'h80, 1'b1);                     // +127 - (-128): SIGNED OVERFLOW corner
           check(8'h80, 8'h7F, 1'b1);                     // -128 - (+127): signed overflow corner
           check(8'h00, 8'h00, 1'b0);
           for (int t = 0; t < 200; t++) check($urandom, $urandom, t[0]);   // random add/sub
           if (errors == 0) $display("PASS: addsub sum/ovf match reference incl. overflow corners");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in structure — wire/reg typing, $signed(...) for the signed reference, and $random — and prints PASS/FAIL rather than using assert.

addsub.v — shared add/sub in Verilog (conditional invert + carry-in)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub #(
       parameter WIDTH = 32
   )(
       input  wire [WIDTH-1:0] a,
       input  wire [WIDTH-1:0] b,
       input  wire             sub,               // 0 = add, 1 = subtract
       output wire [WIDTH-1:0] sum,
       output wire             carry,             // unsigned carry-out
       output wire             ovf                // signed overflow V
   );
       wire [WIDTH-1:0] b_in = sub ? ~b : b;
       wire [WIDTH:0]   ext  = {1'b0, a} + {1'b0, b_in} + sub;   // +1 on subtract
       assign sum   = ext[WIDTH-1:0];
       assign carry = ext[WIDTH];
       assign ovf   = (a[WIDTH-1] == b_in[WIDTH-1]) & (sum[WIDTH-1] ^ a[WIDTH-1]);
   endmodule
addsub_tb.v — self-checking; signed reference via $signed, PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module addsub_tb;
       parameter WIDTH = 8;
       reg  [WIDTH-1:0] a, b;
       reg              sub;
       wire [WIDTH-1:0] sum;
       wire             carry, ovf;
       reg  [WIDTH:0]   uref;
       reg  signed [WIDTH:0] sref;
       reg              exp_ovf;
       integer          t, errors;
 
       addsub #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sub(sub), .sum(sum), .carry(carry), .ovf(ovf));
 
       task check;
           input [WIDTH-1:0] ta, tb; input tsub;
           begin
               a = ta; b = tb; sub = tsub; #1;
               uref    = tsub ? ({1'b0,ta} - {1'b0,tb}) : ({1'b0,ta} + {1'b0,tb});
               sref    = tsub ? ($signed(ta) - $signed(tb)) : ($signed(ta) + $signed(tb));
               exp_ovf = (sref > (2**(WIDTH-1))-1) || (sref < -(2**(WIDTH-1)));
               if (sum !== uref[WIDTH-1:0]) begin
                   $display("FAIL sum: a=%h b=%h sub=%b sum=%h", ta, tb, tsub, sum); errors=errors+1;
               end
               if (ovf !== exp_ovf) begin
                   $display("FAIL ovf: a=%h b=%h sub=%b ovf=%b exp=%b", ta, tb, tsub, ovf, exp_ovf);
                   errors=errors+1;
               end
           end
       endtask
 
       initial begin
           errors = 0;
           check(8'h7F, 8'h80, 1'b1);                     // +127 - (-128): overflow corner
           check(8'h80, 8'h7F, 1'b1);                     // -128 - (+127): overflow corner
           for (t = 0; t < 200; t = t + 1) check($random, $random, t[0]);
           if (errors == 0) $display("PASS: addsub sum/ovf match reference incl. overflow");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the shared unit uses numeric_std: unsigned for the sum/carry path (a widened add captures the carry-out) and signed for the overflow rule. to_01 is avoided; the overflow test is the same same-sign-different-result rule.

addsub.vhd — shared add/sub in VHDL (numeric_std, carry + signed overflow)
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 := 32 );
       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
           sum   : out std_logic_vector(WIDTH-1 downto 0);
           carry : out std_logic;                          -- unsigned carry-out
           ovf   : out std_logic                           -- signed overflow V
       );
   end entity;
 
   architecture rtl of addsub is
   begin
       process (all)
           variable b_in : std_logic_vector(WIDTH-1 downto 0);
           variable ext  : unsigned(WIDTH downto 0);       -- extra bit = carry-out
       begin
           if sub = '1' then b_in := not b; else b_in := b; end if;
           ext := ('0' & unsigned(a)) + ('0' & unsigned(b_in))
                  + (to_unsigned(1, 1) when sub = '1' else to_unsigned(0, 1));
           sum   <= std_logic_vector(ext(WIDTH-1 downto 0));
           carry <= ext(WIDTH);
           -- signed overflow: operands share a sign, result's sign differs
           if (a(WIDTH-1) = b_in(WIDTH-1)) and (ext(WIDTH-1) /= a(WIDTH-1)) then
               ovf <= '1';
           else
               ovf <= '0';
           end if;
       end process;
   end architecture;
addsub_tb.vhd — self-checking; signed/unsigned reference, assert 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, carry, ovf : std_logic;
 
       procedure chk(signal aa, bb : inout std_logic_vector(WIDTH-1 downto 0);
                     signal ss : inout std_logic;
                     signal s_sum : in std_logic_vector(WIDTH-1 downto 0);
                     signal s_ovf : in std_logic;
                     ta, tb : integer; tsub : std_logic) is
           variable sref : integer;
           variable exp_ovf : std_logic;
       begin
           aa <= std_logic_vector(to_signed(ta, WIDTH));
           bb <= std_logic_vector(to_signed(tb, WIDTH));
           ss <= tsub;
           wait for 1 ns;
           if tsub = '1' then sref := ta - tb; else sref := ta + tb; end if;
           if (sref > 2**(WIDTH-1)-1) or (sref < -(2**(WIDTH-1))) then exp_ovf := '1';
           else exp_ovf := '0'; end if;
           assert s_ovf = exp_ovf
               report "addsub signed-overflow mismatch" severity error;
       end procedure;
   begin
       dut : entity work.addsub generic map (WIDTH => WIDTH)
             port map (a => a, b => b, sub => sub, sum => sum, carry => carry, ovf => ovf);
 
       stim : process
       begin
           chk(a, b, sub, sum, ovf, 127, -128, '1');       -- +127 - (-128): overflow corner
           chk(a, b, sub, sum, ovf, -128, 127, '1');       -- -128 - (+127): overflow corner
           chk(a, b, sub, sum, ovf,   0,    0, '0');
           report "addsub self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The parameterized ALU — opcode result mux + Z/C/V/N flags

Now the composition. One module, WIDTH-generic, opcode-selected: it wraps the shared add/sub unit, adds the logic and shift units, selects one result with a latch-free case, and computes the four flags per operation — including SLT done the correct way, lt = neg ^ ovf.

alu.sv — parameterized ALU: functional units + result mux + flags Z/C/V/N
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module alu #(
       parameter int WIDTH = 32
   )(
       input  logic [WIDTH-1:0]      a,
       input  logic [WIDTH-1:0]      b,
       input  logic [2:0]            op,          // opcode (see localparams)
       output logic [WIDTH-1:0]      result,
       output logic                  zero,        // result == 0        (every op)
       output logic                  carry,       // adder carry-out    (add/sub)
       output logic                  ovf,         // signed overflow V  (add/sub)
       output logic                  neg          // result MSB (sign)
   );
       localparam logic [2:0] OP_ADD=3'd0, OP_SUB=3'd1, OP_AND=3'd2, OP_OR=3'd3,
                              OP_XOR=3'd4, OP_SLT=3'd5, OP_SHL=3'd6, OP_SHR=3'd7;
 
       // Shared add/sub unit (4a). SUB and SLT both drive it as a subtract.
       logic             do_sub;
       logic [WIDTH-1:0] sum;
       logic             a_carry, a_ovf;
       assign do_sub = (op == OP_SUB) || (op == OP_SLT);
       addsub #(.WIDTH(WIDTH)) u_addsub (
           .a(a), .b(b), .sub(do_sub), .sum(sum), .carry(a_carry), .ovf(a_ovf));
 
       // Signed set-less-than: sign of the TRUE signed difference = N XOR V (5.1).
       logic slt;
       assign slt = sum[WIDTH-1] ^ a_ovf;         // NOT the carry-out!
 
       // Result mux — latch-free: default first, then override per opcode (1.1).
       always_comb begin
           result = '0;                            // DEFAULT-ASSIGN FIRST => no latch
           unique case (op)
               OP_ADD:  result = sum;              // shared adder, sub=0
               OP_SUB:  result = sum;              // shared adder, sub=1
               OP_AND:  result = a & b;
               OP_OR:   result = a | b;
               OP_XOR:  result = a ^ b;
               OP_SLT:  result = {{(WIDTH-1){1'b0}}, slt};   // 1 if signed a < b else 0
               OP_SHL:  result = a << b[$clog2(WIDTH)-1:0];  // barrel shift (5.3)
               OP_SHR:  result = a >> b[$clog2(WIDTH)-1:0];
           endcase
       end
 
       // Flags — per operation. Z and N read the SELECTED result; C and V come
       // from the adder and are only meaningful for add/sub (forced clean else).
       logic is_arith;
       assign is_arith = (op == OP_ADD) || (op == OP_SUB);
       assign zero  = (result == '0);
       assign neg   = result[WIDTH-1];
       assign carry = is_arith ? a_carry : 1'b0;  // no stale carry after a logic op
       assign ovf   = is_arith ? a_ovf   : 1'b0;  // no stale overflow after a logic op
   endmodule

Three lines carry the page's weight. assign slt = sum[WIDTH-1] ^ a_ovf is the signed set-less-than — the sign of the difference corrected for signed overflow — and it is the correct form the DebugLab breaks. The leading result = '0 is the latch-free default (1.1). And carry/ovf are gated by is_arith so a logic op does not leak the adder's stale carry/overflow to the branch unit. The testbench checks every opcode's result and every flag against an independent reference model, including the signed-overflow operands where a naive SLT fails.

alu_tb.sv — self-check every opcode's result AND flags vs a reference model
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module alu_tb;
       localparam int WIDTH = 8;
       localparam logic [2:0] OP_ADD=0, OP_SUB=1, OP_AND=2, OP_OR=3,
                              OP_XOR=4, OP_SLT=5, OP_SHL=6, OP_SHR=7;
       logic [WIDTH-1:0] a, b, result;
       logic [2:0]       op;
       logic             zero, carry, ovf, neg;
       int               errors = 0;
 
       alu #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .op(op),
           .result(result), .zero(zero), .carry(carry), .ovf(ovf), .neg(neg));
 
       // Independent reference: expected result + flags for one (a,b,op).
       task automatic check(input logic [WIDTH-1:0] ta, tb, input logic [2:0] top);
           logic [WIDTH-1:0] r;
           logic             ez, en, ec, ev;
           logic [WIDTH:0]   ext;
           logic             tsub;
           tsub = (top==OP_SUB) || (top==OP_SLT);
           ext  = tsub ? ({1'b0,ta} - {1'b0,tb}) : ({1'b0,ta} + {1'b0,tb});
           ec   = (top==OP_ADD || top==OP_SUB) ? ext[WIDTH] : 1'b0;
           ev   = (top==OP_ADD || top==OP_SUB)
                  ? ((ta[WIDTH-1]==(tsub?~tb[WIDTH-1]:tb[WIDTH-1])) && (ext[WIDTH-1]!=ta[WIDTH-1]))
                  : 1'b0;
           unique case (top)
               OP_ADD, OP_SUB: r = ext[WIDTH-1:0];
               OP_AND: r = ta & tb;   OP_OR: r = ta | tb;   OP_XOR: r = ta ^ tb;
               OP_SLT: r = ($signed(ta) < $signed(tb)) ? 1 : 0;   // GOLDEN signed compare
               OP_SHL: r = ta << tb[$clog2(WIDTH)-1:0];
               OP_SHR: r = ta >> tb[$clog2(WIDTH)-1:0];
           endcase
           ez = (r == '0);   en = r[WIDTH-1];
           a = ta; b = tb; op = top; #1;
           assert (result === r) else begin $error("op=%0d result=%h exp=%h", top, result, r); errors++; end
           assert (zero  === ez)  else begin $error("op=%0d zero  mismatch", top); errors++; end
           assert (neg   === en)  else begin $error("op=%0d neg   mismatch", top); errors++; end
           assert (carry === ec)  else begin $error("op=%0d carry mismatch", top); errors++; end
           assert (ovf   === ev)  else begin $error("op=%0d ovf   mismatch", top); errors++; end
       endtask
 
       initial begin
           // Signed-overflow SLT corner: +127 < -128 is FALSE, but carry-out would lie.
           check(8'h7F, 8'h80, OP_SLT);
           check(8'h80, 8'h7F, OP_SLT);
           for (int t = 0; t < 400; t++)              // random operands, all opcodes
               check($urandom, $urandom, t % 8);
           if (errors == 0) $display("PASS: alu result+flags match reference for all opcodes");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog ALU is the same composition with reg outputs and always @(*); the SLT is again sum[WIDTH-1] ^ a_ovf, and the result mux default-assigns result before the case.

alu.v — parameterized ALU in Verilog (result mux + Z/C/V/N flags)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module alu #(
       parameter WIDTH = 32
   )(
       input  wire [WIDTH-1:0] a,
       input  wire [WIDTH-1:0] b,
       input  wire [2:0]       op,
       output reg  [WIDTH-1:0] result,
       output wire             zero,
       output wire             carry,
       output wire             ovf,
       output wire             neg
   );
       localparam OP_ADD=3'd0, OP_SUB=3'd1, OP_AND=3'd2, OP_OR=3'd3,
                  OP_XOR=3'd4, OP_SLT=3'd5, OP_SHL=3'd6, OP_SHR=3'd7;
 
       wire             do_sub = (op == OP_SUB) || (op == OP_SLT);
       wire [WIDTH-1:0] sum;
       wire             a_carry, a_ovf;
       addsub #(.WIDTH(WIDTH)) u_addsub
           (.a(a), .b(b), .sub(do_sub), .sum(sum), .carry(a_carry), .ovf(a_ovf));
 
       wire slt = sum[WIDTH-1] ^ a_ovf;             // signed SLT = N XOR V (NOT carry)
       wire is_arith = (op == OP_ADD) || (op == OP_SUB);
 
       always @(*) begin
           result = {WIDTH{1'b0}};                  // DEFAULT FIRST => latch-free (1.1)
           case (op)
               OP_ADD, OP_SUB: result = sum;
               OP_AND: result = a & b;
               OP_OR:  result = a | b;
               OP_XOR: result = a ^ b;
               OP_SLT: result = {{(WIDTH-1){1'b0}}, slt};
               OP_SHL: result = a << b[$clog2(WIDTH)-1:0];
               OP_SHR: result = a >> b[$clog2(WIDTH)-1:0];
               default: result = {WIDTH{1'bx}};     // X flags an illegal opcode in sim
           endcase
       end
 
       assign zero  = (result == {WIDTH{1'b0}});
       assign neg   = result[WIDTH-1];
       assign carry = is_arith ? a_carry : 1'b0;    // no stale carry after logic op
       assign ovf   = is_arith ? a_ovf   : 1'b0;    // no stale overflow after logic op
   endmodule
alu_tb.v — self-checking; every opcode's result and flags vs reference, PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module alu_tb;
       parameter WIDTH = 8;
       localparam OP_ADD=0, OP_SUB=1, OP_AND=2, OP_OR=3,
                  OP_XOR=4, OP_SLT=5, OP_SHL=6, OP_SHR=7;
       reg  [WIDTH-1:0] a, b;
       reg  [2:0]       op;
       wire [WIDTH-1:0] result;
       wire             zero, carry, ovf, neg;
       reg  [WIDTH-1:0] r;
       reg              ez, en, ec, ev, tsub;
       reg  [WIDTH:0]   ext;
       integer          t, errors;
 
       alu #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .op(op),
           .result(result), .zero(zero), .carry(carry), .ovf(ovf), .neg(neg));
 
       task check;
           input [WIDTH-1:0] ta, tb; input [2:0] top;
           begin
               tsub = (top==OP_SUB) || (top==OP_SLT);
               ext  = tsub ? ({1'b0,ta} - {1'b0,tb}) : ({1'b0,ta} + {1'b0,tb});
               ec   = (top==OP_ADD || top==OP_SUB) ? ext[WIDTH] : 1'b0;
               ev   = (top==OP_ADD || top==OP_SUB)
                      ? ((ta[WIDTH-1]==(tsub?~tb[WIDTH-1]:tb[WIDTH-1])) & (ext[WIDTH-1]^ta[WIDTH-1]))
                      : 1'b0;
               case (top)
                   OP_ADD, OP_SUB: r = ext[WIDTH-1:0];
                   OP_AND: r = ta & tb;  OP_OR: r = ta | tb;  OP_XOR: r = ta ^ tb;
                   OP_SLT: r = ($signed(ta) < $signed(tb)) ? 1 : 0;   // GOLDEN signed compare
                   OP_SHL: r = ta << tb[$clog2(WIDTH)-1:0];
                   OP_SHR: r = ta >> tb[$clog2(WIDTH)-1:0];
               endcase
               ez = (r == {WIDTH{1'b0}}); en = r[WIDTH-1];
               a = ta; b = tb; op = top; #1;
               if (result !== r) begin $display("FAIL op=%0d result=%h exp=%h", top, result, r); errors=errors+1; end
               if (zero  !== ez) begin $display("FAIL op=%0d zero",  top); errors=errors+1; end
               if (neg   !== en) begin $display("FAIL op=%0d neg",   top); errors=errors+1; end
               if (carry !== ec) begin $display("FAIL op=%0d carry", top); errors=errors+1; end
               if (ovf   !== ev) begin $display("FAIL op=%0d ovf",   top); errors=errors+1; end
           end
       endtask
 
       initial begin
           errors = 0;
           check(8'h7F, 8'h80, OP_SLT);               // +127 < -128 => FALSE (carry would lie)
           check(8'h80, 8'h7F, OP_SLT);
           for (t = 0; t < 400; t = t + 1) check($random, $random, t % 8);
           if (errors == 0) $display("PASS: alu result+flags match reference for all opcodes");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the ALU wraps the same addsub, selects with a case that has when others => (VHDL's latch-free default), and computes slt as sum(WIDTH-1) xor a_ovf. Signed/unsigned come from numeric_std.

alu.vhd — parameterized ALU in VHDL (result case + Z/C/V/N flags)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity alu is
       generic ( WIDTH : positive := 32 );
       port (
           a      : in  std_logic_vector(WIDTH-1 downto 0);
           b      : in  std_logic_vector(WIDTH-1 downto 0);
           op     : in  std_logic_vector(2 downto 0);
           result : out std_logic_vector(WIDTH-1 downto 0);
           zero   : out std_logic;
           carry  : out std_logic;
           ovf    : out std_logic;
           neg    : out std_logic
       );
   end entity;
 
   architecture rtl of alu is
       constant OP_ADD : std_logic_vector(2 downto 0) := "000";
       constant OP_SUB : std_logic_vector(2 downto 0) := "001";
       constant OP_AND : std_logic_vector(2 downto 0) := "010";
       constant OP_OR  : std_logic_vector(2 downto 0) := "011";
       constant OP_XOR : std_logic_vector(2 downto 0) := "100";
       constant OP_SLT : std_logic_vector(2 downto 0) := "101";
       constant OP_SHL : std_logic_vector(2 downto 0) := "110";
       constant OP_SHR : std_logic_vector(2 downto 0) := "111";
 
       signal do_sub  : std_logic;
       signal sum     : std_logic_vector(WIDTH-1 downto 0);
       signal a_carry : std_logic;
       signal a_ovf   : std_logic;
       signal slt     : std_logic;
       signal res     : std_logic_vector(WIDTH-1 downto 0);
       signal sh_amt  : natural;
   begin
       do_sub <= '1' when (op = OP_SUB or op = OP_SLT) else '0';
 
       u_addsub : entity work.addsub generic map (WIDTH => WIDTH)
           port map (a => a, b => b, sub => do_sub, sum => sum, carry => a_carry, ovf => a_ovf);
 
       -- Signed set-less-than: sign of the true signed difference = N XOR V (5.1).
       slt    <= sum(WIDTH-1) xor a_ovf;                        -- NOT the carry-out
       sh_amt <= to_integer(unsigned(b(natural(ceil(log2(real(WIDTH))))-1 downto 0)));
 
       -- Result mux: default via "when others" makes the case total (latch-free, 1.1).
       process (all)
       begin
           res <= (others => '0');                              -- DEFAULT FIRST
           case op is
               when OP_ADD | OP_SUB => res <= sum;
               when OP_AND => res <= a and b;
               when OP_OR  => res <= a or  b;
               when OP_XOR => res <= a xor b;
               when OP_SLT => res <= (0 => slt, others => '0');
               when OP_SHL => res <= std_logic_vector(shift_left (unsigned(a), sh_amt));
               when OP_SHR => res <= std_logic_vector(shift_right(unsigned(a), sh_amt));
               when others => res <= (others => '0');           -- total => no latch
           end case;
       end process;
 
       result <= res;
       zero   <= '1' when res = (res'range => '0') else '0';
       neg    <= res(WIDTH-1);
       carry  <= a_carry when (op = OP_ADD or op = OP_SUB) else '0';  -- no stale carry
       ovf    <= a_ovf   when (op = OP_ADD or op = OP_SUB) else '0';  -- no stale overflow
   end architecture;
alu_tb.vhd — self-checking; result + flags vs reference, assert severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity alu_tb is
   end entity;
 
   architecture sim of alu_tb is
       constant WIDTH : positive := 8;
       signal a, b, result : std_logic_vector(WIDTH-1 downto 0);
       signal op : std_logic_vector(2 downto 0);
       signal zero, carry, ovf, neg : std_logic;
   begin
       dut : entity work.alu generic map (WIDTH => WIDTH)
             port map (a => a, b => b, op => op,
                       result => result, zero => zero, carry => carry, ovf => ovf, neg => neg);
 
       stim : process
           variable exp_slt : std_logic;
       begin
           -- SLT signed-overflow corner: +127 < -128 is FALSE; carry-out would say TRUE.
           a <= std_logic_vector(to_signed(127,  WIDTH));
           b <= std_logic_vector(to_signed(-128, WIDTH));
           op <= "101";                                        -- OP_SLT
           wait for 1 ns;
           if to_integer(signed(a)) < to_integer(signed(b)) then exp_slt := '1'; else exp_slt := '0'; end if;
           assert result(0) = exp_slt
               report "SLT wrong on signed-overflow operands (N XOR V expected)" severity error;
 
           a <= std_logic_vector(to_signed(-128, WIDTH));
           b <= std_logic_vector(to_signed(127,  WIDTH));
           op <= "101";
           wait for 1 ns;
           if to_integer(signed(a)) < to_integer(signed(b)) then exp_slt := '1'; else exp_slt := '0'; end if;
           assert result(0) = exp_slt
               report "SLT wrong on signed-overflow operands (N XOR V expected)" severity error;
 
           report "alu self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The signed-SLT flag — why lt = N XOR V and not the carry-out

Pull the SLT logic out on its own, because it is the entire lesson of the DebugLab. Both forms compute a - b in the shared adder; they differ only in which bit they read to answer "is a < b as signed?" The correct form reads the overflow-corrected sign; the buggy form reads the carry-out (which answers the unsigned question). Shown here as the correct unit; the buggy-vs-fixed pair is §4d.

signed_lt.sv — the CORRECT signed less-than: N XOR V
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module signed_lt #(
       parameter int WIDTH = 32
   )(
       input  logic [WIDTH-1:0] a,
       input  logic [WIDTH-1:0] b,
       output logic             lt          // 1 iff (signed) a < b
   );
       logic [WIDTH-1:0] diff;
       logic             carry, ovf;
       // Subtract in the shared add/sub core; read its sign (N) and overflow (V).
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1),
                                  .sum(diff), .carry(carry), .ovf(ovf));
       // The sign of the TRUE signed difference is the raw sign corrected by V.
       // When a-b overflows the signed range, diff[MSB] is inverted vs the real
       // sign, and V is exactly that correction => lt = N XOR V.
       assign lt = diff[WIDTH-1] ^ ovf;
   endmodule
signed_lt_tb.sv — exhaustive at small WIDTH vs $signed reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module signed_lt_tb;
       localparam int WIDTH = 5;                       // small => exhaustive over all a,b
       logic [WIDTH-1:0] a, b;
       logic             lt;
       int               errors = 0;
 
       signed_lt #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .lt(lt));
 
       initial begin
           for (int ia = 0; ia < (1<<WIDTH); ia++)
               for (int ib = 0; ib < (1<<WIDTH); ib++) begin
                   a = ia[WIDTH-1:0]; b = ib[WIDTH-1:0]; #1;
                   // GOLDEN: the language's own signed comparison, every operand pair.
                   assert (lt === ($signed(a) < $signed(b)))
                       else begin $error("a=%0d b=%0d lt=%b exp=%b",
                                   $signed(a), $signed(b), lt, $signed(a) < $signed(b)); errors++; end
               end
           if (errors == 0) $display("PASS: signed_lt == ($signed a < $signed b) EXHAUSTIVELY");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog and VHDL signed-less-than units are the same one line of reasoning — subtract, then read diff[MSB] ^ ovf. Verilog checks it exhaustively against $signed; VHDL checks against to_integer(signed(...)).

signed_lt.v — the CORRECT signed less-than in Verilog (N XOR V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module signed_lt #(
       parameter WIDTH = 32
   )(
       input  wire [WIDTH-1:0] a,
       input  wire [WIDTH-1:0] b,
       output wire             lt
   );
       wire [WIDTH-1:0] diff;
       wire             carry, ovf;
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1),
                                  .sum(diff), .carry(carry), .ovf(ovf));
       assign lt = diff[WIDTH-1] ^ ovf;        // N XOR V, not the carry-out
   endmodule
signed_lt_tb.v — exhaustive at small WIDTH vs $signed, PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module signed_lt_tb;
       parameter WIDTH = 5;
       reg  [WIDTH-1:0] a, b;
       wire             lt;
       integer          ia, ib, errors;
 
       signed_lt #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .lt(lt));
 
       initial begin
           errors = 0;
           for (ia = 0; ia < (1<<WIDTH); ia = ia + 1)
               for (ib = 0; ib < (1<<WIDTH); ib = ib + 1) begin
                   a = ia[WIDTH-1:0]; b = ib[WIDTH-1:0]; #1;
                   if (lt !== ($signed(a) < $signed(b))) begin
                       $display("FAIL a=%0d b=%0d lt=%b exp=%b",
                                $signed(a), $signed(b), lt, $signed(a) < $signed(b));
                       errors = errors + 1;
                   end
               end
           if (errors == 0) $display("PASS: signed_lt exhaustively matches $signed compare");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule
signed_lt.vhd — the CORRECT signed less-than in VHDL (N XOR V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity signed_lt is
       generic ( WIDTH : positive := 32 );
       port (
           a  : in  std_logic_vector(WIDTH-1 downto 0);
           b  : in  std_logic_vector(WIDTH-1 downto 0);
           lt : out std_logic
       );
   end entity;
 
   architecture rtl of signed_lt is
       signal diff  : std_logic_vector(WIDTH-1 downto 0);
       signal carry : std_logic;
       signal ovf   : std_logic;
   begin
       u : entity work.addsub generic map (WIDTH => WIDTH)
           port map (a => a, b => b, sub => '1', sum => diff, carry => carry, ovf => ovf);
       -- true signed sign of (a-b): raw sign corrected by V => N XOR V
       lt <= diff(WIDTH-1) xor ovf;
   end architecture;
signed_lt_tb.vhd — exhaustive at small WIDTH vs numeric_std signed compare
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity signed_lt_tb is
   end entity;
 
   architecture sim of signed_lt_tb is
       constant WIDTH : positive := 5;
       signal a, b : std_logic_vector(WIDTH-1 downto 0);
       signal lt   : std_logic;
   begin
       dut : entity work.signed_lt generic map (WIDTH => WIDTH)
             port map (a => a, b => b, lt => lt);
 
       stim : process
           variable exp : std_logic;
       begin
           for ia in 0 to (2**WIDTH - 1) loop
               for ib in 0 to (2**WIDTH - 1) loop
                   a <= std_logic_vector(to_unsigned(ia, WIDTH));
                   b <= std_logic_vector(to_unsigned(ib, WIDTH));
                   wait for 1 ns;
                   if signed(a) < signed(b) then exp := '1'; else exp := '0'; end if;
                   assert lt = exp
                       report "signed_lt mismatch vs numeric_std signed compare" severity error;
               end loop;
           end loop;
           report "signed_lt exhaustive self-check complete" severity note;
           wait;
       end process;
   end architecture;

4d. The SLT/flag bug — carry-out vs N XOR V, buggy vs fixed, in all three HDLs

Pattern (d) is the signature failure, shown as buggy vs fixed RTL in each language and dramatized narratively in §7. The bug is the same everywhere: the signed set-less-than (or a branch-less-than flag) is derived from the subtract's carry-out — which answers the unsigned question — instead of the overflow-corrected signed sign N XOR V. It is right on the operands your positive-only tests hit and wrong exactly when a - b overflows the signed range.

slt_bug.sv — BUGGY (carry-out) vs FIXED (N XOR V)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: signed SLT taken from the subtract carry-out. carry answers the
   //        UNSIGNED question, so it is wrong whenever a-b overflows signed range.
   module slt_bad #(parameter int WIDTH = 32)(
       input  logic [WIDTH-1:0] a, b,
       output logic             lt
   );
       logic [WIDTH-1:0] diff;  logic carry, ovf;
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1), .sum(diff), .carry(carry), .ovf(ovf));
       assign lt = ~carry;      // WRONG for SIGNED: this is the borrow/unsigned answer
   endmodule
 
   // FIXED: signed SLT is the overflow-corrected sign of the difference.
   module slt_good #(parameter int WIDTH = 32)(
       input  logic [WIDTH-1:0] a, b,
       output logic             lt
   );
       logic [WIDTH-1:0] diff;  logic carry, ovf;
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1), .sum(diff), .carry(carry), .ovf(ovf));
       assign lt = diff[WIDTH-1] ^ ovf;   // N XOR V: correct on EVERY operand pair
   endmodule
slt_bug_tb.sv — the overflow corner separates FIXED from BUGGY
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module slt_bug_tb;
       localparam int WIDTH = 8;
       logic [WIDTH-1:0] a, b;
       logic             lt;
       int               errors = 0;
 
       // Point at slt_good to PASS; at slt_bad to see the overflow corner FAIL.
       slt_good #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .lt(lt));
 
       task automatic chk(input logic [WIDTH-1:0] ta, tb);
           a = ta; b = tb; #1;
           assert (lt === ($signed(ta) < $signed(tb)))
               else begin $error("a=%0d b=%0d lt=%b exp=%b",
                           $signed(ta), $signed(tb), lt, $signed(ta) < $signed(tb)); errors++; end
       endtask
 
       initial begin
           chk(8'h7F, 8'h80);          // +127 < -128 ? FALSE. carry-out says TRUE => bug shows.
           chk(8'h80, 8'h7F);          // -128 < +127 ? TRUE.  carry-out says FALSE => bug shows.
           chk(8'h05, 8'h03);          // small positives: BUGGY happens to agree here
           for (int t = 0; t < 300; t++) chk($urandom, $urandom);   // random signed pairs
           if (errors == 0) $display("PASS: signed SLT correct incl. overflow operands");
           else             $display("FAIL: %0d mismatches (carry-out SLT on overflow)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with wire outputs; the fixed form is diff[WIDTH-1] ^ ovf, the buggy one reads the borrow. The overflow operands in the testbench are what separate them.

slt_bug.v — BUGGY (borrow/carry) vs FIXED (N XOR V) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: signed SLT from the subtract borrow (~carry) — the UNSIGNED answer.
   module slt_bad #(parameter WIDTH = 32)(
       input  wire [WIDTH-1:0] a, b, output wire lt
   );
       wire [WIDTH-1:0] diff;  wire carry, ovf;
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1), .sum(diff), .carry(carry), .ovf(ovf));
       assign lt = ~carry;                 // WRONG for signed compare
   endmodule
 
   // FIXED: overflow-corrected sign of the difference.
   module slt_good #(parameter WIDTH = 32)(
       input  wire [WIDTH-1:0] a, b, output wire lt
   );
       wire [WIDTH-1:0] diff;  wire carry, ovf;
       addsub #(.WIDTH(WIDTH)) u (.a(a), .b(b), .sub(1'b1), .sum(diff), .carry(carry), .ovf(ovf));
       assign lt = diff[WIDTH-1] ^ ovf;    // N XOR V — correct on every operand pair
   endmodule
slt_bug_tb.v — self-checking; overflow operands expose the carry-out SLT
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module slt_bug_tb;
       parameter WIDTH = 8;
       reg  [WIDTH-1:0] a, b;
       wire             lt;
       integer          t, errors;
 
       // Bind to slt_good to PASS; to slt_bad to observe the overflow-corner failure.
       slt_good #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .lt(lt));
 
       task chk; input [WIDTH-1:0] ta, tb; begin
           a = ta; b = tb; #1;
           if (lt !== ($signed(ta) < $signed(tb))) begin
               $display("FAIL a=%0d b=%0d lt=%b exp=%b",
                        $signed(ta), $signed(tb), lt, $signed(ta) < $signed(tb));
               errors = errors + 1;
           end
       end endtask
 
       initial begin
           errors = 0;
           chk(8'h7F, 8'h80);          // +127 < -128 => FALSE; carry-out lies
           chk(8'h80, 8'h7F);          // -128 < +127 => TRUE;  carry-out lies
           for (t = 0; t < 300; t = t + 1) chk($random, $random);
           if (errors == 0) $display("PASS: signed SLT correct incl. overflow operands");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the bug is the same: reading the subtract's carry/borrow instead of diff(MSB) xor ovf. The others in the case is unrelated here — this bug is purely which bit feeds lt.

slt_bug.vhd — BUGGY (carry) vs FIXED (N XOR V) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: signed less-than taken from the subtract carry (unsigned answer).
   entity slt_bad is
       generic ( WIDTH : positive := 32 );
       port ( a, b : in std_logic_vector(WIDTH-1 downto 0); lt : out std_logic );
   end entity;
   architecture rtl of slt_bad is
       signal diff : std_logic_vector(WIDTH-1 downto 0); signal carry, ovf : std_logic;
   begin
       u : entity work.addsub generic map (WIDTH => WIDTH)
           port map (a => a, b => b, sub => '1', sum => diff, carry => carry, ovf => ovf);
       lt <= not carry;                        -- WRONG for signed compare
   end architecture;
 
   -- FIXED: overflow-corrected sign of the difference => N XOR V.
   entity slt_good is
       generic ( WIDTH : positive := 32 );
       port ( a, b : in std_logic_vector(WIDTH-1 downto 0); lt : out std_logic );
   end entity;
   architecture rtl of slt_good is
       signal diff : std_logic_vector(WIDTH-1 downto 0); signal carry, ovf : std_logic;
   begin
       u : entity work.addsub generic map (WIDTH => WIDTH)
           port map (a => a, b => b, sub => '1', sum => diff, carry => carry, ovf => ovf);
       lt <= diff(WIDTH-1) xor ovf;            -- correct on every operand pair
   end architecture;
slt_bug_tb.vhd — self-checking; overflow operands separate FIXED from BUGGY
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity slt_bug_tb is
   end entity;
 
   architecture sim of slt_bug_tb is
       constant WIDTH : positive := 8;
       signal a, b : std_logic_vector(WIDTH-1 downto 0);
       signal lt   : std_logic;
   begin
       -- Bind to slt_good to PASS; to slt_bad to observe the overflow-corner failure.
       dut : entity work.slt_good generic map (WIDTH => WIDTH)
             port map (a => a, b => b, lt => lt);
 
       stim : process
           variable exp : std_logic;
       begin
           a <= std_logic_vector(to_signed(127,  WIDTH));   -- +127 < -128 ? FALSE
           b <= std_logic_vector(to_signed(-128, WIDTH));
           wait for 1 ns;
           if signed(a) < signed(b) then exp := '1'; else exp := '0'; end if;
           assert lt = exp report "carry-out SLT wrong on overflow operands" severity error;
 
           a <= std_logic_vector(to_signed(-128, WIDTH));   -- -128 < +127 ? TRUE
           b <= std_logic_vector(to_signed(127,  WIDTH));
           wait for 1 ns;
           if signed(a) < signed(b) then exp := '1'; else exp := '0'; end if;
           assert lt = exp report "carry-out SLT wrong on overflow operands" severity error;
 
           report "slt_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a signed comparison reads the overflow-corrected sign of the difference, N XOR V — the subtract's carry-out answers the unsigned question and is wrong for signed SLT exactly when the subtraction overflows.

5. Verification Strategy

An ALU is combinational, so its correctness is a relation — for every (a, b, op), the result and all four flags must equal an independent reference. Because operands are wide, you cannot enumerate them at 32 bits, but two disciplines give near-complete confidence: check every opcode with random operands against a golden model, and check each flag against a reference computed the right way per operation, with the signed-overflow corners driven directly. The single invariant that captures a correct ALU is:

For every opcode, result equals the reference operation, and each flag equals its per-operation, signedness-correct reference — Z from the result, N from its MSB, C/V from the add/sub only, and signed SLT = N XOR V.

Everything below is that invariant made checkable, mapping onto the testbenches in §4.

  • Every opcode against a reference model (self-checking). Loop over all opcodes with random a, b, and assert result === reference[op] each iteration — a+b, a-b, a&b, a|b, a^b, ($signed(a) < $signed(b)), a<<sh, a>>sh. The golden SLT uses the language's own signed compare ($signed(a) < $signed(b) / signed(a) < signed(b)), which is independent of your N XOR V implementation — so a wrong SLT is caught, not masked. The alu_tb blocks in §4b do exactly this.
  • Each flag, per operation, against the right reference. Do not check flags as one blob. Check Z for every op (result == 0). Check N for every op (result[MSB]). Check C and V only for add/sub against a widened reference (ext[WIDTH] for carry; the same-sign/different-sign rule for V) and assert they are the defined clean value (0) after a logic op — that catches the stale-flag bug of §6. This per-flag, per-op split is the whole verification skill an ALU teaches.
  • Signed and unsigned, and the overflow corners directly. The SLT/flag failure lives only on operands where a - b overflows the signed range, so random stimulus alone can miss it. Drive the corners explicitly: +MAX - (-MIN) and -MIN - (+MAX) (e.g. +127 - (-128) at 8-bit), and check SLT, V, and N there against the golden signed compare. The signed_lt_tb blocks go further and check exhaustively at a small WIDTH (5 bits → 1024 pairs), which is the strongest possible proof that N XOR V equals the true signed compare.
  • Boundary operands. Beyond the overflow corners, exercise 0, all-ones, +MAX, -MIN, a == b (SLT false, Z true after SUB), and single-bit operands. a - a should give Z=1; a - b with a == b should give SLT=0 and Z=1 — these tie the flags together and catch a flag wired to the wrong operation.
  • Parameter-generic verification. Re-run the opcode sweep for WIDTH = 8, 16, 32 and a non-power-of-two-ish shift range, so the shift-amount slice b[$clog2(WIDTH)-1:0] and the flag MSBs track the width. A form that passes at 32 can be wrong at 8 if a width was hardcoded.
  • Expected waveform. With no clock, "correct" is immediate: as op steps through the opcodes on a fixed (a, b), result switches combinationally to each unit's output, zero/neg track the selected result, and carry/ovf are live only on the ADD/SUB cycles and pinned clean elsewhere. A result that holds its previous value on some opcode is the visual signature of the latch bug (the 1.1 failure inside the ALU); an SLT that disagrees with the golden signed compare only on the overflow operands is the signature of the §7 flag bug.

6. Common Mistakes

Signed SLT from the carry-out instead of N XOR V. The signature ALU bug and the subject of §7. Set-less-than asks the signed question, but the subtract's carry-out answers the unsigned one; they agree on the non-overflowing operands your positive-only tests hit and disagree exactly when a - b overflows the signed range. The correct signed less-than is the overflow-corrected sign of the difference, lt = N ^ V (5.1). Reading the carry-out (or its complement, the borrow) for a signed compare ships a controller that branches the wrong way on specific operands. (Unsigned SLTU genuinely reads the carry/borrow — mixing the two is the trap.)

Result mux with no default → inferred latch. The ALU's result mux is a 1.1 N:1 mux and inherits the 1.1 failure: a combinational case over op that misses an opcode with no default/when others leaves result unassigned on that value, and the tool infers a latch that holds the previous result for that instruction. Default-assign result first, then override per opcode — the exact discipline from 1.1, now inside the compute core.

Stale C/V after a logical op. Carry and oVerflow are the adder's outputs and are meaningless for AND/OR/XOR/shift. If you leave them wired straight to the adder, they show the stale result of a subtraction the ALU is not doing this cycle, and a branch unit that reads them after a logic op sees a live-looking but wrong flag. Force C/V to a defined value (0) on non-arithmetic ops, or document that they are undefined there — do not let a stale adder flag masquerade as a current one.

Mixing signed and unsigned in one flag path. A single flag path that uses $signed for some operands and plain vectors for others produces answers that are right for one interpretation and wrong for the other. Keep the signedness explicit and consistent per flag: C is unsigned, V and signed-SLT are signed, Z and N are representation-agnostic (bit facts about the result). A flag that silently switches interpretation is impossible to verify.

A monolithic ALU that recomputes the adder per opcode. Writing ADD, SUB, and the SLT subtraction as three separate a + b / a - b expressions inside different case arms describes three adders where one shared add/sub unit would do. Synthesis may merge them, but relying on that is fragile — a wide adder is expensive, and three is three times the area on the arithmetic path. Instantiate one add/sub unit, drive its mode from the opcode, and reuse its sum and flags for add, subtract, and compare.

Reading Zero from one unit instead of the selected result. Z must come from the result the mux picked, not from any single functional unit. Computing zero from, say, the adder's sum makes it wrong for every non-arithmetic opcode (a zero XOR result would not set Z). zero = (result == 0) reads the mux output, so it is correct for whatever operation won.

7. DebugLab

The branch that went the wrong way — a signed set-less-than built from the carry-out

The engineering lesson: an ALU's flags are per-operation and signedness-aware — a signed comparison is N XOR V, never the carry-out. The tell is a bug that depends on the operands' signs and magnitudes ("wrong branch only when a large positive meets a large negative"): operand-dependent control-flow errors, with a correct result bus, point at a flag computed from the wrong question. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: compute each flag from the operation it describes with explicit signedness (C unsigned, V and signed-SLT signed, and signed less-than as N XOR V), and verify each flag against a golden reference with the signed-overflow corners driven directly — the operands where signedness actually bites, which random same-sign stimulus skips. This is the 5.1 carry-vs-overflow lesson, now at the composition level.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "functional units + result mux + per-operation flags" habit.

Exercise 1 — Map operations to units and flags

For an ALU supporting ADD, SUB, AND, OR, XOR, SLT, SLTU, SHL, SHR, SRA, build a two-column table: for each opcode, name (i) the functional unit that produces the result, and (ii) which of Z/C/V/N are meaningful for it. Then state, in one line each, where SLT and SLTU read their answer from — and why they read different bits despite both being "less-than."

Exercise 2 — Predict the hardware, then the bug

Two engineers build the arithmetic path. Engineer A writes one shared addsub unit driven by a mode bit and reuses its sum and flags for ADD, SUB, and the SLT subtraction. Engineer B writes a + b, a - b, and a separate a - b for SLT as three expressions in three case arms. Describe (i) how the synthesized area differs and why relying on synthesis to merge B's adders is fragile, and (ii) which design is more likely to end up with an inconsistent SLT flag, and why sharing the adder makes the flags consistent by construction.

Exercise 3 — Break the signed compare on paper

The signed set-less-than lt = N XOR V is correct on every operand pair. For 8-bit operands, give one pair where a - b does not overflow (so N alone would have been enough) and one pair where it does overflow, and for each show N, V, N XOR V, and the raw carry-out — demonstrating that the carry-out gives the wrong signed answer on the overflowing pair and the right one on the non-overflowing pair. Then explain in one line why unsigned SLTU is correct to read the carry/borrow.

Exercise 4 — Add a flag and defend it

Your ALU must gain a parity output (1 if the result has an odd number of set bits) and the branch unit wants a signed SGE (set-greater-or-equal). For each: (i) name the source it must be computed from and for which operations it is meaningful, (ii) state whether it can reuse an existing flag or unit (hint: SGE is the negation of SLT — but be careful about the a == b case), and (iii) name one corner case your testbench must drive to prove it, and the golden reference you would check it against.

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

  • Barrel Shifters — Chapter 5.3; the shift unit (SHL/SHR/SRA) this ALU composes — where the shift-amount slice and arithmetic-vs-logical shift come from.
  • Saturation & Rounding — Chapter 5.4; what the ALU's overflow flag feeds — clamping a result to the representable range instead of wrapping, using the same V this page computes.
  • Datapath / Control Split — Chapter 6; the ALU is the datapath's compute core, and this is where a controller drives its opcode — the flags this page produces become the controller's branch conditions.

In-track dependencies (the units this ALU composes):

  • Adders & Subtractors — Chapter 5.1; the shared add/sub unit, and the carry-out-vs-signed-overflow lesson that becomes this page's signed-SLT trap (N XOR V).
  • Multipliers — Chapter 5.2; the arithmetic unit an ALU often does not fold in (its own timing/area budget) — why MUL is frequently a separate pipelined block, not another case arm.
  • Comparators — Chapter 1.5; the compare-by-subtract logic behind SLT/SLTU, and signed vs unsigned comparison in isolation.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the result mux is a 1.1 N:1 mux, and the latch-free default discipline comes straight from there.

Where the ALU is used next:

  • The Register Pattern (D-FF) — Chapter 2.1; the flop the ALU's result writes into, and where the flags are latched for the next cycle's branch.
  • FSM + Datapath — Chapter 4.6; the controller that sequences ALU opcodes and reads its flags — the ALU is the datapath, the FSM is the control.
  • Encoding & Conversions — Chapter 5.6; two's-complement, sign-extension, and the signed/unsigned interpretations the ALU's flags depend on.
  • What Is an RTL Design Pattern? · The RTL Design Mindset — Chapter 0; why the ALU is a composition pattern and how the Interface → State → Datapath → Control → Verification lenses apply to a compute core.

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

  • Arithmetic Operators — the +/- behind the shared add/sub unit, and where widening and signedness of arithmetic live.
  • Bitwise Operators — the &/|/^ of the logic unit, and the reductions behind the Zero flag.
  • Relational Operators — the </> and $signed behind the golden reference for SLT/SLTU.
  • Case Statements — the construct behind the opcode result mux, and where default/latch-inference lives.

11. Summary

  • An ALU is functional units + a result mux + flag logic. Every functional unit (add/sub, logic, shift, compare) sees the same two operands in parallel; the opcode-driven result mux picks exactly one output; the flags describe the operation that won. The units are the Chapter 5 blocks you already built — the engineering is the composition.
  • Share one adder for ADD, SUB, and compare. Subtraction is a + ~b + 1 and a compare is a subtract whose flags you read, so one add/sub unit with a mode bit serves all three — less area than three adders, at the cost of a shared critical path. Recomputing the adder per opcode and hoping synthesis merges it is the area anti-pattern.
  • The result mux must be latch-free — the 1.1 discipline, inside the ALU. Default-assign result first, then override per opcode (a leading assignment in SV/Verilog, a when others => in VHDL). A case over the opcode with no default infers a latch that holds the previous result — a history-dependent bug in the compute core.
  • Flags are per-operation and signedness-aware. Zero from the selected result, Negative from its MSB, Carry (unsigned) and oVerflow (signed, V) from the adder — and C/V are meaningful only for add/subtract; force them clean after a logic op so no stale adder flag masquerades as a live one.
  • Signed set-less-than is N XOR V, not the carry-out. The signature ALU bug: a signed compare derived from the subtract's carry-out (the unsigned answer) is right on same-sign operands and wrong exactly when a - b overflows the signed range — the 5.1 lesson at the composition level. Verify by checking every opcode's result and each flag against a golden reference with the signed-overflow corners driven directly — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.