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.
// 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 flowLatch-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.
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]);
endmoduleThe 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.
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
endmoduleThe Verilog form is identical in structure — wire/reg typing, $signed(...) for the signed reference, and $random — and prints PASS/FAIL rather than using assert.
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 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
endmoduleIn 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.
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; 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.
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
endmoduleThree 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.
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
endmoduleThe 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.
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 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
endmoduleIn 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.
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; 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.
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 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
endmoduleThe 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(...)).
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 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 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; 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.
// 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 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
endmoduleThe 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.
// 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 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
endmoduleIn 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.
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; 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,
resultequals the reference operation, and each flag equals its per-operation, signedness-correct reference —Zfrom the result,Nfrom its MSB,C/Vfrom the add/sub only, and signedSLT=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 assertresult === reference[op]each iteration —a+b,a-b,a&b,a|b,a^b,($signed(a) < $signed(b)),a<<sh,a>>sh. The goldenSLTuses the language's own signed compare ($signed(a) < $signed(b)/signed(a) < signed(b)), which is independent of yourN XOR Vimplementation — so a wrong SLT is caught, not masked. Thealu_tbblocks in §4b do exactly this. - Each flag, per operation, against the right reference. Do not check flags as one blob. Check
Zfor every op (result == 0). CheckNfor every op (result[MSB]). CheckCandVonly for add/sub against a widened reference (ext[WIDTH]for carry; the same-sign/different-sign rule forV) 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 - boverflows 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 checkSLT,V, andNthere against the golden signed compare. Thesigned_lt_tbblocks go further and check exhaustively at a smallWIDTH(5 bits → 1024 pairs), which is the strongest possible proof thatN XOR Vequals the true signed compare. - Boundary operands. Beyond the overflow corners, exercise
0, all-ones,+MAX,-MIN,a == b(SLT false,Ztrue after SUB), and single-bit operands.a - ashould giveZ=1;a - bwitha == bshould giveSLT=0andZ=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, 32and a non-power-of-two-ish shift range, so the shift-amount sliceb[$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
opsteps through the opcodes on a fixed(a, b),resultswitches combinationally to each unit's output,zero/negtrack the selected result, andcarry/ovfare live only on the ADD/SUB cycles and pinned clean elsewhere. Aresultthat holds its previous value on some opcode is the visual signature of the latch bug (the 1.1 failure inside the ALU); anSLTthat 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.
10. Related Tutorials
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
Vthis 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
MULis frequently a separate pipelined block, not anothercasearm. - 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
resultwrites 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 theZeroflag. - Relational Operators — the
</>and$signedbehind the golden reference forSLT/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 + 1and 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
resultfirst, then override per opcode (a leading assignment in SV/Verilog, awhen others =>in VHDL). Acaseover 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.
Zerofrom the selected result,Negativefrom its MSB,Carry(unsigned) andoVerflow(signed,V) from the adder — andC/Vare 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 whena - boverflows 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.