Microcontroller Internals — Registers, ALU & Peripherals
You write C code. The compiler turns it into machine instructions. The microcontroller executes those instructions one at a time. But what is actually happening inside the chip?
This lesson opens the hood.
Learning Objectives
Trace fetch, decode, execute, and write-back; explain registers, flags, buses, stack, and interrupts; read a memory-mapped peripheral sequence; and identify concurrency and timing hazards.
The Big Picture — What's Inside
flowchart TD
classDef cpu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef mem fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef per fill:#ffedd5,stroke:#ea580c,color:#9a3412
classDef bus fill:#f3e8ff,stroke:#9333ea,color:#581c87
classDef clk fill:#fef9c3,stroke:#ca8a04,color:#713f12
CORE["🧠 CPU Core\nALU · Registers · Control Unit\nInstruction Decoder"]:::cpu
subgraph MEM_BLOCK ["💾 Memory"]
FL2["Flash Memory\nProgram Storage\nNon-volatile"]:::mem
SR3["SRAM\nData / Stack\nVolatile"]:::mem
EEPROM["EEPROM\nConfig Storage\nNon-volatile"]:::mem
end
subgraph PER_BLOCK ["⚡ Peripherals"]
GPIO4["GPIO\nDigital I/O"]:::per
UART5["UART / USART\nSerial Comms"]:::per
SPI5["SPI\nHigh-speed Serial"]:::per
I2C5["I2C\nTwo-wire Bus"]:::per
ADC5["ADC\nAnalog Input"]:::per
PWM5["PWM / Timers\nMotor, Tone"]:::per
WDT["Watchdog Timer\nSafety Reset"]:::per
end
CLK["🕐 Clock System\nCrystal / PLL / RC Osc"]:::clk
BUS2["Internal Bus\nAHB / APB"]:::bus
INT["Interrupt Controller\nNVIC / INT"]:::cpu
CORE <--> BUS2
BUS2 <--> MEM_BLOCK
BUS2 <--> PER_BLOCK
CLK --> CORE
CLK --> PER_BLOCK
PER_BLOCK --> INT
INT --> CORE
Every part you see above is on the same silicon die, connected by internal wires running at hundreds of MHz. Let's go through each block.
CPU Registers — The Fastest Memory
Registers are small storage cells inside the CPU. They are not SRAM. Most ALU instructions consume register operands within one instruction's execution; this does not mean access takes zero time, and timing varies by core.
flowchart TD
classDef gpr fill:#dbeafe,stroke:#1565c0,color:#1e3a8a
classDef spl fill:#f3e8ff,stroke:#9333ea,color:#581c87
classDef io fill:#ccfbf1,stroke:#0d9488,color:#134e4a
classDef flag fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
subgraph GPR ["General Purpose Registers (AVR — 32 × 8-bit)"]
R0["R0"]:::gpr
R1["R1"]:::gpr
RN["R2 … R25"]:::gpr
X["X = R27:R26\n16-bit pointer"]:::gpr
Y["Y = R29:R28\n16-bit pointer"]:::gpr
Z["Z = R31:R30\n16-bit pointer"]:::gpr
end
subgraph SPL ["Special Purpose Registers"]
PC2["PC — Program Counter\nAddress of next instruction"]:::spl
SP2["SP — Stack Pointer\nTop of current stack frame"]:::spl
SREG["SREG — Status Register\nCondition flags: C Z N V S H T I"]:::flag
end
subgraph IOR ["I/O Registers (0x20–0xFF)"]
PORTB["PORTB — GPIO output values"]:::io
DDRB["DDRB — GPIO direction"]:::io
TIMSK["TIMSK — Timer interrupt mask"]:::io
ADCL["ADCL/ADCH — ADC result"]:::io
end
What each type does
Register Type
Purpose
Speed
General Purpose (R0–R31)
Arithmetic, logic, data movement
Usually consumed within an instruction
Program Counter (PC)
Points to the next instruction
CPU internal
Stack Pointer (SP)
Tracks the top of the call stack
CPU internal
Status Register (SREG)
Flags set after each ALU operation
CPU internal
I/O Registers
Control peripherals (GPIO, UART, ADC)
1 cycle
Memory-mapped registers
Same as I/O but in RAM address space
1–2 cycles
The Status Register (Condition Flags)
The Status Register (SREG on AVR, CPSR on ARM) is the most important feedback mechanism in the CPU. Every arithmetic or logic instruction updates it.
flowchart TD
classDef flag fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef desc fill:#f1f5f9,stroke:#475569,color:#1e293b
subgraph CTRL ["Control Flags"]
I["I — Global Interrupt Enable"]:::flag
ID["1 = interrupts enabled\n0 = all interrupts masked"]:::desc
T["T — Bit Copy Storage"]:::flag
TD2["Scratch bit for BLD / BST instructions"]:::desc
I --> ID
T --> TD2
end
subgraph ARITH ["Arithmetic Flags (set after every ALU op)"]
H["H — Half Carry"]:::flag
HD["Carry from bit 3 → bit 4\nNeeded for BCD arithmetic"]:::desc
C["C — Carry"]:::flag
CD["Carry / borrow out of bit 7\nMulti-byte arithmetic, BRCS/BRCC"]:::desc
V["V — Overflow"]:::flag
VD["Signed overflow: 127+1 → −128\nDetected by XOR of two carry bits"]:::desc
H --> HD
C --> CD
V --> VD
end
subgraph RESULT ["Result Flags (most useful in everyday code)"]
N["N — Negative"]:::flag
ND["Result MSB = 1 (negative in two's complement)"]:::desc
Z["Z — Zero"]:::flag
ZD["Result = 0 exactly\nMost-used flag: BRNE / BREQ in every loop"]:::desc
S["S — Sign"]:::flag
SD["S = N XOR V\nTrue arithmetic sign after overflow"]:::desc
N --> ND
Z --> ZD
S --> SD
end
Why flags matter — conditional branching
Every if, while, for in C compiles down to instructions that check the flags:
sequenceDiagram
participant C as C Code
participant ASM as Assembly
participant CPU as CPU / SREG
C->>ASM: if (a == b)
ASM->>CPU: CP R16, R17 (compare a and b)
CPU->>CPU: Subtract R17 from R16, update SREG
Note over CPU: Z=1 if equal, Z=0 if not
ASM->>CPU: BRNE skip_body (branch if Z=0)
CPU->>CPU: Z=1 → don't branch → execute if-body
CPU->>CPU: Z=0 → branch to skip_body
Without the status register, the CPU could not make any decisions. It would execute instructions blindly, in sequence, forever.
The Arithmetic Logic Unit (ALU)
The ALU is the calculator inside the CPU. Simple operations are often single-cycle; multiplication, division, memory access, and floating-point timing depend on the core.
flowchart TD
classDef input fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef alu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef output fill:#fce7f3,stroke:#db2777,color:#831843
classDef flag2 fill:#ffedd5,stroke:#ea580c,color:#9a3412
RA["Operand A\n(from Register Rn)"]:::input
RB["Operand B\n(from Register Rm\nor immediate value)"]:::input
subgraph ALU_BLOCK ["ALU"]
ADD["ADD / SUB\nAddition, Subtraction"]:::alu
AND2["AND / OR / XOR\nBitwise Logic"]:::alu
SH["LSL / LSR / ROL / ROR\nShift & Rotate"]:::alu
MUL2["MUL\nMultiplication\n(hardware or emulated)"]:::alu
CMP["CP / CPI\nCompare (subtract,\ndiscard result)"]:::alu
end
RA --> ALU_BLOCK
RB --> ALU_BLOCK
ALU_BLOCK --> RESULT["Result\n(written back to register)"]:::output
ALU_BLOCK --> SREG2["SREG Updated\nC Z N V S H flags"]:::flag2
ALU Operations by category
flowchart LR
classDef arith fill:#e0e7ff,stroke:#4338ca,color:#312e81
classDef logic fill:#ccfbf1,stroke:#0d9488,color:#134e4a
classDef shift fill:#f3e8ff,stroke:#9333ea,color:#581c87
classDef comp fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
subgraph ARITH ["Arithmetic"]
A1["ADD — Add two registers"]:::arith
A2["SUB — Subtract"]:::arith
A3["INC — Increment by 1"]:::arith
A4["DEC — Decrement by 1"]:::arith
A5["NEG — Two's complement negate"]:::arith
A6["MUL — Multiply"]:::arith
end
subgraph LOGIC ["Logic"]
L1["AND — Bitwise AND\n(clear bits: mask)"]:::logic
L2["OR — Bitwise OR\n(set bits)"]:::logic
L3["EOR — Bitwise XOR\n(toggle bits)"]:::logic
L4["COM — Complement\n(invert all bits)"]:::logic
end
subgraph SHIFT ["Shift / Rotate"]
S1["LSL — Logical Shift Left\n(× 2)"]:::shift
S2["LSR — Logical Shift Right\n(÷ 2)"]:::shift
S3["ROL — Rotate Left through Carry"]:::shift
S4["ROR — Rotate Right through Carry"]:::shift
end
subgraph COMP2 ["Compare / Test"]
C1["CP — Compare (no result stored)"]:::comp
C2["TST — Test bits (AND, no store)"]:::comp
C3["SBRC — Skip if bit clear"]:::comp
end
How a Program Executes — Step by Step
Let's trace exactly what happens when the CPU runs c = a + b where a=5, b=3:
sequenceDiagram
participant Flash as Flash Memory
participant PC3 as Program Counter
participant IR2 as Instruction Register
participant REG2 as Registers
participant ALU3 as ALU
participant SREG3 as SREG
Note over Flash: Address 0x100: LDI R16, 5
Note over Flash: Address 0x102: LDI R17, 3
Note over Flash: Address 0x104: ADD R18, R16, R17
PC3->>Flash: Fetch instruction at 0x100
Flash-->>IR2: LDI R16, 5
IR2->>REG2: Load immediate 5 into R16
PC3->>PC3: PC = 0x102
PC3->>Flash: Fetch instruction at 0x102
Flash-->>IR2: LDI R17, 3
IR2->>REG2: Load immediate 3 into R17
PC3->>PC3: PC = 0x104
PC3->>Flash: Fetch instruction at 0x104
Flash-->>IR2: ADD R18, R16, R17
IR2->>ALU3: Operands: R16=5, R17=3, operation=ADD
ALU3->>REG2: Result 8 → R18
ALU3->>SREG3: Z=0, N=0, C=0, V=0
PC3->>PC3: PC = 0x106
Every instruction — even something as simple as a + b — goes through this exact sequence. On a modern microcontroller running at 16 MHz, this loop repeats 16 million times per second.
The Call Stack — How Functions Work
When you call a function, the CPU needs to:
Save the return address (where to come back to)
Jump to the function
Execute the function
Return and restore the return address
flowchart TD
classDef stack fill:#ccfbf1,stroke:#0d9488,color:#134e4a
classDef code fill:#e0e7ff,stroke:#4338ca,color:#312e81
classDef sp fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
subgraph STACK ["SRAM — Stack (grows downward)"]
SP_TOP["SP → 0x08FF\n(top of stack, empty)"]:::sp
FR1["0x08FE: Return addr HIGH\n(pushed by CALL)"]:::stack
FR2["0x08FD: Return addr LOW"]:::stack
FR3["0x08FC: Saved R16\n(pushed by function prologue)"]:::stack
FR4["0x08FB: Local variable 'x'\n(allocated on stack)"]:::stack
SP_BOT["SP → 0x08FA\n(after function entry)"]:::sp
end
subgraph FLOW ["Execution Flow"]
MAIN2["main()\nCALL add_values"]:::code
FUNC["add_values()\nPUSH R16\nLDI R16, ...\nADD ...\nPOP R16\nRET"]:::code
BACK["main() continues\n(SP restored)"]:::code
end
MAIN2 -->|"CALL — pushes return addr, jumps"| FUNC
FUNC -->|"RET — pops return addr, jumps back"| BACK
Stack overflow happens when too many nested function calls or too many local variables exhaust SRAM. On an ATmega328P with only 2 KB SRAM, this happens faster than you expect. Use the `-Wstack-usage` compiler flag to track it.
Peripherals — Controlled by Registers
Every peripheral in a microcontroller is controlled by reading and writing memory-mapped registers. There is no "call a function to set GPIO high" in hardware — you write a bit to a register.
flowchart LR
classDef reg fill:#fdf2f8,stroke:#be185d,color:#831843
classDef hw fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef cpu fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
CPU4["CPU\nwrite 1 to PORTB bit 5\n(address 0x25)"]:::cpu
subgraph GPIO_REG ["GPIO Register Bank"]
DDRB2["DDRB — 0x24\nDirection: 1=output\n0=input"]:::reg
PORTB2["PORTB — 0x25\nOutput value:\n1=high, 0=low"]:::reg
PINB["PINB — 0x23\nInput value\n(read only)"]:::reg
end
subgraph GPIO_HW ["Physical GPIO Pin"]
BUF["Output Buffer\n(when DDRB bit = 1)"]:::hw
PIN_PHYS["Pin PB5\n(connects to LED, button...)"]:::hw
PULL["Pull-up Resistor\n(when PORTB bit = 1, DDR = 0)"]:::hw
end
CPU4 --> GPIO_REG
PORTB2 --> BUF
BUF --> PIN_PHYS
DDRB2 --> BUF
PINB -.->|"reads back"| PIN_PHYS
PULL -.-> PIN_PHYS
In C code this looks like:
DDRB |= (1 << PB5); // Set PB5 as output (write to DDRB register)
PORTB |= (1 << PB5); // Set PB5 high (write to PORTB register)
Both lines compile to a single instruction each: write a byte to a fixed memory address.
Interrupts — Hardware Calling Your Code
Peripherals signal the CPU using interrupts — a hardware mechanism that pauses the main program, runs a handler function (ISR), then resumes exactly where it left off.
sequenceDiagram
participant MAIN3 as Main Program
participant CPU5 as CPU
participant UART6 as UART Peripheral
participant ISR as UART ISR
Note over MAIN3: Running loop — doing work
MAIN3->>CPU5: Executing instruction at 0x1A4
UART6->>CPU5: Interrupt signal: byte received!
Note over CPU5: Finish current instruction
CPU5->>CPU5: Push PC and SREG to stack
CPU5->>CPU5: Clear I flag (disable further interrupts)
CPU5->>ISR: Jump to UART ISR vector (0x0026)
ISR->>ISR: Read received byte from UDR0
ISR->>ISR: Store in ring buffer
ISR->>CPU5: RETI instruction
CPU5->>CPU5: Pop SREG and PC from stack
CPU5->>CPU5: Restore I flag
CPU5->>MAIN3: Resume at 0x1A4 — main never knew
Interrupt entry is architecture-specific. Classic AVR hardware pushes the return address and clears the global interrupt-enable bit; compiler-generated ISR code preserves status and used registers. Check the architecture manual and inspect generated code.
Summary
Key Takeaways
Registers are the fastest storage in the system — zero-cycle access, live inside the CPU
SREG / Status Register holds condition flags (Zero, Carry, Negative, Overflow) updated after every ALU operation — it is what makes `if` and `while` possible
ALU performs all arithmetic and logic in one clock cycle, always writes result back to a register and updates flags
Every peripheral is controlled by memory-mapped registers — GPIO, UART, ADC — all registers
Interrupts let hardware pause the CPU, run a short handler, and resume seamlessly
A C statement like `c = a + b` compiles to: load a → load b → ALU add → store c — typically 3–4 instructions
Worked Example: Atomic Register Update
If main code and an ISR both perform a read-modify-write on one control register, an interrupt between load and store can lose an update. Prefer hardware atomic set/clear registers. Otherwise protect only the short sequence and restore the previous interrupt state. volatile forces observable accesses; it does not make an operation atomic.
Common Mistakes
Confusing CPU registers with memory-mapped peripheral registers.
Treating volatile as a lock or memory barrier.
Enabling interrupts before the stack and vector table are valid.
Writing reserved bits or skipping documented read-back delays.
Doing blocking I/O or long computations inside an ISR.
mindmap
root((MCU Internals))
Core concept
Fetch decode execute
Registers feed ALU
Buses join memory and I O
Applications
GPIO drivers
Timers and UART
Interrupt firmware
Timing
Clock period equals 1 over f
CPI depends on instruction
Include memory wait states
Design rules
Follow register sequence
Keep ISRs short
Protect shared state
Preserve prior IRQ state
Practical checks
Inspect disassembly
Read timing tables
Check stack high water
Verify reserved bits
Common mistakes
Zero cycle myth
Volatile means atomic
Assuming saved context
Long blocking ISR