LEARN ESP32 WITHOUT ARDUINO DEEP DIVE
Learn ESP32 Programming Without Arduino IDE: From Zero to Embedded Master
Goal: Deeply understand ESP32 microcontroller programming using professional toolchains (ESP-IDF, PlatformIO, or bare metal), working with any editor you want on macOS, and mastering the full embedded development workflow from blinking LEDs to production IoT devices.
Why Ditch the Arduino IDE?
The Arduino IDE is designed for beginners—it hides complexity behind abstractions. But those abstractions become limitations when you need:
- Full hardware control: Direct register access, custom memory layouts
- Professional debugging: JTAG, breakpoints, memory inspection
- FreeRTOS power: Multi-core tasks, queues, semaphores, mutexes
- Production-ready code: OTA updates, secure boot, encrypted flash
- Faster builds: Incremental compilation, dependency management
- Version control: Proper project structure, not
.inofiles
After completing these projects, you will:
- Set up professional ESP32 toolchains on macOS
- Program in C using ESP-IDF (Espressif’s official SDK)
- Understand FreeRTOS multi-tasking on dual-core processors
- Master all peripherals: GPIO, UART, SPI, I2C, ADC, PWM
- Build WiFi and Bluetooth applications from scratch
- Debug with JTAG and OpenOCD
- Optimize power consumption with sleep modes
- Deploy production firmware with OTA updates
The ESP32 Family
Before we dive in, understand the different ESP32 variants:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP32 FAMILY COMPARISON │
├──────────────┬────────────┬────────────┬────────────┬────────────┬──────────────┤
│ Feature │ ESP32 │ ESP32-S2 │ ESP32-S3 │ ESP32-C3 │ ESP32-C6 │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ CPU │ Dual Xtensa│ Single │ Dual Xtensa│ Single │ Single+LP │
│ │ LX6 240MHz │ LX7 240MHz │ LX7 240MHz │ RISC-V │ RISC-V │
│ │ │ │ │ 160MHz │ 160MHz │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ SRAM │ 520 KB │ 320 KB │ 512 KB │ 400 KB │ 512 KB │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ Wi-Fi │ 802.11b/g/n│ 802.11b/g/n│ 802.11b/g/n│ 802.11b/g/n│ Wi-Fi 6 │
│ │ (Wi-Fi 4) │ (Wi-Fi 4) │ (Wi-Fi 4) │ (Wi-Fi 4) │ 802.11ax │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ Bluetooth │ Classic + │ ❌ None │ BLE 5.0 │ BLE 5.0 │ BLE 5.3 │
│ │ BLE 4.2 │ │ │ │ │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ Thread/ │ ❌ │ ❌ │ ❌ │ ❌ │ ✅ 802.15.4 │
│ Zigbee │ │ │ │ │ │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ USB-OTG │ ❌ │ ✅ │ ✅ │ ❌ (CDC) │ ❌ (CDC) │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ AI/ML Vector │ Basic │ Basic │ ✅ SIMD │ Basic │ Basic │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ FPU │ ✅ Yes │ ❌ No │ ✅ Yes │ ❌ No │ ❌ No │
├──────────────┼────────────┼────────────┼────────────┼────────────┼──────────────┤
│ Best For │ General │ Low-power │ AI/ML, │ Cost- │ Smart Home │
│ │ purpose, │ USB apps │ displays, │ effective │ Matter, │
│ │ BT Classic │ │ cameras │ IoT nodes │ Thread │
└──────────────┴────────────┴────────────┴────────────┴────────────┴──────────────┘
Recommendation for learning: Start with the classic ESP32 (ESP32-WROOM-32) or ESP32-C3 (RISC-V, simpler architecture).
Core Concept Analysis
Development Environment Options
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP32 DEVELOPMENT APPROACHES │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ │
│ │ BARE METAL │ Register-level, no SDK, maximum control │
│ │ (Hardest) │ Use: Learning hardware, ultra-low latency │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ ESP-IDF │ Official SDK, FreeRTOS, full features │
│ │ (Recommended) │ Use: Production code, professional development │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ PlatformIO │ Build system + package manager + ESP-IDF/Arduino │
│ │ (Convenient) │ Use: Multi-board projects, library management │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ Arduino Core │ Beginner-friendly, limited control │
│ │ (Easiest) │ Use: Rapid prototyping (what we're AVOIDING!) │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
ESP-IDF Architecture
ESP-IDF (Espressif IoT Development Framework) is the official SDK:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP-IDF ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ YOUR APPLICATION (app_main.c) │
│ │ │
│ ┌──────▼────────────────────────────────────────────────────────────────┐ │
│ │ ESP-IDF COMPONENTS │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ esp_wifi │ │esp_bt │ │ driver │ │nvs_flash │ │ esp_http │ │ │
│ │ │ (WiFi) │ │(BLE/BT) │ │(GPIO,SPI)│ │(Storage) │ │(HTTP srv)│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │esp_timer │ │ esp_log │ │esp_event │ │ esp_ota │ │ esp_tls │ │ │
│ │ │(Timers) │ │(Logging) │ │(Events) │ │(Updates) │ │(TLS/SSL) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────────────────────────────────────────────────────────────┐ │
│ │ FREERTOS │ │
│ │ Tasks │ Queues │ Semaphores │ Mutexes │ Timers │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────────────────────────────────────────────────────────────┐ │
│ │ HARDWARE ABSTRACTION │ │
│ │ GPIO │ UART │ SPI │ I2C │ ADC │ DAC │ PWM │ RMT │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────────────────────────────────────────────────────────────┐ │
│ │ ESP32 HARDWARE │ │
│ │ Xtensa/RISC-V CPU │ SRAM │ Flash │ Radio │ Peripherals │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
FreeRTOS on ESP32
FreeRTOS is the real-time operating system that powers ESP-IDF:
Single-Core vs Dual-Core Task Execution:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP32 DUAL-CORE FREERTOS │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Core 0 (PRO_CPU) Core 1 (APP_CPU) │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ WiFi/BT Tasks │ │ Your app_main() │ │
│ │ System Tasks │ │ User Tasks │ │
│ │ Event Loop │ │ Sensor Reading │ │
│ └───────────────────┘ └───────────────────┘ │
│ │
│ Task Pinning: │
│ - xTaskCreatePinnedToCore(task, "name", stack, param, priority, &handle, 0) │
│ - xTaskCreatePinnedToCore(task, "name", stack, param, priority, &handle, 1) │
│ - tskNO_AFFINITY = scheduler chooses core │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Memory Architecture
ESP32 Memory Map:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP32 MEMORY LAYOUT │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ 0x4000_0000 ┌──────────────────────────────────────────┐ │
│ │ IRAM (Instruction RAM) │ ~128 KB │
│ │ Fast execution cache │ │
│ 0x4008_0000 ├──────────────────────────────────────────┤ │
│ │ DRAM (Data RAM) │ ~320 KB │
│ │ Variables, heap, stack │ │
│ 0x400C_0000 ├──────────────────────────────────────────┤ │
│ │ Flash Cache (XIP) │ Up to 16 MB │
│ │ Code executed from flash │ │
│ ├──────────────────────────────────────────┤ │
│ 0x5000_0000 │ RTC SLOW Memory │ 8 KB │
│ │ Survives deep sleep │ │
│ │ ULP coprocessor code │ │
│ 0x5000_2000 ├──────────────────────────────────────────┤ │
│ │ RTC FAST Memory │ 8 KB │
│ │ Wake stub code │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Key Attributes: │
│ - IRAM_ATTR: Place function in IRAM (fast, ISR-safe) │
│ - DRAM_ATTR: Place variable in DRAM (not flash) │
│ - RTC_DATA_ATTR: Survives deep sleep │
│ - RTC_NOINIT_ATTR: Survives deep sleep AND reset │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Power Modes
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ESP32 POWER MODES │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Mode │ Current │ What's Running │ Wake Latency │
│ ─────────────────┼─────────────┼──────────────────────────┼───────────────── │
│ Active │ 80-240 mA │ Everything │ N/A │
│ Modem Sleep │ 3-20 mA │ CPU, no WiFi/BT │ Instant │
│ Light Sleep │ 0.8 mA │ RTC, memory preserved │ < 1 ms │
│ Deep Sleep │ 10-150 µA │ RTC only, RAM lost │ ~10 ms │
│ Hibernation │ 5 µA │ RTC timer only │ ~10 ms │
│ │
│ Wake Sources (Deep Sleep): │
│ - Timer: esp_sleep_enable_timer_wakeup(time_us) │
│ - GPIO: esp_sleep_enable_ext0_wakeup(gpio, level) │
│ - Touch: esp_sleep_enable_touchpad_wakeup() │
│ - ULP: esp_sleep_enable_ulp_wakeup() │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Project List
Projects are ordered from environment setup to advanced production systems.
Project 1: ESP-IDF Environment Setup & First Blink
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Toolchain / Build Systems
- Software or Tool: ESP-IDF, idf.py, VS Code
- Main Book: “Programming the ESP32 in C” by Harry Fairhead
What you’ll build: A complete ESP-IDF development environment on macOS with proper PATH configuration, plus the classic LED blink using ESP-IDF APIs—your first program compiled without Arduino.
Why it teaches ESP32 programming: Before you can build anything, you need a working toolchain. This project forces you to understand the ESP-IDF structure, CMake build system, and the idf.py command-line tool.
Core challenges you’ll face:
- Installing prerequisites → maps to Homebrew, Python, CMake, Ninja
- Cloning and configuring ESP-IDF → maps to understanding SDK structure
- Setting up environment variables → maps to shell configuration
- Building and flashing → maps to idf.py workflow
- Connecting to serial monitor → maps to UART debugging
Key Concepts:
- ESP-IDF Installation: Standard Toolchain Setup for macOS
- idf.py Tool: IDF Frontend - idf.py
- Project Structure: ESP-IDF Programming Guide - Chapter 2
- CMake Build System: Build System
Difficulty: Beginner Time estimate: 1-2 days Prerequisites: Basic C knowledge, command line familiarity, macOS
Real world outcome:
# Terminal 1: Initial setup
$ brew install cmake ninja dfu-util ccache
$ mkdir -p ~/esp && cd ~/esp
$ git clone --recursive https://github.com/espressif/esp-idf.git
$ cd esp-idf
$ ./install.sh esp32
Setting up Python environment...
Installing ESP-IDF tools...
- xtensa-esp32-elf-gcc (GCC 12.2.0)
- openocd-esp32 (v0.12.0)
- esptool.py (v4.7.0)
All done! You can now run:
. ./export.sh
# Terminal 2: Build and flash
$ . ~/esp/esp-idf/export.sh
Done! IDF_PATH is now ~/esp/esp-idf
$ cd ~/esp
$ cp -r $IDF_PATH/examples/get-started/blink .
$ cd blink
$ idf.py set-target esp32
$ idf.py menuconfig # Configure GPIO pin for your board
$ idf.py build
[100%] Generating binary image from built executable
esptool.py v4.7.0
Merged 1 ELF section
Generated /Users/you/esp/blink/build/blink.bin
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
Serial port /dev/cu.usbserial-0001
Chip is ESP32-D0WD-V3 (revision v3.1)
Hard resetting via RTS pin...
...
I (325) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (335) gpio: GPIO[2]| InputEn: 0| OutputEn: 1| OpenDrain: 0
I (345) example: Turning the LED ON
I (1345) example: Turning the LED OFF
I (2345) example: Turning the LED ON
Implementation Hints:
macOS-specific setup:
- First, ensure you have Python 3.10+ (macOS ships with older versions):
brew install python@3.11 # Add to ~/.zshrc or ~/.bashrc: export PATH="/opt/homebrew/opt/python@3.11/bin:$PATH" - For Apple Silicon Macs (M1/M2/M3), you may need Rosetta 2:
/usr/sbin/softwareupdate --install-rosetta --agree-to-license
Understanding the project structure:
blink/
├── CMakeLists.txt # Top-level CMake file
├── main/
│ ├── CMakeLists.txt # Component CMake file
│ └── blink_example_main.c
├── sdkconfig # Configuration (generated by menuconfig)
└── build/ # Build output (generated)
The blink code (ESP-IDF style):
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define BLINK_GPIO GPIO_NUM_2
void app_main(void)
{
// Configure GPIO
gpio_reset_pin(BLINK_GPIO);
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
while (1) {
ESP_LOGI("BLINK", "LED ON");
gpio_set_level(BLINK_GPIO, 1);
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP_LOGI("BLINK", "LED OFF");
gpio_set_level(BLINK_GPIO, 0);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
Adding an alias for convenience (add to ~/.zshrc):
alias get_idf='. $HOME/esp/esp-idf/export.sh'
Questions to explore:
- What does
idf.py set-targetactually configure? - Why is there no
main()function, butapp_main()instead? - What does
vTaskDelay()do vs a busy loop? - Where does
ESP_LOGI()output go?
Learning milestones:
- ESP-IDF installed and working → You understand the toolchain
- Build completes successfully → You understand CMake and components
- Flash succeeds → You understand esptool and serial ports
- Monitor shows output → You understand ESP logging and UART
Project 2: VS Code + ESP-IDF Extension Setup
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: IDE Integration / Developer Experience
- Software or Tool: VS Code, ESP-IDF Extension, clangd
- Main Book: “Programming the ESP32 in C” by Harry Fairhead
What you’ll build: A fully configured VS Code environment with IntelliSense, one-click build/flash, integrated terminal, and debugging support—making ESP32 development as smooth as any modern IDE.
Why it teaches ESP32 programming: Professional development requires a proper IDE. This project teaches you how to integrate ESP-IDF with VS Code, giving you autocomplete, error highlighting, and visual debugging.
Core challenges you’ll face:
- Installing the ESP-IDF extension → maps to VS Code marketplace
- Configuring extension settings → maps to pointing to ESP-IDF path
- Setting up IntelliSense → maps to compile_commands.json
- Using build/flash/monitor tasks → maps to VS Code tasks
Key Concepts:
- ESP-IDF Extension: VS Code Marketplace
- Extension Documentation: ESP-IDF Extension Docs
- IntelliSense Configuration: compile_commands.json generation
- Task Configuration:
.vscode/tasks.json
Difficulty: Beginner Time estimate: Weekend Prerequisites: Project 1, VS Code familiarity
Real world outcome:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Visual Studio Code - blink │
├─────────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌───────────────────────────────────────────────────────────┤
│ │ EXPLORER │ │ main/blink_example_main.c │
│ │ │ │ │
│ │ ▼ BLINK │ │ 1 #include "driver/gpio.h" │
│ │ ▼ main │ │ 2 #include "freertos/FreeRTOS.h" │
│ │ 📄 CMake.. │ │ 3 #include "freertos/task.h" │
│ │ 📄 blink.. │ │ 4 #include "esp_log.h" │
│ │ 📄 CMakeLi.. │ │ 5 │
│ │ 📄 sdkconfig │ │ 6 void app_main(void) │
│ │ ▸ build │ │ 7 { │
│ │ │ │ 8 gpio_config_t io_conf = { │
│ │ │ │ 9 .mode = GPIO_MODE_OUTPUT, │
│ │ │ │ 10 .pin_bit_mask = (1ULL << 2), │
│ │ │ │ 11 }; ▼ Autocomplete: gpio_config_t │
│ │ │ │ ├── .mode │
│ │ │ │ ├── .pull_up_en │
│ │ │ │ ├── .pull_down_en │
│ │ │ │ └── .intr_type │
│ │ │ │ │
│ └─────────────────┘ └───────────────────────────────────────────────────────────┤
├─────────────────────────────────────────────────────────────────────────────────┤
│ STATUS BAR: ESP32 | ⚙️ Build | ⚡ Flash | 📺 Monitor | 🔧 menuconfig │
└─────────────────────────────────────────────────────────────────────────────────┘
Implementation Hints:
Step 1: Install Extension:
- Open VS Code
- Go to Extensions (Cmd+Shift+X)
- Search “ESP-IDF”
- Install “Espressif IDF” by Espressif Systems
Step 2: Configure Extension:
- Press Cmd+Shift+P → “ESP-IDF: Configure ESP-IDF Extension”
- Choose “Use existing setup”
- Point to your ESP-IDF path:
~/esp/esp-idf - Let it detect tools
Step 3: Create settings.json:
// .vscode/settings.json
{
"idf.port": "/dev/cu.usbserial-0001",
"idf.flashType": "UART",
"idf.adapterTargetName": "esp32",
"idf.openOcdConfigs": [
"board/esp32-wrover-kit-3.3v.cfg"
],
"C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json"
}
Step 4: Use Status Bar Icons:
- Click ESP32 → Select target chip
- Click ⚙️ → Build project
- Click ⚡ → Flash to device
- Click 📺 → Open serial monitor
- Click 🔧 → Open menuconfig GUI
Alternative: PlatformIO: If you prefer PlatformIO’s project management:
; platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = espidf
monitor_speed = 115200
Learning milestones:
- Extension installed and configured → You understand IDE integration
- IntelliSense works → You understand compile_commands.json
- One-click build/flash works → You understand VS Code tasks
- Status bar shows target → You’re ready for development
Project 3: GPIO Deep Dive - Beyond digitalWrite
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: GPIO / Interrupts / Hardware
- Software or Tool: ESP-IDF, Logic Analyzer (optional)
- Main Book: “Bare Metal C” by Steve Oualline
What you’ll build: A comprehensive GPIO demo: output control, input reading, pull-up/pull-down configuration, interrupt handlers, and direct register access—understanding GPIO at every level of abstraction.
Why it teaches ESP32 programming: GPIO is the foundation of all embedded work. This project teaches you the full ESP-IDF driver API, interrupt handling, and optionally the bare-metal register approach.
Core challenges you’ll face:
- Configuring multiple pins at once → maps to gpio_config()
- Setting up interrupts → maps to ISR handlers and IRAM
- Debouncing buttons → maps to software debounce techniques
- Understanding GPIO matrix → maps to pin multiplexing
Key Concepts:
- GPIO Driver: GPIO & RTC GPIO
- Interrupt Handling: ESP-IDF Programming Guide - GPIO Interrupts
- GPIO Matrix: ESP32 Technical Reference Manual, Chapter 5
- IRAM_ATTR: Placing ISR code in fast memory
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 1-2, basic electronics
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) GPIO_DEMO: === GPIO Deep Dive Demo ===
# Output Demo
I (335) GPIO_DEMO: LED on GPIO2: ON
I (1335) GPIO_DEMO: LED on GPIO2: OFF
# Input Demo (with internal pull-up)
I (2335) GPIO_DEMO: Button on GPIO0: RELEASED (1)
I (2435) GPIO_DEMO: Button on GPIO0: PRESSED (0)
# Interrupt Demo
I (3000) GPIO_DEMO: Waiting for button interrupt...
I (3523) GPIO_DEMO: [ISR] Button pressed! Edge: FALLING
I (3523) GPIO_DEMO: Interrupt count: 1
I (3723) GPIO_DEMO: [ISR] Button released! Edge: RISING
I (3723) GPIO_DEMO: Interrupt count: 2
# Direct Register Demo
I (4000) GPIO_DEMO: Direct register write to GPIO2
I (4000) GPIO_DEMO: GPIO_OUT_W1TS_REG = 0x3FF44008, setting bit 2
I (4100) GPIO_DEMO: LED ON via W1TS register
I (4200) GPIO_DEMO: LED OFF via W1TC register
# Input Speed Test
I (5000) GPIO_DEMO: Reading GPIO0 10000 times...
I (5012) GPIO_DEMO: gpio_get_level(): 12ms for 10000 reads
I (5024) GPIO_DEMO: Direct REG_READ(): 0.8ms for 10000 reads
Implementation Hints:
gpio_config() vs individual calls:
// Method 1: Configure all at once (preferred)
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = (1ULL << GPIO_NUM_2) | (1ULL << GPIO_NUM_4),
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
// Method 2: Individual calls (simpler but slower)
gpio_reset_pin(GPIO_NUM_2);
gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
Interrupt handling:
static volatile uint32_t interrupt_count = 0;
// IRAM_ATTR: ISR must be in IRAM for reliability
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
uint32_t gpio_num = (uint32_t) arg;
interrupt_count++;
// Note: Can't use ESP_LOGI here! ISR context.
// Use a queue to signal the main task instead.
}
void setup_interrupt(void)
{
gpio_config_t io_conf = {
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL << GPIO_NUM_0),
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_ANYEDGE,
};
gpio_config(&io_conf);
// Install ISR service
gpio_install_isr_service(0);
// Hook ISR handler
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);
}
Direct register access (bare metal style):
#include "soc/gpio_reg.h"
// Set GPIO2 HIGH (much faster than gpio_set_level)
REG_WRITE(GPIO_OUT_W1TS_REG, 1 << 2); // W1TS = Write 1 To Set
// Set GPIO2 LOW
REG_WRITE(GPIO_OUT_W1TC_REG, 1 << 2); // W1TC = Write 1 To Clear
// Read GPIO0
uint32_t gpio_in = REG_READ(GPIO_IN_REG);
bool gpio0_level = (gpio_in >> 0) & 1;
Software debounce with FreeRTOS:
#define DEBOUNCE_TIME_MS 50
static void button_task(void* arg)
{
int last_state = 1;
TickType_t last_change = 0;
while (1) {
int current_state = gpio_get_level(GPIO_NUM_0);
TickType_t now = xTaskGetTickCount();
if (current_state != last_state &&
(now - last_change) > pdMS_TO_TICKS(DEBOUNCE_TIME_MS)) {
last_state = current_state;
last_change = now;
ESP_LOGI(TAG, "Button: %s", current_state ? "RELEASED" : "PRESSED");
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
Learning milestones:
- Configure multiple GPIOs at once → You understand gpio_config_t
- ISR fires correctly → You understand interrupt handling
- Debounce works → You understand real-world button handling
- Direct register access works → You understand the hardware layer
Project 4: FreeRTOS Multi-Tasking - Dual Core Power
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: RTOS / Concurrency / Multi-Core
- Software or Tool: ESP-IDF, FreeRTOS
- Main Book: “Mastering the FreeRTOS Real Time Kernel” by Richard Barry
What you’ll build: A multi-task application demonstrating task creation, task pinning to cores, inter-task communication with queues, synchronization with semaphores and mutexes, and software timers.
Why it teaches ESP32 programming: The ESP32’s dual-core Xtensa processor is wasted if you don’t use FreeRTOS properly. This project teaches concurrent programming on embedded systems.
Core challenges you’ll face:
- Creating and managing tasks → maps to xTaskCreate and priorities
- Pinning tasks to cores → maps to xTaskCreatePinnedToCore
- Inter-task communication → maps to queues and event groups
- Avoiding race conditions → maps to mutexes and critical sections
- Using software timers → maps to esp_timer and FreeRTOS timers
Key Concepts:
- FreeRTOS on ESP32: FreeRTOS Overview
- Task Management: FreeRTOS IDF
- Queues: FreeRTOS Guide - Chapter 4
- Semaphores/Mutexes: FreeRTOS Guide - Chapter 7
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-3, understanding of threading concepts
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) RTOS_DEMO: === FreeRTOS Multi-Tasking Demo ===
I (335) RTOS_DEMO: Starting tasks on both cores...
I (345) CORE0_TASK: Running on CPU 0 - System/WiFi core
I (345) CORE1_TASK: Running on CPU 1 - Application core
I (445) SENSOR_TASK: [Core 1] Reading temperature: 23.5°C
I (445) SENSOR_TASK: [Core 1] Sending to queue...
I (446) DISPLAY_TASK: [Core 1] Received from queue: 23.5°C
I (446) DISPLAY_TASK: [Core 1] Updating display...
I (545) SENSOR_TASK: [Core 1] Reading temperature: 23.7°C
I (545) LOGGING_TASK: [Core 0] Waiting for mutex...
I (546) LOGGING_TASK: [Core 0] Got mutex! Logging data...
I (556) LOGGING_TASK: [Core 0] Released mutex.
I (1000) TIMER_TASK: [TIMER] 1 second elapsed
I (2000) TIMER_TASK: [TIMER] 2 seconds elapsed
I (3000) STATS: === Task Statistics ===
I (3000) STATS: Task Name Core Stack Left Priority
I (3000) STATS: sensor_task 1 2048 5
I (3000) STATS: display_task 1 1536 4
I (3000) STATS: logging_task 0 1024 3
I (3000) STATS: IDLE0 0 1016 0
I (3000) STATS: IDLE1 1 1016 0
Implementation Hints:
Task creation with core pinning:
// Create task on specific core
void create_tasks(void)
{
// Sensor task on Core 1 (APP CPU)
xTaskCreatePinnedToCore(
sensor_task, // Function
"sensor_task", // Name
4096, // Stack size (bytes)
NULL, // Parameters
5, // Priority (higher = more important)
&sensor_task_handle, // Handle
1 // Core ID (0 or 1)
);
// WiFi handler on Core 0 (PRO CPU - where WiFi runs)
xTaskCreatePinnedToCore(
wifi_task,
"wifi_task",
8192,
NULL,
4,
&wifi_task_handle,
0
);
// Let scheduler pick core
xTaskCreate(
logging_task,
"logging_task",
2048,
NULL,
3,
&logging_task_handle
);
}
Queue for inter-task communication:
typedef struct {
float temperature;
uint32_t timestamp;
} sensor_data_t;
static QueueHandle_t sensor_queue;
void setup_queue(void)
{
sensor_queue = xQueueCreate(10, sizeof(sensor_data_t));
}
void sensor_task(void* arg)
{
while (1) {
sensor_data_t data = {
.temperature = read_temperature(),
.timestamp = esp_timer_get_time() / 1000
};
// Send to queue, wait up to 100ms if full
if (xQueueSend(sensor_queue, &data, pdMS_TO_TICKS(100)) != pdTRUE) {
ESP_LOGW(TAG, "Queue full!");
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void display_task(void* arg)
{
sensor_data_t received;
while (1) {
// Block until data available
if (xQueueReceive(sensor_queue, &received, portMAX_DELAY) == pdTRUE) {
ESP_LOGI(TAG, "Temp: %.1f°C at %lu ms",
received.temperature, received.timestamp);
}
}
}
Mutex for shared resource protection:
static SemaphoreHandle_t log_mutex;
static FILE* log_file;
void setup_mutex(void)
{
log_mutex = xSemaphoreCreateMutex();
}
void log_data(const char* message)
{
// Wait for mutex (forever)
if (xSemaphoreTake(log_mutex, portMAX_DELAY) == pdTRUE) {
// Critical section - only one task can be here
fprintf(log_file, "%lu: %s\n", esp_timer_get_time(), message);
fflush(log_file);
// Release mutex
xSemaphoreGive(log_mutex);
}
}
Binary semaphore for signaling:
static SemaphoreHandle_t data_ready_sem;
void isr_handler(void* arg)
{
BaseType_t higher_priority_woken = pdFALSE;
// Signal from ISR
xSemaphoreGiveFromISR(data_ready_sem, &higher_priority_woken);
// Yield if higher priority task was woken
if (higher_priority_woken) {
portYIELD_FROM_ISR();
}
}
void processing_task(void* arg)
{
while (1) {
// Block until ISR signals
xSemaphoreTake(data_ready_sem, portMAX_DELAY);
process_data();
}
}
Task watchdog and stack monitoring:
void print_task_stats(void)
{
TaskStatus_t tasks[10];
UBaseType_t count = uxTaskGetSystemState(tasks, 10, NULL);
ESP_LOGI(TAG, "Task Name Core Stack Left");
for (int i = 0; i < count; i++) {
ESP_LOGI(TAG, "%-15s %d %u",
tasks[i].pcTaskName,
tasks[i].xCoreID,
tasks[i].usStackHighWaterMark * 4);
}
}
Learning milestones:
- Multiple tasks run concurrently → You understand task scheduling
- Tasks communicate via queue → You understand producer/consumer
- Shared resources protected → You understand mutexes
- Tasks pinned to specific cores → You understand SMP
Project 5: Serial Protocols - UART, SPI, and I2C
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Communication Protocols / Peripherals
- Software or Tool: ESP-IDF, Logic Analyzer, I2C/SPI sensors
- Main Book: “Making Embedded Systems” by Elecia White
What you’ll build: Interface with real hardware sensors using all three major serial protocols: UART (GPS module or USB-serial), SPI (SD card or display), and I2C (temperature/humidity sensor like BME280).
Why it teaches ESP32 programming: Almost every embedded project needs to communicate with external chips. This project teaches you the ESP-IDF driver APIs for the three most common protocols.
Core challenges you’ll face:
- UART configuration and buffering → maps to baud rate, DMA
- SPI master transactions → maps to clock polarity, chip select
- I2C addressing and timing → maps to 7-bit address, ACK/NACK
- Parsing sensor data → maps to datasheets and protocols
Key Concepts:
- UART Driver: UART Documentation
- SPI Master: SPI Master Driver
- I2C Driver: I2C Driver
- GPIO Matrix: ESP32 can route any peripheral to most pins
Difficulty: Intermediate Time estimate: 2 weeks Prerequisites: Projects 1-4, basic understanding of serial protocols
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) SERIAL_DEMO: === Serial Protocols Demo ===
# UART Demo (GPS Module on UART2)
I (500) UART: Configured UART2: 9600 baud, TX=17, RX=16
I (1000) UART: Received NMEA: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M*47
I (1000) UART: Parsed: Lat=48.1173, Lon=11.5167, Sats=8
# I2C Demo (BME280 Sensor)
I (1500) I2C: Configured I2C0: SDA=21, SCL=22, 400kHz
I (1500) I2C: Scanning bus...
I (1510) I2C: Found device at address 0x76 (BME280)
I (1520) I2C: BME280 Chip ID: 0x60 ✓
I (1600) I2C: Temperature: 23.45°C
I (1600) I2C: Humidity: 45.2%
I (1600) I2C: Pressure: 1013.25 hPa
# SPI Demo (SD Card)
I (2000) SPI: Configured HSPI: CLK=18, MOSI=23, MISO=19, CS=5
I (2000) SPI: SD Card initialized
I (2100) SPI: Card size: 32 GB
I (2200) SPI: Writing test file...
I (2300) SPI: Read back: "Hello from ESP32!"
Implementation Hints:
UART with DMA buffering:
#define UART_NUM UART_NUM_2
#define TX_PIN 17
#define RX_PIN 16
#define BUF_SIZE 1024
void uart_init(void)
{
uart_config_t config = {
.baud_rate = 9600,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_DEFAULT,
};
uart_driver_install(UART_NUM, BUF_SIZE * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM, &config);
uart_set_pin(UART_NUM, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
void uart_read_task(void* arg)
{
uint8_t data[BUF_SIZE];
while (1) {
int len = uart_read_bytes(UART_NUM, data, BUF_SIZE - 1, pdMS_TO_TICKS(100));
if (len > 0) {
data[len] = '\0';
ESP_LOGI(TAG, "Received: %s", data);
}
}
}
I2C master for BME280:
#define I2C_NUM I2C_NUM_0
#define SDA_PIN 21
#define SCL_PIN 22
#define BME280_ADDR 0x76
void i2c_init(void)
{
i2c_config_t config = {
.mode = I2C_MODE_MASTER,
.sda_io_num = SDA_PIN,
.scl_io_num = SCL_PIN,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 400000, // 400 kHz
};
i2c_param_config(I2C_NUM, &config);
i2c_driver_install(I2C_NUM, config.mode, 0, 0, 0);
}
esp_err_t i2c_read_register(uint8_t reg, uint8_t* data, size_t len)
{
return i2c_master_write_read_device(
I2C_NUM,
BME280_ADDR,
®, // Write register address
1,
data, // Read into buffer
len,
pdMS_TO_TICKS(100)
);
}
void i2c_scan(void)
{
ESP_LOGI(TAG, "Scanning I2C bus...");
for (uint8_t addr = 1; addr < 127; addr++) {
if (i2c_master_probe(I2C_NUM, addr, pdMS_TO_TICKS(10)) == ESP_OK) {
ESP_LOGI(TAG, "Found device at 0x%02X", addr);
}
}
}
SPI master for SD card:
#define SPI_HOST HSPI_HOST
#define CLK_PIN 18
#define MOSI_PIN 23
#define MISO_PIN 19
#define CS_PIN 5
spi_device_handle_t spi_dev;
void spi_init(void)
{
spi_bus_config_t bus_cfg = {
.mosi_io_num = MOSI_PIN,
.miso_io_num = MISO_PIN,
.sclk_io_num = CLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4096,
};
spi_bus_initialize(SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
spi_device_interface_config_t dev_cfg = {
.clock_speed_hz = 10 * 1000 * 1000, // 10 MHz
.mode = 0, // CPOL=0, CPHA=0
.spics_io_num = CS_PIN,
.queue_size = 7,
};
spi_bus_add_device(SPI_HOST, &dev_cfg, &spi_dev);
}
esp_err_t spi_transfer(uint8_t* tx, uint8_t* rx, size_t len)
{
spi_transaction_t trans = {
.length = len * 8, // Length in bits
.tx_buffer = tx,
.rx_buffer = rx,
};
return spi_device_transmit(spi_dev, &trans);
}
Learning milestones:
- UART receives data correctly → You understand buffering and baud rates
- I2C scan finds devices → You understand addressing
- I2C reads sensor data → You understand register-based protocols
- SPI transfers work → You understand full-duplex communication
Project 6: WiFi Station & Access Point
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: WiFi / Networking / TCP/IP
- Software or Tool: ESP-IDF, Wireshark (optional)
- Main Book: “TCP/IP Illustrated, Volume 1” by W. Richard Stevens
What you’ll build: A WiFi-enabled ESP32 that can connect to your home network (station mode), create its own network (access point mode), and run both simultaneously (AP+STA mode), with proper event handling and error recovery.
Why it teaches ESP32 programming: WiFi is why people buy ESP32s. This project teaches the full ESP-IDF WiFi stack, event handling, and network configuration.
Core challenges you’ll face:
- WiFi event handling → maps to esp_event_loop and callbacks
- Station mode connection → maps to SSID/password, DHCP
- Access point mode → maps to softAP, DHCP server
- Error handling and reconnection → maps to robust design
Key Concepts:
- WiFi Driver: WiFi Driver
- Event Loop: Event Loop Library
- TCP/IP Stack: LwIP integration
- NVS for credentials: Non-volatile storage for WiFi config
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-5, basic networking knowledge
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) WIFI_DEMO: === WiFi Demo ===
I (335) WIFI_DEMO: Initializing NVS...
I (345) WIFI_DEMO: Initializing WiFi...
# Station Mode
I (500) WIFI_STA: Connecting to "MyHomeNetwork"...
I (1500) WIFI_STA: EVENT: WIFI_EVENT_STA_START
I (2000) WIFI_STA: EVENT: WIFI_EVENT_STA_CONNECTED
I (2500) WIFI_STA: EVENT: IP_EVENT_STA_GOT_IP
I (2500) WIFI_STA: ✓ Connected!
I (2500) WIFI_STA: IP Address: 192.168.1.105
I (2500) WIFI_STA: Subnet Mask: 255.255.255.0
I (2500) WIFI_STA: Gateway: 192.168.1.1
I (2500) WIFI_STA: RSSI: -45 dBm (Excellent)
# Access Point Mode
I (3000) WIFI_AP: Starting Access Point "ESP32_Config"...
I (3100) WIFI_AP: ✓ AP Started!
I (3100) WIFI_AP: SSID: ESP32_Config
I (3100) WIFI_AP: Password: esp32pass
I (3100) WIFI_AP: Channel: 1
I (3100) WIFI_AP: IP: 192.168.4.1
# Client connects to AP
I (10000) WIFI_AP: EVENT: WIFI_EVENT_AP_STACONNECTED
I (10000) WIFI_AP: Client connected: MAC=AA:BB:CC:DD:EE:FF
# Connection lost and recovery
I (60000) WIFI_STA: EVENT: WIFI_EVENT_STA_DISCONNECTED (reason=201)
I (60000) WIFI_STA: Disconnected! Reconnecting in 5s...
I (65000) WIFI_STA: Reconnecting (attempt 1/10)...
I (67000) WIFI_STA: ✓ Reconnected!
Implementation Hints:
WiFi initialization and event handling:
static EventGroupHandle_t wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT) {
switch (event_id) {
case WIFI_EVENT_STA_START:
ESP_LOGI(TAG, "Connecting...");
esp_wifi_connect();
break;
case WIFI_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "Disconnected! Retrying...");
esp_wifi_connect();
break;
case WIFI_EVENT_AP_STACONNECTED: {
wifi_event_ap_staconnected_t* event = event_data;
ESP_LOGI(TAG, "Client connected: "MACSTR, MAC2STR(event->mac));
break;
}
}
} else if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
}
}
}
void wifi_init(void)
{
// Initialize NVS (required for WiFi)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
// Initialize TCP/IP stack
esp_netif_init();
// Create event loop
esp_event_loop_create_default();
// Create default WiFi station
esp_netif_create_default_wifi_sta();
// Initialize WiFi with default config
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
// Register event handlers
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL);
// Configure and start
wifi_config_t sta_config = {
.sta = {
.ssid = "MyHomeNetwork",
.password = "mypassword",
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_set_config(WIFI_IF_STA, &sta_config);
esp_wifi_start();
}
Access Point mode:
void wifi_init_softap(void)
{
esp_netif_create_default_wifi_ap();
wifi_config_t ap_config = {
.ap = {
.ssid = "ESP32_Config",
.ssid_len = strlen("ESP32_Config"),
.password = "esp32pass",
.channel = 1,
.authmode = WIFI_AUTH_WPA2_PSK,
.max_connection = 4,
},
};
esp_wifi_set_mode(WIFI_MODE_AP);
esp_wifi_set_config(WIFI_IF_AP, &ap_config);
esp_wifi_start();
}
AP+STA mode (simultaneous):
void wifi_init_apsta(void)
{
esp_netif_create_default_wifi_sta();
esp_netif_create_default_wifi_ap();
wifi_config_t sta_config = { /* ... */ };
wifi_config_t ap_config = { /* ... */ };
esp_wifi_set_mode(WIFI_MODE_APSTA); // Both modes!
esp_wifi_set_config(WIFI_IF_STA, &sta_config);
esp_wifi_set_config(WIFI_IF_AP, &ap_config);
esp_wifi_start();
}
Learning milestones:
- Station connects to WiFi → You understand WiFi initialization
- Got IP address via DHCP → You understand networking stack
- AP mode works → You understand softAP
- Handles disconnection gracefully → You understand robust design
Project 7: HTTP Server & REST API
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: HTTP / REST / Web
- Software or Tool: ESP-IDF HTTP Server, cURL, Postman
- Main Book: “RESTful Web APIs” by Leonard Richardson
What you’ll build: A full HTTP server running on the ESP32 that serves a web interface, provides REST API endpoints for sensor data, and handles JSON requests/responses.
Why it teaches ESP32 programming: Most IoT devices need a web interface. This project teaches the ESP-IDF HTTP server, URI handling, and embedded web development.
Core challenges you’ll face:
- Setting up HTTP server → maps to httpd_start and handlers
- Serving static files → maps to SPIFFS or embedded files
- REST API design → maps to GET/POST/PUT/DELETE handlers
- JSON parsing → maps to cJSON library
Key Concepts:
- HTTP Server: ESP HTTP Server
- cJSON Library: JSON parsing and generation
- SPIFFS: SPI Flash File System for web assets
- mDNS: Access via
esp32.localinstead of IP
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-6, basic HTTP knowledge
Real world outcome:
# From terminal
$ curl http://192.168.1.105/api/status
{
"device": "ESP32 Sensor Hub",
"uptime": 3600,
"heap_free": 245000,
"temperature": 23.5,
"humidity": 45.2
}
$ curl -X POST http://192.168.1.105/api/led -H "Content-Type: application/json" \
-d '{"state": true}'
{
"success": true,
"led": "on"
}
# From browser: http://esp32.local/
┌─────────────────────────────────────────────────────────────────┐
│ ESP32 Sensor Hub │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 🌡️ Temperature: 23.5°C │
│ 💧 Humidity: 45.2% │
│ 📊 Pressure: 1013 hPa │
│ │
│ 💡 LED Control: [ON] [OFF] │
│ │
│ 📈 Last 24 hours: │
│ ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆ │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation Hints:
HTTP server setup:
#include "esp_http_server.h"
#include "cJSON.h"
static httpd_handle_t server = NULL;
// GET /api/status
static esp_err_t status_get_handler(httpd_req_t *req)
{
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device", "ESP32 Sensor Hub");
cJSON_AddNumberToObject(root, "uptime", esp_timer_get_time() / 1000000);
cJSON_AddNumberToObject(root, "heap_free", esp_get_free_heap_size());
cJSON_AddNumberToObject(root, "temperature", read_temperature());
char *json = cJSON_Print(root);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, json, strlen(json));
free(json);
cJSON_Delete(root);
return ESP_OK;
}
// POST /api/led
static esp_err_t led_post_handler(httpd_req_t *req)
{
char buf[100];
int received = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (received <= 0) return ESP_FAIL;
buf[received] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root) {
cJSON *state = cJSON_GetObjectItem(root, "state");
if (cJSON_IsBool(state)) {
gpio_set_level(LED_GPIO, cJSON_IsTrue(state));
}
cJSON_Delete(root);
}
httpd_resp_set_type(req, "application/json");
httpd_resp_sendstr(req, "{\"success\": true}");
return ESP_OK;
}
void start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 16;
if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t status_uri = {
.uri = "/api/status",
.method = HTTP_GET,
.handler = status_get_handler,
};
httpd_register_uri_handler(server, &status_uri);
httpd_uri_t led_uri = {
.uri = "/api/led",
.method = HTTP_POST,
.handler = led_post_handler,
};
httpd_register_uri_handler(server, &led_uri);
ESP_LOGI(TAG, "HTTP server started on port 80");
}
}
Serving static HTML (embedded):
// In CMakeLists.txt, embed the file
// target_add_binary_data(${COMPONENT_TARGET} "index.html" TEXT)
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
extern const uint8_t index_html_end[] asm("_binary_index_html_end");
static esp_err_t index_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, (const char *)index_html_start,
index_html_end - index_html_start);
return ESP_OK;
}
mDNS for easy access:
#include "mdns.h"
void start_mdns_service(void)
{
mdns_init();
mdns_hostname_set("esp32");
mdns_instance_name_set("ESP32 Sensor Hub");
// Now accessible at http://esp32.local/
}
Learning milestones:
- HTTP server responds → You understand URI handlers
- JSON API works → You understand cJSON
- Web page loads → You understand static file serving
- mDNS works → You understand service discovery
Project 8: BLE GATT Server
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Bluetooth / BLE / GATT
- Software or Tool: ESP-IDF NimBLE, nRF Connect app
- Main Book: “Getting Started with Bluetooth Low Energy” by Kevin Townsend
What you’ll build: A BLE peripheral that advertises services, accepts connections from phones, and allows reading/writing characteristics—the foundation for any BLE-enabled product.
Why it teaches ESP32 programming: BLE is essential for mobile connectivity. This project teaches the NimBLE stack, GATT profiles, and Bluetooth concepts.
Core challenges you’ll face:
- BLE initialization → maps to NimBLE stack setup
- Defining GATT services → maps to UUIDs and characteristics
- Handling connections → maps to GAP events
- Read/write/notify → maps to GATT operations
Key Concepts:
- NimBLE Introduction: BLE Introduction
- GATT Server Example: NimBLE GATT Server
- BLE Advertising: GAP advertising parameters
- Characteristic Properties: Read, Write, Notify, Indicate
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-5, basic BLE concepts
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) BLE_DEMO: === BLE GATT Server Demo ===
I (500) NimBLE: BLE Host Task Started
I (510) BLE_DEMO: Device address: AA:BB:CC:DD:EE:FF
I (520) BLE_DEMO: Starting advertising...
# Phone connects via nRF Connect app
I (5000) BLE_DEMO: GAP event: CONNECT
I (5000) BLE_DEMO: Client connected: 11:22:33:44:55:66
I (5000) BLE_DEMO: Connection handle: 1
# Phone reads temperature characteristic
I (6000) BLE_DEMO: GATT: Read request for Temperature
I (6000) BLE_DEMO: Sending: 23.5°C
# Phone enables notifications
I (7000) BLE_DEMO: GATT: Subscribe to notifications
I (7000) BLE_DEMO: Notifications enabled!
# Sending periodic updates
I (8000) BLE_DEMO: Notifying: Temperature = 23.6°C
I (9000) BLE_DEMO: Notifying: Temperature = 23.7°C
# Phone writes to LED characteristic
I (10000) BLE_DEMO: GATT: Write to LED characteristic
I (10000) BLE_DEMO: LED set to: ON
# Phone disconnects
I (20000) BLE_DEMO: GAP event: DISCONNECT
I (20000) BLE_DEMO: Restarting advertising...
Implementation Hints:
NimBLE GATT server setup:
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
// Custom UUIDs
#define SENSOR_SERVICE_UUID 0x1234
#define TEMP_CHAR_UUID 0x1235
#define LED_CHAR_UUID 0x1236
static uint16_t temp_char_handle;
static uint16_t led_char_handle;
static uint16_t conn_handle;
static bool notify_enabled = false;
// GATT access callback
static int gatt_access_cb(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg)
{
if (attr_handle == temp_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
float temp = read_temperature();
os_mbuf_append(ctxt->om, &temp, sizeof(temp));
ESP_LOGI(TAG, "Read temperature: %.1f", temp);
}
}
else if (attr_handle == led_char_handle) {
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
uint8_t led_state;
ble_hs_mbuf_to_flat(ctxt->om, &led_state, 1, NULL);
gpio_set_level(LED_GPIO, led_state);
ESP_LOGI(TAG, "LED set to: %s", led_state ? "ON" : "OFF");
}
}
return 0;
}
// GATT service definition
static const struct ble_gatt_svc_def gatt_services[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = BLE_UUID16_DECLARE(SENSOR_SERVICE_UUID),
.characteristics = (struct ble_gatt_chr_def[]) {
{
.uuid = BLE_UUID16_DECLARE(TEMP_CHAR_UUID),
.access_cb = gatt_access_cb,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_NOTIFY,
.val_handle = &temp_char_handle,
},
{
.uuid = BLE_UUID16_DECLARE(LED_CHAR_UUID),
.access_cb = gatt_access_cb,
.flags = BLE_GATT_CHR_F_WRITE,
.val_handle = &led_char_handle,
},
{ 0 }, // Terminator
},
},
{ 0 }, // Terminator
};
GAP event handling:
static int gap_event_handler(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
conn_handle = event->connect.conn_handle;
ESP_LOGI(TAG, "Connected!");
} else {
start_advertising();
}
break;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "Disconnected");
start_advertising();
break;
case BLE_GAP_EVENT_SUBSCRIBE:
if (event->subscribe.attr_handle == temp_char_handle) {
notify_enabled = event->subscribe.cur_notify;
ESP_LOGI(TAG, "Notifications %s",
notify_enabled ? "enabled" : "disabled");
}
break;
}
return 0;
}
Sending notifications:
void notify_task(void *arg)
{
while (1) {
if (notify_enabled) {
float temp = read_temperature();
struct os_mbuf *om = ble_hs_mbuf_from_flat(&temp, sizeof(temp));
ble_gatts_notify_custom(conn_handle, temp_char_handle, om);
ESP_LOGI(TAG, "Notified: %.1f°C", temp);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
Learning milestones:
- BLE advertises correctly → You understand GAP advertising
- Phone can connect → You understand connection handling
- Read/write work → You understand GATT operations
- Notifications work → You understand push updates
Project 9: Deep Sleep & Power Optimization
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Power Management / Battery
- Software or Tool: ESP-IDF, Multimeter/Power Profiler
- Main Book: “Making Embedded Systems” by Elecia White
What you’ll build: A battery-powered sensor node that wakes periodically, reads sensors, transmits data via WiFi, and returns to deep sleep—achieving weeks of battery life on a small LiPo.
Why it teaches ESP32 programming: Real IoT devices need to run on batteries. This project teaches power management, sleep modes, and optimization techniques.
Core challenges you’ll face:
- Configuring wake sources → maps to timer, GPIO, ULP
- Preserving data across sleep → maps to RTC memory
- Fast WiFi reconnection → maps to WiFi fast connect
- Measuring power consumption → maps to hardware profiling
Key Concepts:
- Sleep Modes: Sleep Modes Documentation
- Low Power Mode Guide: Low Power Mode
- RTC Memory: Data preservation across deep sleep
- ULP Coprocessor: Ultra-low-power monitoring
Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Projects 1-8, multimeter
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) POWER_DEMO: === Deep Sleep Power Demo ===
I (335) POWER_DEMO: Boot count: 1 (from RTC memory)
I (345) POWER_DEMO: Wake cause: TIMER
I (400) POWER_DEMO: Reading sensors...
I (410) POWER_DEMO: Temperature: 23.5°C, Humidity: 45.2%
I (500) POWER_DEMO: Connecting to WiFi (fast mode)...
I (1200) POWER_DEMO: Connected in 700ms!
I (1300) POWER_DEMO: Sending data to server...
I (1500) POWER_DEMO: Data sent successfully
I (1600) POWER_DEMO: Disconnecting WiFi...
I (1700) POWER_DEMO: Entering deep sleep for 5 minutes...
I (1700) POWER_DEMO: Expected wake at: 12:35:00
# --- ESP32 enters deep sleep, current drops to ~10µA ---
# 5 minutes later...
I (325) POWER_DEMO: Boot count: 2 (from RTC memory)
I (335) POWER_DEMO: Wake cause: TIMER
...
# Power consumption profile:
# Active (WiFi): ~150mA for ~2 seconds
# Deep sleep: ~10µA for ~298 seconds
# Average: ~1mA → 1000mAh battery lasts ~40 days!
Implementation Hints:
RTC memory for data persistence:
// This data survives deep sleep!
RTC_DATA_ATTR static int boot_count = 0;
RTC_DATA_ATTR static uint32_t last_wake_time = 0;
RTC_DATA_ATTR static uint8_t wifi_channel = 0;
RTC_DATA_ATTR static uint8_t wifi_bssid[6];
void app_main(void)
{
boot_count++;
ESP_LOGI(TAG, "Boot count: %d", boot_count);
// Check wake cause
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
switch (cause) {
case ESP_SLEEP_WAKEUP_TIMER:
ESP_LOGI(TAG, "Wake cause: TIMER");
break;
case ESP_SLEEP_WAKEUP_EXT0:
ESP_LOGI(TAG, "Wake cause: GPIO (button)");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
ESP_LOGI(TAG, "Wake cause: TOUCH");
break;
default:
ESP_LOGI(TAG, "Wake cause: POWER ON");
break;
}
}
Fast WiFi reconnection:
// Save connection info before sleep
void save_wifi_info(void)
{
wifi_ap_record_t ap_info;
esp_wifi_sta_get_ap_info(&ap_info);
wifi_channel = ap_info.primary;
memcpy(wifi_bssid, ap_info.bssid, 6);
}
// Fast connect on wake
void fast_wifi_connect(void)
{
wifi_config_t wifi_config = {
.sta = {
.ssid = "MyNetwork",
.password = "password",
.channel = wifi_channel, // Skip channel scanning!
.bssid_set = true,
},
};
memcpy(wifi_config.sta.bssid, wifi_bssid, 6);
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
esp_wifi_connect();
}
Entering deep sleep:
void enter_deep_sleep(uint64_t sleep_time_us)
{
// Disconnect WiFi cleanly
esp_wifi_disconnect();
esp_wifi_stop();
// Save WiFi info for fast reconnect
save_wifi_info();
// Configure wake sources
esp_sleep_enable_timer_wakeup(sleep_time_us);
// Optional: GPIO wake (button on GPIO0)
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0); // Wake on LOW
ESP_LOGI(TAG, "Entering deep sleep for %llu seconds...",
sleep_time_us / 1000000);
// This function does not return!
esp_deep_sleep_start();
}
Light sleep with auto-wakeup:
void enable_auto_light_sleep(void)
{
// Enable power management with light sleep
esp_pm_config_t pm_config = {
.max_freq_mhz = 240,
.min_freq_mhz = 80,
.light_sleep_enable = true,
};
esp_pm_configure(&pm_config);
// CPU will automatically light-sleep when idle
// Wake on any interrupt (timer, GPIO, UART, etc.)
}
Learning milestones:
- Deep sleep works → You understand sleep entry
- Data persists across sleep → You understand RTC memory
- Fast WiFi works → You understand connection optimization
- Measured power is <100µA → You understand power profiling
Project 10: OTA Updates & Secure Boot
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: OTA / Security / Production
- Software or Tool: ESP-IDF, HTTPS server, openssl
- Main Book: “Practical IoT Hacking” by Fotios Chantzis
What you’ll build: A production-ready firmware update system with secure OTA over HTTPS, rollback on failure, and optional secure boot with flash encryption.
Why it teaches ESP32 programming: Real products need secure updates. This project teaches OTA, partition tables, digital signatures, and secure boot.
Core challenges you’ll face:
- Understanding partition tables → maps to ota_0, ota_1, factory
- Implementing OTA client → maps to esp_https_ota
- Rollback on failure → maps to app_valid mark
- Secure boot setup → maps to key management
Key Concepts:
- OTA Updates: Over The Air Updates
- Partition Tables: Partition Tables
- Secure Boot: Secure Boot
- Flash Encryption: Protecting firmware at rest
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Projects 1-9, HTTPS server, understanding of cryptography
Real world outcome:
$ idf.py -p /dev/cu.usbserial-0001 flash monitor
I (325) OTA_DEMO: === OTA Update Demo ===
I (335) OTA_DEMO: Current firmware version: 1.0.0
I (345) OTA_DEMO: Running from partition: ota_0
I (500) OTA_DEMO: Checking for updates at https://firmware.example.com/esp32/
I (2000) OTA_DEMO: New version available: 1.1.0
I (2100) OTA_DEMO: Starting OTA update...
I (2200) OTA_DEMO: Downloading: 10%
I (3000) OTA_DEMO: Downloading: 50%
I (4000) OTA_DEMO: Downloading: 100%
I (4100) OTA_DEMO: Verifying signature...
I (4200) OTA_DEMO: Signature valid ✓
I (4300) OTA_DEMO: Writing to partition: ota_1
I (5000) OTA_DEMO: Write complete
I (5100) OTA_DEMO: Setting boot partition to ota_1...
I (5200) OTA_DEMO: Rebooting to new firmware...
# After reboot
I (325) OTA_DEMO: === OTA Update Demo ===
I (335) OTA_DEMO: Current firmware version: 1.1.0
I (345) OTA_DEMO: Running from partition: ota_1
I (355) OTA_DEMO: Marking firmware as valid...
I (365) OTA_DEMO: OTA update successful! ✓
Implementation Hints:
Partition table (partitions.csv):
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,
ota_data, data, ota, 0x310000, 0x2000,
Simple HTTPS OTA:
#include "esp_https_ota.h"
#include "esp_ota_ops.h"
extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_ca_cert_pem_end");
esp_err_t perform_ota_update(const char *url)
{
esp_http_client_config_t config = {
.url = url,
.cert_pem = (char *)server_cert_pem_start,
.timeout_ms = 30000,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
ESP_LOGI(TAG, "Starting OTA from: %s", url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA successful! Rebooting...");
esp_restart();
} else {
ESP_LOGE(TAG, "OTA failed: %s", esp_err_to_name(ret));
}
return ret;
}
Rollback protection:
void app_main(void)
{
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
// We just updated! Verify the new firmware works
ESP_LOGI(TAG, "New firmware running, verifying...");
// Run self-tests here
bool tests_passed = run_self_tests();
if (tests_passed) {
ESP_LOGI(TAG, "Tests passed! Marking firmware as valid.");
esp_ota_mark_app_valid_cancel_rollback();
} else {
ESP_LOGE(TAG, "Tests failed! Rolling back...");
esp_ota_mark_app_invalid_rollback_and_reboot();
}
}
}
// Normal operation
// ...
}
Checking for updates periodically:
void ota_check_task(void *arg)
{
while (1) {
// Check every hour
vTaskDelay(pdMS_TO_TICKS(3600 * 1000));
// Get current version
const esp_app_desc_t *app_desc = esp_app_get_description();
ESP_LOGI(TAG, "Current version: %s", app_desc->version);
// Check server for updates
char *new_version = check_server_for_update();
if (new_version && strcmp(new_version, app_desc->version) != 0) {
ESP_LOGI(TAG, "Update available: %s", new_version);
perform_ota_update(OTA_URL);
}
free(new_version);
}
}
Learning milestones:
- OTA downloads firmware → You understand HTTP client
- New firmware boots → You understand partition switching
- Rollback works on failure → You understand validation
- Secure boot enabled → You understand production security
Project 11: JTAG Debugging with OpenOCD
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 4: Expert
- Knowledge Area: Debugging / JTAG / Hardware
- Software or Tool: ESP-IDF, OpenOCD, ESP-PROG, VS Code
- Main Book: “The Art of Debugging with GDB, DDD, and Eclipse” by Norman Matloff
What you’ll build: A full debugging setup with hardware JTAG, breakpoints, memory inspection, and core dump analysis—professional-grade debugging for ESP32.
Why it teaches ESP32 programming: Printf debugging only goes so far. This project teaches real debugging with hardware breakpoints and memory inspection.
Core challenges you’ll face:
- JTAG hardware setup → maps to ESP-PROG or FT2232
- OpenOCD configuration → maps to config files
- GDB integration → maps to VS Code debug launch
- Core dump analysis → maps to post-mortem debugging
Key Concepts:
- JTAG Debugging: JTAG Debugging Guide
- OpenOCD ESP32: OpenOCD Branch
- Core Dump: Core Dump
- VS Code Debug: ESP-IDF extension debugging
Difficulty: Expert Time estimate: 1-2 weeks Prerequisites: Projects 1-5, ESP-PROG or FT2232 adapter
Real world outcome:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ VS Code - Debugging ESP32 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────────────────────────────────────────────┤
│ │ DEBUG │ │ main.c │
│ │ │ │ │
│ │ VARIABLES │ │ 42 void process_data(sensor_data_t *data) │
│ │ ▼ Locals │ │ 43 { │
│ │ data: 0x3FF.. │ │ 44 float result = data->value * 1.5; │
│ │ result: 35.25 │ │ 45 ● → if (result > threshold) { ← BREAKPOINT │
│ │ threshold: 50 │ │ 46 trigger_alarm(); │
│ │ │ │ 47 } │
│ │ ▼ data (struct) │ │ 48 } │
│ │ value: 23.5 │ │ │
│ │ timestamp: .. │ │ │
│ │ │ │ │
│ │ WATCH │ │ │
│ │ esp_get_free. │ │ │
│ │ → 234568 │ │ │
│ │ │ │ │
│ │ CALL STACK │ │ │
│ │ process_data │ │ │
│ │ sensor_task │ │ │
│ │ vPortTaskWra..│ │ │
│ └─────────────────┘ └──────────────────────────────────────────────────────────┤
│ │
│ DEBUG CONSOLE │
│ (gdb) info registers │
│ a0 0x400d1234 1074594356 │
│ a1 0x3ffb5f80 1073504128 │
│ a2 0x3ffb5fc0 1073504192 │
│ pc 0x400d5678 0x400d5678 <process_data+24> │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
Implementation Hints:
JTAG wiring (ESP-PROG or FT2232):
ESP32 Pin JTAG Signal ESP-PROG Pin
───────── ─────────── ────────────
GPIO12 TDI TDI
GPIO13 TCK TCK
GPIO14 TMS TMS
GPIO15 TDO TDO
GND GND GND
VS Code launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "espidf",
"name": "ESP-IDF Debug",
"request": "launch",
"debugPort": 3333,
"logLevel": 2,
"mode": "auto",
"initGdbCommands": [
"target remote :3333",
"set remote hardware-watchpoint-limit 2",
"mon reset halt",
"thb app_main",
"c"
]
}
]
}
Starting OpenOCD manually:
# Start OpenOCD
openocd -f board/esp32-wrover-kit-3.3v.cfg
# In another terminal, start GDB
xtensa-esp32-elf-gdb build/your_project.elf
(gdb) target remote :3333
(gdb) monitor reset halt
(gdb) break app_main
(gdb) continue
Core dump configuration (menuconfig):
Component config → ESP System Settings → Core Dump → Core dump data destination → UART
Analyzing core dump:
# After crash, save UART output to file
# Then analyze:
espcoredump.py info_corefile -t raw -c core.bin build/your_project.elf
# Or for base64-encoded from UART:
espcoredump.py info_corefile -t b64 -c core.txt build/your_project.elf
Learning milestones:
- OpenOCD connects to ESP32 → You understand JTAG
- Breakpoints work → You understand GDB integration
- Can inspect variables → You understand debugging workflow
- Core dump analyzed → You understand post-mortem debugging
Project 12: Bare Metal - Direct Register Programming
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: Assembly
- Coolness Level: Level 5: Pure Magic
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 5: Master
- Knowledge Area: Bare Metal / Registers / Hardware
- Software or Tool: ESP-IDF toolchain (xtensa-esp32-elf-gcc), ESP32 TRM
- Main Book: “Bare Metal C” by Steve Oualline
What you’ll build: A minimal ESP32 program that runs without ESP-IDF or FreeRTOS, directly manipulating registers to blink an LED and output to UART—true bare metal programming.
Why it teaches ESP32 programming: This is the deepest level of understanding. You’ll learn how the chip actually works, reading the Technical Reference Manual and writing to hardware registers.
Core challenges you’ll face:
- Understanding the boot process → maps to ROM bootloader
- Reading the TRM → maps to register definitions
- Clock configuration → maps to PLL setup
- GPIO without drivers → maps to GPIO_OUT_REG
Key Concepts:
- ESP32 Technical Reference Manual: TRM
- Bare Metal SDK: MDK by cpq
- Minimal Examples: ESP32_minimal
- Register Definitions: soc/*.h headers in ESP-IDF
Difficulty: Master Time estimate: 3-4 weeks Prerequisites: All previous projects, assembly basics, comfort reading datasheets
Real world outcome:
$ xtensa-esp32-elf-gcc -nostdlib -Wl,-Tbuild/esp32.ld -o blink.elf main.c
$ esptool.py --chip esp32 elf2image blink.elf
$ esptool.py --chip esp32 write_flash 0x1000 blink.bin
# Serial output (9600 baud, manually configured UART)
Bare metal ESP32!
LED ON
LED OFF
LED ON
...
Implementation Hints:
Minimal bare metal blink (conceptual):
// Direct register access - NO ESP-IDF!
#include <stdint.h>
// Register addresses from TRM
#define DR_REG_GPIO_BASE 0x3FF44000
#define GPIO_ENABLE_W1TS_REG (DR_REG_GPIO_BASE + 0x0024)
#define GPIO_OUT_W1TS_REG (DR_REG_GPIO_BASE + 0x0008)
#define GPIO_OUT_W1TC_REG (DR_REG_GPIO_BASE + 0x000C)
#define REG_WRITE(addr, val) (*((volatile uint32_t *)(addr)) = (val))
#define REG_READ(addr) (*((volatile uint32_t *)(addr)))
#define LED_PIN 2
void delay_ms(uint32_t ms)
{
// Crude delay loop (not accurate!)
volatile uint32_t count = ms * 10000;
while (count--);
}
// Entry point - called by bootloader
void app_main(void)
{
// Enable GPIO2 as output
// Set bit 2 in GPIO_ENABLE register
REG_WRITE(GPIO_ENABLE_W1TS_REG, 1 << LED_PIN);
while (1) {
// Set GPIO2 HIGH
REG_WRITE(GPIO_OUT_W1TS_REG, 1 << LED_PIN);
delay_ms(500);
// Set GPIO2 LOW
REG_WRITE(GPIO_OUT_W1TC_REG, 1 << LED_PIN);
delay_ms(500);
}
}
UART output at register level:
#define DR_REG_UART_BASE 0x3FF40000
#define UART_FIFO_REG (DR_REG_UART_BASE + 0x00)
#define UART_STATUS_REG (DR_REG_UART_BASE + 0x1C)
#define UART_TXFIFO_CNT_MASK 0xFF0000
void uart_putc(char c)
{
// Wait for TX FIFO space
while ((REG_READ(UART_STATUS_REG) & UART_TXFIFO_CNT_MASK) >= 0x7F0000);
// Write character
REG_WRITE(UART_FIFO_REG, c);
}
void uart_puts(const char *s)
{
while (*s) {
uart_putc(*s++);
}
}
Understanding GPIO registers:
GPIO_OUT_REG (0x3FF44004):
Current output level for GPIO0-31
Read/write directly
GPIO_OUT_W1TS_REG (0x3FF44008):
Write 1 To Set - atomic set operation
Writing 1 to bit N sets GPIO N high
Writing 0 has no effect
GPIO_OUT_W1TC_REG (0x3FF4400C):
Write 1 To Clear - atomic clear operation
Writing 1 to bit N sets GPIO N low
Writing 0 has no effect
GPIO_ENABLE_REG (0x3FF44020):
1 = Output mode, 0 = Input mode
Why bare metal is hard on ESP32:
Challenges:
1. Clock setup - ESP32 boots at 40MHz, need to configure PLL
2. Flash access - Code runs from flash via cache, complex setup
3. WiFi/BLE - Registers are undocumented by Espressif
4. FreeRTOS dependency - Many features assume RTOS is running
Recommendation:
- Use bare metal for learning and time-critical sections
- Use ESP-IDF for production code
- Hybrid approach: call bare metal functions from ESP-IDF
Learning milestones:
- Understand GPIO registers → You can read the TRM
- LED blinks without drivers → You understand direct hardware access
- UART works at register level → You understand peripheral setup
- Can mix bare metal with ESP-IDF → You understand the abstraction layers
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| 1. ESP-IDF Environment Setup | Beginner | 1-2 days | ⭐⭐⭐ | ⭐⭐ |
| 2. VS Code + ESP-IDF Extension | Beginner | Weekend | ⭐⭐ | ⭐⭐⭐ |
| 3. GPIO Deep Dive | Intermediate | 1 week | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 4. FreeRTOS Multi-Tasking | Advanced | 2 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 5. Serial Protocols | Intermediate | 2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 6. WiFi Station & AP | Advanced | 2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 7. HTTP Server & REST API | Advanced | 2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 8. BLE GATT Server | Advanced | 2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 9. Deep Sleep & Power | Advanced | 2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 10. OTA Updates & Secure Boot | Expert | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 11. JTAG Debugging | Expert | 1-2 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 12. Bare Metal Programming | Master | 3-4 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Recommended Learning Path
If you’re new to ESP32 (but know C):
Start with: Project 1 → 2 → 3 → 4
This gives you the environment, IDE, GPIO basics, and FreeRTOS foundation.
If you want to build IoT devices:
Path: Projects 1-7 → 9 → 10
Focus on WiFi, HTTP, and production features (power, OTA).
If you want Bluetooth/BLE products:
Path: Projects 1-4 → 8 → 9 → 10
Master FreeRTOS first (critical for BLE), then BLE stack.
If you want deep hardware understanding:
Path: Projects 1-3 → 5 → 11 → 12
Focus on peripherals, debugging, and bare metal.
If you need to ship a product ASAP:
Fast path: Projects 1 → 2 → 6 → 7 → 10
Minimum viable: environment, WiFi, HTTP server, OTA updates.
Final Capstone Project: Smart Environmental Monitor
- File: LEARN_ESP32_WITHOUT_ARDUINO_DEEP_DIVE.md
- Main Programming Language: C
- Alternative Programming Languages: C++
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Full-Stack Embedded IoT
- Software or Tool: All previous + Cloud service (AWS IoT, ThingsBoard, etc.)
- Main Book: All previous combined
What you’ll build: A production-ready environmental monitoring system that:
- Reads multiple sensors (temperature, humidity, air quality, light)
- Runs as a BLE beacon AND WiFi-connected device
- Hosts a local web dashboard
- Sends data to a cloud backend via MQTT
- Supports OTA updates
- Runs for months on battery using deep sleep
- Has proper JTAG debugging and logging
- Uses secure boot and flash encryption
Why this is the capstone: This project combines everything: FreeRTOS tasks, all serial protocols, WiFi, BLE, HTTP server, MQTT client, deep sleep, OTA, and security—a complete product.
Core challenges you’ll face:
- Multi-protocol coexistence → WiFi + BLE simultaneously
- Power budget optimization → Balance connectivity vs. battery life
- Robust error handling → Survive network failures, sensor failures
- Cloud integration → MQTT, TLS, device provisioning
- Production security → Secure boot, encrypted credentials
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | ESP-IDF Environment Setup | C |
| 2 | VS Code + ESP-IDF Extension | C |
| 3 | GPIO Deep Dive | C |
| 4 | FreeRTOS Multi-Tasking | C |
| 5 | Serial Protocols (UART, SPI, I2C) | C |
| 6 | WiFi Station & Access Point | C |
| 7 | HTTP Server & REST API | C |
| 8 | BLE GATT Server | C |
| 9 | Deep Sleep & Power Optimization | C |
| 10 | OTA Updates & Secure Boot | C |
| 11 | JTAG Debugging with OpenOCD | C |
| 12 | Bare Metal Programming | C |
| Capstone | Smart Environmental Monitor | C |
Additional Resources
Official Documentation
- ESP-IDF Programming Guide - The definitive reference
- ESP-IDF GitHub - Source code and examples
- ESP32 Technical Reference Manual - Hardware details
Development Tools
- ESP-IDF VS Code Extension - Official IDE support
- PlatformIO - Alternative build system
- ESP-IDF Eclipse Plugin - For Eclipse users
Books
- “Programming the ESP32 in C” by Harry Fairhead - ESP-IDF focused, highly recommended
- “Kolban’s Book on ESP32” by Neil Kolban - Free, comprehensive
- “Mastering the FreeRTOS Real Time Kernel” by Richard Barry - Official FreeRTOS book
Community
- ESP32 Forum - Official community forum
- r/esp32 - Reddit community
- ESP32 Discord - Real-time chat
Bare Metal Resources
- MDK - Bare Metal SDK - Single-header SDK
- ESP32_minimal - Learning examples
- Vivonomicon’s Blog - Detailed tutorials
Generated for your ESP32 professional development journey. Master the chip, not just the framework!