Introduction: from grains of silicon to a digital brain

Why is it that a computer — a machine able to simulate quantum physics, render gorgeous 3D graphics and run enormous AI models — fundamentally understands only two states: on (1) and off (0)? Every programmer knows this, but very few can actually picture how those crude on-off currents manage to add two numbers, compare them, or decide which way a branch should go.

This lesson takes you down to the deepest layer of computer hardware: from the foundations of Boolean algebra, through wiring AND/OR/XOR gates into a half adder (Half Adder) and a full adder (Full Adder), and on to building a complete 4-bit ALU . Finally you will drive an interactive ALU simulator yourself and watch how a CPU computes and sets the Zero, Sign and Overflow.


📚 Prerequisites
It helps to read Series 10: Electronics & Circuit Simulation first (especially the logic-gate part, where a transistor acts as an electrical switch) to see why a physical on-off current can represent Boolean algebra. This lesson needs no advanced programming knowledge — only basic familiarity with C or JavaScript syntax.

1. Boolean algebra & the basic logic gates

In the middle of the 19th century the mathematician George Boole invented an algebra in which variables take only two values: True (represented by 1) and False (represented by 0). That is the theoretical foundation of every binary computer today. As current passes through transistors, they act as extremely fast switches that carry out exactly these basic logic operations.

Four basic logic gates build up the entire machine:

  • The NOT gate (inversion): The output is always the opposite of the input. If \(A = 1\) then \(Y = \neg A = 0\).
  • The AND gate: The output is 1 only when every input is 1. Formula: \(Y = A \land B\).
  • The OR gate: The output is 1 when at least one input is 1. Formula: \(Y = A \lor B\).
  • The XOR gate (exclusive or): The output is 1 when the two inputs differ. Formula: \(Y = A \oplus B = (A \land \neg B) \lor (\neg A \land B)\).
⚠️ Pitfall: confusing bitwise with logical operators
In languages such as C, C++ and JavaScript there is a large difference between bitwise operators (which act on each individual bit) and logical operators (which act on the value's overall truthiness).
  • & and | are the Bitwise AND and Bitwise ORoperators. They walk through the data bit by bit and apply the matching logic operation to each one.
  • && and || are the Logical AND and Logical ORoperators. They treat the whole value as true (non-zero) or false (zero) and apply short-circuiting (stopping early once the answer is known).
For example \(5 \& 3 = 1\) (because \(0101_2 \land 0011_2 = 0001_2\)), but \(5 \&\& 3 = \text{true}\) (because both are non-zero). In hardware design we always work at the bitwise level.

The C below shows bitwise operators doing logic at bit level, and how a bit mask lets you read or toggle one specific bit:

bitwise_ops.c
#include <stdio.h>

int main() {
    unsigned char a = 5;  // binary: 0000 0101
    unsigned char b = 3;  // binary: 0000 0011
    
    // 1. Bitwise AND
    printf("a & b (bitwise AND)  = %d\n", a & b);   // result: 1 (0000 0001)
    
    // 2. Logical AND
    printf("a && b (logical AND) = %d\n", a && b);  // result: 1 (true && true)
    
    // 3. Use a bit mask to test bit 2 (counting from 0)
    unsigned char mask = 0x04; // binary: 0000 0100
    if ((a & mask) != 0) {
        printf("Bit index 2 of A is set (1)\n");
    }
    
    // 4. XOR is how you toggle a bit
    a = a ^ mask; // toggle bit index 2 of a (becomes 0000 0001)
    printf("A after toggling bit 2: %d\n", a); // result: 1
    
    return 0;
}

2. Half adders & full adders

Given the basic logic gates, how do we perform arithmetic — adding two binary numbers? The answer lies in combining those gates cleverly to build adder circuits in hardware.

The half adder

A half adder takes 2 bits \(A\) and \(B\) and produces 2 outputs: the sum bit \(Sum\) (\(S\)) and the carry bit \(Carry-out\) (\(C\)). Reading the binary truth table: \(Sum\) is 1 only when exactly one of the two bits is 1 — that is precisely XOR: \(S = A \oplus B\). \(Carry-out\) is 1 only when both bits are 1 — that is precisely AND: \(C = A \land B\).

It is called "half" because it has no input for a carry coming in from the previous column.

💡 The two code blocks below are Verilog, not C
Verilog is a hardware description language (HDL). It looks like C, but it does not describe steps running one after another — it describes wires and gates that all exist at the same time.

The most important difference is the keyword assign: it is not a one-off assignment like sum = a ^ b; in C. It means "permanently wire the output sum to the output of an XOR gate whose inputs are a and b ". Change a and sum changes with it instantly, with nobody re-running any line — because it is a real wire, not a statement. For the same reason, the order of the two assign lines does not matter: swapping them produces exactly the same circuit.

You do not need to know Verilog to carry on — you only need to read ^ as XOR, & as AND, | as OR, and module … endmodule as the boundary of one circuit block.
half_adder.v
module half_adder (
    input a,
    input b,
    output sum,
    output carry
);
    assign sum = a ^ b;      // XOR gate
    assign carry = a & b;    // AND gate
endmodule

The full adder

To add numbers of more than one bit, the adder must handle the carry coming in from the previous column. That is why the full adder exists. It takes 3 inputs: \(A\), \(B\), and the incoming carry \(C_{in}\). Its logic works out as: \[S = A \oplus B \oplus C_{in}\] \[C_{out} = (A \land B) \lor (C_{in} \land (A \oplus B))\]

full_adder.v
module full_adder (
    input a,
    input b,
    input cin,
    output sum,
    output cout
);
    wire s1, c1, c2;
    
    // Two half adders chained together make one full adder
    assign s1 = a ^ b;
    assign c1 = a & b;
    
    assign sum = s1 ^ cin;
    assign c2 = s1 & cin;
    
    // Combine the two sources that can produce a carry-out
    assign cout = c1 | c2;
endmodule
⚡ A closer look: carry propagation delay
When we chain \(N\) full adders together to make an \(N\)-bit adder (a ripple carry adder, RCA), the carry from each column has to travel to the next. That creates a hardware performance bottleneck called propagation delay. Suppose each logic gate has a propagation delay of \(\tau\). The carry-out of one full adder takes \(2\tau\). The worst-case delay of an \(N\)-bit ripple carry adder is then: \[T_{\text{delay}} \approx (N-1) \times T_{\text{carry}} + T_{\text{sum}}\] For a 32-bit ripple carry adder, the final carry signal only settles after more than 60 gate delays. Modern CPUs therefore use more elaborate circuits such as the carry lookahead adder (CLA), which computes the carry in \(O(\log N)\) time instead of \(O(N)\).

A quick comparison of the two adders we just built:

Criterion Half Adder Full Adder
Inputs 2 (A, B) 3 (A, B, Carry-in)
Sum formula \(S = A \oplus B\) \(S = A \oplus B \oplus C_{in}\)
Carry-out formula \(C = A \land B\) \(C_{out} = (A \land B) \lor (C_{in} \land (A \oplus B))\)
Chaining for more bits Cannot take a carry from the previous column — cannot be chained Chain N of them into an N-bit adder (a ripple carry adder)

3. Designing a 4-bit ALU and its status flags

The arithmetic logic unit (ALU) is the computing heart of a CPU. A simple ALU can be built by placing the functional blocks side by side (the adder, an AND block, an OR block, an XOR block) and using a multiplexer (MUX) to decide which operation's result leaves the unit, based on the operation code (opcode).

🔢 First: what two's complement is
Everything about the status flags below rests on how a computer represents negative numbers, so it is worth stating clearly first. Four bits are just four bits — it is we who decide what they mean. There are two ways to read the same four bits:
  • Unsigned: read it straight as a binary number. 1001 = 9. The 4-bit range is \(0\) to \(15\).
  • Signed, two's complement: the top bit carries a negativeweight. With 4 bits the weights are \(-8, 4, 2, 1\) instead of \(8, 4, 2, 1\). So 1001 = \(-8 + 1 = -7\). The range is \(-8\) to \(+7\).
Why choose this odd scheme instead of reserving one bit as a sign? Because it turns subtraction into addition: \(A - B\) is exactly \(A + (\neg B + 1)\), so the hardware needs only one adder for both — which is precisely what case 1 in the C code at the end of this section does. It also gives exactly one representation of zero, unlike a separate sign bit (which yields both \(+0\) and \(-0\)).

The key point to carry forward: the same bits, two readings. The hardware does not know which reading you intend — it computes both the carry flag (for the unsigned reading) and the overflow flag (for the signed one), and lets your program pick whichever is meaningful.

To let the CPU make branching decisions (an if (a < b)statement, say), the ALU produces status flags from the result it has just computed:

  • The Zero flag (Z): 1 when every bit of the result is 0. For a 4-bit result \(R\): \(Z = \neg(R_3 \lor R_2 \lor R_1 \lor R_0)\).
  • The Sign flag (S): The sign of the result. In two's complement the most significant bit (MSB) decides it. For 4 bits: \(S = R_3\). If \(S = 1\) the result is negative.
  • The Carry-out flag (C): 1 when an unsigned addition overflows (goes past \(15\)), or an unsigned subtraction needs to borrow.
  • The Overflow flag (V): 1 when overflow occurs in signed.
⚠️ Telling the Carry-out (C) and Overflow (V) flags apart
This is an extremely common trap that many engineers get wrong:
  • The Carry flag (C) is only meaningful for unsignedarithmetic. It signals that the result went outside what \(N\) bits can hold (for 4 bits, a result above 15).
  • The Overflow flag (V) is only meaningful for signedarithmetic. It signals that the operation produced a result with the wrong sign, having exceeded the two's complement range (\(-8\) to \(+7\) for 4 bits).
An example. Add two signed 4-bit numbers: \(0101_2\) (\(+5\)) and \(0100_2\) (\(+4\)). The binary result is \(1001_2\), which in two's complement means \(-7\). Clearly \(5 + 4 = 9\) is positive, yet the binary result is negative. That condition raises the Overflow flag, V = 1 , signalling a wrong-signed result. The Carry flag is 0 here, because the binary addition produced no carry out of the fourth bit.

The hardware logic for the Overflow flag (V), given the result \(R\) of \(A + B\) or \(A - B\) on \(N\)-bit values (sign bit \(N-1\)):

  • For addition: \[V = (A_{N-1} \land B_{N-1} \land \neg R_{N-1}) \lor (\neg A_{N-1} \land \neg B_{N-1} \land R_{N-1})\] (Overflow happens when two same-signed numbers add to a result of the opposite sign.)
  • For subtraction: \[V = (A_{N-1} \land \neg B_{N-1} \land \neg R_{N-1}) \lor (\neg A_{N-1} \land B_{N-1} \land R_{N-1})\] (Overflow happens when two differently signed numbers are subtracted and the result takes the opposite sign from the minuend.)

Here is C that simulates, end to end, the algorithm running inside a 4-bit hardware ALU:

alu_4bit.c
#include <stdio.h>
#include <stdbool.h>

typedef struct {
    unsigned char result; // 4-bit result (0-15)
    bool carry;           // Carry-out flag
    bool overflow;        // Overflow flag
    bool zero;            // Zero flag
    bool sign;            // Sign flag
} ALU_Output;

ALU_Output alu_4bit(unsigned char a, unsigned char b, unsigned char opcode) {
    ALU_Output out = {0};
    a &= 0x0F; // clamp the inputs to exactly 4 bits
    b &= 0x0F;
    
    unsigned short temp_res = 0;
    
    switch (opcode) {
        case 0: // ADD
            temp_res = (unsigned short)a + b;
            out.result = temp_res & 0x0F;
            out.carry = (temp_res > 0x0F);
            // Signed overflow: both operands share a sign, the result does not
            out.overflow = (((a >> 3) == (b >> 3)) &&
                            (((a >> 3) ^ (out.result >> 3)) & 1));
            break;
            
        case 1: // SUB
            // A - B is done by adding A to B's two's complement: A + (~B + 1)
            temp_res = (unsigned short)a + ((~b + 1) & 0x0F);
            out.result = temp_res & 0x0F;
            out.carry = (a < b); // for SUB the carry-out acts as a borrow flag
            // Signed overflow for SUB: operands differ in sign, and the result
            // differs in sign from the minuend
            out.overflow = (((a >> 3) != (b >> 3)) &&
                            (((a >> 3) ^ (out.result >> 3)) & 1));
            break;
            
        case 2: // bitwise AND
            out.result = a & b;
            break;
            
        case 3: // bitwise OR
            out.result = a | b;
            break;
            
        case 4: // bitwise XOR
            out.result = a ^ b;
            break;
    }
    
    out.zero = (out.result == 0);
    out.sign = (out.result >> 3) & 1; // the top bit is the sign bit
    
    return out;
}

4. Hands-on: the 4-bit ALU simulator

Below is a visual simulator of a 4-bit ALU. Set the operands \(A\) and \(B\) with the binary toggle buttons, choose an arithmetic or logical operation, and watch the binary and decimal results together with which flags light up. The circuit diagram underneath highlights the path that is active for the operation you picked.

📟 4-bit ALU visualisation and simulator
Operand A (4-bit binary) Decimal: 5 (signed: 5)
Operand B (4-bit binary) Decimal: 3 (signed: 3)
Input A: 0101
Input B: 0011
Result R: 0000 (0)
Signed result: 0
Z
S
C
V
A (4-bit) B (4-bit) Arithmetic block (adder) Logic block MUX Opcode Result R (4-bit) Flags

5. Review quiz

Question 1

Question 1: Two signed two's complement 4-bit numbers: \(A = 0101_2\) (decimal \(+5\)) and \(B = 0100_2\) (decimal \(+4\)). Computing \(A + B\) in the ALU, what is the binary result and the state of the Zero (Z), Sign (S) and Overflow (V) flags?

Question 2

Question 2: What is the essential difference between the Carry-out (C) and Overflow (V) flags in an ALU?

Question 3

Question 3: A 32-bit ripple carry adder is built from 32 full adders (FA). The propagation delay from input to carry-out of each FA is \(2\text{ ns}\), and from input to Sum is \(3\text{ ns}\). What is the worst-case propagation delay of this 32-bit adder?

Download the lesson's practice code

File JavaScript CPUJS — a miniature computer-architecture library used across all 12 lessons. Lesson 1 has just added the half and full adder, the ripple-carry adder and a 4-bit ALU (ADD/SUB/AND/OR/XOR) with all four Zero/Sign/Carry/Overflow flags, plus a self-test that checks every number quoted in this lesson (run node cpu-core.js, nothing to install):

Download cpu-core.js

📖 References

Related lessons in this series

Lesson 2: The Von Neumann Architecture & the ISA Back to the Computer Architecture roadmap

Comments