How to Implement DVFS Firmware on ESP32 with FreeRTOS for IoT Power Optimization
Dynamic Voltage and Frequency Scaling on the ESP32 with FreeRTOS
Battery life makes or breaks most IoT products. Your ESP32 doesn't need to run at 240 MHz while waiting for a sensor reading that comes once per second. Dynamic Voltage and Frequency Scaling (DVFS) lets you drop the CPU frequency (and with it, the supply voltage) when the workload is light, then ramp back up when you actually need the processing power. On the ESP32, this can cut power consumption by 30-50% in typical IoT workloads.
ESP-IDF v5.3+ has a built-in power management framework that handles DVFS properly. The older approach of manually calling frequency-change APIs is fragile and can break Wi-Fi/BLE timing. I'd strongly recommend using the official esp_pm API instead of rolling your own.
Prerequisites
- Familiarity with ESP32 development and FreeRTOS task basics
- ESP-IDF v5.3+ installed (the power management API has improved significantly in recent versions)
- ESP32 development board (ESP32-S3 or ESP32-C6 also work, with slightly different frequency ranges)
- USB cable for flashing
- Working knowledge of C programming
Parts/Tools
- ESP32 Development Board (DevKitC, NodeMCU-32S, or similar)
- USB Cable (USB-C or Micro-USB depending on your board)
- ESP-IDF v5.3+ with VS Code or terminal-based workflow
- Multimeter or USB power monitor (something like a Nordic PPK2 is ideal for profiling current draw over time)
Steps
- Set Up Your ESP-IDF Project
- Create a new project or use an existing one. If starting fresh:
- Enable power management in
menuconfig. This is the step people forget: - Navigate to Component config -> Power Management and enable Support for power management. Also enable Enable dynamic frequency scaling (DFS). Without these flags, the
esp_pmAPI calls will silently do nothing.
idf.py create-project dvfs_demo cd dvfs_demoidf.py menuconfig - Configure Power Management in Code
- Include the required headers in your
main.c: - Define your frequency range. The ESP32 supports 80, 160, and 240 MHz. The power management framework will automatically scale between your min and max:
- Configure the power management policy in
app_main():
#include "esp_pm.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h"#define MAX_FREQ_MHZ 240 #define MIN_FREQ_MHZ 80 static const char *TAG = "DVFS";void app_main(void) { esp_pm_config_t pm_config = { .max_freq_mhz = MAX_FREQ_MHZ, .min_freq_mhz = MIN_FREQ_MHZ, .light_sleep_enable = true // Optional: also enter light sleep when idle }; esp_err_t err = esp_pm_configure(&pm_config); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to configure PM: %s", esp_err_to_name(err)); return; } ESP_LOGI(TAG, "Power management configured: %d-%d MHz", MIN_FREQ_MHZ, MAX_FREQ_MHZ); } - Include the required headers in your
- Use Power Management Locks for Critical Sections
- Here's the key concept: the framework drops to the minimum frequency whenever no task needs full speed. You use PM locks to tell it when you actually need performance:
- This is much better than manually toggling frequencies. The framework considers all active locks and picks the right frequency. Wi-Fi and BLE drivers also acquire their own locks internally, so your radio connections won't break.
esp_pm_lock_handle_t freq_lock; // Create a lock that keeps CPU at max frequency esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "sensor_processing", &freq_lock); // In your task, acquire the lock when doing heavy work void sensor_task(void *pvParameters) { while (1) { // Waiting for data - CPU can drop to 80 MHz vTaskDelay(pdMS_TO_TICKS(1000)); // Got data, need full speed for processing esp_pm_lock_acquire(freq_lock); // ... do your heavy sensor processing here ... esp_pm_lock_release(freq_lock); } } - Create a FreeRTOS Task Structure
- Wire it all together with task creation:
void app_main(void) { // ... PM configuration from step 2 ... esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "sensor_processing", &freq_lock); xTaskCreate(sensor_task, "SensorTask", 4096, NULL, 5, NULL); ESP_LOGI(TAG, "DVFS demo running"); } - Build, Flash, and Test
- Build and flash:
- In the serial monitor, you should see the PM configuration log messages. Enable PM debug logging in menuconfig for more detail about frequency transitions.
- To actually measure the power savings, connect a current meter in series with the USB power line, or use a USB power monitor. You should see current drop noticeably during idle periods when the CPU scales down to 80 MHz.
idf.py build idf.py -p /dev/ttyUSB0 flash monitor
Troubleshooting
- Frequency doesn't seem to change
- Did you enable power management in menuconfig? This is the most common gotcha. The
esp_pm_configure()call will returnESP_ERR_NOT_SUPPORTEDif DFS isn't enabled in the build config. - If you have Wi-Fi or BLE active, their drivers hold frequency locks that may keep the CPU at a higher speed. This is by design -- the radio peripherals need specific clock rates to function correctly.
- Did you enable power management in menuconfig? This is the most common gotcha. The
- Build or upload failures
- Make sure your ESP-IDF version is v5.3 or newer. Older versions have a different PM configuration struct (it used
esp_pm_config_esp32_tinstead of the genericesp_pm_config_t). - Check that your serial port path is correct and the board is in download mode if it doesn't auto-reset.
- Make sure your ESP-IDF version is v5.3 or newer. Older versions have a different PM configuration struct (it used
- System crashes or Wi-Fi disconnects after enabling DFS
- If you enabled
light_sleep_enable, some peripheral configurations may conflict. Try disabling light sleep first and testing with just frequency scaling. - Make sure you're not calling deprecated frequency-change APIs alongside the PM framework. Pick one approach and stick with it.
- If you enabled
Practical Tips
For battery-powered devices, combine DVFS with the ESP32's light sleep and deep sleep modes. DVFS handles the "actively running but not busy" case, while sleep modes handle the "nothing to do for a while" case. Together, they can extend battery life dramatically.
Also, profile your actual workload before picking min/max frequencies. If your application can run fine at 160 MHz, there's no reason to let it scale up to 240 MHz. Tighter bounds mean more predictable power consumption.