feat(ulp): Improved ULP delay API accuracy and removed floating point operations

Closes https://github.com/espressif/esp-idf/issues/17494
Closes https://github.com/espressif/esp-idf/issues/16891
This commit is contained in:
Konstantin Kondrashov
2026-03-20 16:44:14 +08:00
committed by BOT
parent 329ed7ddce
commit 6fe2cf575f
20 changed files with 680 additions and 73 deletions
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "sdkconfig.h"
#if CONFIG_IDF_TARGET_ESP32S2
// ULP-RISC-V runs at 8.5MHz, which corresponds to 17/2 = 8.5 cycles per microsecond.
#define ULP_RISCV_CPU_FREQUENCY_HZ 8500000U
#define ULP_RISCV_CYCLES_PER_US_NUM 17U
#define ULP_RISCV_CYCLES_PER_US_DENOM 2U
#elif CONFIG_IDF_TARGET_ESP32S3
// ULP-RISC-V runs at 17.5MHz, which corresponds to 35/2 = 17.5 cycles per microsecond.
#define ULP_RISCV_CPU_FREQUENCY_HZ 17500000U
#define ULP_RISCV_CYCLES_PER_US_NUM 35U
#define ULP_RISCV_CYCLES_PER_US_DENOM 2U
#endif
@@ -14,6 +14,7 @@ extern "C" {
#include <stdint.h>
#include "ulp_riscv_register_ops.h"
#include "ulp_riscv_interrupt.h"
#include "ulp_riscv_cpu_freq_shared.h"
/**
* @brief Wakeup main CPU from sleep or deep sleep.
@@ -79,15 +80,8 @@ void ulp_riscv_timer_resume(void);
asm volatile("rdcycle %0;" : "=r"(__ccount)); \
__ccount; })
#if CONFIG_IDF_TARGET_ESP32S2
/* These are only approximate default numbers, the default frequency
of the 8M oscillator is 8.5MHz +/- 5%, at the default DCAP setting
*/
#define ULP_RISCV_CYCLES_PER_US 8.5
#elif CONFIG_IDF_TARGET_ESP32S3
#define ULP_RISCV_CYCLES_PER_US 17.5
#endif
#define ULP_RISCV_CYCLES_PER_MS ULP_RISCV_CYCLES_PER_US*1000
#define ULP_RISCV_CYCLES_PER_US ULP_RISCV_CYCLES_PER_US_NUM / ULP_RISCV_CYCLES_PER_US_DENOM
#define ULP_RISCV_CYCLES_PER_MS 1000U * ULP_RISCV_CYCLES_PER_US
/**
* @brief Retrieves the current number of CPU cycles.
@@ -100,25 +94,70 @@ static inline uint32_t ulp_riscv_get_cpu_cycles(void)
}
/**
* @brief Makes the co-processor busy wait for a certain number of cycles
* @brief Check whether an mcycle-based timeout has elapsed.
*
* @note This function is not accurate for delays shorter than 20 cycles,
* as the function's own overhead may exceed the requested delay.
* @note A timeout value of -1 means "wait forever".
* Other values are interpreted as unsigned cycle counts.
*
* @param cycles Number of cycles to busy wait
* @param start_cycle_count Cycle counter value captured at timeout start.
* @param cycles_to_wait Timeout in CPU cycles, or -1 to disable timeout.
*
* @return true if timeout elapsed, false otherwise.
*/
void static inline ulp_riscv_delay_cycles(uint32_t cycles)
static inline bool ulp_riscv_is_timeout_elapsed(uint32_t start_cycle_count, int32_t cycles_to_wait)
{
if (cycles <= 20) { // the estimate of cycles for this function
if (cycles_to_wait == -1) {
return false;
}
return (ulp_riscv_get_cpu_cycles() - start_cycle_count) >= (uint32_t)cycles_to_wait;
}
/**
* @brief Makes the co-processor busy-wait for a certain number of CPU cycles.
*
* @note This function is not accurate for delays shorter than 20 cycles because the
* function overhead may exceed the requested delay.
*
* @note The maximum supported delay is 0x7FFFFFFF cycles.
* For larger values, the behavior is undefined. Split longer delays into smaller
* chunks if needed.
*
* For reference, this corresponds approximately to:
* - ESP32-S2 ULP-RISC-V @ 8.5 MHz: 0x7FFFFFFF cycles ≈ 252.645 s
* - ESP32-S3 ULP-RISC-V @ 17.5 MHz: 0x7FFFFFFF cycles ≈ 122.713 s
*
* @param cycles Number of cycles to busy-wait.
*/
static inline void ulp_riscv_delay_cycles(uint32_t cycles)
{
if (cycles <= 20U) { // estimate of cycles for this function overhead
return;
}
/* Off with the estimate of cycles to improve accuracy */
uint32_t end = ULP_RISCV_GET_CCOUNT() + cycles - 20;
while (ULP_RISCV_GET_CCOUNT() < end) {
/* Wait */
// To improve accuracy subtract (20 + 15) cycles overhead, defined by delay calibration test
uint32_t start = ULP_RISCV_GET_CCOUNT() - 20U - 15U;
while ((uint32_t)(ULP_RISCV_GET_CCOUNT() - start) < cycles) {
/* busy wait */
}
}
/**
* @brief Makes the co-processor busy-wait for a certain number of microseconds.
*
* @note This function is not accurate for short delays because the function overhead
* may exceed the requested delay. For very small delays the implementation uses
* a fixed sequence of NOPs (chip-dependent thresholds).
*
* @note The maximum supported delay depends on the ULP-RISC-V cycle counter width and on
* the internal cycles-per-microsecond conversion. For values above the limits below,
* the computed delay may overflow and the result is undefined.
* - ESP32-S2 ULP-RISC-V @ 8.5 MHz: delay_us must be <= 252645135 (about 252.6 s)
* - ESP32-S3 ULP-RISC-V @ 17.5 MHz: delay_us must be <= 122713351 (about 122.7 s)
*
* @param delay_us Number of microseconds to busy wait.
*/
void ulp_riscv_delay_us(uint32_t delay_us);
/**
* @brief Clears the GPIO wakeup interrupt bit
*
@@ -11,7 +11,7 @@
#include "ulp_riscv_uart_ulp_core.h"
/* We calculate the bit duration at compile time to speed up and avoid pulling in soft-float libs */
#define BIT_DURATION_CYCLES ( ULP_RISCV_CYCLES_PER_US * ((1000*1000) / CONFIG_ULP_RISCV_UART_BAUDRATE) )
#define BIT_DURATION_CYCLES ( (ULP_RISCV_CYCLES_PER_US_NUM * 1000000) / (ULP_RISCV_CYCLES_PER_US_DENOM * CONFIG_ULP_RISCV_UART_BAUDRATE) )
void ulp_riscv_uart_init(ulp_riscv_uart_t *uart, const ulp_riscv_uart_cfg_t *cfg)
{
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -11,6 +11,7 @@
#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"
#include "soc/sens_reg.h"
#include "ulp_riscv_cpu_freq_shared.h"
void ulp_riscv_rescue_from_monitor(void)
{
@@ -77,3 +78,71 @@ void ulp_riscv_trigger_sw_intr(void)
}
#endif /* CONFIG_ULP_RISCV_INTERRUPT_ENABLE */
void ulp_riscv_delay_us(uint32_t delay_us)
{
#if CONFIG_IDF_TARGET_ESP32S3
/*
* For very small delays, entering the generic cycle-count loop adds too much fixed overhead.
* Use a short calibrated NOP path instead to improve small-delay accuracy on ESP32-S3.
*/
if (delay_us <= 5) {
goto fast_return;
}
#elif CONFIG_IDF_TARGET_ESP32S2
/*
* Same principle as S3, but with a different threshold due to target-specific timing/overhead.
*/
if (delay_us <= 10) {
goto fast_return;
}
#endif
/*
* Generic delay path:
* - Convert requested microseconds to CPU cycles using ratio macros.
* - Pre-subtract measured function overhead (55 cycles), so observed delay is closer to request.
*/
uint32_t start = ulp_riscv_get_cpu_cycles() - 55U;
uint32_t req_delay = delay_us * ULP_RISCV_CYCLES_PER_US_NUM / ULP_RISCV_CYCLES_PER_US_DENOM;
/*
* Busy-wait until elapsed cycles reach req_delay.
* uint32_t subtraction intentionally relies on wrap-around-safe arithmetic for cycle counter rollover.
*/
while ((uint32_t)(ulp_riscv_get_cpu_cycles() - start) < req_delay) {
/* busy wait */
}
return;
fast_return:
#if CONFIG_IDF_TARGET_ESP32S3
/*
* Fast path for tiny delays:
* Use discrete NOP counts calibrated for this target.
* Note: (delay_us == 0 || delay_us <= 2) is intentionally kept as-is to avoid behavior changes.
*/
if (delay_us == 0 || delay_us <= 2) {
return;
} else if (delay_us <= 3) {
asm volatile("nop\n");
} else if (delay_us <= 4) {
asm volatile("nop\n nop\n");
} else {
asm volatile("nop\n nop\n nop\n nop\n nop\n");
}
#elif CONFIG_IDF_TARGET_ESP32S2
/*
* ESP32-S2 calibrated NOP mapping for very short delays.
*/
if (delay_us == 0) {
return;
} else if (delay_us <= 6) {
asm volatile("nop\n");
} else if (delay_us <= 8) {
asm volatile("nop\n nop\n");
} else {
asm volatile("nop\n nop\n nop\n nop\n nop\n nop\n");
}
#endif
}