feat(ulp): New example to show ulp fsm and riscv used in same app

This example shows how to have both ULP FSM and RISCV enabled in kconfig
simultaneously, and use them one after another at run time. A new
parameter TYPE is passed to ulp_embed_binary() function to specify fsm
or riscv in CMakeLists.txt. This way, both ulp_fsm and ulp_riscv source
files can be compiled by their respective toolchains under the same
project.

The example shows ULP FSM incrementing a counter from 0 to 100, ULP
RISC-V incrementing from 100 to 500 and main CPU incrementing from 500
to 1500.
This commit is contained in:
Meet Patel
2026-02-06 16:03:03 +05:30
parent a3f167f1c4
commit 13f894799c
9 changed files with 396 additions and 0 deletions
@@ -0,0 +1,7 @@
examples/system/ulp/ulp_fsm_riscv_combined/counter:
enable:
- if: SOC_ULP_FSM_SUPPORTED == 1 and SOC_RISCV_COPROC_SUPPORTED == 1 and IDF_TARGET in ["esp32s2", "esp32s3"]
temporary: false
reason: Only ESP32-S2 and ESP32-S3 support both FSM and RISC-V ULP coprocessors
depends_components:
- ulp
@@ -0,0 +1,6 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ulp_fsm_riscv_combined)
@@ -0,0 +1,119 @@
| Supported Targets | ESP32-S2 | ESP32-S3 |
| ----------------- | -------- | -------- |
# ULP FSM and RISC-V Combined Example
This example demonstrates how to use both ULP FSM and ULP RISC-V coprocessors sequentially in the same application.
## Overview
The example implements a counter that is incremented in three stages:
1. **ULP FSM Stage**: The FSM coprocessor increments the counter from 0 to 100
2. **ULP RISC-V Stage**: The RISC-V coprocessor continues incrementing from 100 to 500
3. **Main CPU Stage**: The main processor completes the count from 500 to 1500
This demonstrates:
- Enabling both `CONFIG_ULP_COPROC_TYPE_FSM` and `CONFIG_ULP_COPROC_TYPE_RISCV` for sequential use
- Using the `TYPE` parameter in `ulp_embed_binary()` to specify which toolchain to use for each ULP program
- Coordinating execution between FSM ULP, RISC-V ULP, and the main CPU
- Sharing data between different coprocessors and the main CPU
## How to Use Example
### Hardware Required
* ESP32-S2 or ESP32-S3 development board
### Build and Flash
Build the project and flash it to the board, then run monitor tool to view serial output:
```
idf.py -p PORT build flash monitor
```
(Replace PORT with the name of the serial port to use)
(To exit the serial monitor, type ``Ctrl-]``)
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
## Example Output
```
ULP FSM and RISC-V Combined Example
====================================
Step 1: Starting ULP FSM to count from 0 to 100...
HP core going to sleep, will be woken by ULP FSM when counting completes...
HP core woken up by: ULP
ULP FSM completed. Counter value: 100
Step 2: Starting ULP RISC-V to count from 100 to 500...
HP core going to sleep, will be woken by ULP RISC-V when counting completes...
HP core woken up by: ULP
ULP RISC-V completed. Counter value: 500
Step 3: Main CPU counting from 500 to 1500...
Main CPU completed. Final counter value: 1500
====================================
All stages completed successfully!
FSM: 0 -> 100
RISC-V: 100 -> 500
Main CPU: 500 -> 1500
```
## Troubleshooting
* If you see an error about missing ULP type when building, ensure both `CONFIG_ULP_COPROC_TYPE_FSM` and `CONFIG_ULP_COPROC_TYPE_RISCV` are enabled in menuconfig.
* The `TYPE` parameter in `ulp_embed_binary()` is mandatory when both ULP types are enabled. Check `main/CMakeLists.txt` to see the correct syntax:
```cmake
ulp_embed_binary(ulp_fsm_main "${ulp_fsm_sources}" "" TYPE fsm)
ulp_embed_binary(ulp_riscv_main "${ulp_riscv_sources}" "" TYPE riscv)
```
## Implementation Details
### CMakeLists.txt
The main component's CMakeLists.txt shows how to embed both FSM and RISC-V ULP binaries:
```cmake
# ULP FSM
set(ulp_fsm_app_name ulp_fsm_main)
set(ulp_fsm_sources "ulp_fsm/main.S")
ulp_embed_binary(${ulp_fsm_app_name} "${ulp_fsm_sources}" "" TYPE fsm)
# ULP RISC-V
set(ulp_riscv_app_name ulp_riscv_main)
set(ulp_riscv_sources "ulp_riscv/main.c")
ulp_embed_binary(${ulp_riscv_app_name} "${ulp_riscv_sources}" "" TYPE riscv)
```
The `TYPE` parameter is crucial - it tells the build system which toolchain to use for each ULP program.
### ULP FSM Program
The FSM program (`ulp_fsm/main.S`) is written in assembly and:
- Maintains a counter in RTC slow memory
- Increments it on each wakeup
- Halts when reaching 100
### ULP RISC-V Program
The RISC-V program (`ulp_riscv/main.c`) is written in C and:
- Continues from where FSM left off
- Increments the counter on each wakeup
- Halts when reaching 500
### Main Application
The main application coordinates the three stages:
1. Loads and starts the FSM ULP program
2. Waits for FSM to complete
3. Transfers the counter value and starts the RISC-V ULP program
4. Waits for RISC-V to complete
5. Completes the final counting stage on the main CPU
@@ -0,0 +1,20 @@
idf_component_register(SRCS "ulp_fsm_riscv_combined_main.c"
INCLUDE_DIRS "")
#
# ULP FSM support additions to component CMakeLists.txt.
#
set(ulp_fsm_app_name ulp_fsm_main)
set(ulp_fsm_sources "ulp_fsm/main.S")
ulp_embed_binary(${ulp_fsm_app_name} "${ulp_fsm_sources}" "" TYPE fsm)
#
# ULP RISC-V support additions to component CMakeLists.txt.
#
set(ulp_riscv_app_name ulp_riscv_main)
set(ulp_riscv_sources "ulp_riscv/main.c")
ulp_embed_binary(${ulp_riscv_app_name} "${ulp_riscv_sources}" "" TYPE riscv)
@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* ULP FSM assembly program to increment counter */
#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"
/* Define variables */
.bss
.global fsm_counter
fsm_counter:
.long 0
/* Code section */
.text
.global entry
entry:
/* Load counter address */
move r1, fsm_counter
/* Load current counter value */
ld r0, r1, 0
/* Check if counter >= 100 */
jumpr done, 100, ge
/* Increment counter */
add r0, r0, 1
/* Store incremented value */
st r0, r1, 0
/* Halt until next wakeup */
halt
done:
/* Counter reached 100, wake up the main processor */
wake
/* Halt - task complete */
halt
@@ -0,0 +1,124 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "esp_sleep.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"
#include "soc/rtc_periph.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "ulp.h"
#include "ulp_riscv.h"
#include "ulp_fsm_main.h"
#include "ulp_riscv_main.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
extern const uint8_t ulp_fsm_main_bin_start[] asm("_binary_ulp_fsm_main_bin_start");
extern const uint8_t ulp_fsm_main_bin_end[] asm("_binary_ulp_fsm_main_bin_end");
extern const uint8_t ulp_riscv_main_bin_start[] asm("_binary_ulp_riscv_main_bin_start");
extern const uint8_t ulp_riscv_main_bin_end[] asm("_binary_ulp_riscv_main_bin_end");
static void init_ulp_fsm_program(void);
static void init_ulp_riscv_program(void);
void app_main(void)
{
/* Initialize shared counter */
ulp_fsm_counter = 0;
ulp_riscv_counter = 0;
printf("ULP FSM and RISC-V Combined Example\n");
printf("====================================\n\n");
/* Enable ULP wakeup */
ESP_ERROR_CHECK(esp_sleep_enable_ulp_wakeup());
/* Step 1: Start ULP FSM to count from 0 to 100 */
printf("Step 1: Starting ULP FSM to count from 0 to 100...\n");
printf("HP core going to sleep, will be woken by ULP FSM when counting completes...\n");
init_ulp_fsm_program();
/* Enter light sleep - ULP FSM will wake us when done */
esp_light_sleep_start();
/* Woken up by ULP FSM */
uint32_t wakeup_causes = esp_sleep_get_wakeup_causes();
printf("HP core woken up by: %s\n", (wakeup_causes & (1U << ESP_SLEEP_WAKEUP_ULP)) ? "ULP" : "other");
/* Stop FSM ULP */
ulp_timer_stop();
uint32_t fsm_final_count = ulp_fsm_counter; // Save FSM result before RISC-V overwrites it
printf("ULP FSM completed. Counter value: %" PRIu32 "\n\n", fsm_final_count);
/* Small delay to ensure FSM is fully stopped before starting RISC-V */
vTaskDelay(pdMS_TO_TICKS(100));
/* Step 2: Start ULP RISC-V to count from 100 to 500 */
printf("Step 2: Starting ULP RISC-V to count from 100 to 500...\n");
printf("HP core going to sleep, will be woken by ULP RISC-V when counting completes...\n");
init_ulp_riscv_program();
/* Transfer counter value to RISC-V ULP (after loading binary, otherwise it will be overwritten) */
ulp_riscv_counter = fsm_final_count;
/* Enter light sleep - ULP RISC-V will wake us when done */
esp_light_sleep_start();
/* Woken up by ULP RISC-V */
wakeup_causes = esp_sleep_get_wakeup_causes();
printf("HP core woken up by: %s\n", (wakeup_causes & (1U << ESP_SLEEP_WAKEUP_ULP)) ? "ULP" : "other");
/* Stop RISC-V ULP */
ulp_riscv_timer_stop();
uint32_t riscv_final_count = ulp_riscv_counter; // Save RISC-V result
printf("ULP RISC-V completed. Counter value: %" PRIu32 "\n\n", riscv_final_count);
/* Stage 3: Main CPU counts from 500 to 1500 */
printf("Step 3: Main CPU counting from 500 to 1500...\n");
uint32_t counter = riscv_final_count;
while (counter < 1500) {
counter++;
}
printf("Main CPU completed. Final counter value: %" PRIu32 "\n\n", counter);
printf("====================================\n");
printf("All stages completed successfully!\n");
printf("FSM: 0 -> %" PRIu32 "\n", fsm_final_count);
printf("RISC-V: %" PRIu32 " -> %" PRIu32 "\n", fsm_final_count, riscv_final_count);
printf("Main CPU: %" PRIu32 " -> %" PRIu32 "\n", riscv_final_count, counter);
}
static void init_ulp_fsm_program(void)
{
esp_err_t err = ulp_load_binary(0, ulp_fsm_main_bin_start,
(ulp_fsm_main_bin_end - ulp_fsm_main_bin_start) / sizeof(uint32_t));
ESP_ERROR_CHECK(err);
/* Set ULP wake up period to 10ms */
ulp_set_wakeup_period(0, 10000);
/* Start the ULP FSM program */
err = ulp_run(&ulp_entry - RTC_SLOW_MEM);
ESP_ERROR_CHECK(err);
}
static void init_ulp_riscv_program(void)
{
esp_err_t err = ulp_riscv_load_binary(ulp_riscv_main_bin_start,
(ulp_riscv_main_bin_end - ulp_riscv_main_bin_start));
ESP_ERROR_CHECK(err);
/* Set ULP wake up period to 1ms */
ulp_set_wakeup_period(0, 1000);
/* Start the ULP RISC-V program */
err = ulp_riscv_run();
ESP_ERROR_CHECK(err);
}
@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* ULP RISC-V program to increment counter */
#include <stdint.h>
#include "ulp_riscv.h"
#include "ulp_riscv_utils.h"
uint32_t riscv_counter = 0;
int main(void)
{
/* Check if counting is complete */
if (riscv_counter >= 500) {
/* Wake up the main processor and halt */
ulp_riscv_wakeup_main_processor();
ulp_riscv_halt();
return 0;
}
/* Increment counter once per wakeup */
riscv_counter++;
/* Continue - will be woken up again by timer */
return 0;
}
@@ -0,0 +1,35 @@
#!/usr/bin/env python
#
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32s2', 'esp32s3'], indirect=['target'])
def test_ulp_fsm_riscv_combined(dut: Dut) -> None:
dut.expect('ULP FSM and RISC-V Combined Example', timeout=10)
# Check Step 1: FSM counting
dut.expect('Step 1: Starting ULP FSM to count from 0 to 100', timeout=5)
dut.expect('HP core going to sleep, will be woken by ULP FSM when counting completes', timeout=5)
dut.expect('HP core woken up by: ULP', timeout=5)
dut.expect('ULP FSM completed. Counter value: 100', timeout=5)
# Check Step 2: RISC-V counting
dut.expect('Step 2: Starting ULP RISC-V to count from 100 to 500', timeout=5)
dut.expect('HP core going to sleep, will be woken by ULP RISC-V when counting completes', timeout=5)
dut.expect('HP core woken up by: ULP', timeout=5)
dut.expect('ULP RISC-V completed. Counter value: 500', timeout=5)
# Check Step 3: Main CPU counting
dut.expect('Step 3: Main CPU counting from 500 to 1500', timeout=5)
dut.expect('Main CPU completed. Final counter value: 1500', timeout=5)
# Verify success message
dut.expect('All stages completed successfully!', timeout=5)
dut.expect('FSM: 0 -> 100', timeout=5)
dut.expect('RISC-V: 100 -> 500', timeout=5)
dut.expect('Main CPU: 500 -> 1500', timeout=5)
@@ -0,0 +1,11 @@
# Enable both ULP types
CONFIG_ULP_COPROC_ENABLED=y
CONFIG_ULP_COPROC_TYPE_FSM=y
CONFIG_ULP_COPROC_TYPE_RISCV=y
CONFIG_ULP_COPROC_RESERVE_MEM=4096
# Set log level to Info to see output
CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y
CONFIG_BOOTLOADER_LOG_LEVEL=3
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LOG_DEFAULT_LEVEL=3