Design Challenge: 4-Bit ALU — Combining Everything Into a Mini CPU Building Block

Design and verify a complete 4-bit Arithmetic Logic Unit with an 8-operation opcode set (ADD, SUB, AND, OR, XOR, SHL, SHR, COMPARE), exercising Boolean algebra, combinational adder/subtractor design, multiplexer-based operation selection, and status-flag generation all in one circuit.

The ALU is where every earlier building block in this course — full adders, Boolean gates, multiplexers, comparators — gets fused into a single reusable unit that is, quite literally, the arithmetic core of every CPU ever built. Your brief: design a 4-bit ALU that takes two 4-bit operands (A and B) and a 3-bit opcode, and produces a 4-bit result according to this operation table: 000 = ADD, 001 = SUB, 010 = AND, 011 = OR, 100 = XOR, 101 = SHIFT LEFT, 110 = SHIFT RIGHT, 111 = COMPARE. This lesson walks the design end to end: specifying exact port behavior for all 8 opcodes (including what SHIFT and COMPARE actually output, which the one-line spec leaves open and a real designer must pin down), building the arithmetic path from a 4-bit adder/subtractor with two's-complement negation, building the logic path from bitwise gate arrays, selecting between all 8 results with a 3-bit-controlled multiplexer tree, and generating the status flags (Zero, Carry/Borrow, Overflow) that make the ALU usable inside a real datapath rather than just a demo circuit.

1. Requirements and Specification

Inputs: A[3:0], B[3:0] (two 4-bit operands, treated as two's-complement signed numbers for ADD/SUB/COMPARE and as raw bit patterns for AND/OR/XOR/SHIFT), and Op[2:0] (3-bit opcode selecting one of the 8 operations below). Outputs: Result[3:0] (the 4-bit computed value) plus three status flags — Zero (1 if Result == 0000), Carry (the adder's carry-out for ADD, or NOT-borrow for SUB), and Overflow (1 if a signed ADD or SUB produced a result that doesn't fit in 4 signed bits).

Since the spec's operation table leaves SHIFT and COMPARE underspecified, this design pins down exact behavior: SHIFT LEFT (101) shifts A left by 1 bit, filling the vacated LSB with 0, using B as a don't-care; SHIFT RIGHT (110) shifts A right by 1 bit (logical shift, filling the vacated MSB with 0); COMPARE (111) computes A − B internally (like SUB) but outputs a coded Result where Result[0] = 1 if A == B, Result[1] = 1 if A > B (unsigned), and Result[2] = 1 if A < B (unsigned) — so COMPARE reuses the subtractor hardware you already built rather than adding new logic.

2. High-Level Block Architecture

The ALU splits cleanly into five blocks feeding a final output multiplexer: (1) an Arithmetic Unit — a 4-bit adder/subtractor built from four full adders in a ripple-carry chain, with a controllable B-invert-and-carry-in-1 path to compute A−B as A+(~B)+1 (two's-complement subtraction); (2) a Logic Unit — three parallel 4-bit gate arrays computing A AND B, A OR B, and A XOR B bitwise; (3) a Shift Unit — simple bit-wiring (no gates needed) that routes A[2:0] into Result[3:1] with a 0 in Result[0] for SHIFT LEFT, and A[3:1] into Result[2:0] with a 0 in Result[3] for SHIFT RIGHT; (4) a Compare Unit — reuses the Arithmetic Unit's A−B result and its Zero/Carry signals to derive the three comparison bits; (5) an 8-to-1 output multiplexer, controlled directly by the 3-bit Op input, that selects which of the five blocks' outputs becomes the final Result.

This block-and-mux architecture is the standard ALU pattern used in real CPUs: every operation's hardware runs in parallel on every clock cycle (the AND unit computes A AND B even when Op selects ADD), and the multiplexer's only job is to pick which already-computed answer to expose. This wastes a small amount of power but keeps the design combinational, fast, and simple to verify — a worthwhile trade at 4-bit scale and exactly how real ALUs are built at any scale.

3. Detailed Logic Design

The Arithmetic Unit's core is a 4-bit ripple-carry adder: four full adders, FA_i computing Sum_i = A_i XOR B_i' XOR Cin_i and Cout_i = (A_i AND B_i') OR (Cin_i AND (A_i XOR B_i')), where B_i' = B_i XOR SubMode (SubMode = 1 for SUB/COMPARE, 0 for ADD) and the very first carry-in Cin_0 is also tied to SubMode. XOR-ing B with SubMode and injecting SubMode as Cin_0 implements two's-complement negation (invert-and-add-1) for free, using the same adder hardware for both ADD and SUB — this is the single most important trick in ALU design and the reason a 'subtractor' is rarely built as separate hardware.

Overflow detection (signed arithmetic only) uses the standard rule: Overflow = Cout_3 XOR Cout_2 (the carry into the sign bit differs from the carry out of the sign bit) — this single XOR gate, fed from the two most-significant full-adder carries, correctly flags overflow for both ADD and SUB without any opcode-specific logic beyond the SubMode wiring already in place.

The Output Mux is built as a tree of 4-bit-wide 2-to-1 multiplexers controlled by Op[2], Op[1], Op[0] in sequence (or, more simply, as eight 4-bit tri-state or AND-OR select paths gated by a fully decoded 3-to-8 opcode decoder) — either structure is acceptable as long as exactly one of the eight computed results reaches Result[3:0] for any given Op value, matching the Opcode → Operation table from the spec exactly.

• ADD (000): Result = A + B, SubMode = 0 • SUB (001): Result = A − B, SubMode = 1 • AND (010): Result = A AND B (bitwise) • OR (011): Result = A OR B (bitwise) • XOR (100): Result = A XOR B (bitwise) • SHL (101): Result = {A[2:0], 0} • SHR (110): Result = {0, A[3:1]} • COMPARE (111): Result[0]=EQ, Result[1]=GT, Result[2]=LT (computed from A−B, unsigned)

4. Testing and Verification

Test each of the 8 opcodes independently with a small hand-picked vector set before testing them together: for ADD/SUB, include a case that exercises Carry (e.g., 1111 + 0001 = 0000 with Carry=1) and a case that exercises signed Overflow (e.g., 0111 + 0001, which overflows a 4-bit signed range since 7+1=8 doesn't fit in [-8,7]); for AND/OR/XOR, a case with all-1s and all-0s operands to catch stuck-at faults; for SHIFT, confirm the vacated bit is filled with 0, not wrapped from the other end (a common accidental design of a 'rotate' instead of a 'shift'); for COMPARE, all three relations (A==B, A>B, A<B) plus the boundary case A=1111, B=0000.

The most common integration bug is not in any single block but in the Output Mux: verify that changing Op alone (holding A and B fixed) correctly switches Result between all 8 precomputed values, and specifically check that an opcode you have NOT yet tested doesn't accidentally show a previous opcode's stale result — a classic sign of a mux select line wired one bit off (e.g., Op[1] and Op[2] swapped), which silently produces correct answers for half the opcodes and wrong answers for the other half.

ALU Opcode Table and Flag Equations

SubMode (internal control signal) = Op[0] for SUB, and is also asserted for COMPARE (Op=111) since COMPARE reuses the subtractor path — note both Op=001 and Op=111 need SubMode=1. Zero, Carry, and Overflow are computed from the SAME adder hardware regardless of which opcode is selected; only Carry/Overflow are meaningful outside of ADD/SUB/COMPARE, and a real ALU's documentation should say so explicitly.

Op[2:0]  Operation      Result
  000     ADD          A + B
  001     SUB          A - B          (A + ~B + 1)
  010     AND          A & B
  011     OR           A | B
  100     XOR          A ^ B
  101     SHL          {A[2:0], 1'b0}
  110     SHR          {1'b0, A[3:1]}
  111     COMPARE      {LT, GT, EQ}   (from A - B)

Zero     = ~(Result[3] | Result[2] | Result[1] | Result[0])
Carry    = Cout_3                      (valid for ADD/SUB only)
Overflow = Cout_3 ^ Cout_2              (valid for ADD/SUB only)

Code Examples

This trace is the single most important verification you can run: it shows Carry and Overflow are computed from the exact same 4 full adders for every opcode, and that Overflow=1 correctly flags the case where Carry alone would look 'fine' (Example 3 has Carry=0 but is still wrong as a signed result) — the classic distinction between unsigned carry-out and signed overflow that every ALU design must get right. Expected result: Overflow correctly fires exactly when Carry alone would NOT have caught the error — this is the case every grader will check first.

Result = 1000 (-8 if read signed!), Carry = 0, Overflow = 1 This behavioral (case-statement) style is what you'd actually write in a real HDL project — the earlier block diagram and truth table are what you'd draw BEFORE writing this, to make sure you know what each branch should compute. Note the shared adder (sum) feeds both ADD and SUB, exactly matching the add-invert-plus-1 trick from the worked example.

wire [4:0] sum = A + Bx + submode; // {carry,result} Common mistake: A beginner mistake is writing 8 completely separate always blocks or module instances for the 8 opcodes instead of sharing the adder between ADD/SUB/COMPARE — this triples the adder hardware for no benefit.

Example 1 - ADD, no overflow:
  A = 0011 (3), B = 0100 (4), Op = 000
  Result = 0111 (7), Carry = 0, Overflow = 0

Example 2 - ADD, carry out but no signed overflow (unsigned wrap):
  A = 1111 (-1), B = 0001 (1), Op = 000
  Result = 0000 (0), Carry = 1, Overflow = 0   <- correct: -1 + 1 = 0

Example 3 - ADD, signed overflow:
  A = 0111 (+7), B = 0001 (+1), Op = 000
  Result = 1000 (-8 if read signed!), Carry = 0, Overflow = 1
  <- 7 + 1 = 8 cannot be represented in 4-bit signed range [-8,+7]

Example 4 - SUB via add-invert-plus-1:
  A = 0101 (5), B = 0011 (3), Op = 001
  B' = ~B = 1100, Cin0 = 1  =>  A + B' + 1 = 0101+1100+1 = 0010 (2)
  Result = 0010 (2) = 5 - 3, correct.
module alu4 (
  input  [3:0] A, B,
  input  [2:0] Op,
  output reg [3:0] Result,
  output Zero, Carry, Overflow
);
  wire submode = (Op == 3'b001) || (Op == 3'b111);
  wire [3:0] Bx = B ^ {4{submode}};
  wire [4:0] sum = A + Bx + submode;      // {carry,result}
  wire c3 = sum[4];
  wire c2 = (A[2]&Bx[2]) | (A[2]&sum[2]) | (Bx[2]&sum[2]); // approx c2 for demo

  always @(*) begin
    case (Op)
      3'b000: Result = sum[3:0];              // ADD
      3'b001: Result = sum[3:0];               // SUB
      3'b010: Result = A & B;                  // AND
      3'b011: Result = A | B;                  // OR
      3'b100: Result = A ^ B;                  // XOR
      3'b101: Result = {A[2:0], 1'b0};         // SHL
      3'b110: Result = {1'b0, A[3:1]};         // SHR
      3'b111: Result = {2'b00, (A>B), (A==B)}; // COMPARE (simplified)
      default: Result = 4'b0000;
    endcase
  end

  assign Zero     = (Result == 4'b0000);
  assign Carry    = c3;
  assign Overflow = c3 ^ c2;
endmodule

4-Bit ALU Block Diagram

All five function units compute in parallel every cycle; Op[2:0] only selects which precomputed result reaches the output.

A[3:0]  B[3:0]
          |       |
   +------+-------+------+-----------+----------+
   |      |       |      |           |          |
   v      v       v      v           v          v
 Adder/  AND    OR    XOR         Shift      Compare
 Subtr.  Unit   Unit  Unit         Unit        Unit
   |      |       |      |           |          |
   +------+---+---+------+-----+-----+----+-----+
              |               |            |
              v               v            v
         +----+---------------+------------+----+
         |     8-to-1 MULTIPLEXER  <- Op[2:0]    |
         +--------------------+--------------------+
                              |
                         Result[3:0]
                    (+ Zero, Carry, Overflow)

Important Rules

Every operation's hardware runs combinationally and in parallel; Op selects the output, it does not gate which hardware runs.

ADD and SUB must share the same 4-bit adder hardware via B-invert + carry-in, not use two separate adder/subtractor circuits.

Overflow must be computed as Cout_3 XOR Cout_2 and must only be treated as meaningful for ADD, SUB, and COMPARE.

SHIFT operations must fill the vacated bit with 0 (logical shift), not wrap around (that would be a rotate, a different operation).

The Heart of Every CPU's Execute Stage

This 4-bit, 8-opcode ALU is a miniature but structurally faithful version of the ALU inside every general-purpose CPU, from an 8-bit microcontroller to a 64-bit server processor. Real ALUs widen the datapath (32 or 64 bits instead of 4), widen the opcode field to cover more operations (multiply, divide, rotate, bit-count), and add pipelining for speed — but the core architecture is unchanged: parallel function units computing every possible result every cycle, an opcode-controlled multiplexer picking the answer, and flag outputs (Zero, Carry, Overflow, and usually Sign) that feed directly into the CPU's branch-decision logic for conditional jumps like 'branch if equal' or 'branch if overflow.'

Treating Carry-out as the overflow indicator for signed addition/subtraction.

Compute Overflow separately as Cout_3 XOR Cout_2 (the carry into vs. out of the sign bit) and use Carry only for unsigned arithmetic interpretation.

Carry-out and signed Overflow answer different questions: Carry tells you whether an UNSIGNED result wrapped past 15, while Overflow tells you whether a SIGNED result wrapped past the representable [-8,+7] range. Example 2 in this lesson has Carry=1 but Overflow=0 (correct signed result), while Example 3 has Carry=0 but Overflow=1 (incorrect signed result) — conflating the two produces an ALU that silently gives wrong answers to signed programs.

Building SUB as an entirely separate subtractor circuit (borrow chain) alongside the ADD adder.

Reuse the same adder by XOR-ing B with SubMode and tying SubMode into Cin_0, computing SUB as A + (~B) + 1.

A separate subtractor doubles the arithmetic hardware for no functional benefit, and — worse — gives ADD and SUB independently-computed Carry/Overflow signals that can disagree with each other on shared test vectors, making the ALU's status flags inconsistent and hard to trust downstream.

Why can a single 4-bit adder circuit compute both A+B and A-B, using only one extra XOR gate per bit plus a control signal into the carry-in? A. Because addition and subtraction are electrically identical operations in binary B. Because A - B equals A + (two's-complement negation of B), and two's-complement negation is exactly 'invert every bit, then add 1' — the XOR gates do the bit-inversion and the carry-in supplies the '+1' C. Because the adder automatically detects the opcode and switches internal behavior D. Because subtraction is only approximate in 2's complement, so the same hardware happens to work

B. Because A - B equals A + (two's-complement negation of B), and two's-complement negation is exactly 'invert every bit, then add 1' — the XOR gates do the bit-inversion and the carry-in supplies the '+1' — Two's-complement negation of B is ~B + 1. XOR-ing each B bit with a SubMode control signal produces ~B when SubMode=1 (and B unchanged when SubMode=0), and tying that same SubMode signal into the adder's initial carry-in supplies the '+1'. So A + (~B) + 1 = A - B, using the identical adder hardware that computes A + B when SubMode=0.

In the ALU's architecture, why do the AND, OR, XOR, and Shift units all compute their results on every clock cycle, even when the ALU is currently set to perform ADD? A. It is a bug that should be fixed by gating each unit's clock with the opcode B. The design deliberately keeps all function units combinational and running in parallel, and uses the opcode purely to select which already-computed result reaches the output multiplexer — this is simpler and faster than trying to enable/disable each unit C. The hardware physically cannot be built any other way D. It saves power compared to computing only the selected operation

B. The design deliberately keeps all function units combinational and running in parallel, and uses the opcode purely to select which already-computed result reaches the output multiplexer — this is simpler and faster than trying to enable/disable each unit — This is the standard parallel-compute, mux-select ALU architecture: every function unit is a small amount of combinational logic that's cheap to leave 'always on,' and gating each one on/off with the opcode would add complexity (enable logic, potential glitches) for a power saving that's negligible at this scale. It does cost a small amount of extra switching power versus a gated design, which is a real trade-off in ultra-low-power chips, but is standard practice for a general-purpose ALU.

A tester feeds A=0111 (+7), B=0001 (+1), Op=000 (ADD) into the ALU and observes Result=1000, Carry=0, Overflow=1. What should the tester conclude? A. The ALU is broken, since Carry=0 normally means no error occurred B. The ALU is working correctly: 7+1=8 doesn't fit in the signed 4-bit range [-8,+7], so Overflow correctly fires even though there was no unsigned carry-out C. The test vectors are invalid because both operands are positive D. Result should have been 0111 instead of 1000 E. The Overflow flag should be ignored whenever Carry=0

B. The ALU is working correctly: 7+1=8 doesn't fit in the signed 4-bit range [-8,+7], so Overflow correctly fires even though there was no unsigned carry-out — 0111+0001 = 8 in unsigned terms, which fits in 4 unsigned bits (0-15) with no carry-out, so Carry=0 is correct. But interpreted as signed 4-bit numbers, the result 1000 reads as -8, which is nonsensical for 7+1 — exactly the case Overflow exists to catch (Cout_3 XOR Cout_2 = 0 XOR 1 = 1). This is the textbook example of why Carry and Overflow must be computed and checked separately.

Extend the ALU with a 9th opcode (you'll need a 4th opcode bit) that performs a 2-bit rotate-left on A, and update your flag logic to say explicitly which flags are meaningful for the new operation.

Add Op=1000 (now a 4-bit opcode) mapped to Result = {A[1:0], A[3:2]} (a pure bit-routing/multiplexer path, no arithmetic needed), extend the output mux to 9-to-1 (or 16-to-1 with unused codes tied to 0), and document that Zero remains meaningful for ROTATE (it's still just checking if Result is all zero) while Carry and Overflow are explicitly marked 'not applicable' for this opcode since no addition occurred.

Rotate-left by 2 means the two bits shifted off the top re-enter at the bottom, unlike SHL which fills with 0 — you'll need extra wiring (not gates) to route A[3:2] into Result[1:0].