LEARN FIRMWARE DEEP DIVE
Learn Firmware: From Power-On to Bare-Metal Mastery
Goal: Deeply understand firmware—what runs before your operating system, how bootloaders work, how hardware gets initialized, and why firmware is fundamentally different from an OS. You’ll build everything from blinking LEDs without any framework to writing your own UEFI applications.
Why Firmware Matters
When you press the power button on any computer or device, something has to wake up the hardware before your operating system can run. That “something” is firmware—the low-level code burned into chips that brings dead silicon to life.
After completing these projects, you will:
- Understand exactly what happens from power-on to OS boot
- Know the difference between firmware, bootloader, BIOS, UEFI, and OS
- Write code that runs directly on hardware with no OS underneath
- Build your own bootloaders that load other programs
- Understand hardware initialization at the register level
- Appreciate why certain design decisions were made in the boot process
Core Concept Analysis
The Boot Sequence: From Dead Silicon to Running OS
Power On
│
▼
┌─────────────┐
│ RESET │ ◄── CPU starts at fixed address (reset vector)
└─────────────┘
│
▼
┌─────────────┐
│ FIRMWARE │ ◄── BIOS/UEFI: Lives in ROM/Flash, initializes hardware
│ (BIOS/UEFI)│ - Memory controller initialization
└─────────────┘ - CPU configuration
│ - Peripheral detection
▼
┌─────────────┐
│ BOOTLOADER │ ◄── Loaded from disk/flash by firmware
│ (Stage 1) │ - First 512 bytes (MBR) or UEFI app
└─────────────┘ - May load Stage 2 bootloader
│
▼
┌─────────────┐
│ BOOTLOADER │ ◄── Optional: GRUB, Windows Boot Manager
│ (Stage 2) │ - Full filesystem access
└─────────────┘ - Kernel selection menu
│
▼
┌─────────────┐
│ KERNEL │ ◄── Linux, Windows, your OS
│ │ - Sets up virtual memory, drivers
└─────────────┘ - Starts init/systemd
│
▼
┌─────────────┐
│ USERSPACE │ ◄── Applications, services, you
└─────────────┘
Firmware vs Bootloader vs Operating System
| Aspect | Firmware | Bootloader | Operating System |
|---|---|---|---|
| Where it lives | ROM/Flash chip on motherboard | First sectors of disk OR Flash | Hard drive/SSD |
| When it runs | First thing after power-on | After firmware hands off control | After bootloader loads it |
| Primary job | Initialize hardware, provide basic services | Load the OS kernel into memory | Manage resources, run applications |
| Can be replaced? | Only via “flashing” | Yes, by writing to boot sector | Yes, by installation |
| Examples | BIOS, UEFI, Arduino bootloader | GRUB, LILO, Windows Boot Mgr | Linux, Windows, macOS |
| Has filesystem access? | Limited or none (UEFI has FAT) | Stage 1: No, Stage 2: Yes | Full access |
| Memory model | Real mode (16-bit) or early protected | Transitions to protected/long mode | Full virtual memory |
Key Questions This Learning Path Answers
- “What code runs first?” — The CPU starts at a fixed address (reset vector) where firmware lives
- “How does hardware ‘wake up’?” — Firmware initializes memory controllers, clocks, and peripherals
- “Why can’t we just boot directly into Linux?” — The kernel expects initialized hardware and needs to be loaded from disk
- “What’s the difference between BIOS and UEFI?” — BIOS is legacy 16-bit real mode; UEFI is modern 32/64-bit with drivers
- “Do embedded systems have bootloaders?” — Some do (for OTA updates), some boot directly into firmware
- “Why 512 bytes for a bootloader?” — That’s the size of one disk sector (MBR), a hardware constraint from the 1980s
The Firmware Spectrum
Less Abstraction More Abstraction
◄─────────────────────────────────────────────────────────────────────►
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│Bare Metal│ │ Firmware │ │ RTOS │ │Embedded │ │ Full │
│ (No OS) │ │ (Custom) │ │(FreeRTOS)│ │ Linux │ │ OS │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │ │
│ │ │ │ │
LED blink Printer Car ECU Router Desktop
Sensor read Keyboard Drone flight Smart TV Laptop
Motor control Mouse Motor IoT device Server
Project List
Projects are ordered from fundamental understanding to advanced implementations.
Project 1: The Simplest Bare-Metal Program (LED Blinker Without Any Framework)
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C (with Assembly startup)
- Alternative Programming Languages: Rust, Assembly, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Bare-Metal / Embedded Systems
- Software or Tool: Raspberry Pi Pico (RP2040) or STM32
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: An LED that blinks on a microcontroller, but with ZERO frameworks—no Arduino, no SDK, no HAL. Just your code, the datasheet, and raw register manipulation.
Why it teaches firmware: This is the purest form of firmware. You’ll understand that all those “libraries” are just convenience wrappers around memory-mapped registers. When you write GPIO->ODR = 1, you’re literally writing to a memory address that the hardware interprets as “turn on pin.”
Core challenges you’ll face:
- Understanding the startup code → maps to how does the CPU even begin executing your code?
- Clock configuration → maps to why do peripherals need clocks enabled?
- Memory-mapped I/O → maps to how does writing to an address control hardware?
- Reading the datasheet → maps to the most critical skill in embedded development
Key Concepts:
- Reset Vector & Startup: “Making Embedded Systems” Chapter 2 - Elecia White
- Memory-Mapped I/O: “Computer Systems: A Programmer’s Perspective” Chapter 6 - Bryant & O’Hallaron
- ARM Cortex-M Architecture: ARM Cortex-M for Beginners
- GPIO Registers: RP2040 Datasheet Chapter 2.19 or STM32 Reference Manual
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Basic C programming, understanding of binary/hexadecimal, willingness to read datasheets. No prior embedded experience required.
Real world outcome:
When you power on your Raspberry Pi Pico (or STM32 board):
- The LED connected to GPIO25 (Pico) or GPIO13 (STM32) blinks at 1Hz
- No USB serial, no printf—just hardware doing exactly what you told it
- You can modify the blink rate by changing a delay value
Verification: If the LED blinks, you've successfully:
✓ Set up the vector table
✓ Configured system clocks
✓ Enabled GPIO peripheral clock
✓ Configured pin as output
✓ Toggled pin state in a loop
Implementation Hints:
The boot process for ARM Cortex-M:
1. CPU reads vector table at address 0x00000000
2. First entry: Initial Stack Pointer value
3. Second entry: Reset Handler address
4. CPU jumps to Reset Handler
5. Your code runs!
What your startup code must do:
startup.s (Assembly):
1. Define the vector table (stack pointer + reset handler at minimum)
2. Reset handler: Copy .data from Flash to RAM
3. Reset handler: Zero out .bss section
4. Reset handler: Call main()
linker.ld (Linker Script):
1. Define memory regions (Flash at 0x10000000, RAM at 0x20000000 for RP2040)
2. Place .text (code) in Flash
3. Place .data initial values in Flash, but runtime location in RAM
4. Place .bss in RAM
Key questions to answer as you build:
- What is the vector table and why is it at address 0?
- Why do you need to copy .data from Flash to RAM?
- What happens if you don’t zero .bss?
- Why must you enable the GPIO peripheral clock before using it?
Resources for key challenges:
- Bare Metal Programming Guide (GitHub) - Complete walkthrough for ARM microcontrollers
- RP2040 Bare Metal Examples - NO SDK examples for Pi Pico
- vxj9800/bareMetalRP2040 - Detailed guide to RP2040 without SDK
Learning milestones:
- Vector table compiles and links correctly → You understand the boot process
- Clock configuration works, GPIO toggles → You understand peripheral initialization
- LED blinks at predictable rate → You understand timing without OS
- You can add a second LED or button → You’ve internalized memory-mapped I/O
Project 2: x86 Bootloader - The First 512 Bytes
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: x86 Assembly (NASM)
- Alternative Programming Languages: FASM, GAS Assembly
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Boot Process / x86 Architecture
- Software or Tool: QEMU, NASM
- Main Book: “Write Great Code, Volume 1” by Randall Hyde
What you’ll build: A 512-byte program that runs immediately when a computer boots—before any operating system. It will print a message to the screen using BIOS interrupts.
Why it teaches firmware/bootloaders: This is the classic way to understand what happens at power-on. You’ll see that the BIOS loads exactly 512 bytes from the first sector of the disk to address 0x7C00 and jumps there. That’s it. No magic—just a convention.
Core challenges you’ll face:
- Real Mode programming → maps to the CPU starts in 16-bit mode with 1MB address limit
- BIOS interrupts → maps to how early code communicates with hardware
- The 0x7C00 origin → maps to understanding memory layout at boot
- The 0xAA55 boot signature → maps to how BIOS knows this is bootable
Key Concepts:
- x86 Real Mode: “Write Great Code, Volume 1” Chapter 4 - Randall Hyde
- BIOS Interrupts: Ralf Brown’s Interrupt List
- Boot Sector Format: “Operating Systems: Three Easy Pieces” Appendix - Arpaci-Dusseau
- Segment:Offset Addressing: “x64 Assembly Language Step-by-Step” Chapter 3 - Jeff Duntemann
Difficulty: Advanced Time estimate: Weekend to 1 week Prerequisites: Basic understanding of assembly concepts, hexadecimal, familiarity with how memory works. Project 1 helps but isn’t required.
Real world outcome:
$ nasm -f bin bootloader.asm -o bootloader.bin
$ qemu-system-x86_64 -drive format=raw,file=bootloader.bin
# QEMU window appears showing:
"Hello from the bootloader!"
"This code runs before any OS!"
_ (blinking cursor)
You can also write this to a USB drive and boot a real computer from it (carefully!).
Implementation Hints:
The boot sector structure:
┌────────────────────────────────┐ 0x0000
│ │
│ Your Code (510 bytes) │
│ │
│ - Set up segments │
│ - Print message via INT 10h │
│ - Maybe read more sectors │
│ - Halt or loop forever │
│ │
├────────────────────────────────┤ 0x01FE
│ Boot Signature: 0x55, 0xAA │
└────────────────────────────────┘ 0x0200 (512 bytes total)
Key BIOS interrupts you’ll use:
INT 10h, AH=0Eh - Teletype output (print character)
INT 13h, AH=02h - Read sectors from disk
INT 16h, AH=00h - Wait for keypress
Questions to answer:
- Why 0x7C00? (Historical: IBM PC design decision)
- Why does the CPU start in Real Mode? (Backward compatibility with 8086)
- What if your bootloader is bigger than 512 bytes? (Load more sectors!)
- How does GRUB work? (It’s a multi-stage bootloader)
Resources for key challenges:
- Building a Bootloader from Scratch - 2025 tutorial by Aayush Gid
- Writing My Own Boot Loader - DEV Community walkthrough
- OSDev Wiki - Bootloader - Comprehensive reference
Learning milestones:
- “Hello” prints in QEMU → You understand BIOS interrupts and boot sector structure
- You understand segment:offset addressing → You grasp Real Mode memory model
- You can read additional sectors from disk → You understand multi-stage bootloaders
- You can switch to protected mode → You’re ready for OS development
Project 3: Multi-Stage Bootloader with Kernel Loading
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: x86 Assembly + C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 4: Expert
- Knowledge Area: Boot Process / Protected Mode
- Software or Tool: QEMU, NASM, GCC cross-compiler
- Main Book: “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau
What you’ll build: A complete boot chain: Stage 1 bootloader (512 bytes) loads Stage 2 (larger), which switches to protected mode and loads a simple C kernel that prints “Kernel loaded!”
Why it teaches firmware/bootloaders: This shows you exactly how real bootloaders (GRUB, Windows Boot Manager) work. The 512-byte limit is a real constraint, so bootloaders chain-load larger pieces. You’ll also understand the critical transition from Real Mode (16-bit BIOS world) to Protected Mode (32-bit OS world).
Core challenges you’ll face:
- Chain loading → maps to how bootloaders overcome the 512-byte limit
- A20 line enabling → maps to historical x86 quirks you must handle
- GDT setup and protected mode switch → maps to fundamental CPU mode transition
- Calling C from Assembly → maps to the firmware/kernel interface
Key Concepts:
- Protected Mode: “Operating Systems: Three Easy Pieces” Chapter 6 - Arpaci-Dusseau
- Global Descriptor Table (GDT): “Write Great Code, Volume 2” Chapter 11 - Randall Hyde
- A20 Line History: OSDev Wiki - A20 Line
- Cross-Compilation: “The Art of 64-Bit Assembly” Chapter 1 - Randall Hyde
Difficulty: Expert Time estimate: 2-4 weeks Prerequisites: Project 2 (x86 bootloader basics), understanding of x86 assembly, basic C knowledge. Familiarity with linker scripts helpful.
Real world outcome:
$ make
$ qemu-system-i386 -fda os.img
# QEMU shows:
Stage 1: Loading stage 2...
Stage 2: Setting up GDT...
Stage 2: Enabling A20...
Stage 2: Switching to protected mode...
Stage 2: Loading kernel...
=====================================
Hello from the C Kernel!
Running in 32-bit Protected Mode
Free memory: 640KB conventional
=====================================
Implementation Hints:
The boot chain:
┌─────────────────────────────────────────────────────────┐
│ BIOS loads Stage 1 (sector 0) to 0x7C00 │
└─────────────────┬───────────────────────────────────────┘
│ Stage 1 loads Stage 2 (sectors 1-N)
▼
┌─────────────────────────────────────────────────────────┐
│ Stage 2 runs at 0x7E00 (just after Stage 1) │
│ 1. Enable A20 gate │
│ 2. Load GDT │
│ 3. Set PE bit in CR0 (switch to protected mode) │
│ 4. Far jump to flush pipeline │
│ 5. Load kernel from disk │
│ 6. Jump to kernel entry point │
└─────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ C Kernel runs at 0x100000 (1MB mark) │
│ - Full 32-bit protected mode │
│ - Can access all memory │
│ - Your OS begins here! │
└─────────────────────────────────────────────────────────┘
The GDT (Global Descriptor Table) - essential for protected mode:
Entry 0: Null descriptor (required)
Entry 1: Code segment (base=0, limit=4GB, executable)
Entry 2: Data segment (base=0, limit=4GB, read/write)
Resources for key challenges:
- Nick Blundell’s “Writing a Simple Operating System” - Classic tutorial
- OSDev Wiki - Protected Mode - Definitive reference
- lukearend/x86-bootloader - Well-documented example
Learning milestones:
- Stage 2 loads and prints → You understand disk reading in real mode
- Protected mode switch works → You understand GDT and CPU modes
- C kernel runs → You understand the assembly-to-C handoff
- You can add simple features to kernel → You’re ready for OS development
Project 4: UART Driver from Scratch (Serial Communication)
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++, Assembly
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Peripheral Drivers / Serial Communication
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “The Linux Programming Interface” by Michael Kerrisk
What you’ll build: A complete UART (serial port) driver that lets your microcontroller send and receive text over a USB-to-serial adapter. You’ll implement uart_init(), uart_putc(), uart_getc(), and eventually printf()-style output.
Why it teaches firmware: UART is the “printf of embedded”—the first peripheral you debug everything else with. By building it from registers, you’ll understand baud rate calculation, FIFO buffers, interrupts, and the general pattern all peripheral drivers follow.
Core challenges you’ll face:
- Baud rate calculation → maps to understanding clock dividers
- Register configuration sequence → maps to reading datasheets properly
- Polling vs interrupts → maps to fundamental driver design choice
- Ring buffers for TX/RX → maps to data structure design in firmware
Key Concepts:
- UART Protocol: “Making Embedded Systems” Chapter 10 - Elecia White
- Baud Rate Generation: Your chip’s Reference Manual, UART/USART chapter
- Ring Buffers: “C Interfaces and Implementations” Chapter 11 - David Hanson
- Interrupt-Driven I/O: “Computer Systems: A Programmer’s Perspective” Chapter 8
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 1 (bare-metal basics), understanding of how peripherals work. C programming proficiency.
Real world outcome:
# On your computer, connect via USB-serial adapter:
$ screen /dev/ttyUSB0 115200
# Your microcontroller prints:
UART initialized at 115200 baud
Enter a character:
# You type 'H':
You typed: H (0x48)
Enter a character:
# Eventually, you implement printf:
Temperature: 23.5°C
Uptime: 00:01:23
Free heap: 2048 bytes
Implementation Hints:
UART register access pattern (typical ARM):
// Enable peripheral clock (RCC)
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// Configure GPIO pins for UART (alternate function)
GPIOA->MODER |= GPIO_MODER_MODE2_1; // PA2 = TX
GPIOA->AFR[0] |= (7 << 8); // AF7 for USART2
// Configure UART: 8N1 @ 115200 baud
USART2->BRR = SystemCoreClock / 115200;
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
Baud rate calculation:
BRR = f_CLK / desired_baud_rate
For 16MHz clock and 115200 baud:
BRR = 16000000 / 115200 ≈ 139
Questions to answer:
- What happens if TX and RX have different baud rates?
- Why do we need start and stop bits?
- What’s the difference between UART and USART?
- How does hardware flow control (RTS/CTS) work?
Resources for key challenges:
- Bare-metal UART Driver for STM32F411 - Complete walkthrough
- Vivonomicon’s UART Tutorial - Part of bare-metal STM32 series
- STM32 Bare-Metal Drivers - GPIO, I2C, SPI, USART from scratch
Learning milestones:
- Single character transmit works → You understand basic UART registers
- Receive works (polling) → You understand the full protocol
- Interrupt-driven TX/RX with buffers → You understand driver architecture
- You implement printf over UART → You’ve built a debugging foundation
Project 5: GPIO and Interrupt Controller from Scratch
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Peripheral Drivers / Interrupts
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A GPIO driver that handles input/output configuration, pull-up/pull-down resistors, and external interrupts. When you press a button, an interrupt fires and toggles an LED—all without polling.
Why it teaches firmware: Interrupts are how real firmware works. Your UART driver from Project 4 probably polled—that wastes CPU cycles. Real firmware uses interrupts to react instantly to hardware events while the CPU does other work.
Core challenges you’ll face:
- NVIC (Nested Vectored Interrupt Controller) → maps to how ARM handles priorities
- EXTI (External Interrupt) configuration → maps to connecting GPIO to interrupts
- Debouncing in ISR → maps to real-world hardware issues
- Critical sections → maps to avoiding race conditions
Key Concepts:
- ARM NVIC: “The Definitive Guide to ARM Cortex-M3 and Cortex-M4” Chapter 7 - Joseph Yiu
- Interrupt Priorities: “Making Embedded Systems” Chapter 5 - Elecia White
- Critical Sections: “Real-Time Concepts for Embedded Systems” Chapter 8 - Qing Li
- Debouncing: Jack Ganssle’s “A Guide to Debouncing”
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 1 (bare-metal basics), Project 4 (UART for debugging). Understanding of basic C and pointers.
Real world outcome:
# Your microcontroller runs, LED is off
# You press the button connected to PA0
[UART output]
Button pressed! (interrupt fired)
LED toggled to: ON
ISR execution time: 2.3 microseconds
# You press again
Button pressed! (interrupt fired)
LED toggled to: OFF
ISR execution time: 2.1 microseconds
# Your main loop continues doing other work:
Main loop iteration: 15342
Sensor reading: 127
Main loop iteration: 15343
Implementation Hints:
Interrupt flow on ARM Cortex-M:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ GPIO Pin │────▶│ EXTI │────▶│ NVIC │
│ (Button) │ │ Controller │ │ (enables/ │
└─────────────┘ │ (edge │ │ priorities)│
│ detect) │ └──────┬──────┘
└─────────────┘ │
▼
┌─────────────────┐
│ Vector Table │
│ (your ISR │
│ address) │
└─────────────────┘
ISR best practices:
void EXTI0_IRQHandler(void) {
// 1. Clear interrupt flag FIRST (or it fires again immediately)
EXTI->PR |= EXTI_PR_PR0;
// 2. Do minimal work (set a flag, increment counter)
button_pressed = true;
// 3. Return quickly (< 100 microseconds ideally)
}
// In main loop:
if (button_pressed) {
button_pressed = false;
// Do the actual work here, not in ISR
toggle_led();
}
Resources for key challenges:
- EmbeTronicX STM32 GPIO Tutorial - Bare metal GPIO without HAL
- stm32f4-gpio-driver - Complete GPIO driver from scratch
Learning milestones:
- Button read works (polling) → You understand GPIO input configuration
- Interrupt fires on button press → You understand EXTI and NVIC
- Debouncing prevents multiple triggers → You understand real hardware issues
- You can chain multiple interrupt sources → You understand interrupt priorities
Project 6: SPI Driver and Talking to External Chips
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Communication Protocols / Peripheral Drivers
- Software or Tool: STM32 or Raspberry Pi Pico + SPI device (e.g., SD card, display, sensor)
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A SPI driver that can communicate with external chips—like reading a sensor, writing to a display, or reading/writing an SD card. You’ll implement clock polarity, phase, and chip select management.
Why it teaches firmware: Most interesting embedded projects involve multiple chips talking to each other. SPI is the most common high-speed peripheral bus. Understanding it means you can interface with displays, sensors, flash memory, and more.
Core challenges you’ll face:
- Clock polarity and phase (CPOL/CPHA) → maps to timing diagrams in datasheets
- Chip select management → maps to multi-device buses
- DMA for high-speed transfers → maps to efficient data movement
- Decoding device-specific protocols → maps to reading sensor datasheets
Key Concepts:
- SPI Protocol: “Making Embedded Systems” Chapter 9 - Elecia White
- Clock Modes (0-3): Device datasheet + SparkFun SPI Tutorial
- DMA Transfers: Your chip’s Reference Manual, DMA chapter
- SD Card SPI Mode: SD Specification Part 1
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4 (UART for debugging), Project 5 (GPIO for chip select). Understanding of timing diagrams.
Real world outcome:
# Connect an SPI temperature sensor (e.g., MAX31855) and SD card
[UART output]
SPI initialized: 1 MHz, Mode 0
Probing MAX31855...
Thermocouple: 23.75°C
Internal: 24.50°C
SD Card detected: SDHC 8GB
Reading sector 0... OK
FAT32 filesystem found
Files: boot.txt, data.csv
Logging temperature to data.csv...
23.75°C written
Implementation Hints:
SPI timing modes (most devices are mode 0 or 3):
Mode 0: CPOL=0, CPHA=0 (most common)
- Clock idle LOW
- Sample on rising edge
Mode 3: CPOL=1, CPHA=1
- Clock idle HIGH
- Sample on rising edge
Basic SPI transaction:
uint8_t spi_transfer(uint8_t data) {
// 1. Wait until TX buffer empty
while (!(SPI1->SR & SPI_SR_TXE));
// 2. Write data
SPI1->DR = data;
// 3. Wait until RX buffer full
while (!(SPI1->SR & SPI_SR_RXNE));
// 4. Read received data
return SPI1->DR;
}
// Using it with a device:
void read_sensor(uint8_t reg, uint8_t *buf, size_t len) {
gpio_clear(CS_PIN); // Assert chip select
spi_transfer(reg | 0x80); // Read command (bit 7 set)
for (size_t i = 0; i < len; i++) {
buf[i] = spi_transfer(0xFF); // Send dummy, receive data
}
gpio_set(CS_PIN); // Deassert chip select
}
Resources for key challenges:
- BareMetalDrivers - SPI - SPI driver from scratch
- The Book of I2C by Randall Hyde - Also covers SPI concepts
Learning milestones:
- SPI loopback works (MOSI to MISO) → You understand basic SPI timing
- External device responds → You understand chip select and clock modes
- You can read a sensor reliably → You understand the full protocol
- DMA transfer works → You understand high-performance I/O
Project 7: I2C Driver and Multi-Device Bus
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Communication Protocols / Peripheral Drivers
- Software or Tool: STM32 or Raspberry Pi Pico + I2C devices (OLED display, EEPROM, sensors)
- Main Book: “The Book of I2C” by Randall Hyde
What you’ll build: An I2C driver that handles addressing, repeated starts, clock stretching, and multi-master arbitration. You’ll connect multiple devices (like an OLED display and a temperature sensor) on the same bus.
Why it teaches firmware: I2C is everywhere—sensors, EEPROMs, displays, RTCs. Unlike SPI, it uses only 2 wires and supports multiple devices with addressing. It’s more complex than SPI but teaches you about bus protocols, ACK/NACK, and error handling.
Core challenges you’ll face:
- Start/stop conditions and addressing → maps to the I2C state machine
- ACK/NACK handling → maps to error detection and recovery
- Clock stretching → maps to slave-controlled timing
- Multi-device communication → maps to bus arbitration
Key Concepts:
- I2C Protocol: “The Book of I2C” Chapters 1-5 - Randall Hyde
- Pull-up Resistor Selection: “Making Embedded Systems” Chapter 9 - Elecia White
- I2C State Machine: Your chip’s Reference Manual, I2C chapter
- Debugging I2C: Logic analyzer is essential
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4-5 (UART/GPIO basics), Project 6 (SPI) helpful for comparison. Ideally have a logic analyzer.
Real world outcome:
# Connect SSD1306 OLED (0x3C) and BME280 sensor (0x76)
[UART output]
I2C bus scan:
0x3C: ACK (SSD1306 OLED)
0x76: ACK (BME280)
Found 2 devices
OLED initialized (128x64)
Drawing "Hello, I2C!"... done
BME280 calibration loaded
Reading: 23.5°C, 45% RH, 1013 hPa
# Your OLED displays the sensor readings in real-time
Implementation Hints:
I2C transaction structure:
Write transaction:
START | ADDR+W | ACK | DATA | ACK | DATA | ACK | STOP
Read transaction:
START | ADDR+W | ACK | REG | ACK | RESTART | ADDR+R | ACK | DATA | NACK | STOP
I2C state machine (simplified):
typedef enum {
I2C_IDLE,
I2C_START_SENT,
I2C_ADDR_SENT,
I2C_DATA_TRANSMITTING,
I2C_DATA_RECEIVING,
I2C_STOP_SENT,
I2C_ERROR
} i2c_state_t;
Common I2C issues:
- No pull-ups → bus stays low → timeout
- Wrong address → NACK after address → error
- Clock stretching → your driver must wait
- Multi-master → arbitration loss → retry
Resources for key challenges:
- The Book of I2C by Randall Hyde - The definitive resource
- BareMetalDrivers - I2C - I2C driver from scratch
Learning milestones:
- Bus scan finds devices → You understand addressing
- Single byte read/write works → You understand the basic protocol
- Multi-byte transactions work → You understand repeated start
- Multiple devices on one bus → You’ve mastered I2C
Project 8: Timer and PWM Controller
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Timing / Motor Control
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A timer driver that provides precise timing (for scheduling tasks), input capture (for measuring signals), and PWM output (for controlling LEDs, motors, and servos).
Why it teaches firmware: Timers are the heart of embedded systems. They provide the “tick” for RTOS schedulers, generate waveforms for motor control, measure external signals, and create delays without blocking. Every embedded system uses timers.
Core challenges you’ll face:
- Prescaler and auto-reload calculation → maps to achieving precise frequencies
- PWM duty cycle control → maps to compare register manipulation
- Input capture for signal measurement → maps to capturing external events
- Timer interrupts → maps to periodic task scheduling
Key Concepts:
- PWM Fundamentals: “Making Embedded Systems” Chapter 8 - Elecia White
- Timer Architecture: Your chip’s Reference Manual, Timer chapter
- Servo Control: 50Hz PWM, 1-2ms pulse width
- Periodic Interrupts: SysTick timer for OS ticks
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 4-5 (UART/GPIO/Interrupts). Understanding of frequency and duty cycle.
Real world outcome:
# Connect an LED (for brightness) and a servo motor
[UART output]
Timer 2 configured: 1kHz PWM
Timer 3 configured: 50Hz servo PWM
LED brightness sweep: 0% -> 100% -> 0%
[You see LED fade up and down smoothly]
Servo position: 0° -> 90° -> 180° -> 90° -> 0°
[Servo sweeps back and forth]
SysTick configured: 1ms tick
System uptime: 00:00:05.123
Implementation Hints:
PWM calculation:
PWM Frequency = Timer Clock / ((Prescaler + 1) × (ARR + 1))
Duty Cycle = CCR / ARR × 100%
Example for 1kHz PWM on 48MHz clock:
Prescaler = 47 (divide by 48 → 1MHz)
ARR = 999 (divide by 1000 → 1kHz)
CCR = 500 → 50% duty cycle
Servo control (typical):
Servo expects 50Hz (20ms period)
Pulse width determines angle:
1.0ms → 0°
1.5ms → 90° (center)
2.0ms → 180°
With ARR = 20000 (1µs resolution at 20ms period):
CCR = 1000 → 0°
CCR = 1500 → 90°
CCR = 2000 → 180°
Learning milestones:
- LED brightness control works → You understand basic PWM
- Servo moves to commanded angle → You understand precise timing
- Input capture measures external frequency → You understand capture mode
- Periodic interrupt fires at exact intervals → You’re ready for RTOS
Project 9: DMA Controller - Efficient Data Movement
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Memory / Performance
- Software or Tool: STM32 (has sophisticated DMA)
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A DMA-driven data transfer system that moves data between peripherals and memory without CPU intervention. You’ll implement DMA for UART RX, ADC continuous conversion, and memory-to-memory transfers.
Why it teaches firmware: DMA is how professional firmware achieves high performance. Instead of the CPU moving every byte, DMA hardware does it in the background. This is essential for audio, video, high-speed communication, and any data-intensive application.
Core challenges you’ll face:
- DMA channel configuration → maps to source, destination, size, direction
- Circular vs. normal mode → maps to continuous vs. one-shot transfers
- Double buffering (ping-pong) → maps to seamless continuous streaming
- DMA + peripheral synchronization → maps to trigger sources
Key Concepts:
- DMA Architecture: Your chip’s Reference Manual, DMA chapter
- Double Buffering: “Making Embedded Systems” Chapter 10 - Elecia White
- Memory-Mapped Peripherals: “Computer Systems: A Programmer’s Perspective” Chapter 6
- Scatter-Gather DMA: Advanced Reference Manual sections
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4 and 6-7 (UART, SPI, I2C), Project 8 (Timers). Understanding of memory and pointers.
Real world outcome:
# UART receives data via DMA while CPU does other work
[UART output]
DMA configured: UART1 RX -> buffer (circular mode)
CPU utilization during 1KB transfer:
Without DMA: 98% (busy waiting)
With DMA: 2% (just processing)
ADC continuous sampling at 1MHz via DMA:
Buffer A filling... complete
Buffer B filling... (processing A)
Samples processed: 1,000,000/second
CPU load: 15%
Memory copy benchmark (1KB):
CPU memcpy: 42 µs
DMA memcpy: 12 µs (3.5× faster)
Implementation Hints:
DMA configuration for UART RX:
// DMA Channel config for USART2 RX
DMA1_Channel5->CPAR = (uint32_t)&USART2->DR; // Peripheral address
DMA1_Channel5->CMAR = (uint32_t)rx_buffer; // Memory address
DMA1_Channel5->CNDTR = BUFFER_SIZE; // Number of transfers
DMA1_Channel5->CCR = DMA_CCR_MINC | // Memory increment
DMA_CCR_CIRC | // Circular mode
DMA_CCR_TCIE | // Transfer complete interrupt
DMA_CCR_EN; // Enable
// Enable DMA request in UART
USART2->CR3 |= USART_CR3_DMAR;
Double buffering pattern:
┌─────────────┐ ┌─────────────┐
│ Buffer A │◄────────│ DMA │
└─────────────┘ └─────────────┘
│ │
▼ │ (switches automatically)
┌─────────────┐ │
│ CPU │ │
│ Processing │ │
└─────────────┘ ▼
┌─────────────┐
│ Buffer B │
└─────────────┘
Learning milestones:
- Single DMA transfer works → You understand basic DMA config
- Circular mode streams continuously → You understand ring buffer pattern
- Double buffering with zero copy → You understand high-performance I/O
- DMA error handling works → You’ve mastered DMA
Project 10: Watchdog Timer and System Reliability
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 2: Intermediate
- Knowledge Area: Reliability / Safety
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A watchdog timer system that automatically resets your microcontroller if the firmware crashes or hangs. You’ll also implement a “task watchdog” that monitors individual tasks and logs the crash reason.
Why it teaches firmware: Real embedded systems must be reliable. They run for months or years without human intervention. The watchdog timer is your last line of defense—if software hangs, hardware resets it. This is how industrial, automotive, and medical firmware works.
Core challenges you’ll face:
- Watchdog configuration and feeding → maps to timeout calculation
- Windowed watchdog (WWDG) → maps to detecting both too-early and too-late
- Reset reason detection → maps to debugging field failures
- Multi-task monitoring → maps to software watchdog patterns
Key Concepts:
- Watchdog Timer: “Making Embedded Systems” Chapter 11 - Elecia White
- Fault Handling: Your chip’s Reference Manual, Watchdog chapter
- Reset Source Register: How to tell why a reset occurred
- Safe State Design: What happens when watchdog fires?
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 1 and 8 (bare-metal basics, timers). Understanding of system reliability.
Real world outcome:
[UART output - normal boot]
System boot
Reset reason: Power-on reset
Watchdog configured: 2 second timeout
All systems nominal
[Simulating a hang by not feeding watchdog]
...
[UART output - after watchdog reset]
System boot
Reset reason: WATCHDOG RESET
Last fed by: SensorTask at 00:00:05.123
Stack pointer at reset: 0x20001234
Entering safe mode...
[Task watchdog demo]
Registering tasks:
MainTask: 500ms timeout
SensorTask: 1000ms timeout
CommTask: 2000ms timeout
SensorTask missed deadline!
Logging fault and resetting SensorTask...
Implementation Hints:
Independent Watchdog (IWDG) configuration:
// Unlock IWDG registers
IWDG->KR = 0x5555;
// Set prescaler: LSI (40kHz) / 256 = 156Hz
IWDG->PR = IWDG_PR_PR_2 | IWDG_PR_PR_1; // /256
// Set reload: 156Hz × 2s = 312 counts
IWDG->RLR = 312;
// Start watchdog
IWDG->KR = 0xCCCC;
// Feed (reset) watchdog - call this regularly!
void watchdog_feed(void) {
IWDG->KR = 0xAAAA;
}
Software watchdog pattern for multiple tasks:
typedef struct {
const char *name;
uint32_t last_check_in;
uint32_t timeout_ms;
bool alive;
} task_watchdog_t;
task_watchdog_t tasks[] = {
{"MainTask", 0, 500, true},
{"SensorTask", 0, 1000, true},
{"CommTask", 0, 2000, true},
};
void task_check_in(task_watchdog_t *task) {
task->last_check_in = get_tick();
task->alive = true;
}
void watchdog_monitor(void) {
uint32_t now = get_tick();
for (int i = 0; i < NUM_TASKS; i++) {
if ((now - tasks[i].last_check_in) > tasks[i].timeout_ms) {
log_fault(&tasks[i]);
// Handle: reset task, enter safe mode, or system reset
}
}
}
Learning milestones:
- Watchdog resets on hang → You understand basic watchdog
- Reset reason is logged → You understand diagnostic registers
- Multi-task monitoring works → You understand software watchdog
- System enters safe mode on failure → You understand reliability design
Project 11: Flash Memory Driver and Wear Leveling
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 4: Expert
- Knowledge Area: Storage / Filesystems
- Software or Tool: STM32 (internal flash) or external SPI flash
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A flash memory driver that handles page/sector programming, erasing, wear leveling, and power-fail safety. You’ll build a simple key-value store on top that survives power loss.
Why it teaches firmware: Configuration, calibration, and logs must survive power cycles. Flash memory has quirks: you can only write 0→1 direction, you must erase entire sectors, and cells wear out after ~100K writes. Understanding flash is essential for embedded storage.
Core challenges you’ll face:
- Page vs. sector organization → maps to understanding flash architecture
- Write-before-erase requirement → maps to flash write rules
- Wear leveling → maps to extending flash lifetime
- Power-fail atomicity → maps to ensuring data integrity
Key Concepts:
- Flash Architecture: “Making Embedded Systems” Chapter 11 - Elecia White
- Wear Leveling Algorithms: Academic papers on FTL (Flash Translation Layer)
- Journaling for Crash Safety: “Operating Systems: Three Easy Pieces” Chapter 42
- CRC for Data Integrity: “Practical Packet Analysis” - Various
Difficulty: Expert Time estimate: 2-4 weeks Prerequisites: Projects 4-6 (basic peripherals), strong C skills. Understanding of data structures.
Real world outcome:
[UART output]
Flash initialized: 256KB internal flash
Sector size: 2KB
Page size: 256 bytes
Erase cycles remaining: ~99,950
Key-value store initialized
Writing: wifi_ssid = "MyNetwork"
Writing: calibration = [1.05, 0.98, 1.02]
[Simulating power loss...]
[Rebooting...]
Key-value store recovered from flash
wifi_ssid = "MyNetwork"
calibration = [1.05, 0.98, 1.02]
Recovery: 0 bytes lost (journal replay)
Wear stats after 1000 writes:
Most-used sector: 12 erases
Least-used sector: 2 erases
Average: 5.2 erases
Estimated lifetime: 19,000 years
Implementation Hints:
Flash programming sequence (STM32):
void flash_write(uint32_t address, uint32_t data) {
// 1. Unlock flash
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 2. Wait for not busy
while (FLASH->SR & FLASH_SR_BSY);
// 3. Enable programming
FLASH->CR |= FLASH_CR_PG;
// 4. Write data
*(volatile uint32_t *)address = data;
// 5. Wait for completion
while (FLASH->SR & FLASH_SR_BSY);
// 6. Lock flash
FLASH->CR |= FLASH_CR_LOCK;
}
Simple wear leveling strategy:
┌─────────────────────────────────────────────────────┐
│ Sector 0: Config (current) │
│ Sector 1: Config (backup) │
│ Sector 2: Log (ring buffer head) │
│ Sector 3: Log │
│ Sector 4: Log │
│ Sector 5: Log (ring buffer tail) │
└─────────────────────────────────────────────────────┘
On config write:
1. Write new data to backup sector
2. Set "valid" flag in backup
3. Clear "valid" flag in old current
4. Swap current/backup pointers
Resources for key challenges:
- LittleFS - Study this wear-leveled filesystem for embedded
- Your chip’s Flash programming reference manual
Learning milestones:
- Basic read/write/erase works → You understand flash operations
- Power-fail safe writes work → You understand journaling
- Wear leveling distributes writes → You understand lifetime management
- Key-value store is reliable → You’ve built real embedded storage
Project 12: USB Device Firmware (CDC Serial)
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 4: Expert
- Knowledge Area: USB Protocol / Enumeration
- Software or Tool: STM32 with USB or RP2040
- Main Book: “USB Complete” by Jan Axelson
What you’ll build: A USB device that appears as a virtual serial port (CDC ACM) when connected to a computer. The computer can send and receive data as if it were a regular COM port, but it’s all happening over USB.
Why it teaches firmware: USB is the most common interface on modern devices, but it’s complex. The enumeration process (where the host asks “what are you?”) requires responding with descriptors in the exact right format. This project shows you how USB really works.
Core challenges you’ll face:
- USB descriptors → maps to device, configuration, interface, endpoint
- Enumeration process → maps to control transfers and standard requests
- Endpoint management → maps to bulk IN/OUT for data transfer
- CDC class specifics → maps to line coding, control line state
Key Concepts:
- USB Architecture: “USB Complete” Chapters 1-4 - Jan Axelson
- CDC Class Specification: USB-IF CDC specification document
- USB Descriptors: “USB Complete” Chapter 5 - Jan Axelson
- USB States and Transfers: “USB Complete” Chapters 6-8
Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: All previous projects, strong C skills. Patience for complex protocols.
Real world outcome:
# Plug in your USB device
[dmesg on Linux]
usb 1-1: new full-speed USB device number 5
usb 1-1: New USB device found, idVendor=1234, idProduct=5678
usb 1-1: Product: My USB Serial Device
usb 1-1: Manufacturer: MyCompany
cdc_acm 1-1:1.0: ttyACM0: USB ACM device
$ screen /dev/ttyACM0 115200
# Your device responds:
Hello from USB CDC!
Type something:
# You type "test":
Echo: test
Implementation Hints:
USB descriptor hierarchy:
Device Descriptor (1)
└── Configuration Descriptor (1)
├── Interface Descriptor 0 (CDC Control)
│ ├── CDC Header Functional Descriptor
│ ├── CDC ACM Functional Descriptor
│ ├── CDC Union Functional Descriptor
│ └── Endpoint Descriptor (Interrupt IN)
└── Interface Descriptor 1 (CDC Data)
├── Endpoint Descriptor (Bulk IN)
└── Endpoint Descriptor (Bulk OUT)
Enumeration sequence:
1. Device plugs in, host detects
2. Host resets device
3. Host requests Device Descriptor (control transfer)
4. Device responds with Device Descriptor
5. Host sets address
6. Host requests full Configuration Descriptor
7. Device responds with all descriptors
8. Host selects configuration
9. Device is ready for class-specific communication
Resources for key challenges:
- TinyUSB - Study this well-written USB stack
- USB in a NutShell - Great reference
Learning milestones:
- Device enumerates (shows in lsusb) → You understand descriptors
- Control transfers work (set line coding) → You understand requests
- Bulk transfers work (send/receive data) → You understand endpoints
- Full CDC serial works → You’ve mastered USB basics
Project 13: UEFI Application - Modern Firmware Development
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 4: Expert
- Knowledge Area: UEFI / System Firmware
- Software or Tool: EDK II, QEMU with OVMF
- Main Book: “Beyond BIOS” by Vincent Zimmer
What you’ll build: A UEFI application that runs before any operating system, draws graphics to the screen, reads keyboard input, and accesses disk. This is modern firmware development—the code that runs on every PC and server.
Why it teaches firmware: UEFI replaced BIOS as the firmware interface on all modern PCs. Understanding UEFI means understanding what happens when you press the power button on any modern computer. UEFI applications can do almost anything an OS can do, but they run in privileged firmware context.
Core challenges you’ll face:
- UEFI build environment (EDK II) → maps to understanding the UEFI ecosystem
- Boot services vs. runtime services → maps to UEFI lifecycle
- GOP (Graphics Output Protocol) → maps to framebuffer access
- Protocol-based architecture → maps to UEFI design philosophy
Key Concepts:
- UEFI Architecture: “Beyond BIOS” Chapters 1-3 - Vincent Zimmer
- UEFI Protocols: UEFI Specification, available from uefi.org
- EDK II Build System: UEFI-Lessons
- GOP and Console: “Beyond BIOS” Chapter 8
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Projects 2-3 (bootloader concepts), C programming. Familiarity with x86 or ARM64.
Real world outcome:
# Build your UEFI app
$ cd edk2 && source edksetup.sh
$ build -a X64 -p MyPkg/MyPkg.dsc
# Run in QEMU with OVMF
$ qemu-system-x86_64 -bios OVMF.fd -hda fat:rw:esp
# Your UEFI app runs before any OS:
┌─────────────────────────────────────────────────────┐
│ │
│ My First UEFI Application │
│ │
│ Memory Map: │
│ Conventional: 2048 MB │
│ Reserved: 256 MB │
│ │
│ ACPI Tables Found: 12 │
│ PCI Devices: 5 │
│ │
│ Press any key to continue... │
│ │
└─────────────────────────────────────────────────────┘
Implementation Hints:
Minimal UEFI application structure:
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
EFI_STATUS EFIAPI UefiMain(
EFI_HANDLE ImageHandle,
EFI_SYSTEM_TABLE *SystemTable
) {
// Access UEFI services through SystemTable
SystemTable->ConOut->ClearScreen(SystemTable->ConOut);
Print(L"Hello from UEFI!\n");
// Wait for keypress
EFI_INPUT_KEY Key;
SystemTable->ConIn->Reset(SystemTable->ConIn, FALSE);
while (SystemTable->ConIn->ReadKeyStroke(SystemTable->ConIn, &Key) == EFI_NOT_READY);
return EFI_SUCCESS;
}
UEFI protocol access pattern:
// Find Graphics Output Protocol
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
Status = gBS->LocateProtocol(
&gEfiGraphicsOutputProtocolGuid,
NULL,
(VOID **)&Gop
);
// Draw a pixel
Gop->Blt(Gop, &White, EfiBltVideoFill, 0, 0, 100, 100, 1, 1, 0);
Resources for key challenges:
- UEFI-Lessons - Step-by-step UEFI programming
- Baeldung UEFI Tutorial - Bare-metal UEFI apps
- UEFI Spec - Official specification
Learning milestones:
- “Hello World” prints in QEMU → You understand the UEFI build system
- Graphics output works → You understand GOP protocol
- You can read files from disk → You understand file system protocols
- You can boot a kernel → You understand the UEFI boot process
Project 14: Simple RTOS Implementation
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 5: Master
- Knowledge Area: Operating Systems / Real-Time
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau
What you’ll build: A minimal RTOS with context switching, a scheduler (round-robin and priority-based), mutexes, and semaphores. Multiple “tasks” run concurrently on a single-core microcontroller.
Why it teaches firmware: This is where firmware meets operating systems. An RTOS is still firmware (it runs on bare metal), but it provides concurrency, timing guarantees, and abstractions. Understanding how context switching works demystifies all of concurrent programming.
Core challenges you’ll face:
- Context switching → maps to saving/restoring CPU registers
- Scheduler implementation → maps to deciding which task runs next
- Synchronization primitives → maps to mutex, semaphore, queue
- Stack management per task → maps to memory layout for concurrent tasks
Key Concepts:
- Context Switching: “Operating Systems: Three Easy Pieces” Chapter 6
- Scheduling Algorithms: “Operating Systems: Three Easy Pieces” Chapters 7-9
- Synchronization: “Operating Systems: Three Easy Pieces” Chapters 26-31
- ARM PendSV: Your chip’s programming manual
Difficulty: Master Time estimate: 1-2 months Prerequisites: All previous projects, especially interrupts (Project 5) and timers (Project 8). Strong C and assembly skills.
Real world outcome:
[UART output]
RTOS initialized
Tick rate: 1000 Hz
Stack size per task: 1024 bytes
Creating tasks:
Task 1: Blink LED (priority 2)
Task 2: Read sensor (priority 1)
Task 3: Serial echo (priority 3)
Idle task (priority 0)
Scheduler started!
[Task 2] Sensor reading: 23.5°C
[Task 1] LED toggled (ON)
[Task 3] Echo: hello
[Task 2] Sensor reading: 23.6°C
[Task 1] LED toggled (OFF)
[Task 2] Sensor reading: 23.5°C
...
Context switches in last second: 342
CPU idle time: 45%
Implementation Hints:
Context switch mechanism (ARM Cortex-M):
┌─────────────────────────────────────────────────────────┐
│ 1. SysTick interrupt fires every 1ms │
│ 2. SysTick handler sets PendSV pending │
│ 3. PendSV runs at lowest priority (no nesting) │
│ 4. PendSV handler: │
│ a. Save current task's registers to its stack │
│ b. Save stack pointer to TCB (Task Control Block) │
│ c. Call scheduler to pick next task │
│ d. Load new task's stack pointer │
│ e. Restore new task's registers │
│ f. Return (execution continues in new task) │
└─────────────────────────────────────────────────────────┘
Task Control Block (TCB):
typedef struct tcb {
uint32_t *stack_ptr; // Saved stack pointer
uint32_t priority; // Task priority
enum { READY, BLOCKED, RUNNING } state;
struct tcb *next; // For linked list
const char *name; // For debugging
} tcb_t;
PendSV handler skeleton (ARM assembly):
PendSV_Handler:
; Save current context
mrs r0, psp ; Get Process Stack Pointer
stmdb r0!, {r4-r11} ; Save R4-R11 onto stack
; current_task->stack_ptr = r0
ldr r1, =current_task
ldr r2, [r1]
str r0, [r2]
; Call scheduler (returns new task)
bl scheduler_next
; Switch to new task
str r0, [r1] ; current_task = new_task
ldr r0, [r0] ; r0 = new_task->stack_ptr
; Restore new context
ldmia r0!, {r4-r11}
msr psp, r0
bx lr
Resources for key challenges:
- FreeRTOS source code - Study a real RTOS
- “The Definitive Guide to ARM Cortex-M3 and Cortex-M4” by Joseph Yiu - Context switch details
Learning milestones:
- Two tasks alternate via context switch → You understand the fundamental mechanism
- Round-robin scheduler works → You understand basic scheduling
- Priority scheduler works → You understand priority-based scheduling
- Mutex prevents race conditions → You understand synchronization
Project 15: OTA (Over-The-Air) Firmware Update System
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: Bootloader / Security
- Software or Tool: STM32 or ESP32
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A bootloader and firmware update system that can receive a new firmware image over the network (or serial), validate it with cryptographic signatures, and safely switch to the new version with rollback capability.
Why it teaches firmware: This is how professional IoT devices work. You can’t physically access devices in the field, so they must update themselves. But firmware updates are dangerous—a failed update could brick the device. This project teaches you to design for reliability.
Core challenges you’ll face:
- Dual-partition (A/B) design → maps to atomic updates with rollback
- Cryptographic verification → maps to ensuring update authenticity
- Bootloader security → maps to the root of trust
- Power-fail safety → maps to surviving update interruption
Key Concepts:
- Secure Boot: “Making Embedded Systems” Chapter 11 - Elecia White
- A/B Partitioning: Android’s update mechanism documentation
- Cryptographic Signatures: “Serious Cryptography” Chapter 12 - Jean-Philippe Aumasson
- Image Verification: MCUboot documentation
Difficulty: Expert Time estimate: 3-4 weeks Prerequisites: Project 11 (flash driver), Project 12 (USB or network). Understanding of cryptography basics.
Real world outcome:
[Device UART output - normal boot]
Bootloader v1.0
Checking firmware partitions...
Partition A: v2.1.0, VALID, ACTIVE
Partition B: v2.0.0, VALID, BACKUP
Booting partition A...
Firmware v2.1.0 started
[OTA update via HTTP]
Downloading firmware v2.2.0... 100%
Verifying SHA256... OK
Verifying Ed25519 signature... OK
Writing to partition B... 100%
Setting B as pending...
Rebooting...
[After reboot]
Bootloader v1.0
Partition B pending, attempting boot...
Firmware v2.2.0 started
Running self-test... PASS
Confirming partition B as active
[If firmware v2.2.0 had a bug...]
Bootloader v1.0
Partition B failed 3 boot attempts
Rolling back to partition A...
Firmware v2.1.0 started (ROLLBACK)
Implementation Hints:
Flash partition layout:
┌──────────────────────────────────────────┐ 0x00000000
│ Bootloader (16KB, write-protected) │
├──────────────────────────────────────────┤ 0x00004000
│ Partition Table / Metadata │
│ - Active partition (A or B) │
│ - Partition A version, hash, state │
│ - Partition B version, hash, state │
│ - Boot attempt counter │
├──────────────────────────────────────────┤ 0x00008000
│ Partition A: Application Firmware │
│ (128KB) │
├──────────────────────────────────────────┤ 0x00028000
│ Partition B: Application Firmware │
│ (128KB) │
├──────────────────────────────────────────┤ 0x00048000
│ User data / Config │
└──────────────────────────────────────────┘
Boot logic flowchart:
Start
│
▼
Read partition metadata
│
├─▶ Pending update? ────▶ Try booting new partition
│ │
│ ├─▶ Success ──▶ Confirm as active
│ │
│ └─▶ Fail (3x) ──▶ Rollback
│
└─▶ No pending ────▶ Boot active partition
Resources for key challenges:
- MCUboot - Study this production bootloader
- ESP-IDF OTA documentation
Learning milestones:
- Bootloader can choose partition → You understand boot logic
- Update downloads and writes → You understand the update flow
- Signature verification works → You understand security
- Rollback on failure works → You understand reliability
Project 16: Power Management Firmware
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Power / Battery
- Software or Tool: STM32L (low-power series) or nRF52
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: Firmware that manages power states—active, sleep, deep sleep, and standby. You’ll wake from interrupts, manage a battery fuel gauge, and implement power budgeting to achieve months of battery life.
Why it teaches firmware: Battery-powered devices must sip power. Understanding sleep modes, wake sources, and peripheral power domains is essential for IoT devices, wearables, and sensors. This is about understanding what the hardware does when it’s “off.”
Core challenges you’ll face:
- Sleep mode configuration → maps to understanding clock domains
- Wake source configuration → maps to what can wake the CPU?
- Peripheral power domains → maps to which peripherals stay on?
- Current measurement → maps to verifying your power design
Key Concepts:
- Low Power Modes: “Making Embedded Systems” Chapter 12 - Elecia White
- Sleep Mode Entry/Exit: Your chip’s Reference Manual, Power chapter
- Wake Sources: RTC alarm, GPIO interrupt, UART character
- Current Measurement: Using a µA-capable multimeter
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 5 and 8 (interrupts and timers), Project 10 (watchdog for reliability). Understanding of electronics helpful.
Real world outcome:
[UART output]
Power management demo
Current mode: ACTIVE
CPU: 48 MHz
Peripherals: All on
Current: ~15 mA
Entering SLEEP mode...
CPU stopped, peripherals on
Current: ~2 mA
Entering STOP mode...
CPU stopped, most peripherals off
RTC running for wake
Current: ~10 µA
Entering STANDBY mode...
Everything off except RTC and wake pin
Current: ~2 µA
[Button press wakes device]
Wake source: WKUP pin
Boot reason: Standby wake
Uptime preserved in RTC backup registers
Battery: 87% (3.92V)
Estimated runtime at 10µA: 438 days
Implementation Hints:
STM32 power modes (typical):
┌─────────────────────────────────────────────────────────┐
│ Mode │ CPU │ RAM │ Clocks │ Wake Sources │
├─────────────────────────────────────────────────────────┤
│ ACTIVE │ ON │ ON │ All │ N/A │
│ SLEEP │ OFF │ ON │ All │ Any interrupt │
│ STOP │ OFF │ ON │ LSE only │ EXTI, RTC │
│ STANDBY │ OFF │ OFF │ LSE only │ WKUP pin, RTC │
└─────────────────────────────────────────────────────────┘
Entering Stop mode:
void enter_stop_mode(uint32_t wake_time_sec) {
// Configure RTC alarm as wake source
rtc_set_alarm(wake_time_sec);
EXTI->IMR |= EXTI_IMR_IM17; // RTC alarm on EXTI17
// Enter Stop mode
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
PWR->CR |= PWR_CR_LPDS; // Low-power deep sleep
__WFI(); // Wait For Interrupt (CPU stops here)
// Execution resumes here after wake
// Reconfigure clocks (HSE may have stopped)
clock_init();
}
Learning milestones:
- Sleep and wake from timer works → You understand basic power modes
- Stop mode with 10µA achieved → You understand peripheral shutdown
- Battery percentage calculated → You understand voltage curves
- System runs for days on battery → You’ve mastered power management
Project 17: Hardware Abstraction Layer (HAL) Design
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Software Architecture / Portability
- Software or Tool: Multiple microcontroller targets
- Main Book: “C Interfaces and Implementations” by David Hanson
What you’ll build: A HAL that abstracts GPIO, UART, SPI, and I2C so the same application code runs on both STM32 and Raspberry Pi Pico (or any two different chips). You’ll design the interface and implement it twice.
Why it teaches firmware: Professional firmware needs to be portable. When the chip goes obsolete, you need to move to a new one. A good HAL separates “what the application does” from “how the hardware does it.” This is software architecture for embedded.
Core challenges you’ll face:
- Interface design → maps to what abstraction level is right?
- Compile-time vs. runtime selection → maps to performance vs. flexibility
- Error handling across platforms → maps to common error codes
- Testing without hardware → maps to mock implementations
Key Concepts:
- Interface Design: “C Interfaces and Implementations” - David Hanson
- Opaque Pointers (PIMPL): Hiding implementation details in C
- Build System: CMake for multi-target builds
- Hardware Abstraction: Study Zephyr RTOS or mbed OS HAL
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 4-8 (peripheral drivers for at least one chip). Strong C skills.
Real world outcome:
// Application code - runs on ANY supported chip
void app_main(void) {
gpio_handle_t led = gpio_init(GPIO_LED_PIN, GPIO_MODE_OUTPUT);
uart_handle_t uart = uart_init(UART_DEBUG, 115200);
uart_printf(uart, "Running on: %s\n", hal_chip_name());
while (1) {
gpio_toggle(led);
uart_printf(uart, "LED toggled\n");
hal_delay_ms(500);
}
}
// Build for STM32:
$ make TARGET=stm32f4
# Uses src/hal/stm32f4/gpio.c, uart.c
// Build for RP2040:
$ make TARGET=rp2040
# Uses src/hal/rp2040/gpio.c, uart.c
// Same application binary works on either chip!
Implementation Hints:
HAL interface design (gpio.h):
// Opaque handle - hides implementation
typedef struct gpio_handle* gpio_handle_t;
// Error codes (platform-independent)
typedef enum {
GPIO_OK = 0,
GPIO_ERR_INVALID_PIN,
GPIO_ERR_BUSY,
GPIO_ERR_NOT_SUPPORTED
} gpio_error_t;
// Interface functions
gpio_handle_t gpio_init(uint8_t pin, gpio_mode_t mode);
gpio_error_t gpio_deinit(gpio_handle_t handle);
gpio_error_t gpio_write(gpio_handle_t handle, bool value);
bool gpio_read(gpio_handle_t handle);
gpio_error_t gpio_toggle(gpio_handle_t handle);
Implementation selection (CMakeLists.txt):
if(TARGET STREQUAL "stm32f4")
add_subdirectory(hal/stm32f4)
elseif(TARGET STREQUAL "rp2040")
add_subdirectory(hal/rp2040)
else()
message(FATAL_ERROR "Unknown target: ${TARGET}")
endif()
Resources for key challenges:
- Zephyr HAL - Study a professional HAL design
- “C Interfaces and Implementations” by David Hanson - The definitive guide
Learning milestones:
- Interface compiles without implementation → You understand abstraction
- One implementation works → You understand the pattern
- Second implementation works → You understand portability
- Application runs on both chips unchanged → You’ve mastered HAL design
Project 18: Custom Bootloader for Your MCU
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C + Assembly
- Alternative Programming Languages: Rust
- Coolness Level: Level 5: Pure Magic
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 4: Expert
- Knowledge Area: Bootloader / Embedded
- Software or Tool: STM32 or Raspberry Pi Pico
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A custom bootloader for your ARM microcontroller that runs first at power-on, can update firmware via UART or USB, validates the application, and jumps to it. This is like GRUB, but for microcontrollers.
Why it teaches firmware: This ties together everything—startup code, vector table manipulation, flash programming, protocol handling, and the handoff to application code. You’ll understand exactly what the chip does from power-on to running your app.
Core challenges you’ll face:
- Vector table relocation → maps to the app has its own vectors
- Jump to application → maps to setting up MSP and branching
- UART bootloader protocol → maps to receiving firmware over serial
- Flash programming from bootloader → maps to self-modifying code
Key Concepts:
- ARM Vector Table: ARM Cortex-M documentation
- VTOR Register: Relocating the vector table
- Stack Pointer Setup: MSP vs PSP
- Flash IAP (In-Application Programming): Your chip’s flash programming guide
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Projects 2-3 (bootloader concepts), Projects 4 and 11 (UART and flash). Strong understanding of ARM Cortex-M.
Real world outcome:
[Power on - bootloader runs]
Custom Bootloader v1.0
Checking for firmware update...
Holding BOOT button: No
UART activity: No
Validating application at 0x08008000...
Header magic: OK
CRC32: OK
Jumping to application...
[Application starts]
Application v2.0 running!
[Next boot, holding BOOT button]
Custom Bootloader v1.0
Entering firmware update mode!
Send Intel HEX file over UART...
[Send firmware via terminal]
Receiving: ######################100%
CRC32: OK
Programming flash: ######################100%
Rebooting...
[Application v2.1 starts]
Application v2.1 running!
Implementation Hints:
Memory layout:
┌──────────────────────────────────────────┐ 0x08000000 (Flash start)
│ Bootloader (32KB) │
│ - Vector table (bootloader) │
│ - Startup code │
│ - UART/flash drivers │
│ - Jump-to-app logic │
├──────────────────────────────────────────┤ 0x08008000
│ Application (remaining flash) │
│ - Vector table (application) │
│ - Application code │
└──────────────────────────────────────────┘
Jump to application:
void jump_to_application(uint32_t app_address) {
// Application's vector table is at app_address
uint32_t *app_vectors = (uint32_t *)app_address;
// First entry: Initial stack pointer
uint32_t app_sp = app_vectors[0];
// Second entry: Reset handler address
uint32_t app_reset = app_vectors[1];
// Disable interrupts
__disable_irq();
// Relocate vector table to application's
SCB->VTOR = app_address;
// Set stack pointer
__set_MSP(app_sp);
// Jump to application reset handler
void (*app_entry)(void) = (void (*)(void))app_reset;
app_entry();
// Never returns
}
Learning milestones:
- Bootloader runs, jumps to app → You understand the handoff
- UART update mode works → You understand firmware reception
- Flash programming works → You understand self-update
- CRC validation prevents bad firmware → You understand safety
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| 1. Bare-Metal LED Blinker | Intermediate | Weekend-1 week | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 2. x86 Bootloader (512 bytes) | Advanced | Weekend-1 week | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 3. Multi-Stage Bootloader | Expert | 2-4 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 4. UART Driver | Intermediate | 1 week | ⭐⭐⭐ | ⭐⭐⭐ |
| 5. GPIO + Interrupts | Intermediate | 1 week | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 6. SPI Driver | Advanced | 1-2 weeks | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 7. I2C Driver | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 8. Timer/PWM | Intermediate | 1 week | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 9. DMA Controller | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 10. Watchdog Timer | Intermediate | 1 week | ⭐⭐⭐ | ⭐⭐⭐ |
| 11. Flash Memory Driver | Expert | 2-4 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 12. USB Device (CDC) | Expert | 3-4 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 13. UEFI Application | Expert | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 14. Simple RTOS | Master | 1-2 months | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 15. OTA Update System | Expert | 3-4 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 16. Power Management | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 17. HAL Design | Advanced | 2-3 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 18. Custom Bootloader | Expert | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Recommended Learning Path
If you’re starting from scratch (no embedded experience):
- Project 1: Bare-Metal LED Blinker - Get hardware, blink an LED without any framework
- Project 4: UART Driver - Build your debugging tool
- Project 5: GPIO + Interrupts - Learn how real firmware responds to events
- Project 8: Timer/PWM - Understand timing and waveforms
- Project 2: x86 Bootloader - See what happens at power-on
- Continue with projects 6-7, 9-11 to build driver expertise
- Project 18: Custom Bootloader - Tie everything together
- Project 14: Simple RTOS - The capstone that combines everything
If you have some embedded experience:
- Project 2: x86 Bootloader - Fill in the knowledge gap about boot process
- Project 3: Multi-Stage Bootloader - Understand OS loading
- Project 13: UEFI Application - Modern firmware development
- Project 14: Simple RTOS - Deep dive into concurrency
- Project 15: OTA Update System - Production-quality firmware
If you want to understand how computers boot (focus on bootloaders):
- Project 2: x86 Bootloader → Project 3: Multi-Stage Bootloader → Project 13: UEFI Application
Hardware Recommendations:
| Hardware | Cost | Best For |
|---|---|---|
| Raspberry Pi Pico | ~$4 | Projects 1, 4-11, 14-18 (great for beginners) |
| STM32F4 Discovery | ~$20 | Projects 1, 4-18 (more peripherals, better documentation) |
| QEMU (emulator) | Free | Projects 2-3, 13 (x86 and UEFI, no hardware needed) |
Final Capstone Project: Complete Embedded System
- File: LEARN_FIRMWARE_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Rust
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Full-Stack Embedded
- Software or Tool: STM32 + sensors + display + WiFi
- Main Book: “Making Embedded Systems, 2nd Edition” by Elecia White
What you’ll build: A complete IoT sensor node with:
- Custom bootloader with OTA updates
- Multi-task RTOS application
- Sensor reading via I2C
- Display output via SPI
- WiFi connectivity
- Power management for battery operation
- Cloud reporting
This project integrates:
- Project 1: Bare-metal foundations
- Projects 4-9: All peripheral drivers
- Project 10: Watchdog for reliability
- Project 11: Flash storage for config
- Project 14: RTOS for multitasking
- Project 15: OTA for updates
- Project 16: Power management
- Project 17: HAL for portability
- Project 18: Bootloader
Real world outcome:
┌─────────────────────────────────────────────────────────┐
│ IoT Sensor Node │
├─────────────────────────────────────────────────────────┤
│ OLED Display shows: │
│ ┌───────────────────────────────────┐ │
│ │ Temp: 23.5°C Humidity: 45% │ │
│ │ Pressure: 1013 hPa │ │
│ │ Battery: 87% (est. 45 days) │ │
│ │ Last sync: 2 min ago │ │
│ └───────────────────────────────────┘ │
│ │
│ [Button] → Wakes device, forces sync │
│ [LED] → Blinks on activity │
│ │
│ WiFi → Reports to cloud dashboard │
│ UART → Debug output (115200 baud) │
│ OTA → Updates automatically when new version available │
└─────────────────────────────────────────────────────────┘
Why this is the ultimate firmware project: It’s a real product. You could sell this. It runs on battery for weeks. It updates itself. It’s reliable (watchdog, rollback). It’s debuggable (UART, error logging). It demonstrates mastery of every concept from power-on to running application.
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | Bare-Metal LED Blinker | C (with Assembly startup) |
| 2 | x86 Bootloader (512 bytes) | x86 Assembly (NASM) |
| 3 | Multi-Stage Bootloader with Kernel Loading | x86 Assembly + C |
| 4 | UART Driver (Serial Communication) | C |
| 5 | GPIO and Interrupt Controller | C |
| 6 | SPI Driver | C |
| 7 | I2C Driver | C |
| 8 | Timer and PWM Controller | C |
| 9 | DMA Controller | C |
| 10 | Watchdog Timer and System Reliability | C |
| 11 | Flash Memory Driver and Wear Leveling | C |
| 12 | USB Device Firmware (CDC Serial) | C |
| 13 | UEFI Application | C |
| 14 | Simple RTOS Implementation | C |
| 15 | OTA Firmware Update System | C |
| 16 | Power Management Firmware | C |
| 17 | Hardware Abstraction Layer (HAL) Design | C |
| 18 | Custom Bootloader for Your MCU | C + Assembly |
| Final | Complete Embedded System (Capstone) | C |
Key Resources Referenced
Books
- “Making Embedded Systems, 2nd Edition” by Elecia White
- “Operating Systems: Three Easy Pieces” by Arpaci-Dusseau
- “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron
- “Write Great Code, Volume 1” by Randall Hyde
- “The Book of I2C” by Randall Hyde
- “USB Complete” by Jan Axelson
- “Beyond BIOS” by Vincent Zimmer
- “C Interfaces and Implementations” by David Hanson
- “Serious Cryptography” by Jean-Philippe Aumasson
Online Resources
- Bare Metal Programming Guide (GitHub)
- FreeCodeCamp Embedded Systems Handbook
- UEFI-Lessons
- OSDev Wiki
- Building a Bootloader from Scratch
Hardware Documentation
- Your chip’s Reference Manual (essential!)
- Your chip’s Datasheet
- ARM Cortex-M Programming Guide
- UEFI Specification (uefi.org)