Back to Fastled

ESP32 NMI Integration Example

src/platforms/esp/32/interrupts/NMI_INTEGRATION_EXAMPLE.md

3.10.39.9 KB
Original Source

ESP32 NMI Integration Example

Overview

This document provides complete examples showing how to integrate Level 7 NMI (Non-Maskable Interrupt) handling for RMT buffer refill operations on ESP32 Xtensa platforms (ESP32, ESP32-S2, ESP32-S3).

Prerequisites

Before using Level 7 NMI, ensure you understand:

  1. NMI Restrictions: Cannot call FreeRTOS APIs, cannot access flash memory, must use DRAM
  2. Hardware Limitation: Only ONE Level 7 NMI can be active per CPU core
  3. Risk Level: NMI handlers are higher risk than standard interrupts
  4. When to Use: Only when Level 3 interrupts are insufficient (e.g., WiFi interference with LED timing)

Complete Integration Example

Step 1: Include Required Headers

cpp
#include "platforms/esp/32/interrupts/ASM_2_C_SHIM.h"  // NMI assembly shim macros
#include "platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.h"  // RMT worker
#include "esp_intr_alloc.h"  // ESP-IDF interrupt allocation

Step 2: Declare NMI Handler (Static Approach)

The static approach uses compile-time known function names for best performance (~65ns overhead).

cpp
// Use the existing NMI-safe wrapper function
// This function is already defined in rmt5_worker.cpp:
//   extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void);

// Generate the assembly NMI shim
// This macro creates an assembly handler named "xt_nmi" that calls rmt5_nmi_buffer_refill
FASTLED_NMI_ASM_SHIM_STATIC(xt_nmi, rmt5_nmi_buffer_refill)

What this does:

  • Creates an assembly function xt_nmi placed in IRAM (.iram1.text section)
  • On NMI entry, saves all 16 Xtensa registers (a0-a15)
  • Calls rmt5_nmi_buffer_refill() using Call0 ABI
  • Restores all registers
  • Returns via rfi 7 (Return From Interrupt level 7)

Step 3: Initialize RMT Worker and Global Pointer

cpp
#include "platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.h"

// External global pointer (defined in rmt5_worker.cpp)
extern fl::RmtWorker* DRAM_ATTR g_rmt5_nmi_worker;

void setup() {
    // Create RMT worker instance
    fl::RmtWorker* worker = new fl::RmtWorker(
        RMT_CHANNEL_0,      // RMT channel
        GPIO_NUM_2,         // Output GPIO
        1000                // Buffer size
    );

    // CRITICAL: Set global pointer BEFORE enabling NMI
    // The NMI handler will dereference this pointer
    g_rmt5_nmi_worker = worker;

    // Initialize worker (allocates buffers, configures RMT)
    worker->begin();
}

Step 4: Allocate Level 7 NMI

cpp
void allocate_nmi() {
    esp_intr_handle_t nmi_handle = nullptr;

    // Allocate Level 7 NMI for RMT peripheral
    esp_err_t err = esp_intr_alloc(
        ETS_RMT_INTR_SOURCE,                      // Interrupt source
        ESP_INTR_FLAG_LEVEL7 | ESP_INTR_FLAG_IRAM, // Flags: Level 7 + IRAM
        nullptr,                                   // Handler (nullptr for Level 7)
        nullptr,                                   // Arg (nullptr for Level 7)
        &nmi_handle                                // Output handle
    );

    if (err != ESP_OK) {
        Serial.printf("Failed to allocate NMI: %d\n", err);
        return;
    }

    Serial.println("✓ Level 7 NMI allocated successfully");
    Serial.println("  Handler: xt_nmi → rmt5_nmi_buffer_refill → RmtWorker::fillNextHalf");
}

Key Points:

  • ESP_INTR_FLAG_LEVEL7: Requests Level 7 interrupt (NMI)
  • ESP_INTR_FLAG_IRAM: Ensures handler code is in IRAM
  • handler=nullptr: ESP-IDF looks for xt_nmi symbol at link time
  • arg=nullptr: No argument passing for Level 7

Step 5: Complete Arduino Example

cpp
#include <Arduino.h>
#include "FastLED.h"
#include "platforms/esp/32/interrupts/ASM_2_C_SHIM.h"
#include "platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.h"
#include "esp_intr_alloc.h"

// External global pointer
extern fl::RmtWorker* DRAM_ATTR g_rmt5_nmi_worker;

// Generate NMI handler
FASTLED_NMI_ASM_SHIM_STATIC(xt_nmi, rmt5_nmi_buffer_refill)

// Configuration
#define LED_PIN 2
#define NUM_LEDS 100

CRGB leds[NUM_LEDS];
fl::RmtWorker* g_worker = nullptr;
esp_intr_handle_t g_nmi_handle = nullptr;

void setup() {
    Serial.begin(115200);
    delay(1000);

    Serial.println("FastLED with Level 7 NMI Example");
    Serial.println("==================================");

    // Initialize FastLED
    FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);

    // Create RMT worker
    g_worker = new fl::RmtWorker(RMT_CHANNEL_0, GPIO_NUM_2, NUM_LEDS * 3);
    g_worker->begin();

    // Set global pointer for NMI handler
    g_rmt5_nmi_worker = g_worker;

    // Allocate Level 7 NMI
    esp_err_t err = esp_intr_alloc(
        ETS_RMT_INTR_SOURCE,
        ESP_INTR_FLAG_LEVEL7 | ESP_INTR_FLAG_IRAM,
        nullptr, nullptr,
        &g_nmi_handle
    );

    if (err == ESP_OK) {
        Serial.println("✓ Level 7 NMI enabled");
    } else {
        Serial.printf("✗ NMI allocation failed: %d\n", err);
    }

    // Fill LEDs with rainbow
    fill_rainbow(leds, NUM_LEDS, 0, 255 / NUM_LEDS);
    FastLED.show();
}

void loop() {
    // Cycle rainbow
    static uint8_t hue = 0;
    fill_rainbow(leds, NUM_LEDS, hue++, 255 / NUM_LEDS);
    FastLED.show();
    delay(20);
}

Alternative: Dynamic Function Pointer Approach

For scenarios requiring runtime handler changes:

Step 1: Declare Function Pointer

cpp
// Define function pointer type
typedef void (*nmi_handler_t)(void);

// Declare global function pointer in DRAM
nmi_handler_t DRAM_ATTR g_nmi_handler = nullptr;

Step 2: Generate Dynamic Shim

cpp
// Generate dynamic NMI handler
FASTLED_NMI_ASM_SHIM_DYNAMIC(xt_nmi, g_nmi_handler)

Step 3: Set Handler at Runtime

cpp
void setup() {
    // Initialize worker
    g_worker = new fl::RmtWorker(RMT_CHANNEL_0, GPIO_NUM_2, 1000);
    g_rmt5_nmi_worker = g_worker;

    // Set function pointer to handler
    g_nmi_handler = &rmt5_nmi_buffer_refill;

    // Allocate NMI (same as static example)
    esp_intr_alloc(ETS_RMT_INTR_SOURCE, ESP_INTR_FLAG_LEVEL7 | ESP_INTR_FLAG_IRAM,
                   nullptr, nullptr, &nmi_handle);
}

void switch_handler() {
    // Disable NMI first
    esp_intr_disable(nmi_handle);

    // Change handler
    g_nmi_handler = &alternate_nmi_handler;

    // Re-enable NMI
    esp_intr_enable(nmi_handle);
}

Performance Trade-off:

  • Static: ~65ns overhead
  • Dynamic: ~73ns overhead (+8ns for pointer load)

Safety Checklist

Before deploying Level 7 NMI in production:

Code Requirements

  • All functions in call chain marked IRAM_ATTR
  • No FreeRTOS API calls anywhere in path
  • All global variables marked DRAM_ATTR
  • C handler marked extern "C" for linkage
  • Null pointer check in wrapper function

Testing Requirements

  • Compilation test passes (test_nmi_asm_shim_compile)
  • Disassembly shows correct register save/restore
  • NMI handler executes without crashing
  • LED timing maintained under WiFi load
  • System stable after 1000+ NMI triggers
  • Timing measured with logic analyzer

Documentation Requirements

  • Design decisions documented (DESIGN_DECISIONS.md)
  • Architecture validated (ARCHITECTURE_FINDINGS.md)
  • Integration example provided (this file)
  • Known limitations documented

Troubleshooting

Problem: System crashes when NMI fires

Possible Causes:

  1. Function not marked IRAM_ATTR → Flash cache miss
  2. FreeRTOS API called → Deadlock or corruption
  3. Global pointer is nullptr → Null dereference
  4. Stack overflow → Insufficient stack space

Solution:

  1. Grep for all functions in call chain, verify IRAM_ATTR
  2. Grep for FreeRTOS APIs (portENTER_CRITICAL, xSemaphore, etc.)
  3. Add null check in wrapper: if (worker == nullptr) return;
  4. Increase stack size or reduce local variable usage

Problem: LEDs still glitch under WiFi load

Possible Causes:

  1. NMI not actually allocated (check return code)
  2. Wrong interrupt source (should be ETS_RMT_INTR_SOURCE)
  3. Buffer refill too slow (>1.25µs)
  4. RMT hardware issue

Solution:

  1. Check esp_intr_alloc() returns ESP_OK
  2. Verify interrupt source matches RMT channel
  3. Profile fillNextHalf() with GPIO timing
  4. Test on different ESP32 module

Problem: Compilation errors with macros

Possible Causes:

  1. ESP32 not defined (host platform compilation)
  2. Wrong architecture (RISC-V not supported)
  3. Missing includes
  4. Syntax error in macro usage

Solution:

  1. Wrap code in #ifdef ESP32 for platform-specific tests
  2. Only use on ESP32/ESP32-S2/ESP32-S3 (Xtensa platforms)
  3. Include ASM_2_C_SHIM.h before using macros
  4. Verify macro syntax matches examples

Performance Characteristics

Measured Timing (ESP32 at 240 MHz)

OperationCyclesTime (ns)Notes
Assembly prologue16 s32i~30nsRegister saves
call0 instruction1~4nsDirect call
rmt5_nmi_buffer_refill5~20nsWrapper overhead
RmtWorker::fillNextHalf120~500nsBuffer refill logic
Assembly epilogue16 l32i~30nsRegister restores
rfi 7 instruction1~4nsReturn from NMI
Total NMI Latency~159~588nsWithin WS2812 spec

WS2812 Timing Requirements:

  • High pulse: 700ns ± 150ns
  • Low pulse: 600ns ± 150ns
  • Reset: >50µs
  • Buffer refill must complete in <1.25µs
  • Design Decisions: DESIGN_DECISIONS.md - Rationale for all implementation choices
  • Architecture Findings: ARCHITECTURE_FINDINGS.md - Xtensa ISA research and validation
  • ASM Shim Header: ASM_2_C_SHIM.h - Macro implementations with inline docs
  • RMT5 Worker: rmt5_worker.cpp - NMI-safe wrapper function
  • Main Loop Guide: LOOP.md - Agent iteration planning and status

License

This integration example is part of the FastLED project and follows the same license.


Last Updated: 2025-11-06 Status: Phase 3 Testing - Integration examples complete Validated On: ESP32 (LX6), ESP32-S3 (LX7) pending hardware testing