vTaskDelete(nullptr) does not unwind the C++ stack, so the RAII HalPowerManager::Lock destructor never ran -- permanently holding the lock and preventing low-power mode. Fix by wrapping the lock in a block scope that exits before vTaskDelete(). Revert the setPowerSaving(false) calls in the clock refresh block; the RenderLock already handles CPU frequency during display updates. Made-with: Cursor
2.4 KiB
Fix Idle Freeze: NTP Power Lock (Corrected)
Date: 2026-03-09
Task Description
Fix device freeze during idle caused by power management issues with NTP sync and clock refresh. This is a correction to the initial fix (commit 6cf212d) which introduced two new problems.
Root Cause
The original freeze at the clock minute boundary was caused by the BootNtpSync background task running WiFi/NTP operations without a power lock, allowing the main loop to drop the CPU to 10 MHz during active WiFi/SPI operations.
Initial Fix Attempt (commit 6cf212d) -- Two Problems
-
BootNtpSync Lock: Added
HalPowerManager::Lockas a local variable intaskFunc(). However,vTaskDelete(nullptr)at the end of the function kills the FreeRTOS task immediately without C++ stack unwinding, so the lock destructor never ran. The lock was permanently held, preventing the device from ever entering low-power mode. -
Clock Refresh Power Override: Added
setPowerSaving(false)andlastActivityTimereset in the clock refresh block ofmain.cpp. This was unnecessary -- theRenderLockinside the render path already handles CPU frequency restoration. The explicit calls prevented the device from entering low-power mode by resetting the idle timer every minute.
Corrected Fix
src/util/BootNtpSync.cpp
- Wrapped the
HalPowerManager::Lockand all WiFi/NTP work in a block scope{}that ends beforevTaskDelete(nullptr) - The lock destructor now runs when the block scope exits, properly releasing the power lock
running = false,taskHandle = nullptr, log message, andvTaskDelete()remain outside the scope since they don't need full CPU speed
src/main.cpp
- Reverted the
setPowerSaving(false)andlastActivityTime = millis()additions from both clock refresh branches - Clock refresh relies on the existing
RenderLockmechanism to handle CPU frequency during display updates
Key Lesson
FreeRTOS vTaskDelete(nullptr) does not unwind the C++ stack. RAII objects (like HalPowerManager::Lock) placed as local variables in a task function will never have their destructors called. Always use explicit block scoping or manual release before vTaskDelete().
Follow-up Items
- Verify on device that low-power mode is entered after idle and that NTP sync still completes
- Verify the clock still updates at minute boundaries without freezing
- Consider auditing other FreeRTOS task functions in the codebase for similar RAII +
vTaskDeleteissues