Loading header...

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:

  1. Save the return address (where to come back to)
  2. Jump to the function
  3. Execute the function
  4. 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

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

Summary


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.

Further Reading

Mind Map

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