feat(ulp): implement ulp lp core spinlock

This commit is contained in:
Xiaoyu Liu 2024-07-30 14:42:47 +08:00
parent a760bd2472
commit 446528d40f
12 changed files with 431 additions and 2 deletions

View File

@ -52,7 +52,8 @@ endif()
if(CONFIG_ULP_COPROC_TYPE_LP_CORE)
list(APPEND srcs
"lp_core/lp_core.c"
"lp_core/shared/ulp_lp_core_memory_shared.c")
"lp_core/shared/ulp_lp_core_memory_shared.c"
"lp_core/shared/ulp_lp_core_critical_section_shared.c")
if(CONFIG_SOC_ULP_LP_UART_SUPPORTED)
list(APPEND srcs "lp_core/lp_core_uart.c")

View File

@ -125,7 +125,8 @@ function(ulp_apply_default_sources ulp_app_name)
"${IDF_PATH}/components/ulp/lp_core/lp_core/lp_core_i2c.c"
"${IDF_PATH}/components/ulp/lp_core/lp_core/lp_core_spi.c"
"${IDF_PATH}/components/ulp/lp_core/lp_core/lp_core_ubsan.c"
"${IDF_PATH}/components/ulp/lp_core/shared/ulp_lp_core_lp_adc_shared.c")
"${IDF_PATH}/components/ulp/lp_core/shared/ulp_lp_core_lp_adc_shared.c"
"${IDF_PATH}/components/ulp/lp_core/shared/ulp_lp_core_critical_section_shared.c")
set(target_folder ${IDF_TARGET})

View File

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LOCK_CANDIDATE_LP_CORE = 0,
LOCK_CANDIDATE_HP_CORE_0,
#if CONFIG_FREERTOS_NUMBER_OF_CORES > 1
LOCK_CANDIDATE_HP_CORE_1,
#endif
LOCK_CANDIDATE_NUM_MAX,
} ulp_lp_core_spinlock_candidate_t;
typedef struct {
volatile int level[LOCK_CANDIDATE_NUM_MAX];
volatile int last_to_enter[LOCK_CANDIDATE_NUM_MAX - 1];
volatile unsigned int prev_int_level;
} ulp_lp_core_spinlock_t;
/**
* @brief Initialize the spinlock that protects shared resources between main CPU and LP CPU.
*
* @note The spinlock can be initialized in either main program or LP program.
*
* @param lock Pointer to lock struct
*/
void ulp_lp_core_spinlock_init(ulp_lp_core_spinlock_t *lock);
/**
* @brief Enter the critical section that protects shared resources between main CPU and LP CPU.
*
* @note This critical section is designed for being used by multiple threads, it is safe to try to enter it
* simultaneously from multiple threads on multiple main CPU cores and LP CPU.
*
* @note This critical section does not support nesting entering and exiting.
*
* @param lock Pointer to lock struct
*/
void ulp_lp_core_enter_critical(ulp_lp_core_spinlock_t *lock);
/**
* @brief Exit the critical section that protect shared resource between main CPU and LP CPU.
*
* @note This critical section does not support nesting entering and exiting.
*
* @param lock Pointer to lock struct
*/
void ulp_lp_core_exit_critical(ulp_lp_core_spinlock_t *lock);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "ulp_lp_core_critical_section_shared.h"
#include "esp_cpu.h"
#if IS_ULP_COCPU
#include "ulp_lp_core_interrupts.h"
#endif
/* Masked interrupt threshold varies between different types of interrupt controller */
#if !SOC_INT_CLIC_SUPPORTED
#define INT_MASK_THRESHOLD RVHAL_EXCM_LEVEL
#else /* SOC_INT_CLIC_SUPPORTED */
#define INT_MASK_THRESHOLD RVHAL_EXCM_LEVEL_CLIC
#endif /* !SOC_INIT_CLIC_SUPPORTED */
void ulp_lp_core_spinlock_init(ulp_lp_core_spinlock_t *lock)
{
int i = 0;
for (i = 0; i < LOCK_CANDIDATE_NUM_MAX; i++) {
lock->level[i] = -1;
}
for (i = 0; i < LOCK_CANDIDATE_NUM_MAX - 1; i++) {
lock->last_to_enter[i] = 0;
}
}
#if RTC_MEM_AMO_INSTRUCTIONS_VERIFIED
/* ULP spinlock ACQ/REL functions also have a hardware implementation base on AMOSWAP instructions,
which has much better performance but have not been thoroughly verified yet on RTC memory. This set
of implementation will be adapted once it's verified.
*/
static void ulp_lp_core_spinlock_acquire(ulp_lp_core_spinlock_t *lock)
{
/* Based on sample code for AMOSWAP from RISCV specs v2.1 */
asm volatile(
"li t0, 1\n" // Initialize swap value.
"1:\n"
"lw t1, (%0)\n" // Check if lock is held.
"bnez t1, 1b\n" // Retry if held.
"amoswap.w.aq t1, t0, (%0)\n" // Attempt to acquire lock.
"bnez t1, 1b\n" // Retry if held.
:
: "r"(lock)
: "t0", "t1", "memory"
);
}
static void ulp_lp_core_spinlock_release(ulp_lp_core_spinlock_t *lock)
{
asm volatile(
"amoswap.w.rl x0, x0, (%0)\n" // Release lock by storing 0
:
: "r"(lock)
: "memory"
);
}
#else // !RTC_MEM_AMO_INSTRUCTIONS_VERIFIED
/**
* @brief Obtain the id of current lock candidate.
*
* @return lock candidate id
*/
static int ulp_lp_core_spinlock_get_candidate_id(void)
{
int lock_candidate_id = 0;
#if !IS_ULP_COCPU
/* Refer to ulp_lp_core_spinlock_candidate_t, each HP_CORE lock candidate's index is the core id plus 1 */
lock_candidate_id = esp_cpu_get_core_id() + 1;
#else
lock_candidate_id = LOCK_CANDIDATE_LP_CORE;
#endif
return lock_candidate_id;
}
/**
* @brief Software lock implementation is based on the filter algorithm, which is a generalization of Peterson's algorithm, https://en.wikipedia.org/wiki/Peterson%27s_algorithm#Filter_algorithm:_Peterson's_algorithm_for_more_than_two_processes
*
*/
/**
* @brief Attempt to acquire the lock. Spins until lock is acquired.
*
* @note This lock is designed for being used by multiple threads, it is safe to try to acquire it
* simultaneously from multiple threads on multiple main CPU cores and LP CPU.
*
* @note This function is private to ulp lp core critical section and shall not be called from anywhere else.
*
* @param lock Pointer to lock struct, shared with ULP
*/
static void ulp_lp_core_spinlock_acquire(ulp_lp_core_spinlock_t *lock)
{
/* Level index */
int lv = 0;
/* Candidate index */
int candidate = 0;
/* Index of the current lock candidate */
int lock_candidate_id = ulp_lp_core_spinlock_get_candidate_id();
for (lv = 0; lv < (int)LOCK_CANDIDATE_NUM_MAX - 1; lv++) {
/* Each candidate has to go through all the levels in order to get the spinlock. Start by notifying other candidates, we have reached level `lv` */
lock->level[lock_candidate_id] = lv;
/* Notify other candidates we are the latest one who entered level `lv` */
lock->last_to_enter[lv] = lock_candidate_id;
/* If there is any candidate that reached the same or a higher level than this candidate, wait for it to finish. Advance to the next level if another candidate becomes the latest one to arrive at our current level */
for (candidate = 0; candidate < (int)LOCK_CANDIDATE_NUM_MAX; candidate++) {
while ((candidate != lock_candidate_id) && (lock->level[candidate] >= lv && lock->last_to_enter[lv] == lock_candidate_id)) {
}
}
}
}
/**
* @brief Release the lock.
*
* @note This function is private to ulp lp core critical section and shall not be called from anywhere else.
*
* @param lock Pointer to lock struct, shared with ULP
*/
static void ulp_lp_core_spinlock_release(ulp_lp_core_spinlock_t *lock)
{
int lock_candidate_id = ulp_lp_core_spinlock_get_candidate_id();
lock->level[lock_candidate_id] = -1;
}
#endif
void ulp_lp_core_enter_critical(ulp_lp_core_spinlock_t *lock)
{
/* disable interrupt */
#if !IS_ULP_COCPU // HP core
unsigned prev_int_level = rv_utils_set_intlevel_regval(INT_MASK_THRESHOLD);
lock->prev_int_level = prev_int_level;
#else // LP core
ulp_lp_core_intr_disable();
#endif
/* Busy-wait to acquire the spinlock. Use caution when deploying this lock in time-sensitive scenarios. */
ulp_lp_core_spinlock_acquire(lock);
}
void ulp_lp_core_exit_critical(ulp_lp_core_spinlock_t *lock)
{
ulp_lp_core_spinlock_release(lock);
/* re-enable interrupt */
#if !IS_ULP_COCPU // HP core
unsigned prev_int_level = lock->prev_int_level;
rv_utils_restore_intlevel_regval(prev_int_level);
#else // LP core
ulp_lp_core_intr_enable();
#endif
}

View File

@ -301,6 +301,12 @@ examples/system/ulp/lp_core/gpio_intr_pulse_counter:
depends_components:
- ulp
examples/system/ulp/lp_core/inter_cpu_critical_section/:
enable:
- if: SOC_LP_CORE_SUPPORTED == 1
depends_components:
- ulp
examples/system/ulp/lp_core/interrupt:
enable:
- if: SOC_LP_CORE_SUPPORTED == 1

View File

@ -0,0 +1,7 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.16)
list(APPEND SDKCONFIG_DEFAULTS "sdkconfig.defaults")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(lp_inter_cpu_critical_section_example)

View File

@ -0,0 +1,34 @@
| Supported Targets | ESP32-C5 | ESP32-C6 | ESP32-P4 |
| ----------------- | -------- | -------- | -------- |
# LP Core simple example with inter-CPU critical section:
(See the README.md file in the upper level 'examples' directory for more information about examples.)
## Overview
This example demonstrates the use of an inter-CPU critical section to safeguard shared resources between the main program and the ULP (Ultra-Low-Power) processor.
In this example, a global shared counter `shared_cnt` is incremented by both the high-performance CPU (HP CPU) and the low-power CPU (LP CPU) in turn. To ensure mutual exclusion, a global inter-CPU spinlock is used. With this protection in place, both the HP and LP CPUs attempt to increment the shared counter 100,000 times each.
The inter-CPU critical section is implemented using a global spinlock of type ulp_lp_core_spinlock_t. This type of spinlock is especially designed to protect shared resources between main program and ULP program. It supports multiple threads to attempt to enter critical section simultaneously, while eventually only one thread can succeed. This spinlock must be declared within the ULP program and shall be initialized in either the main program or the ULP program before using it. Note that this critical section doesn't support nested entering and exiting.
## How to use this example
### Build and Flash
Enter `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects.
To observe the mutual exclusion between HP and LP CPUs straight forward, the output of LP CPU is routed to HP CPUs console.
## Example output
```
LP CPU's increment starts, shared counter = 0
core 0 started, cnt = 5868
HP CPU's increment ends, shared counter = 165882
LP CPU's increment ends, shared counter = 250000
...
```

View File

@ -0,0 +1,24 @@
# Register the component
idf_component_register(SRCS "lp_inter_cpu_critical_section_main.c"
INCLUDE_DIRS ""
REQUIRES ulp)
#
# ULP support additions to component CMakeLists.txt.
#
# 1. The LP Core app name must be unique (if multiple components use LP Core).
set(ulp_app_name lp_core_${COMPONENT_NAME})
#
# 2. Specify all C files.
# Files should be placed into a separate directory (in this case, lp_core/),
# which should not be added to COMPONENT_SRCS.
set(ulp_lp_core_sources "lp_core/main.c")
#
# 3. List all the component source files which include automatically
# generated LP Core export file, ${ulp_app_name}.h:
set(ulp_exp_dep_srcs "lp_inter_cpu_critical_section_main..c")
#
# 4. Call function to build ULP binary and embed in project using the argument
# values above.
ulp_embed_binary(${ulp_app_name} "${ulp_lp_core_sources}" "${ulp_exp_dep_srcs}")

View File

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "ulp_lp_core_print.h"
#include "ulp_lp_core_utils.h"
#include "ulp_lp_core_critical_section_shared.h"
volatile uint32_t shared_cnt = 0;
ulp_lp_core_spinlock_t lp_spinlock;
int main (void)
{
/* Initialize the inter-processor spinlock. This must be done on either of HP core and LP core */
ulp_lp_core_spinlock_init(&lp_spinlock);
/* Delay 10ms in case of interleaved console output */
ulp_lp_core_delay_us(10000);
lp_core_printf("LP CPU's increment starts, shared counter = %d\r\n", shared_cnt);
for (int i = 0; i < 100000; i++) {
ulp_lp_core_enter_critical(&lp_spinlock);
shared_cnt++;
ulp_lp_core_exit_critical(&lp_spinlock);
}
lp_core_printf("LP CPU's increment ends, shared counter = %d\r\n", shared_cnt);
return 0;
}

View File

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "lp_core_main.h"
#include "ulp_lp_core.h"
#include "freertos/FreeRTOS.h"
#include "ulp_lp_core_critical_section_shared.h"
extern const uint8_t lp_core_main_bin_start[] asm("_binary_lp_core_main_bin_start");
extern const uint8_t lp_core_main_bin_end[] asm("_binary_lp_core_main_bin_end");
static void lp_core_init(void)
{
/* Set LP core wakeup source as the HP CPU */
ulp_lp_core_cfg_t cfg = {
.wakeup_source = ULP_LP_CORE_WAKEUP_SOURCE_HP_CPU,
.lp_timer_sleep_duration_us = 10000,
};
/* Load LP core firmware */
ESP_ERROR_CHECK(ulp_lp_core_load_binary(lp_core_main_bin_start, (lp_core_main_bin_end - lp_core_main_bin_start)));
/* Run LP core */
ESP_ERROR_CHECK(ulp_lp_core_run(&cfg));
printf("LP core loaded with firmware and running successfully\n");
}
static void inc_num_spinlock_iter(void *arg)
{
int core_id = esp_cpu_get_core_id();
/* Delay 100ms in case of interleaved console output and ensure LP CPU starts first */
vTaskDelay(pdMS_TO_TICKS(100));
printf("core %d started, cnt = %ld\n", core_id, ulp_shared_cnt);
for (int i = 0; i < 100000; i++) {
ulp_lp_core_enter_critical((ulp_lp_core_spinlock_t*)&ulp_lp_spinlock);
ulp_shared_cnt++;
ulp_lp_core_exit_critical((ulp_lp_core_spinlock_t*)&ulp_lp_spinlock);
}
printf("HP CPU's increment ends, shared counter = %ld\n", ulp_shared_cnt);
vTaskDelete(NULL);
}
void app_main(void)
{
/* If user is using USB-serial-jtag then idf monitor needs some time to
* re-connect to the USB port. We wait 1 sec here to allow for it to make the reconnection
* before we print anything. Otherwise the chip will go back to sleep again before the user
* has time to monitor any output.
*/
/* Load LP Core binary and start the coprocessor */
lp_core_init();
// create tasks on each core which would be accessing a shared resource protected by spinlock
for (int core_id = 0; core_id < CONFIG_FREERTOS_NUMBER_OF_CORES; core_id++) {
xTaskCreatePinnedToCore(inc_num_spinlock_iter, NULL, 4096, NULL, 3, NULL, core_id);
}
}

View File

@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.esp32c5
@pytest.mark.esp32c6
@pytest.mark.generic
def test_lp_core_critical_section_main_1_task(dut: Dut) -> None:
dut.expect("LP CPU's increment starts, shared counter = 0")
dut.expect(r'core 0 started, cnt = \d+')
# Either LP and HP can stop increasing earlier
dut.expect('increment ends, shared counter = 200000')
@pytest.mark.esp32p4
@pytest.mark.generic
def test_lp_core_critical_section_main_2_tasks(dut: Dut) -> None:
dut.expect("LP CPU's increment starts, shared counter = 0")
dut.expect(r'core 0 started, cnt = \d+')
dut.expect(r'core 1 started, cnt = \d+')
# Either LP and HP can stop increasing earlier
dut.expect('increment ends, shared counter = 300000')

View File

@ -0,0 +1,11 @@
# Enable LP Core
CONFIG_ULP_COPROC_ENABLED=y
CONFIG_ULP_COPROC_TYPE_LP_CORE=y
CONFIG_ULP_COPROC_RESERVE_MEM=8192
# Route LP Core output to HP
CONFIG_ULP_PANIC_OUTPUT_ENABLE=y
CONFIG_ULP_HP_UART_CONSOLE_PRINT=y
# Extend Task Watchdog timeout period to 10 seconds
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10