ULP LP-Core Coprocessor Programming
The ULP LP-Core (Low-power core) coprocessor is a variant of the ULP present in ESP32-C6. It features ultra-low power consumption while also being able to stay powered on while the main CPU stays in low-power modes. This enables the LP-Core coprocessor to handle tasks like GPIO or sensor readings while the main CPU is in sleep mode, resulting in significant overall power savings for the entire system.
The ULP LP-Core coprocessor has the following features:
Utilizes a 32-bit processor based on the RISC-V ISA, encompassing the standard extensions integer (I), multiplication/division (M), atomic (A), and compressed (C).
Interrupt controller.
Includes a debug module that supports external debugging via JTAG.
Can access all of the High-power (HP) SRAM and peripherals when the entire system is active.
Can access the Low-power (LP) SRAM and peripherals when the HP system is in sleep mode.
Compiling Code for the ULP LP-Core
The ULP LP-Core code is compiled together with your ESP-IDF project as a separate binary and automatically embedded into the main project binary. To acheive this do the following:
Place the ULP LP-Core code, written in C or assembly (with the
.S
extension), in a dedicated directory within the component directory, such asulp/
.After registering the component in the CMakeLists.txt file, call the
ulp_embed_binary
function. Here is an example:idf_component_register()
set(ulp_app_name ulp_${COMPONENT_NAME}) set(ulp_sources "ulp/ulp_c_source_file.c" "ulp/ulp_assembly_source_file.S") set(ulp_exp_dep_srcs "ulp_c_source_file.c")
ulp_embed_binary(${ulp_app_name} "${ulp_sources}" "${ulp_exp_dep_srcs}")
The first argument to ulp_embed_binary
specifies the ULP binary name. The name specified here is also used by other generated artifacts such as the ELF file, map file, header file, and linker export file. The second argument specifies the ULP source files. Finally, the third argument specifies the list of component source files which include the header file to be generated. This list is needed to build the dependencies correctly and ensure that the generated header file is created before any of these files are compiled. See the section below for the concept of generated header files for ULP applications.
Enable both CONFIG_ULP_COPROC_ENABLED and CONFIG_ULP_COPROC_TYPE in menucofig, and set CONFIG_ULP_COPROC_TYPE to
CONFIG_ULP_COPROC_TYPE_LP_CORE
. The CONFIG_ULP_COPROC_RESERVE_MEM option reserves RTC memory for the ULP and must be set to a value big enough to store both the ULP LP-Core code and data. If the application components contain multiple ULP programs, then the size of the RTC memory must be sufficient to hold the largest one.Build the application as usual (e.g.,
idf.py app
).
During the build process, the following steps are taken to build ULP program:
Run each source file through the C compiler and assembler. This step generates the object files
.obj.c
or.obj.S
in the component build directory depending on the source file processed.Run the linker script template through the C preprocessor. The template is located in
components/ulp/ld
directory.Link the object files into an output ELF file (
ulp_app_name.elf
). The Map fileulp_app_name.map
generated at this stage may be useful for debugging purposes.Dump the contents of the ELF file into a binary (
ulp_app_name.bin
) which can then be embedded into the application.Generate a list of global symbols (
ulp_app_name.sym
) in the ELF file usingriscv32-esp-elf-nm
.Create an LD export script and a header file
ulp_app_name.ld
andulp_app_name.h
containing the symbols fromulp_app_name.sym
. This is done using theesp32ulp_mapgen.py
utility.Add the generated binary to the list of binary files to be embedded into the application.
Accessing the ULP LP-Core Program Variables
Global symbols defined in the ULP LP-Core program may be used inside the main program.
For example, the ULP LP-Core program may define a variable measurement_count
which defines the number of GPIO measurements the program needs to make before waking up the chip from deep sleep.
volatile int measurement_count;
int some_function()
{
//read the measurement count for later use.
int temp = measurement_count;
...do something.
}
The main program can access the global ULP LP-Core program variables as the build system makes this possible by generating the ${ULP_APP_NAME}.h
and ${ULP_APP_NAME}.ld
files which define the global symbols present in the ULP LP-Core program. Each global symbol defined in the ULP LP-Core program is included in these files and are prefixed with ulp_
.
The header file contains the declaration of the symbol:
extern uint32_t ulp_measurement_count;
Note that all symbols (variables, arrays, functions) are declared as uint32_t
. For functions and arrays, take the address of the symbol and cast it to the appropriate type.
The generated linker script file defines the locations of symbols in LP_MEM:
PROVIDE ( ulp_measurement_count = 0x50000060 );
To access the ULP LP-Core program variables from the main program, the generated header file should be included using an include
statement. This allows the ULP LP-Core program variables to be accessed as regular variables.
#include "ulp_app_name.h"
void init_ulp_vars() {
ulp_measurement_count = 64;
}
Starting the ULP LP-Core Program
To run a ULP LP-Core program, the main application needs to load the ULP program into RTC memory using the ulp_lp_core_load_binary()
function, and then start it using the ulp_lp_core_run()
function.
Each ULP LP-Core program is embedded into the ESP-IDF application as a binary blob. The application can reference this blob and load it in the following way (supposed ULP_APP_NAME was defined to ulp_app_name
):
extern const uint8_t bin_start[] asm("_binary_ulp_app_name_bin_start");
extern const uint8_t bin_end[] asm("_binary_ulp_app_name_bin_end");
void start_ulp_program() {
ESP_ERROR_CHECK( ulp_lp_core_load_binary( bin_start,
(bin_end - bin_start)) );
}
Once the program is loaded into LP memory, the application can be configured and started by calling ulp_lp_core_run()
:
ulp_lp_core_cfg_t cfg = {
.wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER, // LP core will be woken up periodically by LP timer
.lp_timer_sleep_duration_us = 10000,
};
ESP_ERROR_CHECK( ulp_lp_core_run(&cfg) );
ULP LP-Core Program Flow
How the ULP LP-Core coprocessor is started depends on the wakeup source selected in ulp_lp_core_cfg_t
. The most common use-case is for the ULP to periodically wake-up, do some measurements before either waking up the main CPU or going back to sleep again.
- The ULP has the following wake-up sources:
ULP_LP_CORE_WAKEUP_SOURCE_HP_CPU
- LP Core can be woken up by the HP CPU.ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER
- LP Core can be woken up by the LP timer.ULP_LP_CORE_WAKEUP_SOURCE_ETM
- LP Core can be woken up by a ETM event. (Not yet supported)ULP_LP_CORE_WAKEUP_SOURCE_LP_IO
- LP Core can be woken up when LP IO level changes. (Not yet supported)ULP_LP_CORE_WAKEUP_SOURCE_LP_UART
- LP Core can be woken up after receiving a certain number of UART RX pulses. (Not yet supported)
When the ULP is woken up, it will go through the following steps:
Initialize system feature, e.g., interrupts
Call user code
main()
Return from
main()
If
lp_timer_sleep_duration_us
is specified, then configure the next wake-up alarmCall
ulp_lp_core_halt()
ULP LP-Core Peripheral Support
To enhance the capabilities of the ULP LP-Core coprocessor, it has access to peripherals which operate in the low-power domain. The ULP LP-Core coprocessor can interact with these peripherals when the main CPU is in sleep mode, and can wake up the main CPU once a wakeup condition is reached. The following peripherals are supported:
LP IO
LP I2C
LP UART
Application Examples
system/ulp/lp_core/gpio polls GPIO while main CPU is in deep sleep.
system/ulp/lp_core/lp_i2c reads external I2C ambient light sensor (BH1750) while the main CPU is in Deep-sleep and wakes up the main CPU once a threshold is met.
system/ulp/lp_core/lp_uart/lp_uart_echo reads data written to a serial console and echoes it back. This example demonstrates the usage of the LP UART driver from the LP core.
system/ulp/lp_core/lp_uart/lp_uart_print shows how to print various statements from a program running on the LP core.
API Reference
Main CPU API Reference
Header File
This header file can be included with:
#include "ulp_lp_core.h"
This header file is a part of the API provided by the
ulp
component. To declare that your component depends onulp
, add the following to your CMakeLists.txt:REQUIRES ulp
or
PRIV_REQUIRES ulp
Functions
-
esp_err_t ulp_lp_core_run(ulp_lp_core_cfg_t *cfg)
Configure the ULP and run the program loaded into RTC memory.
- Returns
ESP_OK on success
-
esp_err_t ulp_lp_core_load_binary(const uint8_t *program_binary, size_t program_size_bytes)
Load the program binary into RTC memory.
- Parameters
program_binary -- pointer to program binary
program_size_bytes -- size of the program binary
- Returns
ESP_OK on success
ESP_ERR_INVALID_SIZE if program_size_bytes is more than KiB
-
void ulp_lp_core_stop(void)
Puts the ulp to sleep and disables all wakeup sources. To restart the ULP call ulp_lp_core_run() with the desired wake up trigger.
Structures
-
struct ulp_lp_core_cfg_t
ULP LP core init parameters.
Macros
-
ULP_LP_CORE_WAKEUP_SOURCE_HP_CPU
-
ULP_LP_CORE_WAKEUP_SOURCE_LP_UART
-
ULP_LP_CORE_WAKEUP_SOURCE_LP_IO
-
ULP_LP_CORE_WAKEUP_SOURCE_ETM
-
ULP_LP_CORE_WAKEUP_SOURCE_LP_TIMER
Header File
This header file can be included with:
#include "lp_core_i2c.h"
This header file is a part of the API provided by the
ulp
component. To declare that your component depends onulp
, add the following to your CMakeLists.txt:REQUIRES ulp
or
PRIV_REQUIRES ulp
Functions
-
esp_err_t lp_core_i2c_master_init(i2c_port_t lp_i2c_num, const lp_core_i2c_cfg_t *cfg)
Initialize and configure the LP I2C for use by the LP core Currently LP I2C can only be used in master mode.
Note
The internal pull-up resistors for SDA and SCL pins, if enabled, will provide a weak pull-up value of about 30-50 kOhm. Users are adviced to enable external pull-ups for better performance at higher SCL frequencies.
- Parameters
lp_i2c_num -- LP I2C port number
cfg -- Configuration parameters
- Returns
esp_err_t ESP_OK when successful
Structures
-
struct lp_core_i2c_pin_cfg_t
LP Core I2C pin config parameters.
Public Members
-
gpio_num_t sda_io_num
GPIO pin for SDA signal. Only GPIO#6 can be used as the SDA pin.
-
gpio_num_t scl_io_num
GPIO pin for SCL signal. Only GPIO#7 can be used as the SCL pin.
-
bool sda_pullup_en
SDA line enable internal pullup. Can be configured if external pullup is not used.
-
bool scl_pullup_en
SCL line enable internal pullup. Can be configured if external pullup is not used.
-
gpio_num_t sda_io_num
-
struct lp_core_i2c_timing_cfg_t
LP Core I2C timing config parameters.
Public Members
-
uint32_t clk_speed_hz
LP I2C clock speed for master mode
-
uint32_t clk_speed_hz
-
struct lp_core_i2c_cfg_t
LP Core I2C config parameters.
Public Members
-
lp_core_i2c_pin_cfg_t i2c_pin_cfg
LP I2C pin configuration
-
lp_core_i2c_timing_cfg_t i2c_timing_cfg
LP I2C timing configuration
-
soc_periph_lp_i2c_clk_src_t i2c_src_clk
LP I2C source clock type
-
lp_core_i2c_pin_cfg_t i2c_pin_cfg
Macros
-
LP_I2C_DEFAULT_GPIO_CONFIG()
-
LP_I2C_FAST_MODE_CONFIG()
-
LP_I2C_STANDARD_MODE_CONFIG()
-
LP_I2C_DEFAULT_SRC_CLK()
-
LP_CORE_I2C_DEFAULT_CONFIG()
Header File
This header file can be included with:
#include "lp_core_uart.h"
This header file is a part of the API provided by the
ulp
component. To declare that your component depends onulp
, add the following to your CMakeLists.txt:REQUIRES ulp
or
PRIV_REQUIRES ulp
Functions
-
esp_err_t lp_core_uart_init(const lp_core_uart_cfg_t *cfg)
Initialize and configure the LP UART to be used from the LP core.
Note
The LP UART initialization must be called from the main core (HP CPU)
- Parameters
cfg -- Configuration parameters
- Returns
esp_err_t ESP_OK when successful
Structures
-
struct lp_core_uart_pin_cfg_t
LP UART IO pins configuration.
Public Members
-
gpio_num_t tx_io_num
GPIO pin for UART Tx signal. Only GPIO#5 can be used as the UART Tx pin
-
gpio_num_t rx_io_num
GPIO pin for UART Rx signal. Only GPIO#4 can be used as the UART Rx pin
-
gpio_num_t rts_io_num
GPIO pin for UART RTS signal. Only GPIO#2 can be used as the UART RTS pin
-
gpio_num_t cts_io_num
GPIO pin for UART CTS signal. Only GPIO#3 can be used as the UART CTS pin
-
gpio_num_t tx_io_num
-
struct lp_core_uart_proto_cfg_t
LP UART protocol configuration.
Public Members
-
int baud_rate
LP UART baud rate
-
uart_word_length_t data_bits
LP UART byte size
-
uart_parity_t parity
LP UART parity mode
-
uart_stop_bits_t stop_bits
LP UART stop bits
-
uart_hw_flowcontrol_t flow_ctrl
LP UART HW flow control mode (cts/rts)
-
uint8_t rx_flow_ctrl_thresh
LP UART HW RTS threshold
-
int baud_rate
-
struct lp_core_uart_cfg_t
LP UART configuration parameters.
Public Members
-
lp_core_uart_pin_cfg_t uart_pin_cfg
LP UART pin configuration
-
lp_core_uart_proto_cfg_t uart_proto_cfg
LP UART protocol configuration
-
lp_uart_sclk_t lp_uart_source_clk
LP UART source clock selection
-
lp_core_uart_pin_cfg_t uart_pin_cfg
Macros
-
LP_UART_DEFAULT_GPIO_CONFIG()
-
LP_UART_DEFAULT_PROTO_CONFIG()
-
LP_UART_DEFAULT_CLOCK_CONFIG()
-
LP_CORE_UART_DEFAULT_CONFIG()
LP Core API Reference
Header File
Functions
-
void ulp_lp_core_update_wakeup_cause(void)
Traverse all possible wake-up sources and update the wake-up cause so that ulp_lp_core_get_wakeup_cause can obtain the bitmap of the wake-up reasons.
-
uint32_t ulp_lp_core_get_wakeup_cause(void)
Get the wakeup source which caused LP_CPU to wakeup from sleep.
- Returns
Wakeup cause in bit map, for the meaning of each bit, refer to the definition of wakeup source in lp_core_ll.h
-
void ulp_lp_core_wakeup_main_processor(void)
Wakeup main CPU from sleep or deep sleep.
This raises a software interrupt signal, if the main CPU has configured the ULP as a wakeup source calling this function will make the main CPU to exit from sleep or deep sleep.
-
void ulp_lp_core_delay_us(uint32_t us)
Makes the co-processor busy wait for a certain number of microseconds.
- Parameters
us -- Number of microseconds to busy-wait for
-
void ulp_lp_core_delay_cycles(uint32_t cycles)
Makes the co-processor busy wait for a certain number of cycles.
- Parameters
cycles -- Number of cycles to busy-wait for
-
void ulp_lp_core_halt(void)
Finishes the ULP program and powers down the ULP until next wakeup.
Note
This function does not return. After called it will fully reset the ULP.
Note
The program will automatically call this function when returning from main().
Note
To stop the ULP from waking up, call ulp_lp_core_lp_timer_disable() before halting.
-
void ulp_lp_core_stop_lp_core(void)
The LP core puts itself to sleep and disables all wakeup sources.
Header File
Functions
-
static inline void ulp_lp_core_gpio_init(lp_io_num_t lp_io_num)
Initialize a rtcio pin.
- Parameters
lp_io_num -- The rtc io pin to initialize
-
static inline void ulp_lp_core_gpio_output_enable(lp_io_num_t lp_io_num)
Enable output.
- Parameters
lp_io_num -- The rtc io pin to enable output for
-
static inline void ulp_lp_core_gpio_output_disable(lp_io_num_t lp_io_num)
Disable output.
- Parameters
lp_io_num -- The rtc io pin to disable output for
-
static inline void ulp_lp_core_gpio_input_enable(lp_io_num_t lp_io_num)
Enable input.
- Parameters
lp_io_num -- The rtc io pin to enable input for
-
static inline void ulp_lp_core_gpio_input_disable(lp_io_num_t lp_io_num)
Disable input.
- Parameters
lp_io_num -- The rtc io pin to disable input for
-
static inline void ulp_lp_core_gpio_set_level(lp_io_num_t lp_io_num, uint8_t level)
Set rtcio output level.
- Parameters
lp_io_num -- The rtc io pin to set the output level for
level -- 0: output low; 1: output high.
-
static inline uint32_t ulp_lp_core_gpio_get_level(lp_io_num_t lp_io_num)
Get rtcio output level.
- Parameters
lp_io_num -- The rtc io pin to get the output level for
-
static inline void ulp_lp_core_gpio_set_output_mode(lp_io_num_t lp_io_num, rtcio_ll_out_mode_t mode)
Set rtcio output mode.
- Parameters
lp_io_num -- The rtc io pin to set the output mode for
mode -- RTCIO_LL_OUTPUT_NORMAL: normal, RTCIO_LL_OUTPUT_OD: open drain
-
static inline void ulp_lp_core_gpio_pullup_enable(lp_io_num_t lp_io_num)
Enable internal pull-up resistor.
- Parameters
lp_io_num -- The rtc io pin to enable pull-up for
-
static inline void ulp_lp_core_gpio_pullup_disable(lp_io_num_t lp_io_num)
Disable internal pull-up resistor.
- Parameters
lp_io_num -- The rtc io pin to disable pull-up for
-
static inline void ulp_lp_core_gpio_pulldown_enable(lp_io_num_t lp_io_num)
Enable internal pull-down resistor.
- Parameters
lp_io_num -- The rtc io pin to enable pull-down for
-
static inline void ulp_lp_core_gpio_pulldown_disable(lp_io_num_t lp_io_num)
Disable internal pull-down resistor.
- Parameters
lp_io_num -- The rtc io pin to disable pull-down for
Macros
-
RTCIO_OUTPUT_NORMAL
-
RTCIO_OUTPUT_OD
Enumerations
-
enum lp_io_num_t
Values:
-
enumerator LP_IO_NUM_0
GPIO0, input and output
-
enumerator LP_IO_NUM_1
GPIO1, input and output
-
enumerator LP_IO_NUM_2
GPIO2, input and output
-
enumerator LP_IO_NUM_3
GPIO3, input and output
-
enumerator LP_IO_NUM_4
GPIO4, input and output
-
enumerator LP_IO_NUM_5
GPIO5, input and output
-
enumerator LP_IO_NUM_6
GPIO6, input and output
-
enumerator LP_IO_NUM_7
GPIO7, input and output
-
enumerator LP_IO_NUM_0
Header File
Functions
-
esp_err_t lp_core_i2c_master_read_from_device(i2c_port_t lp_i2c_num, uint16_t device_addr, uint8_t *data_rd, size_t size, int32_t ticks_to_wait)
Read from I2C device.
Note
The LP I2C must have been initialized from the HP core using the lp_core_i2c_master_init() API before invoking this API.
Note
the LP I2C does not support 10-bit I2C device addresses.
Note
the LP I2C port number is ignored at the moment.
- Parameters
lp_i2c_num -- LP I2C port number
device_addr -- I2C device address (7-bit)
data_rd -- Buffer to hold data to be read
size -- Size of data to be read in bytes
ticks_to_wait -- Operation timeout in CPU cycles. Set to -1 to wait forever.
- Returns
esp_err_t ESP_OK when successful
-
esp_err_t lp_core_i2c_master_write_to_device(i2c_port_t lp_i2c_num, uint16_t device_addr, const uint8_t *data_wr, size_t size, int32_t ticks_to_wait)
Write to I2C device.
Note
The LP I2C must have been initialized from the HP core using the lp_core_i2c_master_init() API before invoking this API.
Note
the LP I2C does not support 10-bit I2C device addresses.
Note
the LP I2C port number is ignored at the moment.
- Parameters
lp_i2c_num -- LP I2C port number
device_addr -- I2C device address (7-bit)
data_wr -- Buffer which holds the data to be written
size -- Size of data to be written in bytes
ticks_to_wait -- Operation timeout in CPU cycles. Set to -1 to wait forever.
- Returns
esp_err_t ESP_OK when successful
-
esp_err_t lp_core_i2c_master_write_read_device(i2c_port_t lp_i2c_num, uint16_t device_addr, const uint8_t *data_wr, size_t write_size, uint8_t *data_rd, size_t read_size, int32_t ticks_to_wait)
Write to and then read from an I2C device in a single transaction.
Note
The LP I2C must have been initialized from the HP core using the lp_core_i2c_master_init() API before invoking this API.
Note
the LP I2C does not support 10-bit I2C device addresses.
Note
the LP I2C port number is ignored at the moment.
- Parameters
lp_i2c_num -- LP I2C port number
device_addr -- I2C device address (7-bit)
data_wr -- Buffer which holds the data to be written
write_size -- Size of data to be written in bytes
data_rd -- Buffer to hold data to be read
read_size -- Size of data to be read in bytes
ticks_to_wait -- Operation timeout in CPU cycles. Set to -1 to wait forever.
- Returns
esp_err_t ESP_OK when successful
-
void lp_core_i2c_master_set_ack_check_en(i2c_port_t lp_i2c_num, bool ack_check_en)
Enable or disable ACK checking by the LP_I2C controller during write operations.
When ACK checking is enabled, the hardware will check the ACK/NACK level received during write operations against the expected ACK/NACK level. If the received ACK/NACK level does not match the expected ACK/NACK level then the hardware will generate the I2C_NACK_INT and a STOP condition will be generated to stop the data transfer.
Note
ACK checking is enabled by default
Note
the LP I2C port number is ignored at the moment.
- Parameters
lp_i2c_num -- LP I2C port number
ack_check_en -- true: enable ACK check false: disable ACK check
Header File
Functions
-
int lp_core_uart_tx_chars(uart_port_t lp_uart_num, const void *src, size_t size)
Send data to the LP UART port if there is space available in the Tx FIFO.
This function will not wait for enough space in the Tx FIFO to be available. It will just fill the available Tx FIFO slots and return when the FIFO is full. If there are no empty slots in the Tx FIFO, this function will not write any data.
- Parameters
lp_uart_num -- LP UART port number
src -- data buffer address
size -- data length to send
- Returns
- (-1) Error
OTHERS (>=0) The number of bytes pushed to the Tx FIFO
-
esp_err_t lp_core_uart_write_bytes(uart_port_t lp_uart_num, const void *src, size_t size, int32_t timeout)
Write data to the LP UART port.
This function will write data to the Tx FIFO. If a timeout value is configured, this function will timeout once the number of CPU cycles expire.
- Parameters
lp_uart_num -- LP UART port number
src -- data buffer address
size -- data length to send
timeout -- Operation timeout in CPU cycles. Set to -1 to wait forever.
- Returns
esp_err_t ESP_OK when successful
-
int lp_core_uart_read_bytes(uart_port_t lp_uart_num, void *buf, size_t size, int32_t timeout)
Read data from the LP UART port.
This function will read data from the Rx FIFO. If a timeout value is configured, then this function will timeout once the number of CPU cycles expire.
- Parameters
lp_uart_num -- LP UART port number
buf -- data buffer address
size -- data length to send
timeout -- Operation timeout in CPU cycles. Set to -1 to wait forever.
- Returns
- (-1) Error
OTHERS (>=0) The number of bytes read from the Rx FIFO
Header File
Functions
-
void lp_core_printf(const char *format, ...)
Print from the LP core.
Note
This function uses the LP UART peripheral to enable prints.The LP UART must be initialized with lp_core_uart_init() before using this function.
Note
This function is not a standard printf function and may not support all format specifiers or special characters.
- Parameters
format -- string to be printed
... -- variable argument list