Scaling CPU Frequency on the ESP32-C6 DevKitC to Save Battery Power
Every milliampere saved during idle periods extends the field life of a battery-powered sensor. Developers invest significant time tuning deep sleep cycles, but the current drawn while the application runs at full CPU speed is equally consequential. The ESP32-C6 DevKitC defaults to its maximum clock rate the moment it exits sleep, even when it has nothing demanding to do. Dynamic frequency scaling (DFS) changes this: the chip drops to a lower clock rate during idle periods and returns to full speed only when your code explicitly requests it.
How DFS Works on the ESP32-C6
The ESP32-C6 can run its CPU at 40 MHz, 80 MHz, or 160 MHz. Both DFS and automatic light sleep use the same ESP-IDF power management API, and the distinction between them is in what the chip does during idle time.
With DFS alone, the chip scales its clock rate based on whether power management locks are held. With no lock acquired, the CPU runs at the configured minimum frequency. When your application acquires a lock to handle a burst of computation or service a peripheral, the CPU steps up to the configured maximum. Releasing the lock drops it back down, and the CPU keeps running throughout. It executes instructions and responds to interrupts at the lower clock rate without any wake-up latency, which makes DFS the right choice when your application needs to remain continuously responsive.
Automatic light sleep goes further. When no locks are held and FreeRTOS has no runnable tasks, the chip halts the CPU entirely, gates the clocks, and enters a true sleep state. Current draw drops from tens of milliamps to the hundreds of microamps. The trade-off is a short resume latency each time the chip wakes. For applications that can tolerate that gap, light sleep compounds the savings significantly. You can read more about the different sleep modes on the ESP32 in another tutorial we’ve written.
Setting the DFS minimum to 40 MHz targets the XTAL oscillator directly. At this clock rate the chip bypasses the PLL entirely, which reduces idle current compared to running at 80 MHz. Locks are reference-counted: acquiring the same lock three times requires three releases before the CPU scales down.
Configuring DFS in Application Code
You need to use compile-time options to enable DFS on your ESP32-C6 board. In the ESP-IDF menuconfig, enable CONFIG_PM_ENABLE and CONFIG_FREERTOS_USE_TICKLESS_IDLE. Then add esp_pm to the REQUIRES list in your component’s CMakeLists.txt. With those two changes in place, call esp_pm_configure() once during initialization, passing an esp_pm_config_t struct with three fields: max_freq_mhz sets the CPU frequency when a lock is held, min_freq_mhz sets the idle frequency, and light_sleep_enable controls whether the chip enters light sleep when no locks are held.
The following example configures the ESP32-C6 DevKitC to alternate between 160 MHz and 40 MHz on a 10-second cycle, which produces a measurable step in current draw on a power profiler:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_pm.h"
#include "esp_err.h"
void app_main(void)
{
// Configure DFS: 160 MHz under load, 40 MHz (XTAL) when idle
esp_pm_config_t pm_config = {
.max_freq_mhz = 160,
.min_freq_mhz = 40,
.light_sleep_enable = false
};
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
// Create a lock that requests maximum CPU frequency
esp_pm_lock_handle_t cpu_lock;
ESP_ERROR_CHECK(esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "cpu_max", &cpu_lock));
while (1) {
// PHASE 1: Active — lock held, CPU at 160 MHz
ESP_ERROR_CHECK(esp_pm_lock_acquire(cpu_lock));
vTaskDelay(pdMS_TO_TICKS(5000));
// PHASE 2: Idle — lock released, CPU scales to 40 MHz
ESP_ERROR_CHECK(esp_pm_lock_release(cpu_lock));
vTaskDelay(pdMS_TO_TICKS(5000));
}
}Results
Running this code on the DevKitC with a Nordic Power Profiler Kit II produces a clear repeating step pattern. During the active phase the board draws around 20 mA on average. During the idle phase that drops to around 12.5 mA, a reduction of roughly 37%.

The trace is not a flat line because the USB-Serial-JTAG interface runs its own polling loop, and FreeRTOS executes background housekeeping tasks on a regular cadence. These produce a wave-like oscillation on top of each baseline, with a period of around 3 to 4 seconds. This is expected behavior for a DevKitC connected over USB with the default IDF system tasks running. The signal to look for is the step between the two average current levels, which confirms that DFS is working.
The Cumulative Benefit
A 37% reduction in current during idle periods adds up quickly across the duty cycle of a field-deployed node. Pairing DFS with the bootloader optimizations covered in our guide to cutting ESP32-C6 bootloader latency addresses two separate segments of the power budget: the time before your application starts and the time it spends waiting between tasks.
The goal with DFS is not to run at the lowest clock rate at all times. It is to stop paying for CPU speed you are during idle, and give your application a precise mechanism to request that speed when it genuinely needs it.