The ULP low-power core
A tiny processor that stays awake while the rest of the chip sleeps, reading an ADC or a pin for about a hundred microamps. It is transformative when the answer is usually nothing happened, and effort for nothing when it is not.
When it is worth the trouble
Before you write any ULP assembly
Check whether the hardware can do it for free. Most "watch a value and wake up" problems are really:
- A threshold output on the sensor plus
ext0wake. Zero extra current. - A touch pad with a hardware threshold. A few microamps.
- A timer wake every few seconds, if the event is not that urgent.
The ULP earns its place when you genuinely need an analog value compared thousands of times an hour and no sensor will do it for you.
What it is like to write
Very small assembly on the original ESP32 and S2, or restricted C on the S2 and S3. No Wi-Fi, no I2C drivers, no floating point, no debugger, and a handful of registers. It talks to the main core through RTC slow memory and nothing else. That is the cost, and it is why the hit rate above matters so much.
The code
The main core sets up the ULP program and goes to sleep. The ULP polls, compares, and only wakes the big cores when a threshold is crossed - so the expensive part of the chip runs once an hour instead of once a second.
#include <esp32/ulp.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
RTC_DATA_ATTR uint32_t crossings = 0;
void setup() {
Serial.begin(115200);
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_ULP) {
crossings++;
Serial.printf("the ULP woke us, %u times so far\n", crossings);
// ... react: send a reading, sound an alarm ...
} else {
Serial.println("cold start - loading the ULP program");
// ulp_load_binary(...) then ulp_run(...) with a program that
// samples the ADC every 100 ms and wakes us past a threshold
}
esp_sleep_enable_ulp_wakeup();
esp_deep_sleep_start();
}
void loop() {}RTC_SLOW_MEM is the shared workspace. It is the only memory both processors can see, and it is how the ULP hands over what it measured.
MicroPython cannot program the ULP. On the S2 and S3 there is a RISC-V ULP that ESP-IDF can build C for, but nothing exposed to Python - so the practical alternative is a wake source that costs nothing.
import machine, esp32
# A hardware comparator or a sensor with a threshold output
# pulls this pin low, and the chip wakes. No ULP, no polling,
# and lower current than the ULP would use.
esp32.wake_on_ext0(pin=machine.Pin(33), level=0)
machine.deepsleep()Most jobs people reach for the ULP for are actually a comparator and an ext0 wake. That draws less than the ULP and needs no second program at all.
When it does not work
There is no debugger and no serial output. Write intermediate values into RTC_SLOW_MEM and print them from the main core after waking - that is the only visibility you get.
Your threshold is inside the noise. The ULP's ADC readings are noisier than the main core's. Average several samples in the ULP program and add hysteresis.
The C3 has no ULP. The original ESP32 and S2 have the assembly one, the S2 and S3 also have a RISC-V ULP you can write C for.
Then the main core was waking often anyway. Check the hit rate first - below a few percent the ULP is dramatic, and above a third it is not worth writing.
Sleep is the demand side. The supply side is a regulator with a few hundred milliamps in it and a radio that wants a quarter of an amp.
Power and the 3V3 rail →Edit this page — content/esp32/the-ulp-low-power-core.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.