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-05 10:39:34 +02:00
parent bbee94f718
commit 74561963e1
20 changed files with 640 additions and 73 deletions
@@ -6,7 +6,7 @@ set(ulp_sources4 "ulp/test_main_i2c.c")
idf_component_register(SRCS ${app_sources}
INCLUDE_DIRS "ulp"
REQUIRES ulp unity test_utils driver
REQUIRES ulp unity test_utils driver esp_timer
WHOLE_ARCHIVE)
set(ulp_app_name ulp_test_app)
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2010-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2010-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -23,6 +23,8 @@
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_adc/adc_oneshot.h"
#include "ulp_riscv_cpu_freq_shared.h"
#include "esp_timer.h"
#define ULP_WAKEUP_PERIOD 1000000 // 1 second
@@ -468,3 +470,163 @@ TEST_CASE("ULP ADC can init-deinit-init", "[ulp]")
TEST_ASSERT_EQUAL(ESP_OK, ulp_adc_init(&riscv_adc_cfg));
TEST_ASSERT_EQUAL(ESP_OK, ulp_adc_deinit());
}
TEST_CASE("Test ULP RISCV delay", "[ulp]")
{
int64_t start, diff;
const uint32_t delay_period_us = 5000000;
const uint32_t delta_us = 500000; // RTC FAST is not very accurate
volatile uint32_t *command_resp = (volatile uint32_t *)&ulp_command_resp;
volatile uint32_t *main_cpu_command = (volatile uint32_t *)&ulp_main_cpu_command;
volatile uint32_t *main_cpu_reply = (volatile uint32_t *)&ulp_main_cpu_reply;
volatile uint32_t *test_data_in = (volatile uint32_t *)&ulp_riscv_test_data_in;
/* Load ULP RISC-V firmware and start the coprocessor */
load_and_start_ulp_firmware(ulp_main_bin_start, ulp_main_bin_length);
/* Setup test data */
*command_resp = RISCV_NO_COMMAND;
*main_cpu_reply = RISCV_NO_COMMAND;
*test_data_in = delay_period_us;
*main_cpu_command = RISCV_DELAY_TEST;
/* Wait till we receive the correct command response */
while (*command_resp != RISCV_DELAY_TEST) {
}
start = esp_timer_get_time();
/* Wait till we receive COMMAND_OK reply */
while (*main_cpu_reply != RISCV_COMMAND_OK) {
}
diff = esp_timer_get_time() - start;
printf("Waited for %" PRIi64 "us, expected: %" PRIu32 "us\n", diff, delay_period_us);
TEST_ASSERT_INT_WITHIN(delta_us, delay_period_us, diff);
/* Clear test data */
*main_cpu_command = RISCV_NO_COMMAND;
}
static void print_delay_table_header(void)
{
printf("%10s %10s %10s %10s %10s\n",
"----------", "----------", "----------", "----------", "----------");
printf("%10s %10s %10s %10s %10s\n",
"delay(us)", "expec_tk", "avg_tk", "error_tk", "in_range");
printf("%10s %10s %10s %10s %10s\n",
"----------", "----------", "----------", "----------", "----------");
}
static void run_delay_calibration_test(riscv_test_commands_t delay_calibration_cmd, const char *delay_api_name)
{
volatile uint32_t *command_resp = (volatile uint32_t *)&ulp_command_resp;
volatile uint32_t *main_cpu_command = (volatile uint32_t *)&ulp_main_cpu_command;
volatile uint32_t *delay_sub_command = (volatile uint32_t *)&ulp_delay_sub_command;
volatile uint32_t *test_data_in = (volatile uint32_t *)&ulp_riscv_test_data_in;
const uint32_t test_delays_us[] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 40, 50, 70, 90,
100, 200, 500, 1000, 2000, 5000,
10000, 20000, 50000, 100000,
};
const uint32_t ulp_cpu_freq_hz = ULP_RISCV_CPU_FREQUENCY_HZ;
const double ulp_tick_time_us = 1000000.0 / ulp_cpu_freq_hz;
const double ulp_ticks_per_us = 1.0 / ulp_tick_time_us;
#if CONFIG_IDF_TARGET_ESP32S2
const uint32_t warmup_exclude_ticks = 25;
const double error_limit_time_us = 2.0;
#else // ESP32S3
const uint32_t warmup_exclude_ticks = 17;
const double error_limit_time_us = 1.0;
#endif
const int32_t error_limit_ticks = (int32_t)(error_limit_time_us * ulp_ticks_per_us + 0.5);
int32_t correction_ticks_checked = 0;
uint32_t out_of_range_count = 0;
uint32_t checked_measurements = 0;
uint32_t skipped_warmup_measurements = 0;
printf("Delay API under test: %s\n", delay_api_name);
printf("ULP CPU frequency: %" PRIu32 " Hz, tick time: %.6f us, ticks/us: %.3f\n",
ulp_cpu_freq_hz, ulp_tick_time_us, ulp_ticks_per_us);
printf("Tolerance: +/- %" PRId32 " ticks (%.3f us)\n", error_limit_ticks, error_limit_time_us);
printf("Skipping delay calibration points with expected ticks <= %" PRIu32 " ticks (%.3f us) as warmup\n",
warmup_exclude_ticks, warmup_exclude_ticks * ulp_tick_time_us);
load_and_start_ulp_firmware(ulp_main_bin_start, ulp_main_bin_length);
*delay_sub_command = RISCV_DELAY_SUBCMD_NONE;
*command_resp = RISCV_NO_COMMAND;
*main_cpu_command = delay_calibration_cmd;
while (*command_resp != delay_calibration_cmd) { };
print_delay_table_header();
const uint32_t num_delays = sizeof(test_delays_us) / sizeof(test_delays_us[0]);
for (uint32_t delay_idx = 0; delay_idx < num_delays; delay_idx++) {
const uint32_t requested_delay_us = test_delays_us[delay_idx];
uint64_t sum_delay_ticks = 0;
const uint32_t samples = 3;
const uint32_t expected_ticks = requested_delay_us * ulp_ticks_per_us;
for (uint32_t sample_idx = 0; sample_idx < samples; sample_idx++) {
*test_data_in = (delay_calibration_cmd == RISCV_DELAY_CYCLES_CALIBRATION_TEST) ? expected_ticks : requested_delay_us;
while (*delay_sub_command != RISCV_DELAY_SUBCMD_READY) { };
*delay_sub_command = RISCV_DELAY_SUBCMD_RUN;
/* Wait for the ULP to finish its delay before reading the cycle counters from RTC memory.
* The HP core delay must be at least as long as the requested ULP delay, with some margin
* to ensure that we do not read RTC memory while the ULP is running the delay calibration code. */
vTaskDelay(((requested_delay_us / 1000 + portTICK_PERIOD_MS + 1) / portTICK_PERIOD_MS) * 3);
sum_delay_ticks += ulp_delay_end_cycles - ulp_delay_start_cycles;
}
const uint64_t avg_delay_ticks = sum_delay_ticks / samples;
const int32_t error_tk = expected_ticks - avg_delay_ticks;
const char *range_status = "PASS";
if (expected_ticks <= warmup_exclude_ticks) {
skipped_warmup_measurements++;
range_status = "SKIP";
} else {
checked_measurements++;
correction_ticks_checked += error_tk;
if ((error_tk < -error_limit_ticks) || (error_tk > error_limit_ticks)) {
out_of_range_count++;
range_status = "FAIL";
}
}
printf("%10" PRIu32 " %10" PRIu32 " %10" PRIu64 " %10" PRId32 " %10s\n",
requested_delay_us, expected_ticks, avg_delay_ticks, error_tk, range_status);
}
print_delay_table_header();
// Release the ULP from the delay test loop
*main_cpu_command = RISCV_NO_COMMAND;
*test_data_in = 0;
*delay_sub_command = RISCV_DELAY_SUBCMD_RUN;
TEST_ASSERT_MESSAGE(checked_measurements > 0, "No delay calibration points were checked");
double total_correct_ticks = (double)correction_ticks_checked / (double)checked_measurements;
printf("Average correction ticks: %.1f (%.3f us)\n", total_correct_ticks, total_correct_ticks * ulp_tick_time_us);
printf("Range-check summary (%s): checked=%" PRIu32 ", skipped_warmup=%" PRIu32 ", out_of_range=%" PRIu32
", limit=+/- %" PRId32 " ticks\n",
delay_api_name, checked_measurements, skipped_warmup_measurements, out_of_range_count, error_limit_ticks);
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, out_of_range_count, "Delay calibration has out-of-range error_tk values");
TEST_ASSERT_DOUBLE_WITHIN(7.0, 0.0, total_correct_ticks);
}
TEST_CASE("Test delay calibration for ulp_riscv_delay_cycles", "[ulp]")
{
run_delay_calibration_test(RISCV_DELAY_CYCLES_CALIBRATION_TEST, "ulp_riscv_delay_cycles");
}
TEST_CASE("Test delay calibration for ulp_riscv_delay_us", "[ulp]")
{
run_delay_calibration_test(RISCV_DELAY_US_CALIBRATION_TEST, "ulp_riscv_delay_us");
}
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2010-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2010-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -18,10 +18,51 @@ volatile riscv_test_commands_t command_resp = RISCV_NO_COMMAND;
volatile uint32_t riscv_test_data_in = 0;
volatile uint32_t riscv_test_data_out = 0;
volatile uint32_t riscv_counter = 0;
volatile uint32_t delay_start_cycles = 0;
volatile uint32_t delay_end_cycles = 0;
volatile uint32_t delay_sub_command = RISCV_DELAY_SUBCMD_NONE;
volatile uint32_t riscv_incrementer = 0;
ulp_riscv_lock_t lock;
static void run_delay_calibration_test(riscv_test_commands_t calibration_cmd, bool use_cycles_delay)
{
register uint32_t start, end, overhead = 0;
const uint32_t iterations = 5;
for (int i = 0; i < iterations; i++) {
start = ulp_riscv_get_cpu_cycles();
end = ulp_riscv_get_cpu_cycles();
overhead += end - start;
}
overhead /= iterations;
/* Enter delay-test session and stay here until HP signals completion. */
command_resp = calibration_cmd;
while (main_cpu_command == calibration_cmd) {
delay_sub_command = RISCV_DELAY_SUBCMD_READY;
while (delay_sub_command != RISCV_DELAY_SUBCMD_RUN) { };
register uint32_t requested_delay = riscv_test_data_in;
ulp_riscv_delay_us(1);
if (use_cycles_delay) {
start = ulp_riscv_get_cpu_cycles();
ulp_riscv_delay_cycles(requested_delay);
end = ulp_riscv_get_cpu_cycles();
} else {
start = ulp_riscv_get_cpu_cycles();
ulp_riscv_delay_us(requested_delay);
end = ulp_riscv_get_cpu_cycles();
}
delay_sub_command = RISCV_DELAY_SUBCMD_NONE;
delay_start_cycles = start;
delay_end_cycles = end - overhead;
}
}
void handle_commands(riscv_test_commands_t cmd)
{
riscv_counter++;
@@ -41,6 +82,25 @@ void handle_commands(riscv_test_commands_t cmd)
ulp_riscv_wakeup_main_processor();
break;
case RISCV_DELAY_TEST:
/* Echo the command ID back to the main CPU */
command_resp = RISCV_DELAY_TEST;
ulp_riscv_delay_us(riscv_test_data_in);
main_cpu_reply = RISCV_COMMAND_OK;
main_cpu_command = RISCV_NO_COMMAND;
/* Wakeup the main CPU */
ulp_riscv_wakeup_main_processor();
break;
case RISCV_DELAY_CYCLES_CALIBRATION_TEST:
run_delay_calibration_test(RISCV_DELAY_CYCLES_CALIBRATION_TEST, true);
break;
case RISCV_DELAY_US_CALIBRATION_TEST:
run_delay_calibration_test(RISCV_DELAY_US_CALIBRATION_TEST, false);
break;
case RISCV_DEEP_SLEEP_WAKEUP_SHORT_DELAY_TEST:
/* Echo the command ID back to the main CPU */
command_resp = RISCV_DEEP_SLEEP_WAKEUP_SHORT_DELAY_TEST;
@@ -48,7 +108,7 @@ void handle_commands(riscv_test_commands_t cmd)
/* Set the command reply status */
main_cpu_reply = RISCV_COMMAND_OK;
ulp_riscv_delay_cycles(1000 * ULP_RISCV_CYCLES_PER_MS);
ulp_riscv_delay_us(1000000);
/* Wakeup the main CPU */
ulp_riscv_wakeup_main_processor();
@@ -61,7 +121,7 @@ void handle_commands(riscv_test_commands_t cmd)
/* Set the command reply status */
main_cpu_reply = RISCV_COMMAND_OK;
ulp_riscv_delay_cycles(10000 * ULP_RISCV_CYCLES_PER_MS);
ulp_riscv_delay_us(10000000);
/* Wakeup the main CPU */
ulp_riscv_wakeup_main_processor();
@@ -11,7 +11,7 @@
int main(void)
{
// Wait for the main core in the test case to enter lightsleep
ulp_riscv_delay_cycles(100 * ULP_RISCV_CYCLES_PER_MS);
ulp_riscv_delay_us(100000);
/* Make sure ULP core crashes by doing a NULL pointer access */
uint32_t *null_ptr = NULL;
*null_ptr = 1;
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -16,6 +16,9 @@
typedef enum {
RISCV_READ_WRITE_TEST = 1,
RISCV_DELAY_TEST,
RISCV_DELAY_CYCLES_CALIBRATION_TEST,
RISCV_DELAY_US_CALIBRATION_TEST,
RISCV_DEEP_SLEEP_WAKEUP_SHORT_DELAY_TEST,
RISCV_DEEP_SLEEP_WAKEUP_LONG_DELAY_TEST,
RISCV_LIGHT_SLEEP_WAKEUP_TEST,
@@ -29,3 +32,9 @@ typedef enum {
RISCV_COMMAND_NOK,
RISCV_COMMAND_INVALID,
} riscv_test_command_reply_t;
typedef enum {
RISCV_DELAY_SUBCMD_NONE = 0,
RISCV_DELAY_SUBCMD_READY,
RISCV_DELAY_SUBCMD_RUN,
} riscv_delay_sub_command_t;