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:
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:
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.
The AVR runs at up to 20 MHz. Every flip-flop in every register captures its input 20 million times per second. The clock is the heartbeat that synchronizes every bit of state in the chip.
Step 3: An 8-bit Register
Eight D flip-flops sharing a common clock and a write-enable signal form one 8-bit register:
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:
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
Key Takeaways
Every ALU operation is built from combinational logic: adders (XOR + AND chains), bitwise units (AND/OR/XOR arrays), and a multiplexer to select the result
Subtraction is addition: the ALU inverts B and uses Cin=1 (two's complement) — no separate subtractor needed
Every register is built from D flip-flops that capture input on the rising clock edge
Flags (SREG) are flip-flops wired to specific combinational outputs of the ALU (zero detector, carry-out, MSB, overflow XOR)
Conditional branches are multiplexers: the flag flip-flop output selects between the next sequential address and the branch target
The clock synchronizes everything — every flip-flop in the chip captures its input simultaneously on each rising edge
**Digital Electronics Deep Dives**
The concepts above are introduced here in the context of a CPU. For full coverage with exercises:
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.
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