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.
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)\).
-
&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 astrue(non-zero) orfalse(zero) and apply short-circuiting (stopping early once the answer is known).
The C below shows bitwise operators doing logic at bit level, and how a bit mask lets you read or toggle one specific bit:
#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 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.
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))\]
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 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).
-
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\).
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.
- 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).
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:
#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.
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):
📖 References
- The core textbook: Computer Organization and Design, MIPS Edition (Patterson & Hennessy) — Chapter 3, Arithmetic for Computers, covers the ALU block diagram and two's complement representation in detail.
- A visual walkthrough: All About Circuits - Binary Adders — half adders and full adders illustrated vividly.
- Boolean algebra: Wikipedia - Boolean Algebra — an overview of the mathematics underpinning digital design.
Comments