A Developer’s Guide to Achieving Ultra-Long Battery Life in Monitoring Devices with ESP32’s ULP Coprocessor
Ultra low power coprocessor - ULP

Image source – https://portal.vidadesilicio.com.br/

The ESP32’s Ultra-Low Power (ULP) coprocessor presents a sophisticated avenue for developers aiming to extend battery life and reduce power consumption without sacrificing performance. This guide delves into the uses of the ULP coprocessor and outlines methods for programming it effectively.

Understanding the ULP Coprocessor

The ULP coprocessor in the ESP32 is a simple FSM (Finite State Machine)-style processor designed to execute small programs independently from the main CPU, primarily while the main processor remains in deep sleep mode.

Key Features

  • Operates at ultra-low power.
  • ULP runs periodically as per the ULP Timer
  • Can access peripherals like ADC, I2C and GPIO.
  • ULP has access to the RTC Slow SRAM memory space for storing code and data.
  • Suitable for applications requiring continuous sensor monitoring without waking the main CPU.

Image source – https://www.espressif.com/

Common Uses of the ULP Coprocessor

  1. Monitoring Environmental Sensors: Continuously monitor sensors for changes without waking the main CPU.
  2. Data Logging: Collect data periodically and store it in RTC memory for later processing.
  3. Wake-up Triggers: Initiate actions or wake the main CPU based on specific sensor readings or conditions.

Main core and ultra core

Image source – https://www.espressif.com/

Programming the ULP Coprocessor

The ULP coprocessor can be programmed using its supported instruction set or through C macros on the main CPU. Here’s how you can start programming the ULP coprocessor:

Step 1: Setting Up the Development Environment

Ensure your development environment is set up with the latest version of the ESP-IDF that supports ULP programming.

Step 2: Writing ULP Programs

ULP programs are typically written in assembly language and should be placed in separate `.S` files within a directory named `ulp/` inside your component directory.

Step 3: Compiling the ULP Code

To compile the ULP code, follow these steps:
1. Add your ULP assembly files inside the `ulp/` directory without adding it to `SRC_DIRS` in `idf_component_register()`.
2. Use `ulp_embed_binary` in the component `CMakeLists.txt` to include your ULP program.

set(ulp_app_name ulp_${COMPONENT_NAME})

set(ulp_s_sources ulp/ulp_program.S)

ulp_embed_binary(${ulp_app_name} “${ulp_s_sources}” “src/main.c”)


Step 4: Loading and Running the ULP Program

After building your main application, load the ULP program into RTC memory and start it using:

extern const uint8_t ulp_program_bin_start[] asm(“_binary_ulp_program_bin_start”);

extern const uint8_t ulp_program_bin_end[] asm(“_binary_ulp_program_bin_end”);

void start_ulp() {

ulp_load_binary(0, ulp_program_bin_start, (ulp_program_bin_end – ulp_program_bin_start) / sizeof(uint32_t));

ulp_run((&entry – RTC_SLOW_MEM) / sizeof(uint32_t));


Accessing the ULP FSM Program Variables

Global symbols defined in the ULP FSM program can be utilized within the main program. For example, consider a scenario where the ULP program needs to count a number of ADC measurements before waking up the chip from deep sleep. The variable measurement_count might be defined in the ULP program as follows:

.global measurement_count

measurement_count: .long 0

// Later in the ULP program, you can use measurement_count

move r3, measurement_count

ld r3, r3, 0

Initialization in the Main Program:

Before the ULP program starts, the main program needs to initialize measurement_count. This is facilitated by the build system generating ulp_app_name.h and ulp_app_name.ld, which define the global symbols present in the ULP program. Symbols are prefixed with ulp_ to avoid naming conflicts.

The header file ulp_app_name.h will declare the symbol like so:

extern uint32_t ulp_measurement_count;

Accessing and Using Global Variables:

To use these global variables in your main application, include the generated header file:

#include “ulp_app_name.h”

// Initialization function

void init_ulp_vars() {

ulp_measurement_count = 64; // Set the measurement count for the ULP program


Special Considerations for Variable Access:

The ULP FSM coprocessor is capable of using only the lower 16 bits of each 32-bit word in RTC memory, as it uses 16-bit registers. The ULP’s store instruction writes register values into these lower 16 bits, and the upper 16 bits receive a value dependent on the store instruction’s address. When reading variables written by the ULP, ensure to mask the upper 16 bits to get the correct data:

printf(“Last measurement value: %d\n”, ulp_last_measurement & UINT16_MAX);


Programming the ESP32’s ULP coprocessor opens a world of possibilities for developing power-efficient applications in IoT devices. By understanding its intricacies and leveraging its capabilities wisely, developers can significantly enhance the functionality and battery life of their ESP32-based projects.


[1] Espressif, ” ESP-IDF Programming Guide”, https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/ulp.html


Was this article of help to you?
Subscribe to our newsletter. We write about developing embedded and electronic systems.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe Our Newsletter