Back to Fastled

ESP32 Platform Architecture

src/platforms/esp/32/ARCHITECTURE.md

3.10.321.2 KB
Original Source

ESP32 Platform Architecture

Overview

The ESP32 platform implementation supports 9+ ESP32 variants (ESP32 classic, S2, S3, C3, C6, H2, P4, etc.) with 6 different LED output driver types, all organized into a clean hierarchical structure.

Directory Structure

src/platforms/esp/32/
├── core/                        # Core platform infrastructure
│   ├── fastled_esp32.h          # Master aggregator (driver dispatcher)
│   ├── led_sysdefs_esp32.h      # System defines
│   ├── fastpin_esp32.h          # Pin mappings (all variants)
│   ├── fastspi_esp32.h          # Hardware SPI abstraction
│   ├── clock_cycles.h           # Timing utilities
│   ├── esp_log_control.h        # Logging control
│   ├── delay.h                  # Xtensa delay
│   ├── delay_riscv.h            # RISC-V delay
│   ├── delaycycles.h            # Xtensa cycle-accurate delay
│   └── delaycycles_riscv.h      # RISC-V cycle-accurate delay
│
├── drivers/                     # LED output drivers (organized by type)
│   ├── rmt/                     # RMT (Remote Control Module)
│   │   ├── clockless_rmt_esp32.h    # RMT dispatcher
│   │   ├── rmt_4/                   # ESP-IDF 4.x implementation (8 files)
│   │   └── rmt_5/                   # ESP-IDF 5.x implementation (13 files)
│   │
│   ├── i2s/                     # I2S parallel output
│   │   ├── clockless_i2s_esp32.h     # I2S driver
│   │   ├── clockless_i2s_esp32s3.*   # S3 variant
│   │   └── i2s_esp32dev.*            # I2S helpers
│   │
│   ├── lcd_cam/                 # LCD_CAM drivers (S3/P4 only)
│   │   ├── clockless_lcd_i80_esp32.*     # I80 interface
│   │   ├── clockless_lcd_rgb_esp32.*     # RGB interface
│   │   ├── lcd_driver_*.h                # Helper files (6 files)
│   │   └── implementation_notes.md       # Implementation docs
│   │
│   ├── parlio/                  # Parallel IO (P4 only)
│   │   ├── clockless_parlio_esp32p4.*    # PARLIO driver
│   │   └── parlio_driver*.h              # Helper files
│   │
│   ├── spi/                     # Hardware SPI (clocked LEDs)
│   │   ├── spi_hw_1_esp32.cpp        # 1-lane SPI
│   │   ├── spi_hw_2_esp32.cpp        # 2-lane SPI
│   │   ├── spi_hw_4_esp32.cpp        # 4-lane SPI
│   │   ├── spi_hw_8_esp32.cpp        # 8-lane SPI (P4 only)
│   │   ├── spi_platform_esp32.cpp    # Platform abstraction
│   │   ├── spi_output_template.h     # SPI output template
│   │   └── spi_device_proxy.h        # SPI device proxy
│   │
│   └── spi_ws2812/              # Clockless SPI (WS2812 over SPI)
│       ├── clockless_spi_esp32.h     # WS2812-over-SPI driver
│       └── strip_spi.*               # SPI strip implementation
│
├── interrupts/                  # Architecture-specific ISRs
│   ├── xtensa_lx6.hpp           # ESP32 classic
│   ├── xtensa_lx7.hpp           # ESP32-S2/S3
│   ├── riscv.hpp                # ESP32-C3/C6/H2/P4
│   ├── riscv.cpp                # RISC-V implementation
│   └── INVESTIGATE.md           # Research notes
│
├── audio/                       # Audio processing subsystem
├── ota/                         # OTA update support
│
└── [Root-level files]           # Platform abstraction
    ├── interrupt.h              # Interrupt control interface
    ├── interrupt.cpp            # Interrupt control implementation
    ├── isr_esp32.cpp            # ISR API implementation (IDF 5.0+)
    └── clockless_block_esp32.h  # Experimental (not in production)

Driver Dispatcher Pattern

The ESP32 platform uses a dispatcher pattern to select the appropriate LED output driver at compile time:

User Code (Arduino/Platform IO sketch)
    ↓
FastLED.h
    ↓
platforms/esp/32/core/fastled_esp32.h (Master Aggregator)
    ↓
    ├─→ drivers/rmt/clockless_rmt_esp32.h (Default driver)
    │       ↓
    │       ├─→ rmt_4/ (ESP-IDF 4.x - legacy)
    │       │     ├── idf4_clockless_rmt_esp32.h (ChannelManager-based)
    │       │     └── channel_engine_rmt4.h / channel_engine_rmt4.cpp
    │       │
    │       └─→ rmt_5/ (ESP-IDF 5.x - default, DMA-backed)
    │             ├── idf5_clockless.h (ChannelManager-based)
    │             ├── idf5_rmt.h / idf5_rmt.cpp
    │             ├── strip_rmt.h / strip_rmt.cpp
    │             └── rmt5_* worker pool implementation (7 files)
    │
    ├─→ drivers/i2s/clockless_i2s_esp32.h (Parallel output)
    │       ↓
    │       ├─→ clockless_i2s_esp32s3.h (ESP32-S3 variant)
    │       └─→ i2s_esp32dev.h (ESP32 classic)
    │
    ├─→ drivers/lcd_cam/clockless_lcd_i80_esp32.h (S3/P4 high-bandwidth)
    │       ↓
    │       └─→ lcd_driver_i80.h / lcd_driver_i80_impl.h
    │
    ├─→ drivers/lcd_cam/clockless_lcd_rgb_esp32.h (P4 RGB interface)
    │       ↓
    │       └─→ lcd_driver_rgb.h / lcd_driver_rgb_impl.h
    │
    ├─→ drivers/parlio/clockless_parlio_esp32p4.h (P4 only)
    │       ↓
    │       └─→ parlio_engine.h / parlio_engine.cpp.hpp
    │
    └─→ drivers/spi_ws2812/clockless_spi_esp32.h (WS2812 over SPI)
            ↓
            └─→ strip_spi.h / strip_spi.cpp

Target-Specific Code Organization

The ESP32 platform supports 9+ chip variants using inline conditional compilation rather than separate directories:

Why No Target Directories?

90% of code is target-agnostic - Most driver code works identically across all ESP32 variants. Only pin definitions, interrupt handlers, and peripheral availability differ.

Example: Pin mappings (core/fastpin_esp32.h):

cpp
#if CONFIG_IDF_TARGET_ESP32
    // ESP32 classic pin definitions
    #define NUM_DIGITAL_PINS 40
    #define FASTLED_HAS_CLOCKLESS 1
#elif CONFIG_IDF_TARGET_ESP32S3
    // ESP32-S3 pin definitions
    #define NUM_DIGITAL_PINS 49
    #define FASTLED_HAS_CLOCKLESS 1
    #define FASTLED_ESP32_S3_USB_JTAG_WORKAROUND 1  // S3-specific
#elif CONFIG_IDF_TARGET_ESP32C3
    // ESP32-C3 pin definitions (RISC-V)
    #define NUM_DIGITAL_PINS 22
    #define FASTLED_HAS_CLOCKLESS 1
    #define FASTLED_RISCV 1
// ... etc for 9+ targets
#endif

Architecture-Specific Code

Architectural differences (Xtensa vs RISC-V) are handled in two ways:

  1. Timing functions: Separate files for Xtensa and RISC-V

    • core/delay.h / core/delay_riscv.h
    • core/delaycycles.h / core/delaycycles_riscv.h
  2. Interrupt handlers: Dedicated interrupts/ directory

    • interrupts/xtensa_lx6.hpp - ESP32 classic (LX6 core)
    • interrupts/xtensa_lx7.hpp - ESP32-S2/S3 (LX7 core)
    • interrupts/riscv.hpp - ESP32-C3/C6/H2/P4 (RISC-V)

IDF Version Branching

The RMT driver has completely separate implementations for ESP-IDF 4.x vs 5.x due to incompatible APIs:

cpp
// drivers/rmt/clockless_rmt_esp32.h
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
    // ESP-IDF 4.x path
    #include "rmt_4/idf4_clockless_rmt_esp32.h"
#else
    // ESP-IDF 5.x path
    #include "rmt_5/idf5_clockless.h"  // ChannelManager-based
#endif

Why Separate rmt_4/ and rmt_5/?

Incompatible APIs: ESP-IDF 5.x completely redesigned the RMT peripheral API. The two implementations cannot coexist.

Different architectures:

  • RMT4: ISR-driven, manually refills RMT buffer during transmission
  • RMT5: DMA-backed, asynchronous transmission via led_strip driver
  • RMT5 V2: Worker pool pattern - reusable workers + ephemeral controllers

Cannot be merged: Attempting to unify them would create unmaintainable #ifdef spaghetti.

Driver Selection Logic

Compile-Time Selection

Drivers are selected via preprocessor macros, evaluated in core/fastled_esp32.h:

cpp
// Priority order (first match wins):

#if defined(FASTLED_ESP32_I2S) && FASTLED_ESP32_I2S
    #include "../drivers/i2s/clockless_i2s_esp32.h"  // I2S parallel

#elif defined(FASTLED_ESP32_LCD_I80) && FASTLED_ESP32_LCD_I80
    #include "../drivers/lcd_cam/clockless_lcd_i80_esp32.h"  // LCD I80 (S3/P4)

#elif defined(FASTLED_ESP32_LCD_RGB) && FASTLED_ESP32_LCD_RGB
    #include "../drivers/lcd_cam/clockless_lcd_rgb_esp32.h"  // LCD RGB (P4)

#elif defined(FASTLED_ESP32_PARLIO) && FASTLED_ESP32_PARLIO
    #include "../drivers/parlio/clockless_parlio_esp32p4.h"  // PARLIO (P4)

#elif defined(FASTLED_ESP32_HAS_CLOCKLESS_SPI)
    #include "../drivers/spi_ws2812/clockless_spi_esp32.h"  // Clockless SPI

#elif defined(FASTLED_ESP32_HAS_RMT)
    #include "../drivers/rmt/clockless_rmt_esp32.h"  // RMT (default)

#endif

Runtime Detection

Some features are detected at runtime:

  • SPI lane count: Auto-detected based on pin configuration (1/2/4/8-lane)
  • RMT channel availability: Queries hardware capabilities
  • DMA availability: Automatically enabled when supported

Hardware SPI vs Clockless SPI

The ESP32 platform has two completely different SPI subsystems:

Hardware SPI (drivers/spi/)

Purpose: Drive clocked LED chipsets (APA102, SK9822, LPD8806, etc.)

Key features:

  • Uses physical SPI clock line (SCK/SCLK)
  • DMA-backed parallel transmission
  • Multi-lane support: 1/2/4/8-lane configurations
  • Zero CPU usage during transmission
  • ~40× faster than software bitbang

Files:

  • spi_hw_1_esp32.cpp - 1-lane SPI
  • spi_hw_2_esp32.cpp - 2-lane SPI (dual)
  • spi_hw_4_esp32.cpp - 4-lane SPI (quad)
  • spi_hw_8_esp32.cpp - 8-lane SPI (octal, ESP32-P4 only)
  • spi_platform_esp32.cpp - Platform abstraction
  • spi_output_template.h - Template for SPI output
  • spi_device_proxy.h - SPI device proxy

When to use: LEDs with clock lines (APA102, SK9822, LPD8806, etc.)

Clockless SPI (drivers/spi_ws2812/)

Purpose: Emulate WS2812 timing protocol using SPI peripheral as a timing hack

Key features:

  • SPI clock hidden (not connected externally)
  • Data line creates precisely-timed pulses matching WS2812 protocol
  • Only supports WS2811/WS2812/WS2812B timing
  • Cannot support other clockless LEDs (SK6812, APA106, etc.)

Files:

  • clockless_spi_esp32.h - WS2812-over-SPI driver
  • strip_spi.h / strip_spi.cpp - SPI strip implementation

When to use: WS2812-style LEDs when RMT channels are exhausted

Limitations: Fixed timing (cannot support SK6812, APA106, etc. due to SPI constraints)

Key Design Decisions

1. No Target Directories

Decision: Use inline #ifdef CONFIG_IDF_TARGET_* within files rather than separate directories per target.

Rationale:

  • 90%+ of code is target-agnostic
  • Pin mappings are the primary difference
  • Reduces code duplication
  • Easier to maintain cross-target features

When might this change?: If target-specific code exceeds 100 lines per file, consider extracting to target-specific files.

2. No Environment Directories (Arduino vs ESP-IDF)

Decision: Handle Arduino vs ESP-IDF differences inline with minimal #ifdef ARDUINO.

Rationale:

  • Differences are typically <10 lines per file (e.g., #include <Arduino.h> vs #include "esp_system.h")
  • Arduino-ESP32 framework uses ESP-IDF underneath, so most APIs are identical
  • Separate directories would duplicate 95% of code

When might this change?: If Arduino-specific code becomes substantial (e.g., >50 lines per file).

3. IDF Version Separation (rmt_4/ vs rmt_5/)

Decision: Completely separate implementations for ESP-IDF 4.x and 5.x.

Rationale:

  • ESP-IDF 5.x completely redesigned RMT API (incompatible with 4.x)
  • RMT4 uses manual ISR refill, RMT5 uses DMA + led_strip driver
  • Cannot coexist in the same binary (Espressif runtime abort)
  • Merging would create unmaintainable #ifdef complexity

Not subject to change: This separation is permanent due to API incompatibility.

4. Worker Pool Pattern (RMT5 V2)

Decision: RMT5 V2 uses persistent worker pool + ephemeral controllers.

Rationale:

  • Solves N > K problem (more LED strips than RMT channels)
  • Workers are reused across FastLED.show() calls
  • Controllers are lightweight, created on-demand
  • Eliminates channel exhaustion issues

Files:

  • rmt5_worker_base.h - Base worker interface
  • rmt5_worker.h / rmt5_worker.cpp - Persistent worker
  • rmt5_worker_oneshot.h / rmt5_worker_oneshot.cpp - One-shot worker
  • rmt5_worker_pool.h / rmt5_worker_pool.cpp - Worker pool manager
  • rmt5_controller_lowlevel.h / rmt5_controller_lowlevel.cpp - Controller

5. Logging Control

Decision: Lightweight logging via core/esp_log_control.h to avoid heavy vfprintf dependency.

Rationale:

  • ESP-IDF's esp_log pulls in 10-20KB of printf/vfprintf code
  • Many sketches don't need logging (constrained flash/RAM)
  • Conditional logging: enabled only when SKETCH_HAS_LOTS_OF_MEMORY is true
  • Minimal error handling: FASTLED_ESP32_MINIMAL_ERROR_HANDLING skips logging entirely

6. Interrupt Handlers in Separate Directory

Decision: Move architecture-specific interrupt handlers to interrupts/ directory.

Rationale:

  • Clear separation between Xtensa LX6, LX7, and RISC-V implementations
  • Interrupt handling is orthogonal to driver selection
  • Easier to add new architectures (e.g., future Xtensa LX8)

Driver Characteristics

RMT (Remote Control Module)

  • Supported targets: All ESP32 variants
  • Max strips: 1-8 per channel (depends on variant)
  • Timing precision: Excellent (hardware-timed)
  • DMA: IDF5 only
  • Async: IDF5 only
  • Best for: General-purpose LED control, mixed timing requirements

I2S (Parallel Output)

  • Supported targets: ESP32 classic, ESP32-S3
  • Max strips: Up to 24 simultaneously
  • Timing precision: Excellent (hardware-timed)
  • DMA: Yes
  • Async: Yes
  • Constraint: All strips must use identical timing
  • Best for: High strip count with uniform chipsets (e.g., 20× WS2812 strips)

LCD_CAM (I80/RGB)

The LCD_CAM driver family includes two variants using different peripherals:

LCD_I80 (ESP32-S3/P4)

  • Supported targets: ESP32-S3, ESP32-P4
  • Peripheral: LCD_CAM in I80 mode (Intel 8080 parallel interface)
  • Max strips: Up to 16 parallel data lines
  • Encoding: 3-word per bit (6 bytes/bit, 144 bytes/LED)
  • PCLK Range: 1-80 MHz
  • Control Signals: CS, DC
  • GPIO Constraints: Flexible (avoid GPIO0, GPIO45, GPIO46 strapping pins)
  • Timing precision: Excellent (hardware-timed)
  • DMA: Yes
  • Async: Yes
  • Testing Status: ✅ Production-tested on ESP32-S3
  • Best for: High-bandwidth parallel output on S3/P4

LCD_RGB (ESP32-P4 only)

  • Supported targets: ESP32-P4 only
  • Peripheral: RGB LCD controller (dedicated RGB parallel display peripheral)
  • Max strips: Up to 16 parallel data lines
  • Encoding: 4-pixel per bit (8 bytes/bit, 192 bytes/LED)
  • PCLK Range: 1-40 MHz (optimal: 3.2 MHz for WS2812B)
  • Control Signals: HSYNC, VSYNC, DE, DISP
  • GPIO Constraints: STRICT - GPIO39-54 only (16 pins available)
    • ❌ Rejects GPIO34-38 (strapping pins - cause boot failures)
    • ❌ Rejects GPIO24-25 (USB-JTAG - prevents debugging)
  • Timing precision: Excellent (hardware-timed)
  • DMA: Yes
  • Async: Yes
  • Testing Status: ⚠️ EXPERIMENTAL - Code complete, untested on hardware
  • Memory Usage: 33% higher than LCD_I80 (PSRAM recommended for >240 LEDs)
  • Reset Gap: VSYNC front porch timing (≥50µs between frames)
  • DMA Callback: VSYNC interrupt-driven synchronization
  • Best for: ESP32-P4 parallel output (when hardware becomes available)
  • Documentation: See LCD_I80_vs_RGB_MIGRATION_GUIDE.md for migration details

PARLIO (Parallel IO)

  • Supported targets: ESP32-P4 only
  • Max strips: Parallel output (up to 16 data lines)
  • Timing precision: Excellent (hardware-timed)
  • DMA: Yes
  • Async: Yes
  • Best for: ESP32-P4 high-performance parallel output

Hardware SPI

  • Supported targets: All ESP32 variants (8-lane requires ESP32-P4)
  • Max strips: 1/2/4/8-lane configurations
  • Timing precision: N/A (clocked LEDs use external clock)
  • DMA: Yes
  • Async: Yes
  • Best for: Clocked LEDs (APA102, SK9822, LPD8806) with parallel strips

Clockless SPI (WS2812 over SPI)

  • Supported targets: All ESP32 variants
  • Max strips: Limited (typically 1-2)
  • Timing precision: Good (SPI peripheral timing)
  • DMA: Partial
  • Async: No
  • Constraint: Only WS2811/WS2812 timing supported
  • Best for: WS2812 LEDs when RMT channels are exhausted

File Count Summary

  • Core files: 10 files
  • Driver files: 52 files
    • RMT: 21 files (1 dispatcher + 8 rmt_4 + 12 rmt_5)
    • I2S: 5 files
    • LCD_CAM: 12 files
    • PARLIO: 4 files
    • Hardware SPI: 7 files
    • Clockless SPI: 3 files
  • Interrupt handlers: 5 files
  • Root-level files: 4 files (platform abstraction)
  • Audio/OTA: Pre-existing subdirectories

Total: ~71 files organized into logical hierarchy

Include Path Examples

From User Code (Arduino sketch)

cpp
#include <FastLED.h>  // Top-level include (finds ESP32 automatically)

From Core Aggregator (core/fastled_esp32.h)

cpp
#include "../drivers/rmt/clockless_rmt_esp32.h"  // RMT dispatcher
#include "../drivers/i2s/clockless_i2s_esp32.h"  // I2S driver
#include "../drivers/spi_ws2812/clockless_spi_esp32.h"  // Clockless SPI

From Driver Files (e.g., drivers/rmt/clockless_rmt_esp32.h)

cpp
#include "rmt_4/idf4_clockless_rmt_esp32.h"  // Relative (same parent)
#include "rmt_5/idf5_clockless.h"  // Relative (same parent)

From Global Headers (e.g., src/fastspi.h)

cpp
#include "platforms/esp/32/drivers/spi/spi_device_proxy.h"  // Absolute from src/
#include "platforms/esp/32/drivers/spi/spi_output_template.h"

Backward Compatibility

Old Includes (Before Refactoring)

cpp
#include "platforms/esp/32/fastled_esp32.h"  // ❌ Old path
#include "platforms/esp/32/clockless_rmt_esp32.h"  // ❌ Old path

New Includes (After Refactoring)

cpp
#include "platforms/esp/32/core/fastled_esp32.h"  // ✅ New path
#include "platforms/esp/32/drivers/rmt/clockless_rmt_esp32.h"  // ✅ New path

Migration: All includes were updated during the refactoring. User code remains unchanged (uses #include <FastLED.h>).

Testing & Validation

Compilation Validation

  • ESP32 classic: ✅ Compiles (RMT default)
  • ESP32-S3: ✅ Compiles (RMT default, I2S available, LCD_CAM available)
  • ESP32-C3: ✅ Compiles (RMT default, RISC-V)
  • ESP32-P4: ⚠️ Limited (PARLIO, 8-lane SPI, LCD RGB)

Test Coverage

  • C++ unit tests: All tests pass (driver logic validated)
  • Compilation tests: Multi-platform validation (ESP32, S3, C3)
  • QEMU tests: Runtime validation on ESP32 emulator

Known Limitations

  • ESP32-P4 support limited (hardware not widely available yet)
  • QEMU test infrastructure has path issues (merged.bin location)
  • Some advanced drivers (LCD_CAM, PARLIO) not fully tested in CI

Future Enhancements

Potential Improvements

  1. Common driver utilities: Extract shared code to drivers/common/
  2. Target directories: Only if target-specific code exceeds 100 lines per file
  3. Environment separation: Only if Arduino-specific code becomes substantial
  4. Enhanced testing: Expand QEMU coverage to all drivers

Not Planned

  • Merging rmt_4 and rmt_5: Incompatible APIs, intentionally separate
  • Single-file drivers: Current organization is already maintainable
  • Removing architecture-specific files: Necessary for Xtensa vs RISC-V differences

References

  • ESP32 README: src/platforms/esp/32/README.md - Driver selection, build flags
  • RMT5 Research: src/platforms/esp/32/drivers/rmt/rmt_5/RMT_RESEARCH.md - RMT5 internals
  • LCD Implementation: src/platforms/esp/32/drivers/lcd_cam/implementation_notes.md - LCD_CAM details
  • LCD_RGB Research: src/platforms/esp/32/drivers/lcd_cam/RGB_LCD_RESEARCH.md - RGB LCD peripheral analysis
  • LCD Migration Guide: src/platforms/esp/32/drivers/lcd_cam/LCD_I80_vs_RGB_MIGRATION_GUIDE.md - I80→RGB migration
  • LCD_RGB Release Notes: src/platforms/esp/32/drivers/lcd_cam/LCD_RGB_RELEASE_NOTES.md - Feature announcement
  • PARLIO Design: src/platforms/esp/32/ESP32-P4_PARLIO_DESIGN.md - PARLIO architecture
  • Interrupt Research: src/platforms/esp/32/interrupts/INVESTIGATE.md - ISR implementation notes

Summary

The ESP32 platform architecture uses a hierarchical structure with:

  • Clear separation: Core vs drivers vs interrupt handlers
  • Conditional compilation: Target and IDF version branching
  • Driver diversity: 6 different driver types for different use cases
  • Backward compatibility: Zero functional changes, pure refactoring
  • Maintainability: Easy to find, understand, and modify driver code

This organization makes it straightforward for developers to:

  • Find driver implementations quickly
  • Understand driver selection process
  • Add new drivers or modify existing ones
  • Navigate the codebase efficiently
  • Contribute to the project

Version: 1.1 Last Updated: 2025-11-05 Status: Complete (Post-Refactoring + LCD_RGB Documentation)