AI EngineeringZero to ProductionHome·About·Contact
Developer Foundations · Chapter DF7

Boolean algebra & digital logic

From true/false to silicon: Boolean laws and truth-table proofs, the six gates and NAND universality, half/full adders you simulate, Karnaugh-map minimization, and the feedback that becomes memory.

⏱️ ~3 hours🧪 7 runnable labs🎯 Beginner→Industry

Learning objectives

  • State and apply the core Boolean laws (identity, De Morgan, distribution, absorption).
  • Read and write truth tables for AND/OR/NOT/NAND/NOR/XOR.
  • Explain why NAND (and NOR) are universal gates.
  • Build and simulate a half adder and a full adder in Python.
  • Minimize a Boolean function with a Karnaugh map.
  • Describe sequential logic — latches, flip-flops, clocks, state — conceptually.
🌱 Start here — from zeroUnder every CPU is a sea of logic gates, and under every gate is Boolean algebra. This lesson connects the algebra (true/false, AND/OR/NOT) to the hardware (gates) to the arithmetic (adders) you met in df6. We prove laws with truth tables, show NAND can build anything, and simulate a real adder in Python. Runnable blocks use only the standard library and produce the exact outputs shown.
▶ Runnable companionGates are modelled as plain Python functions returning 0/1, so a 'circuit' is just function composition you can print and test.

1 · Boolean values & the core laws essential

Boolean algebra works on two values — 1/true and 0/false — with three operations: AND (·), OR (+) and NOT (¬ or ′). The laws below let you simplify expressions, which in hardware means fewer gates — cheaper, faster, cooler chips.

LawAND formOR form
IdentityA · 1 = AA + 0 = A
NullA · 0 = 0A + 1 = 1
IdempotentA · A = AA + A = A
ComplementA · ¬A = 0A + ¬A = 1
AbsorptionA · (A + B) = AA + (A · B) = A
DistributionA · (B + C) = A·B + A·CA + (B·C) = (A+B)·(A+C)
De Morgan¬(A · B) = ¬A + ¬B¬(A + B) = ¬A · ¬B

De Morgan is the workhorse: it lets you push a NOT through an AND/OR by flipping the operator. We can prove any law by exhausting the truth table — if both sides agree on all input rows, they're equal:

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · prove De Morgan by exhaustion (runs)
demorgan.py# ¬(A and B)  ==  (¬A) or (¬B)   for every A, B
def NOT(x): return 1 - x
def AND(a, b): return a & b
def OR(a, b):  return a | b

print("A B | ¬(A·B)  ¬A+¬B  equal?")
ok = True
for A_ in (0, 1):
    for B_ in (0, 1):
        left  = NOT(AND(A_, B_))
        right = OR(NOT(A_), NOT(B_))
        ok = ok and (left == right)
        print(f"{A_} {B_} |   {left}       {right}     {left == right}")
print("law holds for all rows:", ok)
A B | ¬(A·B)  ¬A+¬B  equal?
0 0 |   1       1     True
0 1 |   1       1     True
1 0 |   1       1     True
1 1 |   0       0     True
law holds for all rows: True

2 · Logic gates & truth tables essential

A gate is the physical realisation of a Boolean operation. Six matter. Note NAND (not-and) and NOR (not-or) are the negations of AND/OR, and XOR (exclusive-or) is 1 when the inputs differ.

ABANDORNANDNORXOR
0000110
0101101
1001101
1111000
Python · a truth-table generator for any gate (runs)
gates.pygates = {
    "AND":  lambda a, b: a & b,
    "OR":   lambda a, b: a | b,
    "NAND": lambda a, b: 1 - (a & b),
    "NOR":  lambda a, b: 1 - (a | b),
    "XOR":  lambda a, b: a ^ b,
}
def truth_table(name):
    fn = gates[name]
    print(f"-- {name} --")
    for a in (0, 1):
        for b in (0, 1):
            print(f"  {a} {b} -> {fn(a, b)}")

truth_table("NAND")
truth_table("XOR")
-- NAND --
  0 0 -> 1
  0 1 -> 1
  1 0 -> 1
  1 1 -> 0
-- XOR --
  0 0 -> 0
  0 1 -> 1
  1 0 -> 1
  1 1 -> 0

3 · NAND is universal intermediate

A gate set is universal if you can build every other gate from it. NAND alone is universal — which is why fabs love it: one gate type, laid down millions of times, becomes an entire processor. The constructions:

NAND(A,A) tie inputs = NOT A inverter
Target gateBuilt from NAND
NOT ANAND(A, A)
AND A,BNOT(NAND(A, B)) = NAND(NAND(A,B), NAND(A,B))
OR A,BNAND(NOT A, NOT B) = NAND(NAND(A,A), NAND(B,B))
Python · build NOT/AND/OR from NAND only (runs)
nand_universal.pydef NAND(a, b): return 1 - (a & b)

def NOT(a):    return NAND(a, a)
def AND(a, b): return NOT(NAND(a, b))
def OR(a, b):  return NAND(NOT(a), NOT(b))

for a in (0, 1):
    for b in (0, 1):
        print(a, b, "| NOT_a", NOT(a),
              "AND", AND(a, b), "OR", OR(a, b))
0 0 | NOT_a 1 AND 0 OR 0
0 1 | NOT_a 1 AND 0 OR 1
1 0 | NOT_a 0 AND 0 OR 1
1 1 | NOT_a 0 AND 1 OR 1
Why this mattersBecause NAND is universal, chip designers optimise a single, well-understood gate. The same idea recurs in software: give me one universal primitive (a Turing machine, lambda, NAND) and I can compute anything computable.

4 · Combinational logic: half & full adders advanced

Combinational logic has no memory: outputs depend only on current inputs. The classic example is binary addition. A half adder adds two bits and outputs a sum and a carry: sum = A XOR B, carry = A AND B. A full adder also takes a carry-in, so you can chain them into a ripple-carry adder for multi-bit numbers — this is arithmetic in df6, built from the gates in this lesson.

A, B two bits XOR → Sum 0/1 AND → Carry 0/1
Python · half adder, full adder, 4-bit ripple add (runs)
adders.pydef half_adder(a, b):
    return (a ^ b, a & b)                 # (sum, carry)

def full_adder(a, b, cin):
    s1, c1 = half_adder(a, b)
    s2, c2 = half_adder(s1, cin)
    return (s2, c1 | c2)                  # (sum, carry_out)

def ripple_add(x, y, bits=4):
    "add two ints via chained full adders; return (result, overflow)"
    carry, out = 0, 0
    for i in range(bits):
        a = (x >> i) & 1
        b = (y >> i) & 1
        s, carry = full_adder(a, b, carry)
        out |= (s << i)
    return out, carry                     # carry = overflow out of top bit

print("half_adder(1,1) =", half_adder(1, 1))       # (0, 1)
print("full_adder(1,1,1) =", full_adder(1, 1, 1))  # (1, 1)
print("7 + 6 (4-bit) ->", ripple_add(7, 6))        # 13 fits, no overflow
print("9 + 8 (4-bit) ->", ripple_add(9, 8))        # 17 > 15 -> wraps + carry
half_adder(1,1) = (0, 1)
full_adder(1,1,1) = (1, 1)
7 + 6 (4-bit) -> (13, 0)
9 + 8 (4-bit) -> (1, 1)
You just built a CPU's ALU coreChaining full adders is literally how an Arithmetic Logic Unit adds. 9 + 8 in 4 bits is 17, which needs 5 bits — the low 4 bits are 0001 and the carry-out is 1, the hardware overflow flag from df6.

5 · Minimizing with Karnaugh maps advanced

A Karnaugh map (K-map) is a visual way to minimise a Boolean function — fewer terms means fewer gates. You arrange the truth table so adjacent cells differ by one variable (Gray code order: 00, 01, 11, 10), then circle groups of 1s in powers of two. Each group drops the variable that changes across it.

Worked example. Function F(A,B,C) is 1 for these minterms: A=0,B=0,C=1; A=0,B=1,C=1; A=1,B=1,C=1; A=1,B=0,C=1 — i.e. whenever C=1. The K-map (rows A, columns BC in Gray order):

A \ BC00011110
00110
10110

Both 1-columns are exactly the C=1 columns (01 and 11) across both rows — one group of four cells. A and B both change inside the group, so they drop out. The minimised function is simply F = C. K-maps turn a 4-term sum-of-products into a single wire.

Python · confirm the minimization is equivalent (runs)
kmap.py# original sum-of-products vs the minimized form F = C
def F_full(A, B, C):
    return (((1-A) & (1-B) & C) | ((1-A) & B & C) |
            (A & B & C)         | (A & (1-B) & C))
def F_min(A, B, C):
    return C

same = all(F_full(a, b, c) == F_min(a, b, c)
           for a in (0, 1) for b in (0, 1) for c in (0, 1))
print("minimized form matches on all 8 rows:", same)
minimized form matches on all 8 rows: True

6 · Sequential logic: memory, clocks & state professional

Combinational logic forgets instantly. To remember a bit you need feedback. Cross-couple two NOR gates and you get an SR latch — the simplest 1-bit memory. A clock paces updates; a flip-flop is a latch that only changes on a clock edge. Registers (df8) are just banks of flip-flops. This is where state enters hardware.

inputs S,R set/reset cross-coupled NORs feedback stable Q 1 bit held clock edge timing flip-flop latches register bit

We can model the latch's behaviour as a state machine in software (this is a behavioural model, not a gate-level circuit — real latches are analog feedback):

Python · model an SR latch as held state (runs)
latch.pyclass SRLatch:
    def __init__(self): self.Q = 0        # stored bit
    def step(self, S, R):
        if S and R:
            raise ValueError("S=R=1 is invalid for an SR latch")
        if S:   self.Q = 1                 # set
        elif R: self.Q = 0                 # reset
        # else: hold previous Q (this is the "memory")
        return self.Q

l = SRLatch()
print("set   ->", l.step(1, 0))
print("hold  ->", l.step(0, 0))   # remembers 1 with no input
print("reset ->", l.step(0, 1))
print("hold  ->", l.step(0, 0))   # remembers 0
set   -> 1
hold  -> 1
reset -> 0
hold  -> 0
Combinational vs sequentialCombinational: output = f(inputs), no memory (adders, muxes, ALUs). Sequential: output = f(inputs, stored state), updated on a clock (registers, counters, the whole CPU). Every stateful machine — including a program's control flow — reduces to this.

✓ Checkpoint — you can move on when you can…

  • Apply De Morgan and absorption to simplify a Boolean expression.
  • Fill in the truth table for AND/OR/NAND/NOR/XOR from memory.
  • Build NOT, AND and OR using only NAND gates.
  • Trace a full adder and chain adders to add two 4-bit numbers.
  • Group 1s on a K-map and read off the minimized expression.
  • Explain how feedback + a clock turns gates into 1 bit of memory.

Knowledge check check yourself

✓ Knowledge check

Using a K-map or the laws, minimize F = A·B + A·¬B.

Show answer
Factor out A: F = A·(B + ¬B) = A·1 = A. On a K-map the two minterms sit side-by-side in the A=1 row and B changes across them, so B drops out, leaving F = A.
✓ Knowledge check

Why is a single NAND gate enough to build an entire computer's logic?

Show answer
NAND is functionally complete: NOT = NAND(A,A), AND = NOT(NAND(A,B)), and OR = NAND(NOT A, NOT B). Since {AND, OR, NOT} can express any Boolean function, and all three come from NAND, NAND alone can express any Boolean function — so every combinational circuit, hence every CPU, can be built from NAND gates.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Print a gate's truth tableBeginner

Context: Reading and writing truth tables is the first fluency check for any digital-logic work.

Your task: Write a function that prints the full truth table for a two-input XOR gate.

Requirements:

  • Define XOR with a ^ b
  • Iterate all four (a, b) combinations
  • Print each row as a b -> result

💡 Hint: XOR is 1 exactly when the two inputs differ.

Show solution
def XOR(a, b): return a ^ b
for a in (0, 1):
    for b in (0, 1):
        print(a, b, '->', XOR(a, b))
# 0 0 -> 0 / 0 1 -> 1 / 1 0 -> 1 / 1 1 -> 0

XOR outputs 1 on the two rows where the inputs disagree — the basis of the half-adder sum bit.

Exercise 2 · Verify absorption by exhaustionIntermediate

Context: Proving a law by testing every input row is the discrete-math tool you fall back on when algebra feels risky.

Your task: Prove A + (A·B) = A holds for all inputs by checking the truth table in code.

Requirements:

  • Compute the left side A | (A & B) for all (A, B)
  • Compare against the right side A
  • Report a single boolean: does it hold for every row?

💡 Hint: There are only four rows — exhaustion is a complete proof for a fixed number of variables.

Show solution
ok = all((A | (A & B)) == A
         for A in (0, 1) for B in (0, 1))
print('absorption holds:', ok)   # True

For two variables the truth table has four rows; agreeing on all of them is the proof. Intuitively, if A is 1 the OR is 1; if A is 0 then A·B is 0, so the OR is 0 — either way it equals A.

Exercise 3 · Build XOR from NAND gatesAdvanced

Context: Since NAND is universal, expressing a derived gate purely in NAND cements the concept and mirrors how silicon is actually laid out.

Your task: Implement XOR using only NAND calls, then verify it against Python's ^.

Requirements:

  • Define a single NAND(a, b) primitive
  • Compose XOR from NANDs only (no &, |, ^, or 1-x)
  • Verify equality with a ^ b across all four rows

💡 Hint: The classic 4-NAND XOR: t = NAND(a,b); then XOR = NAND(NAND(a,t), NAND(b,t)).

Show solution
def NAND(a, b): return 1 - (a & b)
def XOR(a, b):
    t = NAND(a, b)
    return NAND(NAND(a, t), NAND(b, t))

ok = all(XOR(a, b) == (a ^ b)
         for a in (0, 1) for b in (0, 1))
print('XOR-from-NAND correct:', ok)   # True

Four NAND gates realise XOR. (The 1 - (a & b) inside NAND is the gate's own definition, not a shortcut in the XOR itself.)

Exercise 4 · N-bit ripple-carry adderExpert

Context: A parameterised adder is the arithmetic heart of an ALU; making it width-configurable shows you understand carry propagation.

Your task: Generalise the ripple-carry adder to any bit width and confirm it detects unsigned overflow.

Requirements:

  • Chain full adders across bits positions
  • Return both the wrapped result and the final carry-out
  • Show 13 + 3 at width 4 overflows (carry-out 1)
  • Show the same addition at width 5 does not overflow

💡 Hint: The carry-out of the most-significant full adder is exactly the unsigned overflow flag.

Show solution
def full_adder(a, b, cin):
    s = a ^ b ^ cin
    cout = (a & b) | (cin & (a ^ b))
    return s, cout

def add(x, y, bits):
    carry, out = 0, 0
    for i in range(bits):
        s, carry = full_adder((x>>i)&1, (y>>i)&1, carry)
        out |= s << i
    return out, carry

print(add(13, 3, 4))   # (0, 1)  -> 16 wraps to 0, carry set
print(add(13, 3, 5))   # (16, 0) -> fits in 5 bits

Same circuit, different width. The carry-out tells you whether the true sum exceeded the representable range — the hardware overflow flag.

Exercise 5 · Simplify a real function two waysProfessional

Context: Engineers must trust algebraic minimization; cross-checking the simplified form against the original by brute force is the professional safety net.

Your task: Minimize F = A·B·C + A·B·¬C + A·¬B·C and prove your minimized form is equivalent.

Requirements:

  • Minimize by algebra or K-map to the simplest sum-of-products
  • Implement both the original and the minimized form
  • Assert they agree on all 8 input rows
  • State the gate-count saving

💡 Hint: Group the first two terms (C drops) and note the third shares A·C; you should reach F = A·B + A·C = A·(B + C).

Show solution
def F_full(A, B, C):
    return (A&B&C) | (A&B&(1-C)) | (A&(1-B)&C)
def F_min(A, B, C):
    return A & (B | C)

assert all(F_full(a,b,c) == F_min(a,b,c)
           for a in (0,1) for b in (0,1) for c in (0,1))
print('F = A(B + C) verified')

A·B·C + A·B·¬C = A·B (C cancels); combined with A·¬B·C and factoring A gives A·(B + C) — three 3-input AND terms collapse to one AND and one OR.

Exercise 6 · A clocked traffic-light controllerIndustry scenario

Context: Real embedded systems are finite state machines driven by a clock; modelling one in software is how you validate the logic before it ever touches hardware.

Your task: Model a traffic light as a clocked finite state machine (GREEN → YELLOW → RED → GREEN) and run it for several clock ticks.

Requirements:

  • Represent state explicitly and transition only on a tick() (the clock edge)
  • Cycle GREEN → YELLOW → RED → GREEN deterministically
  • Expose current state as output (Moore machine: output depends only on state)
  • Run 6 ticks and print the sequence
  • Note this is a behavioural model of clocked sequential logic, not a gate netlist

💡 Hint: A dict mapping each state to its successor makes the transition table explicit and testable.

Show solution
class TrafficLight:
    NEXT = {'GREEN': 'YELLOW', 'YELLOW': 'RED', 'RED': 'GREEN'}
    def __init__(self): self.state = 'RED'   # stored in flip-flops
    def tick(self):                           # clock edge
        self.state = self.NEXT[self.state]
        return self.state

fsm = TrafficLight()
print([fsm.tick() for _ in range(6)])
# ['GREEN', 'YELLOW', 'RED', 'GREEN', 'YELLOW', 'RED']

The current state lives in flip-flops (registers); the NEXT table is combinational logic; the clock (tick) decides when the register updates. That combinational-plus-register-plus-clock pattern is every sequential circuit, from this controller up to a full CPU. This is a behavioural model — real hardware realises NEXT as gates and the state as clocked flip-flops.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in