src/platforms/esp/32/interrupts/NMI_SAFETY_VERIFICATION.md
Date: 2025-11-06 Phase: Iteration 3 - Implementation Testing and Validation Status: ✅ ALL SAFETY CHECKS PASSED
Complete safety verification of the ESP32 Level 7 NMI call chain for RMT buffer refill operations. All critical requirements validated:
Risk Level: LOW - All safety requirements met
Hardware NMI Trigger
↓
xt_nmi (assembly shim) [IRAM, generated by macro]
↓
rmt5_nmi_buffer_refill() [IRAM_ATTR, extern "C"]
↓
RmtWorker::fillNextHalf() [IRAM_ATTR, class method]
↓
RmtWorker::convertByteToRmt() [IRAM_ATTR, inline]
↓
ets_printf() (optional logging) [ROM function, ISR-safe]
All code in the NMI call chain MUST be in IRAM (not flash) to avoid cache misses that would cause system crashes.
xt_nmiLocation: Generated by FASTLED_NMI_ASM_SHIM_STATIC macro
File: src/platforms/esp/32/interrupts/ASM_2_C_SHIM.h:185-266
Section Placement: .section .iram1.text (line 188)
Status: ✅ VERIFIED - Placed in IRAM via assembly directive
Evidence:
#define FASTLED_NMI_ASM_SHIM_STATIC(handler_name, c_function) \
__asm__ ( \
".section .iram1.text\n" \ // ← IRAM placement
".global " #handler_name "\n" \
...
rmt5_nmi_buffer_refillLocation: src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.cpp:118
Attribute: extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void)
Status: ✅ VERIFIED - Marked IRAM_ATTR
Evidence:
extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void) {
fl::RmtWorker* worker = g_rmt5_nmi_worker;
if (worker == nullptr) return;
worker->fillNextHalf();
}
Inspection:
IRAM_ATTRextern "C" linkagefillNextHalf()RmtWorker::fillNextHalfLocation: src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.cpp:514
Attribute: void IRAM_ATTR RmtWorker::fillNextHalf()
Status: ✅ VERIFIED - Marked IRAM_ATTR
Evidence:
void IRAM_ATTR RmtWorker::fillNextHalf() {
volatile rmt_item32_t* pItem = mRMT_mem_ptr;
// ... buffer refill logic ...
convertByteToRmt(mPixelData[mCur], pItem);
}
Inspection:
IRAM_ATTRconvertByteToRmt() (also IRAM_ATTR)ets_printf() for logging (ROM function, safe)RmtWorker::convertByteToRmtLocation: src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.cpp:481
Attribute: FASTLED_FORCE_INLINE void IRAM_ATTR RmtWorker::convertByteToRmt(...)
Status: ✅ VERIFIED - Marked IRAM_ATTR + inline
Evidence:
FASTLED_FORCE_INLINE void IRAM_ATTR RmtWorker::convertByteToRmt(
uint8_t byte_val,
volatile rmt_item32_t* out
) {
// Pure computation, no calls
uint32_t zero_val = *reinterpret_cast<uint32_t*>(&mZero);
uint32_t one_val = *reinterpret_cast<uint32_t*>(&mOne);
// ... bit manipulation ...
}
Inspection:
IRAM_ATTRFASTLED_FORCE_INLINE (likely inlined away)| Function | Line | IRAM_ATTR | Status |
|---|---|---|---|
| xt_nmi (asm) | ASM_2_C_SHIM.h:188 | .iram1.text | ✅ |
| rmt5_nmi_buffer_refill | rmt5_worker.cpp:118 | Yes | ✅ |
| RmtWorker::fillNextHalf | rmt5_worker.cpp:514 | Yes | ✅ |
| RmtWorker::convertByteToRmt | rmt5_worker.cpp:481 | Yes | ✅ |
Conclusion: ✅ ALL FUNCTIONS IN IRAM
NMI handlers CANNOT call FreeRTOS APIs (spinlocks, semaphores, queues, tasks) because:
Command:
grep -n "portENTER_CRITICAL\|portEXIT_CRITICAL\|xSemaphore\|xQueue\|xTask\|vTask" \
src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.cpp
Results:
63: * FreeRTOS APIs (including portENTER_CRITICAL_ISR). This global pointer
84: * - No FreeRTOS API calls (no portENTER_CRITICAL_ISR, no xSemaphore*, etc.)
169: mCompletionSemaphore = xSemaphoreCreateBinary();
462: xSemaphoreTake(mCompletionSemaphore, portMAX_DELAY);
684: portENTER_CRITICAL_ISR(mPoolSpinlock); ← handleThresholdInterrupt()
693: portEXIT_CRITICAL_ISR(mPoolSpinlock); ← handleThresholdInterrupt()
709: xSemaphoreGiveFromISR(mCompletionSemaphore, &xHigherPriorityTaskWoken);
| Function | Line Range | FreeRTOS Calls | In NMI Path? |
|---|---|---|---|
| rmt5_nmi_buffer_refill | 118-139 | NONE | ✅ YES |
| fillNextHalf | 514-556 | NONE | ✅ YES |
| convertByteToRmt | 481-508 | NONE | ✅ YES |
| handleThresholdInterrupt | 683-694 | portENTER/EXIT_CRITICAL_ISR | ❌ NO (OLD path) |
| handleDoneInterrupt | 700-718 | xSemaphoreGiveFromISR | ❌ NO (completion) |
| transmit (constructor) | 169 | xSemaphoreCreateBinary | ❌ NO (setup) |
| transmit (blocking) | 462 | xSemaphoreTake | ❌ NO (main thread) |
Key Finding: FreeRTOS APIs exist in the codebase but are NOT in the NMI call chain.
OLD Path (Level 3, FreeRTOS-safe):
handleThresholdInterrupt() [Has portENTER_CRITICAL_ISR]
↓
fillNextHalf() [No FreeRTOS]
NEW Path (Level 7, NMI-safe):
rmt5_nmi_buffer_refill() [No FreeRTOS]
↓
fillNextHalf() [No FreeRTOS]
Design Decision: Created new wrapper rmt5_nmi_buffer_refill() that calls fillNextHalf() directly, bypassing handleThresholdInterrupt() which has the spinlock.
Consequence: No spinlock protection in NMI path
volatile (visibility guaranteed)| Requirement | Status | Evidence |
|---|---|---|
| No portENTER_CRITICAL in NMI path | ✅ PASS | Lines 118-139, 514-556 have no calls |
| No xSemaphore* in NMI path | ✅ PASS | Semaphores only in setup/completion |
| No xQueue* in NMI path | ✅ PASS | No queue operations anywhere |
| No xTask* in NMI path | ✅ PASS | No task operations in ISR code |
Conclusion: ✅ ZERO FREERTOS CALLS IN NMI PATH
All global variables accessed from NMI MUST be in DRAM (not flash) to avoid cache misses.
g_rmt5_nmi_workerLocation: src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.cpp:73
Declaration: fl::RmtWorker* DRAM_ATTR g_rmt5_nmi_worker = nullptr;
Status: ✅ VERIFIED - Marked DRAM_ATTR
Usage:
extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void) {
fl::RmtWorker* worker = g_rmt5_nmi_worker; // ← Read from DRAM
if (worker == nullptr) return;
worker->fillNextHalf();
}
Access Pattern:
Access: Via pointer worker->member
Storage: Member variables are in worker instance (heap or static)
Status: ✅ SAFE - Accessed via DRAM pointer
Key Members Accessed:
class RmtWorker {
// All accessed members are volatile for cross-thread visibility
volatile rmt_item32_t* mRMT_mem_ptr; // Hardware register pointer
volatile uint8_t mWhichHalf; // Buffer half index
volatile uint32_t mCur; // Current byte index
uint32_t mNumBytes; // Total bytes (read-only after setup)
uint8_t* mPixelData; // Pixel buffer pointer (DRAM)
// ...
};
Volatile Analysis:
mRMT_mem_ptr is volatile (hardware register mapping)mWhichHalf is volatile (shared state)mCur is volatile (shared state)mPixelData points to DRAM buffer (user-provided)Note: Worker instance itself doesn't need DRAM_ATTR because it's accessed via a DRAM pointer (g_rmt5_nmi_worker), and member access translates to *(worker_ptr + offset) which is always a DRAM access.
Access: RMT.conf_ch[x].conf1.mem_rd_rst (ESP-IDF HAL)
Storage: Memory-mapped I/O (always accessible from NMI)
Status: ✅ SAFE - Hardware registers are always accessible
Evidence: Hardware registers are mapped to fixed memory addresses:
0x3FF56000 (peripheral DRAM region)| Variable | Type | DRAM_ATTR | Usage | Status |
|---|---|---|---|---|
| g_rmt5_nmi_worker | Pointer | Yes | Read in NMI | ✅ |
| mRMT_mem_ptr | volatile ptr | N/A | Via worker ptr | ✅ |
| mWhichHalf | volatile u8 | N/A | Via worker ptr | ✅ |
| mCur | volatile u32 | N/A | Via worker ptr | ✅ |
| mPixelData | buffer ptr | N/A | Points to DRAM | ✅ |
| RMT.* | HW registers | N/A | Memory-mapped | ✅ |
Conclusion: ✅ ALL DATA IN DRAM OR MMIO
Variables shared between NMI and main thread MUST be volatile to ensure:
File: src/platforms/esp/32/drivers/rmt/rmt_5/rmt5_worker.h
class RmtWorker {
private:
// Buffer management (shared with NMI)
volatile rmt_item32_t* mRMT_mem_ptr; // Line 137
volatile uint8_t mWhichHalf; // Line 138
volatile uint32_t mCur; // Line 139
volatile uint32_t mThresholdIsrCount; // Line 140
// Transmission state (atomic operations)
fl::atomic<bool> mTransmitting; // Line 142 (atomic, stronger than volatile)
// Constants (no volatility needed, read-only after init)
uint32_t mNumBytes;
uint8_t* mPixelData;
};
void IRAM_ATTR RmtWorker::fillNextHalf() {
volatile rmt_item32_t* pItem = mRMT_mem_ptr; // Volatile read
uint8_t currentHalf = mWhichHalf; // Volatile read
// ... processing ...
mWhichHalf = nextHalf; // Volatile write
mRMT_mem_ptr = pItem; // Volatile write
}
Analysis:
mRMT_mem_ptr accessed as volatile (pointer to volatile)mWhichHalf declared volatilemCur declared volatileCorrectness: Line 532 uses mCur = mCur + 1 instead of mCur++ to avoid deprecated volatile increment.
| Variable | Volatile | Shared NMI<->Main | Status |
|---|---|---|---|
| mRMT_mem_ptr | Yes | Yes | ✅ |
| mWhichHalf | Yes | Yes | ✅ |
| mCur | Yes | Yes | ✅ |
| mThresholdIsrCount | Yes | Yes (stats) | ✅ |
| mTransmitting | Atomic | Yes | ✅ (stronger) |
| mNumBytes | No | No (read-only) | ✅ |
| mPixelData | No | No (read-only) | ✅ |
Conclusion: ✅ ALL SHARED VARIABLES PROPERLY MARKED
C handler MUST use extern "C" linkage for assembly shim to call it correctly (no C++ name mangling).
Declaration: rmt5_worker.cpp:118
extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void) {
// ...
}
Symbol Name:
extern "C": Symbol is rmt5_nmi_buffer_refillextern "C": Symbol would be mangled like _Z22rmt5_nmi_buffer_refillvAssembly Shim Usage:
FASTLED_NMI_ASM_SHIM_STATIC(xt_nmi, rmt5_nmi_buffer_refill)
// Expands to: call0 rmt5_nmi_buffer_refill
Verification: Assembly directly references rmt5_nmi_buffer_refill symbol. This would fail to link if C++ name mangling was used.
Conclusion: ✅ extern "C" LINKAGE CORRECT
Implementation:
extern "C" void IRAM_ATTR rmt5_nmi_buffer_refill(void) {
fl::RmtWorker* worker = g_rmt5_nmi_worker;
if (worker == nullptr) {
return; // ← Early return prevents crash
}
worker->fillNextHalf();
}
Status: ✅ NULL CHECK PRESENT
Analysis:
Estimated Stack Usage:
ESP32 ISR Stack: Typically 8192 bytes (configurable)
Conclusion: ✅ STACK USAGE SAFE (~1.8% of default ISR stack)
Question: Can NMI interrupt itself?
Answer: No. Level 7 interrupts mask all lower interrupts AND themselves.
ESP32 Behavior:
PS.INTLEVEL is set to 7Conclusion: ✅ NO RE-ENTRANCY RISK
Usage: ets_printf() in fillNextHalf()
Safety Analysis:
ets_printf() is a ROM function (always in "RAM")Conclusion: ✅ LOGGING IS ISR-SAFE
| Safety Requirement | Status | Evidence |
|---|---|---|
| All functions marked IRAM_ATTR | ✅ PASS | Lines 118, 514, 481 all marked |
| No FreeRTOS API calls | ✅ PASS | Grep shows zero in NMI path |
| Global variables in DRAM | ✅ PASS | g_rmt5_nmi_worker marked DRAM_ATTR |
| Shared variables volatile | ✅ PASS | mRMT_mem_ptr, mWhichHalf, mCur all volatile |
| extern "C" linkage | ✅ PASS | Line 118 uses extern "C" |
| Null pointer check | ✅ PASS | Line 122-126 early return on nullptr |
| Stack usage reasonable | ✅ PASS | ~144 bytes of 8192 available |
| No re-entrancy issues | ✅ PASS | Level 7 masks itself |
| Logging is ISR-safe | ✅ PASS | ets_printf() is ROM function |
Risk: Main thread modifies worker state during NMI Likelihood: LOW (transmission period is short, user unlikely to interfere) Impact: MEDIUM (could corrupt LED data or buffer state) Mitigation:
Acceptance: NMI requirement forbids spinlocks, race is unavoidable
Risk: Aggressive optimization breaks volatile semantics Likelihood: VERY LOW (volatile is well-supported) Impact: HIGH (silent data corruption) Mitigation:
Acceptance: Standard C++ guarantees should prevent this
Risk: RMT peripheral malfunction or register corruption Likelihood: VERY LOW (robust hardware) Impact: HIGH (system crash or undefined behavior) Mitigation:
Acceptance: Hardware failures are beyond software control
All safety requirements for ESP32 Level 7 NMI have been verified and met. The implementation is ready for hardware testing with LOW overall risk.
Next Steps:
Document Status: ✅ COMPLETE Author: FastLED AI Agent (Iteration 3) Last Updated: 2025-11-06 Confidence Level: HIGH