Loading header...

Digital Foundations of a CPU

Learning Objectives

Derive a one-bit full adder, extend it to an (N)-bit ALU, explain two's-complement subtraction and overflow, show how D flip-flops form registers, and separate combinational delay from clocked state updates.

Before we write a single assembly instruction, let's take a step back and ask: how does a CPU actually work at the transistor and gate level?

Everything in a microprocessor — arithmetic, comparisons, branching, register storage — is built from the same handful of primitive logic gates you studied in digital electronics. This page connects those fundamentals to the real hardware inside an AVR or ARM core.

[!TIP] Prerequisite refresher links
If any concept below feels unfamiliar, these digital electronics lessons cover it in depth:


The Big Picture — Gates → ALU → CPU

flowchart TD classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef combo fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef seq fill:#dcfce7,stroke:#16a34a,color:#14532d classDef cpu fill:#f3e8ff,stroke:#9333ea,color:#581c87 G1["AND Gate"]:::gate G2["OR Gate"]:::gate G3["XOR Gate"]:::gate G4["NOT Gate"]:::gate G5["NAND / NOR Gate"]:::gate FA["Full Adder\n(combinational circuit)"]:::combo MUX["Multiplexer\n(operation selector)"]:::combo CMP["Comparator\n(flag generator)"]:::combo ALU["ALU\n(Arithmetic Logic Unit)\nAdd, Sub, AND, OR, XOR, Shift"]:::cpu FF["D Flip-Flop\n(sequential circuit)"]:::seq REG["8-bit Register\n(8 flip-flops + enable)"]:::seq REGFILE["Register File\n(32 registers in AVR)"]:::seq CPU["CPU Core\nALU + Registers + Control Unit"]:::cpu G1 & G2 & G3 & G4 & G5 --> FA G1 & G2 & G3 --> MUX G1 & G2 & G3 --> CMP FA & MUX & CMP --> ALU G5 --> FF FF --> REG REG --> REGFILE ALU & REGFILE --> CPU

Every box above is made of transistors wired as gates. A modern AVR ATmega328P has roughly 100 million transistors. Every single one is doing Boolean algebra.


Part 1 — Building the ALU

Step 1: The Half Adder

The simplest addition circuit: two input bits, one sum bit, one carry bit.

flowchart LR classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef wire fill:#f1f5f9,stroke:#475569,color:#1e293b classDef out fill:#dcfce7,stroke:#16a34a,color:#14532d A["A (input bit)"]:::wire B["B (input bit)"]:::wire XOR1["XOR Gate"]:::gate AND1["AND Gate"]:::gate SUM["Sum = A ⊕ B\n(0+0=0, 0+1=1, 1+0=1, 1+1=0)"]:::out CARRY["Carry = A · B\n(only 1 when both inputs are 1)"]:::out A --> XOR1 B --> XOR1 A --> AND1 B --> AND1 XOR1 --> SUM AND1 --> CARRY

Truth table:

A B Sum Carry
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1

The half adder cannot add multi-bit numbers because it has no carry input. That is what the full adder fixes.


Step 2: The Full Adder

A full adder has three inputs: A, B, and Cin (carry in from the previous bit position). It is built from two half adders and an OR gate.

flowchart TD classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef ha fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef out fill:#dcfce7,stroke:#16a34a,color:#14532d classDef in fill:#f1f5f9,stroke:#475569,color:#1e293b subgraph INPUTS ["Inputs"] A2["A"]:::in B2["B"]:::in CIN["Cin (carry in\nfrom previous bit)"]:::in end HA1["Half Adder 1\n(XOR + AND)\nAdds A + B"]:::ha HA2["Half Adder 2\n(XOR + AND)\nAdds partial sum + Cin"]:::ha OR2["OR Gate\ncombines both carry signals"]:::gate subgraph OUTPUTS ["Outputs"] SUM2["Sum\n(final result bit)"]:::out COUT["Cout (carry out\nto next bit position)"]:::out end A2 --> HA1 B2 --> HA1 HA1 -->|"partial sum"| HA2 CIN --> HA2 HA2 --> SUM2 HA1 -->|"carry 1"| OR2 HA2 -->|"carry 2"| OR2 OR2 --> COUT

Step 3: The 8-bit Ripple Carry Adder

Chain eight full adders together — carry out of bit 0 feeds into carry in of bit 1, and so on. This is how ADD R16, R17 works physically inside the AVR:

flowchart TD classDef fa fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef io fill:#f1f5f9,stroke:#475569,color:#1e293b classDef flag fill:#fee2e2,stroke:#dc2626,color:#7f1d1d C0["Cin = 0 (ADD)\nor Cin = 1 (ADC — add with carry)"]:::io subgraph CHAIN ["Full Adder chain — carry ripples left to right through each bit"] FA0["FA Bit 0"]:::fa FA1["FA Bit 1"]:::fa FA2["FA Bit 2"]:::fa FADOT["FA Bits 3–6 (same pattern)"]:::io FA7["FA Bit 7"]:::fa FA0 -->|"C1"| FA1 -->|"C2"| FA2 -->|"C3"| FADOT -->|"C7"| FA7 end subgraph SUMS ["Output bits — form the 8-bit result"] S0["S0"]:::io S1["S1"]:::io S2["S2"]:::io S7["S7"]:::io end COUT2["Carry Out → C flag in SREG\n(BRCS tests this wire)"]:::flag C0 -->|"Cin"| FA0 FA0 --> S0 FA1 --> S1 FA2 --> S2 FA7 --> S7 FA7 --> COUT2

The C (Carry) flag in the AVR's SREG register is literally the carry-out wire of FA bit 7. When you do BRCS (Branch if Carry Set), the CPU is testing that one wire.


Step 4: Subtraction — Two's Complement

The ALU does not have a separate subtractor circuit. Subtraction is done by inverting B and adding 1 (two's complement negation):

A − B  =  A + (NOT B) + 1
flowchart TD classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef adder fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef ctrl fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef io fill:#f1f5f9,stroke:#475569,color:#1e293b subgraph OPERANDS ["Operands"] A3["A (8-bit operand)"]:::io B3["B (8-bit operand)"]:::io SUB_SEL["SUB control signal\n0 = ADD, 1 = SUBTRACT\n(from instruction decoder)"]:::ctrl end XINV["XOR array (8 XOR gates)\nEach bit: B[i] XOR SUB\nSUB=0 → B passes unchanged\nSUB=1 → B is fully inverted (~B)"]:::gate ADDER8["8-bit Ripple Carry Adder\nA + XOR_output + Cin\n= A + B when SUB=0\n= A + (~B) + 1 when SUB=1 (= A − B)"]:::adder RESULT["Result (8-bit)\nFlags updated: C, Z, N, V, H"]:::io B3 --> XINV SUB_SEL --> XINV SUB_SEL -->|"also drives Cin\n(provides the +1)"| ADDER8 A3 --> ADDER8 XINV --> ADDER8 ADDER8 --> RESULT ADDER8 --> RESULT

One adder circuit handles both ADD and SUB by routing the SUB control signal to XOR gates on the B input and to Cin. Elegant.


Step 5: Adding More Operations — The Full ALU

The real ALU selects between multiple operations using a multiplexer controlled by the opcode:

flowchart TD classDef op fill:#ffedd5,stroke:#ea580c,color:#9a3412 classDef mux fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef ctrl fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef out fill:#dcfce7,stroke:#16a34a,color:#14532d classDef in fill:#f1f5f9,stroke:#475569,color:#1e293b A4["Operand A\n(8-bit)"]:::in B4["Operand B\n(8-bit)"]:::in ADD_UNIT["Adder/Subtractor\n(A + B or A − B)"]:::op AND_UNIT["AND array\n(A AND B, bitwise)"]:::op OR_UNIT["OR array\n(A OR B, bitwise)"]:::op XOR_UNIT["XOR array\n(A XOR B, bitwise)"]:::op SHIFT_UNIT["Barrel Shifter\n(LSL, LSR, ROL, ROR)"]:::op PASS_UNIT["Passthrough\n(COM, NEG, INC, DEC)"]:::op MUX4["4-bit\nOperation\nMux"]:::mux OPSEL["Operation Select\n(from decoded opcode bits)"]:::ctrl RESULT2["ALU Result (8-bit)"]:::out FLAGS["SREG Flags\nC, Z, N, V, H, S"]:::out A4 & B4 --> ADD_UNIT A4 & B4 --> AND_UNIT A4 & B4 --> OR_UNIT A4 & B4 --> XOR_UNIT A4 --> SHIFT_UNIT A4 --> PASS_UNIT ADD_UNIT & AND_UNIT & OR_UNIT & XOR_UNIT & SHIFT_UNIT & PASS_UNIT --> MUX4 OPSEL --> MUX4 MUX4 --> RESULT2 MUX4 --> FLAGS

All operation units compute their result in parallel every clock cycle. The MUX simply selects which result to output. This is why the ALU is fast — no waiting for the right unit to "turn on."


Part 2 — Building Registers

Step 1: The SR Latch — One Bit of Memory

The simplest memory circuit: two cross-coupled NAND gates. Set (S) forces output to 1. Reset (R) forces output to 0. When both inputs are inactive, the output holds its last value.

flowchart TD classDef gate fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef io fill:#f1f5f9,stroke:#475569,color:#1e293b classDef state fill:#dcfce7,stroke:#16a34a,color:#14532d subgraph INPUTS2 ["Inputs"] S["S̄ (Set — active LOW)\nPull low to force Q=1"]:::io R["R̄ (Reset — active LOW)\nPull low to force Q=0"]:::io end subgraph LATCH ["Cross-coupled NAND gates\n(the memory element)"] NAND1["NAND 1"]:::gate NAND2["NAND 2"]:::gate NAND1 -->|"Q feeds back\nto reinforce itself"| NAND2 NAND2 -->|"Q̄ feeds back\nto reinforce itself"| NAND1 end subgraph OUTPUTS2 ["Stored state outputs"] Q["Q (stored bit)\ncurrent value held here"]:::state QB["Q̄ (complement)\nalways opposite of Q"]:::state end S --> NAND1 R --> NAND2 NAND1 --> Q NAND2 --> QB

The feedback wires are what make this a memory element — the output feeds back to reinforce itself. Remove power and the state is lost (volatile). This is why SRAM loses data on power-off.


Step 2: The D Flip-Flop — Clocked Storage

The SR latch has a problem: S and R must not both be active simultaneously. The D flip-flop solves this with a single data input and a clock:

flowchart TD classDef ff fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef io fill:#f1f5f9,stroke:#475569,color:#1e293b classDef state fill:#dcfce7,stroke:#16a34a,color:#14532d classDef clk fill:#fef9c3,stroke:#ca8a04,color:#713f12 subgraph DFFINPUTS ["Inputs"] D["D (data in)\nthe bit you want to store"]:::io CLK["CLK (clock)\nrising edge = capture moment"]:::clk end MASTER["Master Latch\nTransparent when CLK = 0\nSamples D continuously\nwhile clock is LOW"]:::ff SLAVE["Slave Latch\nTransparent when CLK = 1\nCaptures master output\nwhen clock goes HIGH"]:::ff Q2["Q (output)\nHolds captured value\nuntil next rising edge\nof CLK"]:::state D --> MASTER CLK -->|"CLK=0: open\nCLK=1: closed"| MASTER MASTER -->|"passes value through"| SLAVE CLK -->|"CLK=1: open\nCLK=0: closed\n(opposite of master)"| SLAVE SLAVE --> Q2

On the rising clock edge, the D input is captured and held at Q until the next rising edge. This is how every register in every CPU works — data is sampled once per clock cycle.


Step 3: An 8-bit Register

Eight D flip-flops sharing a common clock and a write-enable signal form one 8-bit register:

flowchart TD classDef ff fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef ctrl fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef io fill:#f1f5f9,stroke:#475569,color:#1e293b D7["D7"]:::io D6["D6"]:::io D5["D5"]:::io D0["D0 ... D4"]:::io CLK2["CLK"]:::ctrl WE["Write Enable\n(from control unit:\nonly latch when this register\nis the destination)"]:::ctrl FF7["DFF\nbit 7"]:::ff FF6["DFF\nbit 6"]:::ff FF5["DFF\nbit 5"]:::ff FF0["DFF\nbits 4–0"]:::ff Q7["Q7"]:::io Q6["Q6"]:::io Q5["Q5"]:::io Q0["Q4–Q0"]:::io D7 --> FF7 D6 --> FF6 D5 --> FF5 D0 --> FF0 CLK2 --> FF7 CLK2 --> FF6 CLK2 --> FF5 CLK2 --> FF0 WE --> FF7 WE --> FF6 WE --> FF5 WE --> FF0 FF7 --> Q7 FF6 --> Q6 FF5 --> Q5 FF0 --> Q0

The Write Enable signal is why you can have 32 registers but only write to one at a time — every register sees the ALU result bus, but only the destination register's WE is asserted.


Step 4: The AVR Register File

32 registers × 8 bits each = 256 flip-flops, plus address decode and read/write mux logic:

flowchart TD classDef regbank fill:#dcfce7,stroke:#16a34a,color:#14532d classDef bus fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a classDef decode fill:#f3e8ff,stroke:#9333ea,color:#581c87 classDef alu fill:#ffedd5,stroke:#ea580c,color:#9a3412 INSTR2["Instruction Register\n(current opcode — 16-bit word)"]:::alu RDECODE["Destination Decode\n(bits [8:4] of opcode →\nwhich register gets written)"]:::decode RSRC1["Source 1 Decode\n(bits [9] + [3:0] of opcode)"]:::decode RSRC2["Source 2 Decode\n(bits [9:5] of opcode)"]:::decode RFILE["Register File\nR0 – R31\n32 × 8-bit DFFs\n= 256 flip-flops total"]:::regbank ABUS["A Bus (operand 1)"]:::bus BBUS["B Bus (operand 2)"]:::bus WBUS["Write Bus\n(ALU result)"]:::bus ALU2["ALU"]:::alu INSTR2 --> RDECODE INSTR2 --> RSRC1 INSTR2 --> RSRC2 RSRC1 -->|"read addr 1"| RFILE RSRC2 -->|"read addr 2"| RFILE RDECODE -->|"write addr + WE"| RFILE RFILE --> ABUS RFILE --> BBUS ABUS --> ALU2 BBUS --> ALU2 ALU2 --> WBUS WBUS --> RFILE

The opcode bits directly encode which registers to read from and write to. The decoder is just combinational logic — it converts 5-bit register address fields in the opcode to one-hot write-enable signals across 32 registers.


Part 3 — Flags: The ALU Output You Don't See

After every arithmetic or logical operation, the ALU updates the SREG (Status Register) flags. These are also D flip-flops — they store one bit each:

flowchart TD classDef flag fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef logic fill:#fef9c3,stroke:#ca8a04,color:#713f12 classDef alu fill:#ffedd5,stroke:#ea580c,color:#9a3412 ALU3["ALU Result (8-bit)\nEvery operation produces this"]:::alu subgraph FLAGLOGIC ["Combinational logic that derives each flag"] ZLOGIC["Z — Zero detect\n8-input NOR gate\noutput 1 only if all 8 bits = 0"]:::logic NLOGIC["N — Negative\nbit 7 of result (the MSB)\nsign bit in two's complement"]:::logic CLOGIC["C — Carry\ncarry-out of Full Adder bit 7\nthe wire coming out of the MSB adder"]:::logic VLOGIC["V — Overflow\nXOR of carry-out bit 6 and carry-out bit 7\ndetects signed overflow"]:::logic HLOGIC["H — Half Carry\ncarry-out of bit 3\nused for BCD packed-decimal arithmetic"]:::logic end subgraph FLAGREGS ["Flag flip-flops in SREG (each stores 1 bit)"] ZFLAG["Z flag"]:::flag NFLAG["N flag"]:::flag CFLAG["C flag"]:::flag VFLAG["V flag"]:::flag HFLAG["H flag"]:::flag end ALU3 --> ZLOGIC --> ZFLAG ALU3 --> NLOGIC --> NFLAG ALU3 --> CLOGIC --> CFLAG ALU3 --> VLOGIC --> VFLAG ALU3 --> HLOGIC --> HFLAG

When you write BRNE (Branch if Not Equal) in assembly, the CPU is checking: is the Z flip-flop currently storing a 0? That's it. A conditional branch is a mux selecting between PC+2 and the target address, controlled by a single flag flip-flop.


Putting It All Together — One Clock Cycle

This is what happens inside the AVR in a single clock cycle when executing ADD R18, R16:

sequenceDiagram participant IF as Instruction Fetch participant ID as Instruction Decode participant RF as Register File participant ALU as ALU participant WB as Write Back Note over IF,WB: Rising clock edge — cycle begins IF->>ID: Opcode 0x0F12 (ADD R18, R16) ID->>RF: Read address: src1=R16, src2=R18 (wait, ADD Rd,Rs: Rd gets Rd+Rs, so read R18 and R16) RF->>ALU: A = R18 value, B = R16 value ALU->>ALU: Compute A + B, generate flags ALU->>WB: Result → R18 write bus ALU->>WB: Flags → SREG update WB->>RF: Write result into R18 on next clock edge Note over IF,WB: Next rising edge — result captured in R18

On a pipelined processor (like modern ARM Cortex-M), these stages overlap across multiple instructions — while one instruction is in ALU, the next is being decoded, and the one after that is being fetched. That is how a 16 MHz AVR can execute up to 16 million instructions per second.


Why This Matters for Firmware

Understanding the gate-level hardware explains real embedded programming constraints:

What you do in firmware What's happening in gates
ADD R16, R17 8-bit adder fires, carry and zero flag flip-flops update
BRNE label Zero flag DFF output feeds a mux that selects the next PC value
LDI R16, 5 Register file write-enable for R16 asserted; constant 5 on write bus
int a = 0 in C SRAM DFFs at the variable's address are cleared
Stack overflow Write bus overwrites DFFs in SRAM belonging to other variables or return addresses
volatile keyword Tells compiler: don't cache in register DFFs, always read from SRAM DFFs
Interrupt latency Minimum cycles needed to push PC + SREG DFFs onto stack

Summary



Worked Timing Check

For a synchronous datapath:

[
T_{clk} \ge t_{CQ} + t_{logic,max} + t_{setup} + t_{skew}
]

With (t_{CQ}=0.4\text{ ns}), logic and routing (=6.8\text{ ns}), setup (=0.6\text{ ns}), and skew/jitter allowance (=0.4\text{ ns}), (T_{clk}\ge8.2\text{ ns}) and (f_{max}\le1/8.2\text{ ns}\approx122\text{ MHz}). Hold time is a separate minimum-delay check and cannot be fixed merely by lowering frequency.

Common Mistakes

  • Treating gates as instantaneous instead of checking propagation delay.
  • Using carry-out as signed overflow.
  • Inferring a latch through an incomplete combinational assignment.
  • Releasing an asynchronous reset without clock synchronization.
  • Checking setup but ignoring hold time and clock skew.

Further Reading

Mind Map

mindmap root((Digital CPU)) Core concept Gates form ALU Flip flops store state Muxes select data Applications Add and subtract Registers and counters Branch decisions Formulas Sum equals A xor B xor Cin Subtract uses not B plus 1 Tclk at least CQ plus logic plus setup Fmax at most 1 over Tclk Design rules Separate state and logic Check signed overflow Meet setup and hold Synchronize reset release Practical checks Simulate corner vectors Inspect critical path Verify flags Test reset sequence Common mistakes Carry equals overflow Ignoring gate delay Inferred latch Unsafe reset release