Battery-powered ESPHome nodes usually work in bursts: wake up, read a sensor, send the value, go back to deep sleep. A new esp32_ulp component gives them a third option. Merged on 21 October 2026 into the development branch for ESPHome 2026.11, it builds a small C program for the ESP32's low-power coprocessor, embeds it in the firmware and runs it. The coprocessor keeps going while the main CPU is in deep sleep, so it can poll pins, sample the ADC or talk to a sensor and wake the main CPU only when something needs attention.
The component closes a feature request (#826) and comes from Jonathan Swoboda.
Which chips
The coprocessor depends on the variant:
- ESP32-S2 and ESP32-S3 use the ULP RISC-V core.
- ESP32-C5, ESP32-C6 and ESP32-P4 use the LP core.
Both are small RISC-V cores that you program in C against the ESP-IDF ulp_riscv_* or ulp_lp_core_* APIs. The original ESP32, whose coprocessor is a different FSM design, is not supported. You also need the ESP-IDF framework with the native ESP-IDF toolchain, which is the default. The PlatformIO toolchain does not work here.
On the S2 and S3 there is a one-time cost. Their coprocessor program needs the RISC-V compiler, which ESPHome normally skips for these Xtensa chips, so the first build with the component downloads it, about 290 MB. Builds without the component are unchanged.
How it fits into a config
You list the program's .c, .S and .h files under sources and set wakeup_period, how often a timer starts the program, from 1 µs to about 71 minutes. The program runs to completion each time. On the LP core, wakeup_sources can add other triggers: the main CPU, an LP IO interrupt, LP UART activity or an ETM event. Other options reserve RTC memory for the program (4096 bytes by default), decide whether it starts at boot and whether it may wake the main CPU.
Every global variable in the program shows up in lambdas as ulp_<name>, so a counter in C becomes ulp_counter in a template sensor. ESP-IDF declares these shared symbols as 32-bit unsigned integers, so the documentation recommends keeping shared values as uint32_t or int32_t and casting signed values back. Otherwise -1 reads as 4294967295.
The important part for battery devices is what happens across sleep. After a deep-sleep wake, a running program is not reloaded, so its variables keep their values. Swoboda tested this on an ESP32-S3-DevKitC-1 and an ESP32-P4 Function EV board: a counter ticking every 100 ms ran at 10 ticks per second on both, and a wake request from the program brought the main core out of deep sleep with the count intact.
Limits to know
- The program can only use RTC-domain peripherals on the S2/S3 (RTC GPIOs, the SAR ADC, RTC I2C) or the LP peripherals on the C5, C6 and P4.
- The coprocessor and the main CPU share the ADC. If the program reads it, do not use ESPHome's ADC component on any channel of the same ADC unit.
- ESPHome's
deep_sleepcomponent does not build for the ESP32-P4 yet. On that chip you call ESP-IDF'sesp_deep_sleep_start()from a lambda instead.