From 5cfe060d8ede211fa20881e10f51d5086b7377ef Mon Sep 17 00:00:00 2001 From: Omar Chebib Date: Fri, 13 Mar 2026 11:16:35 +0800 Subject: [PATCH] feat(esp_hw_support): implement private shared interrupts --- .../esp_hw_support/include/esp_intr_alloc.h | 60 +++- components/esp_hw_support/intr_alloc.c | 214 +++++++++-- .../main/test_intr_alloc.c | 340 +++++++++++++++++- docs/en/api-reference/system/intr_alloc.rst | 14 +- 4 files changed, 585 insertions(+), 43 deletions(-) diff --git a/components/esp_hw_support/include/esp_intr_alloc.h b/components/esp_hw_support/include/esp_intr_alloc.h index b2f1b7bfc6..6a36fd1547 100644 --- a/components/esp_hw_support/include/esp_intr_alloc.h +++ b/components/esp_hw_support/include/esp_intr_alloc.h @@ -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 */ @@ -41,6 +41,7 @@ extern "C" { #define ESP_INTR_FLAG_EDGE (1<<9) ///< Edge-triggered interrupt #define ESP_INTR_FLAG_IRAM (1<<10) ///< ISR can be called if cache is disabled #define ESP_INTR_FLAG_INTRDISABLED (1<<11) ///< Return with this interrupt disabled +#define ESP_INTR_FLAG_SHARED_PRIVATE (1<<12) ///< Interrupt can be shared with `*_bind` functions only #define ESP_INTR_FLAG_LOWMED (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3) ///< Low and medium prio interrupts. These can be handled in C. #define ESP_INTR_FLAG_HIGH (ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|ESP_INTR_FLAG_NMI) ///< High level interrupts. Need to be handled in assembly. @@ -81,6 +82,25 @@ extern "C" { /** Disable interrupt by interrupt number */ #define ESP_INTR_DISABLE(inum) esp_intr_disable_source(inum) +/** + * @brief Structure used to specify all the information needed to allocate an interrupt. + * This structure can be passed to the function esp_intr_alloc_info + */ +typedef struct { + int source; /*!< Interrupt source to map */ + int flags; /*!< ORred mask of the ESP_INTR_FLAG_* defines */ + uint32_t intrstatusreg; /*!< Address of the interrupt status register */ + uint32_t intrstatusmask; /*!< Mask of the interrupt status register */ + intr_handler_t handler; /*!< Interrupt handler to invoke upon triggered interrupt */ + void *arg; /*!< Argument to pass to the interrupt handler */ + struct { + intr_handle_t handle; /*!< Handle of the interrupt to share with */ + const char* name; /*!< Name of the interrupt to share with */ + } bind_by; /*!< Interrupt to share with, to be populated when flags are set to + * ESP_INTR_FLAG_SHARED or ESP_INTR_FLAG_SHARED_PRIVATE, and only provide one of + * `handle` or `name`. */ +} esp_intr_alloc_info_t; + /** * @brief Mark an interrupt as a shared interrupt * @@ -213,13 +233,15 @@ esp_err_t esp_intr_alloc_intrstatus(int source, int flags, uint32_t intrstatusre * @param arg Optional argument for passed to the interrupt handler * @param shared_handle Previously allocated interrupt to share the CPU interrupt line with. If NULL, * calling this function equivalent to esp_intr_alloc, else, ESP_INTR_FLAG_SHARED must - * be provided in the flags parameter. + * be provided in the flags parameter. If not NULL, the given `flags` must present the bit + * ESP_INTR_FLAG_SHARED or ESP_INTR_FLAG_SHARED_PRIVATE. * @param ret_handle Pointer to an intr_handle_t to store a handle that can later be * used to request details or free the interrupt. Can be NULL if no handle * is required. * * @return ESP_ERR_INVALID_ARG if the combination of arguments is invalid. - * ESP_ERR_NOT_FOUND No free interrupt found with the specified flasg or the given level is different + * ESP_ERR_INVALID_STATE if the provided handler refers to an interrupt that on the other core. + * ESP_ERR_NOT_FOUND No free interrupt found with the specified flags or the given level is different * from the one assigned to the share_handle parameter. * ESP_OK on success */ @@ -256,19 +278,47 @@ esp_err_t esp_intr_alloc_bind(int source, int flags, intr_handler_t handler, voi * @param arg Optional argument for passed to the interrupt handler * @param shared_handle Previously allocated interrupt to share the CPU interrupt line with. If NULL, * calling this function equivalent to esp_intr_alloc, else, ESP_INTR_FLAG_SHARED must - * be provided in the flags parameter. + * be provided in the flags parameter. If not NULL, the given `flags` must present the bit + * ESP_INTR_FLAG_SHARED or ESP_INTR_FLAG_SHARED_PRIVATE. * @param ret_handle Pointer to an intr_handle_t to store a handle that can later be * used to request details or free the interrupt. Can be NULL if no handle * is required. * * @return ESP_ERR_INVALID_ARG if the combination of arguments is invalid. - * ESP_ERR_NOT_FOUND No free interrupt found with the specified flasg or the given level is different + * ESP_ERR_INVALID_STATE if the provided handler refers to an interrupt that on the other core. + * ESP_ERR_NOT_FOUND No free interrupt found with the specified flags or the given level is different * from the one assigned to the share_handle parameter. * ESP_OK on success */ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrstatusreg, uint32_t intrstatusmask, intr_handler_t handler, void *arg, intr_handle_t shared_handle, intr_handle_t *ret_handle); + +/** + * @brief Allocate an interrupt with all the parameters in a single structure + * + * This function does the same as `esp_intr_alloc_intrstatus_bind`, but allows specifying all the parameters in a single structure. + * Check the parameters description in `esp_intr_alloc_intrstatus_bind` for more details. + * + * @note This function allows to create or join a group of interrupts to share a same interrupt line. + * When `ESP_INTR_FLAG_SHARED_PRIVATE` or `ESP_INTR_FLAG_SHARED` flags are set, `handle` or `name` can be used to specify + * the interrupt to share with (previously allocated). Only one of `handle` or `name` can be used, not both. + * If a `name` is passed but the interrupt to share with is not found, the function will create a new group of interrupts + * with the given name. + * + * @param info The information to allocate the interrupt. + * @param ret_handle Pointer to an intr_handle_t to store a handle that can later be + * used to request details or free the interrupt. Can be NULL if no handle + * is required. + * + * @return ESP_ERR_INVALID_ARG if the combination of arguments is invalid. + * ESP_ERR_INVALID_STATE if the provided handler refers to an interrupt that on the other core. + * ESP_ERR_NOT_FOUND No free interrupt found with the specified flags or the given level is different + * from the one assigned to the share_handle parameter. + * ESP_OK on success + */ +esp_err_t esp_intr_alloc_info(const esp_intr_alloc_info_t *info, intr_handle_t *ret_handle); + /** * @brief Disable and free an interrupt. * diff --git a/components/esp_hw_support/intr_alloc.c b/components/esp_hw_support/intr_alloc.c index df10ef4736..1ab1835c4a 100644 --- a/components/esp_hw_support/intr_alloc.c +++ b/components/esp_hw_support/intr_alloc.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -77,10 +77,31 @@ struct shared_vector_desc_t { #define VECDESC_FL_RESERVED (1<<0) #define VECDESC_FL_INIRAM (1<<1) +/** + * The following two bits are used to mark whether the interrupt is shared or not (public or private). + * Here is a table to explain the behavior: + * | FL_SHARED | FL_NONSHARED | Description | + * |-----------|------------- |------------------------- | + * | 0 | 0 | Initialization phase | + * | 0 | 1 | Non-shared interrupt | + * | 1 | 0 | Public shared interrupt | + * | 1 | 1 | Private shared interrupt | + * |-----------|------------- |------------------------- | + * + * Private shared interrupt is used for interrupts that are shared with `*_bind` functions only, they cannot + * be allocated using `esp_intr_alloc` functions. + * + * During initialization, both flags are low, it will result in the usage of `esp_cpu_intr_has_handler` + * to determine if the interrupt is in use outside of the interrupt allocator. + */ #define VECDESC_FL_SHARED (1<<2) #define VECDESC_FL_NONSHARED (1<<3) #define VECDESC_FL_TYPE_MASK (0xf) +/* Define an alias for visibility flags */ +#define VECDESC_FL_PRIVATE (VECDESC_FL_NONSHARED) + + #if SOC_CPU_HAS_FLEXIBLE_INTC /* On targets that have configurable interrupts levels, store the assigned level in the flags */ #define VECDESC_FL_LEVEL_SHIFT (8) @@ -96,7 +117,8 @@ struct vector_desc_t { unsigned int cpu: 1; unsigned int intno: 5; int source: 16; //Interrupt mux flags, used when not shared - shared_vector_desc_t *shared_vec_info; //used when VECDESC_FL_SHARED + shared_vector_desc_t *shared_vec_info; //used when VECDESC_FL_SHARED is set + char* group_name; // used when VECDESC_FL_SHARED is set vector_desc_t *next; }; @@ -214,6 +236,24 @@ static vector_desc_t * find_desc_for_source(int source, int cpu) return vd; } +/** + * @brief Find a vector descriptor for a given name + * + * @param name The name of the vector descriptor to find + * @return The vector descriptor if found, NULL otherwise + */ +static vector_desc_t *find_desc_for_name(const char *name) +{ + vector_desc_t *vd = vector_desc_head; + while (vd != NULL) { + if (vd->group_name != NULL && strcmp(vd->group_name, name) == 0) { + break; + } + vd = vd->next; + } + return vd; +} + esp_err_t esp_intr_mark_shared(int intno, int cpu, bool is_int_ram) { if (intno>31) { @@ -259,13 +299,19 @@ esp_err_t esp_intr_reserve(int intno, int cpu) return ESP_OK; } -static bool is_vect_desc_usable(vector_desc_t *vd, int flags, int cpu, int force) +static bool is_vect_desc_usable(vector_desc_t *vd, int flags, int cpu, int force, bool new_group) { //Check if interrupt is not reserved by design int x = vd->intno; esp_cpu_intr_desc_t intr_desc; esp_cpu_intr_get_desc(cpu, x, &intr_desc); + /* If the vector descriptor already contains a name but we need to create a new group, it's unusable */ + if (vd->group_name != NULL && new_group) { + ALCHLOG("....Unusable: already defines a group (%s)", vd->group_name); + return false; + } + if (intr_desc.flags & ESP_CPU_INTR_DESC_FLAG_RESVD) { ALCHLOG("....Unusable: reserved"); return false; @@ -305,29 +351,48 @@ static bool is_vect_desc_usable(vector_desc_t *vd, int flags, int cpu, int force return false; } - //Ints can't be both shared and non-shared. - assert(!((vd->flags & VECDESC_FL_SHARED) && (vd->flags & VECDESC_FL_NONSHARED))); - //check if interrupt already is in use by a non-shared interrupt - if (vd->flags & VECDESC_FL_NONSHARED) { + //check if interrupt is already in use by a non-shared interrupt + if ((vd->flags & VECDESC_FL_NONSHARED) != 0 && (vd->flags & VECDESC_FL_SHARED) == 0) { ALCHLOG("....Unusable: already in (non-shared) use."); return false; } - // check shared interrupt flags - if (vd->flags & VECDESC_FL_SHARED) { - if (flags & ESP_INTR_FLAG_SHARED) { - bool in_iram_flag = ((flags & ESP_INTR_FLAG_IRAM) != 0); - bool desc_in_iram_flag = ((vd->flags & VECDESC_FL_INIRAM) != 0); - //Bail out if int is shared, but iram property doesn't match what we want. - if ((vd->flags & VECDESC_FL_SHARED) && (desc_in_iram_flag != in_iram_flag)) { - ALCHLOG("....Unusable: shared but iram prop doesn't match"); - return false; - } - } else { - //We need an unshared IRQ; can't use shared ones; bail out if this is shared. - ALCHLOG("...Unusable: int is shared, we need non-shared."); + //Check shared interrupt flags + if ((vd->flags & VECDESC_FL_SHARED) != 0) { + const bool vect_private = (vd->flags & VECDESC_FL_PRIVATE) != 0; + const bool flag_shared = (flags & ESP_INTR_FLAG_SHARED) != 0; + const bool flag_shared_private = (flags & ESP_INTR_FLAG_SHARED_PRIVATE) != 0; + + /* Check if requested flag is shared, if not bail out */ + if (!flag_shared && !flag_shared_private) { + ALCHLOG("...Unusable: int is shared, we need non-shared"); return false; } - } else if (esp_cpu_intr_has_handler(x)) { + + /* Make sure the IRAM attribute matches */ + bool flag_iram = ((flags & ESP_INTR_FLAG_IRAM) != 0); + bool desc_iram = ((vd->flags & VECDESC_FL_INIRAM) != 0); + if (desc_iram != flag_iram) { + ALCHLOG("....Unusable: shared but iram prop doesn't match"); + return false; + } + + /* Make sure the visibilities match (both private or both public) */ + if (vect_private != flag_shared_private) { + ALCHLOG("....Unusable: shared but visibility doesn't match"); + return false; + } + + /* Make sure the interrupt number matches! */ + if (vect_private && force != vd->intno) { + ALCHLOG("....Unusable: privately shared but interrupt number doesn't match"); + return false; + } + + /* Success! */ + } + //Both shared and non-shared flags are low, this happens when the descriptor has just been allocated. + //Check if the interrupt is used outside of the interrupt allocator. + else if (esp_cpu_intr_has_handler(x)) { //Check if interrupt already is allocated by esp_cpu_intr_set_handler ALCHLOG("....Unusable: already allocated"); return false; @@ -339,7 +404,8 @@ static bool is_vect_desc_usable(vector_desc_t *vd, int flags, int cpu, int force //Locate a free interrupt compatible with the flags given. //The 'force' argument can be -1, or 0-31 to force checking a certain interrupt. //When a CPU is forced, the ESP_CPU_INTR_DESC_FLAG_SPECIAL marked interrupts are also accepted. -static int get_available_int(int flags, int cpu, int force, int source) +//If new_group is true, we will try to find a free interrupt line vector to attach the new interrupt to. +static int get_available_int(int flags, int cpu, int force, int source, bool new_group) { int x; int best=-1; @@ -362,7 +428,7 @@ static int get_available_int(int flags, int cpu, int force, int source) ALCHLOG("get_available_int: existing vd found. intno: %d", vd->intno); if ( force != -1 && force != vd->intno ) { ALCHLOG("get_available_int: intr forced but does not match existing. existing intno: %d, force: %d", vd->intno, force); - } else if (!is_vect_desc_usable(vd, flags, cpu, force)) { + } else if (!is_vect_desc_usable(vd, flags, cpu, force, new_group)) { ALCHLOG("get_available_int: existing vd invalid."); } else { best = vd->intno; @@ -378,7 +444,7 @@ static int get_available_int(int flags, int cpu, int force, int source) empty_vect_desc.intno = force; vd = &empty_vect_desc; } - if (is_vect_desc_usable(vd, flags, cpu, force)) { + if (is_vect_desc_usable(vd, flags, cpu, force, new_group)) { best = vd->intno; } else { ALCHLOG("get_avalaible_int: forced vd invalid."); @@ -403,14 +469,14 @@ static int get_available_int(int flags, int cpu, int force, int source) x, intr_desc.flags & ESP_CPU_INTR_DESC_FLAG_RESVD, intr_desc.priority, intr_desc.type == ESP_CPU_INTR_TYPE_LEVEL? "LEVEL" : "EDGE", esp_cpu_intr_has_handler(x)); - if (!is_vect_desc_usable(vd, flags, cpu, force)) { + if (!is_vect_desc_usable(vd, flags, cpu, force, new_group)) { continue; } if (flags & ESP_INTR_FLAG_SHARED) { //We're allocating a shared int. - //See if int already is used as a shared interrupt. + //See if int is already used as a shared interrupt. if (vd->flags & VECDESC_FL_SHARED) { //We can use this already-marked-as-shared interrupt. Count the already attached isrs in order to see //how useful it is. @@ -506,10 +572,26 @@ bool esp_intr_ptr_in_isr_region(void* ptr) } -//We use ESP_EARLY_LOG* here because this can be called before the scheduler is running. -esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrstatusreg, uint32_t intrstatusmask, intr_handler_t handler, - void *arg, intr_handle_t shared_handle, intr_handle_t *ret_handle) -{ +/** + * Allocate an interrupt with all the parameters in a single structure + */ + esp_err_t esp_intr_alloc_info(const esp_intr_alloc_info_t *info, intr_handle_t *ret_handle) + { + /* Info parameter is strictly required, the return handle is optional */ + if (info == NULL) { + return ESP_ERR_INVALID_ARG; + } + /* Retrieve back the variables */ + int source = info->source; + int flags = info->flags; + uint32_t intrstatusreg = info->intrstatusreg; + uint32_t intrstatusmask = info->intrstatusmask; + intr_handler_t handler = info->handler; + void *arg = info->arg; + intr_handle_t shared_handle = info->bind_by.handle; + const char* name = info->bind_by.name; + bool create_new_group = false; + intr_handle_data_t *ret=NULL; int force = -1; ESP_EARLY_LOGV(TAG, "esp_intr_alloc_intrstatus (cpu %u): checking args", esp_cpu_get_core_id()); @@ -537,12 +619,19 @@ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrsta return ESP_ERR_INVALID_ARG; } //Shared handler must be passed with share interrupt flag - if (shared_handle != NULL && (flags & ESP_INTR_FLAG_SHARED) == 0) { + const int shared_flags = (flags & ESP_INTR_FLAG_SHARED) != 0 || (flags & ESP_INTR_FLAG_SHARED_PRIVATE) != 0; + // We must only have a handle or a name if shared_flags is set + if ((shared_handle != NULL || name != NULL) && !shared_flags) { + return ESP_ERR_INVALID_ARG; + } + // We must not have both a handle and a name + if (shared_handle != NULL && name != NULL) { + ESP_LOGE(TAG, "Invalid arguments: both shared_handle and name are set"); return ESP_ERR_INVALID_ARG; } //Default to prio 1 for shared interrupts. Default to prio 1, 2 or 3 for non-shared interrupts. if ((flags & ESP_INTR_FLAG_LEVELMASK) == 0) { - if (flags & ESP_INTR_FLAG_SHARED) { + if (shared_flags) { flags |= ESP_INTR_FLAG_LEVEL1; } else { flags |= ESP_INTR_FLAG_LOWMED; @@ -583,19 +672,40 @@ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrsta /* Sanity check, should not occur */ if (shared_handle->vector_desc == NULL) { esp_os_exit_critical(&spinlock); - return ESP_ERR_INVALID_ARG; + return ESP_ERR_INVALID_STATE; } /* If a shared vector was given, force the current interrupt source to same CPU interrupt line */ force = shared_handle->vector_desc->intno; /* Allocate the interrupt on the same core as the given handle */ cpu = shared_handle->vector_desc->cpu; + } else if (name != NULL) { + /* Find the interrupt with the given name, we still want to call `get_available_int` since it does + * further checks so let's keep it like this for now. */ + vector_desc_t *vd = find_desc_for_name(name); + if (vd == NULL) { + /* Allow the descriptor to be NULL, we will need to create it */ + create_new_group = true; + } else { + force = vd->intno; + /* Allocate the interrupt on the same core as the group's descriptor */ + cpu = vd->cpu; + } } - int intr = get_available_int(flags, cpu, force, source); + if (cpu != esp_cpu_get_core_id()) { + esp_os_exit_critical(&spinlock); + free(ret); + return ESP_ERR_INVALID_STATE; + } + int intr = get_available_int(flags, cpu, force, source, create_new_group); if (intr == -1) { //None found. Bail out. esp_os_exit_critical(&spinlock); free(ret); ESP_LOGE(TAG, "No free interrupt inputs for %s interrupt (flags 0x%X)", esp_isr_names[source], flags); + /* If a handler (or a name) is provided, we should return ESP_ERR_INVALID_ARG instead */ + if (shared_handle != NULL || name != NULL) { + return ESP_ERR_INVALID_ARG; + } return ESP_ERR_NOT_FOUND; } //Get an int vector desc for int. @@ -607,7 +717,17 @@ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrsta } //Allocate that int! - if (flags & ESP_INTR_FLAG_SHARED) { + if (shared_flags) { + if (create_new_group) { + /* If we reach here, the vector's name should be empty (else, get_desc_for_int assigned an incorrect vector) */ + assert(vd->group_name == NULL); + vd->group_name = strdup(name); + if (vd->group_name == NULL) { + esp_os_exit_critical(&spinlock); + free(ret); + return ESP_ERR_NO_MEM; + } + } //Populate vector entry and add to linked list. shared_vector_desc_t *sh_vec = heap_caps_malloc(sizeof(shared_vector_desc_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); if (sh_vec == NULL) { @@ -625,6 +745,9 @@ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrsta sh_vec->disabled = 0; vd->shared_vec_info = sh_vec; vd->flags |= VECDESC_FL_SHARED; + if (flags & ESP_INTR_FLAG_SHARED_PRIVATE) { + vd->flags |= VECDESC_FL_PRIVATE; + } //(Re-)set shared isr handler to new value. esp_cpu_intr_set_handler(intr, (esp_cpu_intr_handler_t)shared_intr_isr, vd); } else { @@ -712,6 +835,22 @@ esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrsta return ESP_OK; } +//We use ESP_EARLY_LOG* here because this can be called before the scheduler is running. +esp_err_t esp_intr_alloc_intrstatus_bind(int source, int flags, uint32_t intrstatusreg, uint32_t intrstatusmask, intr_handler_t handler, + void *arg, intr_handle_t shared_handle, intr_handle_t *ret_handle) +{ + esp_intr_alloc_info_t info = { + .source = source, + .flags = flags, + .intrstatusreg = intrstatusreg, + .intrstatusmask = intrstatusmask, + .handler = handler, + .arg = arg, + .bind_by.handle = shared_handle, + }; + return esp_intr_alloc_info(&info, ret_handle); +} + esp_err_t esp_intr_alloc_intrstatus(int source, int flags, uint32_t intrstatusreg, uint32_t intrstatusmask, intr_handler_t handler, void *arg, intr_handle_t *ret_handle) { @@ -822,6 +961,11 @@ static esp_err_t intr_free_for_current_cpu(intr_handle_t handle) //If nothing left, disable interrupt. if (handle->vector_desc->shared_vec_info == NULL) { free_shared_vector = true; + /* If the interrupt was a named group, free the name (free the group) */ + if (handle->vector_desc->group_name != NULL) { + free(handle->vector_desc->group_name); + handle->vector_desc->group_name = NULL; + } } ESP_EARLY_LOGV(TAG, "esp_intr_free: Deleting shared int: %s. Shared int is %s", diff --git a/components/esp_hw_support/test_apps/esp_hw_support_unity_tests/main/test_intr_alloc.c b/components/esp_hw_support/test_apps/esp_hw_support_unity_tests/main/test_intr_alloc.c index d3f7d98add..73a0251ad1 100644 --- a/components/esp_hw_support/test_apps/esp_hw_support_unity_tests/main/test_intr_alloc.c +++ b/components/esp_hw_support/test_apps/esp_hw_support_unity_tests/main/test_intr_alloc.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -130,6 +130,23 @@ void static test_isr(void*arg) } +TEST_CASE("Intr_alloc test, private ints cannot have names", "[intr_alloc]") +{ + intr_handle_t handle_1; + + const char *name = "test_private_interrupt_line"; + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1, + .handler = test_isr, + .arg = NULL, + .bind_by.name = name, + }; + + esp_err_t err = esp_intr_alloc_info(&info, &handle_1); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); +} + TEST_CASE("Intr_alloc test, shared interrupts don't affect level", "[intr_alloc]") { intr_handle_t handle_lvl_1; @@ -158,6 +175,40 @@ TEST_CASE("Intr_alloc test, shared interrupts don't affect level", "[intr_alloc] TEST_ESP_OK(esp_intr_free(handle_lvl_2)); } +TEST_CASE("Intr_alloc test, shared interrupts groups don't affect each other", "[intr_alloc]") +{ + intr_handle_t handle_group1; + intr_handle_t handle_group2; + + /* Create a first group of interrupts with name "group1" */ + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED, + .handler = test_isr, + .arg = NULL, + .bind_by.name = "group1", + }; + esp_err_t err = esp_intr_alloc_info(&info, &handle_group1); + TEST_ESP_OK(err); + + /* Allocate a second group of interrupts with name "group2" */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED; + info.bind_by.name = "group2"; + err = esp_intr_alloc_info(&info, &handle_group2); + TEST_ESP_OK(err); + + /* Make sure the allocated CPU line is NOT the same for both sources */ + const int intlvl1 = esp_intr_get_intno(handle_group1); + const int intlvl2 = esp_intr_get_intno(handle_group2); + printf("Group 1 interrupt allocated: %d\n", intlvl1); + printf("Group 2 interrupt allocated: %d\n", intlvl2); + TEST_ASSERT(intlvl1 != intlvl2); + + TEST_ESP_OK(esp_intr_free(handle_group1)); + TEST_ESP_OK(esp_intr_free(handle_group2)); +} + #if SOC_CPU_HAS_FLEXIBLE_INTC @@ -223,13 +274,298 @@ TEST_CASE("Intr_alloc test, shared interrupt line for two sources", "[intr_alloc err = esp_intr_alloc_bind(SYS_CPU_INTR_FROM_CPU_3_SOURCE, ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_SHARED, test_isr, NULL, handle_1, &handle_2); - TEST_ASSERT(err != ESP_OK); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); + + /* Try to allocate a new interrupt to the same handler but with IRAM attribute, it must fail */ + err = esp_intr_alloc_bind(SYS_CPU_INTR_FROM_CPU_3_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_IRAM, + test_isr, NULL, handle_1, &handle_2); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); /* Free the remaining handler */ TEST_ESP_OK(esp_intr_free(handle_1)); } + +/** + * Make sure we can map two given sources to the same interrupt line thanks to a name. + */ +TEST_CASE("Intr_alloc test, shared interrupt line for two sources using name", "[intr_alloc]") +{ + intr_handle_t handle_1; + intr_handle_t handle_2; + + const char *name = "test_shared_interrupt_line"; + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED, + .handler = test_isr, + .arg = NULL, + .bind_by.name = name, + }; + esp_err_t err = esp_intr_alloc_info(&info, &handle_1); + TEST_ESP_OK(err); + + /* Map another source to the exact same interrupt line */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED; + err = esp_intr_alloc_info(&info, &handle_2); + TEST_ESP_OK(err); + /* Make sure they are both using the same interrupt line */ + TEST_ASSERT_EQUAL(esp_intr_get_intno(handle_1), esp_intr_get_intno(handle_2)); + + /* Reallocate the second interrupt source with a higher level, it must fail */ + TEST_ESP_OK(esp_intr_free(handle_2)); + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_SHARED; + err = esp_intr_alloc_info(&info, &handle_2); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_1)); +} + + +/** + * Make sure we can allocate a privately shared interrupt line and map two given sources to it. + */ + TEST_CASE("Intr_alloc test, privately shared interrupt line for two sources", "[intr_alloc]") + { + intr_handle_t handle_1; + intr_handle_t handle_2; + + esp_err_t err = esp_intr_alloc(SYS_CPU_INTR_FROM_CPU_2_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + test_isr, NULL, &handle_1); + TEST_ESP_OK(err); + + /* Map another source to the exact same interrupt line */ + err = esp_intr_alloc_bind(SYS_CPU_INTR_FROM_CPU_3_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + test_isr, NULL, handle_1, &handle_2); + TEST_ESP_OK(err); + /* Make sure they are both using the same interrupt line */ + TEST_ASSERT_EQUAL(esp_intr_get_intno(handle_1), esp_intr_get_intno(handle_2)); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_2)); + TEST_ESP_OK(esp_intr_free(handle_1)); +} + +/** + * Make sure we can allocate a privately shared interrupt line and map two given sources to it thanks to a name. + */ + TEST_CASE("Intr_alloc test, privately shared interrupt line for two sources using name", "[intr_alloc]") + { + intr_handle_t handle_1; + intr_handle_t handle_2; + + const char *name = "test_shared_interrupt_line"; + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + .handler = test_isr, + .arg = NULL, + .bind_by.name = name, + }; + + esp_err_t err = esp_intr_alloc_info(&info, &handle_1); + TEST_ESP_OK(err); + + /* Map another source to the exact same interrupt line */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE; + err = esp_intr_alloc_info(&info, &handle_2); + TEST_ESP_OK(err); + + /* Make sure they are both using the same interrupt line */ + TEST_ASSERT_EQUAL(esp_intr_get_intno(handle_1), esp_intr_get_intno(handle_2)); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_2)); + TEST_ESP_OK(esp_intr_free(handle_1)); +} + +/** + * Make sure we can allocate a privately shared interrupt line and other shared interrupts would NOT be mapped to it. + */ +TEST_CASE("Intr_alloc test, privately shared interrupt line does not affect other shared interrupts", "[intr_alloc]") +{ + intr_handle_t handle_1; + intr_handle_t handle_2; + + esp_err_t err = esp_intr_alloc(SYS_CPU_INTR_FROM_CPU_2_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + test_isr, NULL, &handle_1); + TEST_ESP_OK(err); + + /* Try to map the other interrupt source to the same interrupt line, it should fail because of the different flags */ + err = esp_intr_alloc_bind(SYS_CPU_INTR_FROM_CPU_3_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED, + test_isr, NULL, handle_1, &handle_2); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); + + /* Map another source to any other interrupt line */ + printf("Allocating second source to the same interrupt line...\n"); + err = esp_intr_alloc_bind(SYS_CPU_INTR_FROM_CPU_3_SOURCE, + ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED, + test_isr, NULL, NULL, &handle_2); + TEST_ESP_OK(err); + + /* Make sure the allocated interrupt lines are different */ + TEST_ASSERT_NOT_EQUAL(esp_intr_get_intno(handle_1), esp_intr_get_intno(handle_2)); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_2)); + TEST_ESP_OK(esp_intr_free(handle_1)); +} + + +TEST_CASE("Intr_alloc test, public shared interrupt cannot be allocated to a private group using a name", "[intr_alloc]") +{ + intr_handle_t handle_1; + intr_handle_t handle_2; + + const char *name = "test_shared_interrupt_line"; + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + .handler = test_isr, + .arg = NULL, + .bind_by.name = name, + }; + + esp_err_t err = esp_intr_alloc_info(&info, &handle_1); + TEST_ESP_OK(err); + + /* Mark this interrupt source as looking for a publicly shared interrupt line */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED; + err = esp_intr_alloc_info(&info, &handle_2); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_1)); +} + + +TEST_CASE("Intr_alloc test, shared ints fails giving both handler and name", "[intr_alloc]") +{ + intr_handle_t handle_1; + intr_handle_t handle_2; + + const char *name = "test_shared_interrupt_line"; + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + .handler = test_isr, + .arg = NULL, + .bind_by.name = name, + }; + + esp_err_t err = esp_intr_alloc_info(&info, &handle_1); + TEST_ESP_OK(err); + + /* Provide the returned handle and a name, it should fail */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + info.bind_by.handle = handle_1; + info.flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE; + err = esp_intr_alloc_info(&info, &handle_2); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, err); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(handle_1)); +} + +/* Allocate an interrupt on the other core */ +#if !CONFIG_ESP_SYSTEM_SINGLE_CORE_MODE + +typedef struct { + TaskHandle_t main_task; + esp_intr_alloc_info_t info; +} args_t; + +void IRAM_ATTR test_isr_other_core(void *arg) +{ + args_t *args = (args_t *)arg; + printf("Interrupt on other core called.\n"); + + /* Should return an error */ + esp_err_t err = esp_intr_alloc_info(&args->info, NULL); + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, err); + + /* Send a signal to the main task to continue */ + xTaskNotifyGive(args->main_task); + vTaskDelete(NULL); +} + +TEST_CASE("Intr_alloc test, shared ints fails giving a handler on other core", "[intr_alloc]") +{ + intr_handle_t isr_handle; + args_t args = { + .main_task = NULL, + .info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + .handler = test_isr, + .arg = NULL, + .bind_by.name = "test_shared_interrupt_line", + }, + }; + args.main_task = xTaskGetCurrentTaskHandle(); + + esp_err_t err = esp_intr_alloc_info(&args.info, &isr_handle); + TEST_ESP_OK(err); + + int core_id = esp_cpu_get_core_id(); + TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore(test_isr_other_core, "test_isr_other_core", + 2048, (void *)&args, 10, NULL, core_id == 0 ? 1 : 0)); + + /* Wait for the task to finish */ + TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdTRUE, portMAX_DELAY)); + + /* Free the remaining handler */ + TEST_ESP_OK(esp_intr_free(isr_handle)); +} +#endif + + +TEST_CASE("Intr_alloc test, named group gets freed when the last interrupt is freed", "[intr_alloc]") +{ + intr_handle_t handle; + + esp_intr_alloc_info_t info = { + .source = SYS_CPU_INTR_FROM_CPU_2_SOURCE, + .flags = ESP_INTR_FLAG_LEVEL1 | ESP_INTR_FLAG_SHARED_PRIVATE, + .handler = test_isr, + .arg = NULL, + .bind_by.name = "test_shared_interrupt_line_1", + }; + + esp_err_t err = esp_intr_alloc_info(&info, &handle); + TEST_ESP_OK(err); + + /* ave the interrupt number */ + int intno = esp_intr_get_intno(handle); + TEST_ASSERT_NOT_EQUAL(-1, intno); + + /* Free the interrupt we just allocated and make sure we can get it back with another name right after */ + TEST_ESP_OK(esp_intr_free(handle)); + + /* Allocate a new interrupt with the same name, it should succeed */ + info.source = SYS_CPU_INTR_FROM_CPU_3_SOURCE; + /* Change the name to make sure the former interrupt name got renamed */ + info.bind_by.name = "test_shared_interrupt_line_2"; + err = esp_intr_alloc_info(&info, &handle); + TEST_ESP_OK(err); + + /* Make sure that the interrupt number is the same */ + TEST_ASSERT_EQUAL(intno, esp_intr_get_intno(handle)); + + TEST_ESP_OK(esp_intr_free(handle)); +} + TEST_CASE("Allocate previously freed interrupt, with different flags", "[intr_alloc]") { intr_handle_t intr; diff --git a/docs/en/api-reference/system/intr_alloc.rst b/docs/en/api-reference/system/intr_alloc.rst index 07d1038f9b..c300dd5cfa 100644 --- a/docs/en/api-reference/system/intr_alloc.rst +++ b/docs/en/api-reference/system/intr_alloc.rst @@ -32,7 +32,7 @@ Overview Because there are more interrupt sources than interrupts, sometimes it makes sense to share an interrupt in multiple drivers. The :cpp:func:`esp_intr_alloc` abstraction exists to hide all these implementation details. -A driver can allocate an interrupt for a certain peripheral by calling :cpp:func:`esp_intr_alloc`, :cpp:func:`esp_intr_alloc_bind`, :cpp:func:`esp_intr_alloc_intrstatus`, or :cpp:func:`esp_intr_alloc_intrstatus_bind`. It can use the flags passed to this function to specify the type, priority, and trigger method of the interrupt to allocate. The interrupt allocation code will then find an applicable interrupt, use the interrupt matrix to hook it up to the peripheral, and install the given interrupt handler and ISR to it. +A driver can allocate an interrupt for a certain peripheral by calling :cpp:func:`esp_intr_alloc`, :cpp:func:`esp_intr_alloc_bind`, :cpp:func:`esp_intr_alloc_intrstatus`, :cpp:func:`esp_intr_alloc_intrstatus_bind`, or :cpp:func:`esp_intr_alloc_info`. It can use the flags passed to this function to specify the type, priority, and trigger method of the interrupt to allocate. The interrupt allocation code will then find an applicable interrupt, use the interrupt matrix to hook it up to the peripheral, and install the given interrupt handler and ISR to it. The interrupt allocator presents two different types of interrupts, namely shared interrupts and non-shared interrupts, both of which require different handling. Non-shared interrupts will allocate a separate interrupt for every :cpp:func:`esp_intr_alloc` call, and this interrupt is use solely for the peripheral attached to it, with only one ISR that will get called. Shared interrupts can have multiple peripherals triggering them, with multiple ISRs being called when one of the peripherals attached signals an interrupt. Thus, ISRs that are intended for shared interrupts should check the interrupt status of the peripheral they service in order to check if any action is required. @@ -140,6 +140,12 @@ Sources attached to non-shared interrupt do not support this feature. By default, when ``ESP_INTR_FLAG_SHARED`` flag is specified, the interrupt allocator will allocate only priority level 1 interrupts. Use ``ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_LOWMED`` to also allow allocating shared interrupts at priority levels 2 and 3. +**Private shared interrupts** (``ESP_INTR_FLAG_SHARED_PRIVATE``) behave like ``ESP_INTR_FLAG_SHARED`` in that they allow multiple sources to share the same CPU interrupt line. However, unlike regular shared interrupts, these lines are never selected automatically by the interrupt allocator. + +In other words, even when using ``ESP_INTR_FLAG_SHARED``, no allocation function will ever choose an interrupt line that was created with ``ESP_INTR_FLAG_SHARED_PRIVATE``. These lines are effectively reserved and can only be used by explicitly binding new sources to them (e.g., via ``esp_intr_alloc_bind()`` or related APIs). + +This allows you to define a fixed set of interrupt sources sharing a single CPU line (for example, multiple GPIO sources) without the risk of the allocator reusing that line for unrelated interrupts. + Though the framework supports this feature, you have to use it **very carefully**. There usually exist two ways to stop an interrupt from being triggered: **disable the source** or **mask peripheral interrupt status**. ESP-IDF only handles enabling and disabling of the source itself, leaving status and mask bits to be handled by users. **Status bits shall either be masked before the handler responsible for it is disabled, or be masked and then properly handled in another enabled interrupt**. @@ -150,6 +156,12 @@ Though the framework supports this feature, you have to use it **very carefully* When calling :cpp:func:`esp_intr_alloc` or :cpp:func:`esp_intr_alloc_intrstatus`, the interrupt allocator selects the first interrupt that meets the level requirements for mapping the specified source, without considering other sources already mapped to the shared interrupt line. However, by using the functions :cpp:func:`esp_intr_alloc_bind` or :cpp:func:`esp_intr_alloc_intrstatus_bind`, you can explicitly specify the interrupt handler to be shared with the given interrupt source. +For private shared interrupts, the first allocation uses ``ESP_INTR_FLAG_SHARED_PRIVATE`` (with :cpp:func:`esp_intr_alloc` or :cpp:func:`esp_intr_alloc_intrstatus`), and subsequent sources are attached via the same functions, still with ``ESP_INTR_FLAG_SHARED_PRIVATE`` flag and the returned handle. + +Named groups +^^^^^^^^^^^^ + +Shared interrupts groups, public or private, can be defined by name. This is done via the ``bind_by.name`` field of the :cpp:type:`esp_intr_alloc_info_t` structure. If a shared interrupt group with the specified name already exists (created by an earlier call using the same name), the new source is attached to that group. Otherwise, a new shared interrupt group is allocated and tagged with that name. Only one of ``bind_by.handle`` or ``bind_by.name`` may be set. The usual flag rules (e.g., level, SHARED vs SHARED_PRIVATE) still apply. This does not affect the behavior of public shared interrupts: interrupt allocation functions can still attach new sources to existing public shared lines without specifying a name or handle. Troubleshooting Interrupt Allocation ------------------------------------