中断分配

[English]

概述

ESP32 有两个核,每个核有 32 个中断。每个中断都有一个确定的优先级别,大多数中断(但不是全部)都连接到中断矩阵。

由于中断源数量多于中断,有时多个驱动程序可以共用一个中断。esp_intr_alloc() 抽象隐藏了这些实现细节。

驱动程序可以通过调用 esp_intr_alloc(),或 esp_intr_alloc_intrstatus() 为某个外设分配中断。通过向此函数传递 flag,可以指定中断类型、优先级和触发方式。然后,中断分配代码会找到适用的中断,使用中断矩阵将其连接到外设,并为其安装给定的中断处理程序和 ISR。

中断分配器提供两种不同的中断类型:共享中断和非共享中断,这两种中断需要不同处理方式。非共享中断在每次调用 esp_intr_alloc() 时,都会分配一个单独的中断,该中断仅用于与其相连的外设,只调用一个 ISR。共享中断则可以由多个外设触发,当其中一个外设发出中断信号时,会调用多个 ISR。因此,针对共享中断的 ISR 应检查对应外设的中断状态,以确定是否需要采取任何操作。

非共享中断可由电平或边缘触发。共享中断只能由电平触发,因为使用边缘触发可能会错过中断。

要解释为什么共享中断只能由电平触发,以外设 A 和外设 B 共用一个边缘触发中断为例进行说明:当外设 B 触发中断时,会将其中断信号设置为高电平,产生一个从低到高的边缘,进而锁存 CPU 中断位,并触发 ISR。接着,ISR 开始执行,检查到此时外设 A 没有触发中断,于是继续处理外设 B 的中断信号,最后将外设 B 的中断状态清除。最后,CPU 会在 ISR 返回之前清除中断位锁存器。因此在整个中断处理过程中,如果外设 A 触发了中断,该中断会因 CPU 清除中断位锁存器而丢失。

多核问题

可以生成中断的外设包括以下两种类型:

  • 外部外设,属于 ESP32,但不属于 Xtensa 核。大多数 ESP32 外设都属于此类型。

  • 内部外设,是 Xtensa CPU 的一部分。

这两种外设的中断处理略有不同。

内部外设中断

每个 Xtensa CPU 核都有六个内部外设:

  • 三个定时器比较器

  • 一个性能监视器

  • 两个软件中断

内部中断源在 esp_intr_alloc.h 中定义为 ETS_INTERNAL_*_INTR_SOURCE

这些外设只能通过关联的内核进行配置。在生成中断时,它们生成的中断被硬连线到关联的内核上,例如,一个内核的内部定时器比较器不能生成另一个内核上的中断。因此,这些中断源只能通过在特定内核上运行任务来进行管理。内部中断源仍然可用 esp_intr_alloc() 正常分配,但不能共用,而且始终具有固定的中断级别(即与外设关联的硬件级别)。

外部外设中断

剩余的中断源来自外部外设,在 soc/soc.h 中定义为 ETS_*_INTR_SOURCE

两个 CPU 的非内部中断源槽都与中断矩阵相连,可以将任何外部中断源发送到这些中断槽中。

  • 外部中断会始终被分配到执行该分配的内核上。

  • 释放外部中断必须在分配该中断的内核上进行。

  • 允许从另一个内核禁用和启用外部中断。

  • 多个外部中断源可以通过向 esp_intr_alloc() 发送 ESP_INTR_FLAG_SHARED flag 来共享一个中断槽。

须注意从未关联到内核的任务中调用 esp_intr_alloc() 的情况。在任务切换期间,这些任务可能在不同内核之间进行迁移,因此无法确定中断分配到了哪个 CPU,给释放中断句柄造成困难,也可能引起调试问题。建议使用特定 CoreID 参数的 xTaskCreatePinnedToCore() 来创建中断分配任务,这对于内部中断源而言是必要的。

IRAM-safe 中断处理程序

ESP_INTR_FLAG_IRAM flag 注册的中断处理程序始终在 IRAM(并从 DRAM 读取其所有数据)中运行,因此在擦除和写入 flash 时无需禁用。

这对于需要保证最小执行延迟的中断来说非常有用,因为 flash 写入和擦除操作可能很慢(擦除可能需要数十毫秒或数百毫秒才能完成)。

如果中断被频繁调用,可以将中断处理程序保留在 IRAM 中,避免 flash cache 丢失。

有关更多详细信息,请参阅 SPI flash API 相关文档

多个处理程序共用一个中断源

如果用 ESP_INTR_FLAG_SHARED flag 分配所有处理程序,可能将多个处理程序分配给同一个源。这些程序会被分配给与源关联的中断,并在源可用时按顺序调用。处理程序可以单独禁用和释放。如果启用了一个或多个处理程序,则会将源关联到中断(启用),否则会取消关联。禁用的处理程序永远不会被调用,但是只要启用了源的任何一个处理程序,这个源仍然能被触发。

关联到非共享中断的源不支持此功能。

默认情况下,指定 ESP_INTR_FLAG_SHARED flag 时,中断分配器仅分配优先级为 1 的中断。可以使用 ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_LOWMED 允许分配优先级为 2 和 3 的共享中断。

尽管支持此功能,使用时也必须 非常小心。通常存在两种办法可以阻止中断触发: 禁用源屏蔽外设中断状态。ESP-IDF 仅处理源本身的启用和禁用,中断源的状态位和屏蔽位须由用户操作。

状态位须在负责该位的处理程序禁用前屏蔽,也可以在另一个启用的中断中屏蔽和处理该状态位。

备注

如果不屏蔽状态位而让其处于未处理状态,同时禁用这些状态位的处理程序,就会导致无限次触发中断,引起系统崩溃。

排除中断分配故障

CPU 中断在大多数 Espressif SoC 上都是有限的资源。因此,一个运行的程序有可能耗尽 CPU 中断,例如初始化多个外设驱动程序的情况。这通常导致驱动程序的初始化函数返回 ESP_ERR_NOT_FOUND 错误。

这种情况下,可使用 esp_intr_dump() 函数打印中断列表及其状态。此函数输出通常如下:

CPU 0 interrupt status:
Int  Level  Type   Status
0     1    Level  Reserved
1     1    Level  Reserved
2     1    Level  Used: RTC_CORE
3     1    Level  Used: TG0_LACT_LEVEL
...

输出列含义如下:

  • Int:CPU 中断输入编号。通常不直接在软件中使用,仅作为参考。

  • Level:CPU 中断的优先级(1-7)。此优先级固定在硬件上,无法更改。

  • Type:CPU 中断的中断类型(电平或边缘中断)。此类型在硬件上固定,无法更改。

  • Status:中断的可能状态:
    • Reserved:中断在硬件层面保留,或由 ESP-IDF 的某些部分保留。不能使用 esp_intr_alloc() 分配。

    • Used: <source>:中断已分配并连接到单个外设。

    • Shared: <source1> <source2> ...:中断已分配并连接到多个外设。参见本文档 多个处理程序共用一个中断源 章节。

    • Free:中断未分配,可以由 esp_intr_alloc() 使用。

  • Free (not general-use):中断未分配,但它是高优先级中断(级别 4-7)或边缘触发中断。高优先级中断可以使用 esp_intr_alloc() 分配,但要求处理程序必须用汇编语言编写,参见 高优先级中断处理程序。低优先级和中优先级的边缘触发中断也可以用 esp_intr_alloc() 分配,但很少使用,因为大多数外设中断是电平触发的。

如果已确认应用程序的确用完了中断,可组合采用下列建议解决问题:

  • 在多核 SoC 上,尝试通过固定在第二个核的任务来初始化某些外设驱动程序。中断通常分配在运行外设驱动程序初始化函数的同一个内核上,因此,通过在第二个内核上运行初始化函数,就可以使用更多的中断输入。

  • 找到可接受更高延迟的中断,并用 ESP_INTR_FLAG_SHARED flag (或与 ESP_INTR_FLAG_LOWMED 进行 OR 运算)分配这些中断。对两个或更多外设使用此 flag 能让它们使用单个中断输入,从而为其他外设节约中断输入。参见 多个处理程序共用一个中断源

  • 一些外设驱动程序可能默认使用 ESP_INTR_FLAG_LEVEL1 flag 来分配中断,因此默认情况下不会使用优先级为 2 或 3 的中断。如果 esp_intr_dump() 显示某些优先级为 2 或 3 的中断可用,尝试在初始化驱动程序时将中断分配 flag 改为 ESP_INTR_FLAG_LEVEL2ESP_INTR_FLAG_LEVEL3

  • 检查是否有些外设驱动程序不需要一直启用,并按需将其初始化或取消初始化。这样可以减少同时分配的中断数量。

API 参考

Header File

Macros

ESP_INTR_CPU_AFFINITY_TO_CORE_ID(cpu_affinity)

Convert esp_intr_cpu_affinity_t to CPU core ID.

Type Definitions

typedef void (*intr_handler_t)(void *arg)

Function prototype for interrupt handler function

typedef struct intr_handle_data_t *intr_handle_t

Handle to an interrupt handler

Enumerations

enum esp_intr_cpu_affinity_t

Interrupt CPU core affinity.

This type specify the CPU core that the peripheral interrupt is connected to.

Values:

enumerator ESP_INTR_CPU_AFFINITY_AUTO

Install the peripheral interrupt to ANY CPU core, decided by on which CPU the interrupt allocator is running.

enumerator ESP_INTR_CPU_AFFINITY_0

Install the peripheral interrupt to CPU core 0.

enumerator ESP_INTR_CPU_AFFINITY_1

Install the peripheral interrupt to CPU core 1.

Header File

Functions

esp_err_t esp_intr_mark_shared(int intno, int cpu, bool is_in_iram)

Mark an interrupt as a shared interrupt.

This will mark a certain interrupt on the specified CPU as an interrupt that can be used to hook shared interrupt handlers to.

参数
  • intno -- The number of the interrupt (0-31)

  • cpu -- CPU on which the interrupt should be marked as shared (0 or 1)

  • is_in_iram -- Shared interrupt is for handlers that reside in IRAM and the int can be left enabled while the flash cache is disabled.

返回

ESP_ERR_INVALID_ARG if cpu or intno is invalid ESP_OK otherwise

esp_err_t esp_intr_reserve(int intno, int cpu)

Reserve an interrupt to be used outside of this framework.

This will mark a certain interrupt on the specified CPU as reserved, not to be allocated for any reason.

参数
  • intno -- The number of the interrupt (0-31)

  • cpu -- CPU on which the interrupt should be marked as shared (0 or 1)

返回

ESP_ERR_INVALID_ARG if cpu or intno is invalid ESP_OK otherwise

esp_err_t esp_intr_alloc(int source, int flags, intr_handler_t handler, void *arg, intr_handle_t *ret_handle)

Allocate an interrupt with the given parameters.

This finds an interrupt that matches the restrictions as given in the flags parameter, maps the given interrupt source to it and hooks up the given interrupt handler (with optional argument) as well. If needed, it can return a handle for the interrupt as well.

The interrupt will always be allocated on the core that runs this function.

If ESP_INTR_FLAG_IRAM flag is used, and handler address is not in IRAM or RTC_FAST_MEM, then ESP_ERR_INVALID_ARG is returned.

参数
  • source -- The interrupt source. One of the ETS_*_INTR_SOURCE interrupt mux sources, as defined in soc/soc.h, or one of the internal ETS_INTERNAL_*_INTR_SOURCE sources as defined in this header.

  • flags -- An ORred mask of the ESP_INTR_FLAG_* defines. These restrict the choice of interrupts that this routine can choose from. If this value is 0, it will default to allocating a non-shared interrupt of level 1, 2 or 3. If this is ESP_INTR_FLAG_SHARED, it will allocate a shared interrupt of level 1. Setting ESP_INTR_FLAG_INTRDISABLED will return from this function with the interrupt disabled.

  • handler -- The interrupt handler. Must be NULL when an interrupt of level >3 is requested, because these types of interrupts aren't C-callable.

  • arg -- Optional argument for passed to the interrupt handler

  • 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.

返回

ESP_ERR_INVALID_ARG if the combination of arguments is invalid. ESP_ERR_NOT_FOUND No free interrupt found with the specified flags ESP_OK otherwise

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)

Allocate an interrupt with the given parameters.

This essentially does the same as esp_intr_alloc, but allows specifying a register and mask combo. For shared interrupts, the handler is only called if a read from the specified register, ANDed with the mask, returns non-zero. By passing an interrupt status register address and a fitting mask, this can be used to accelerate interrupt handling in the case a shared interrupt is triggered; by checking the interrupt statuses first, the code can decide which ISRs can be skipped

参数
  • source -- The interrupt source. One of the ETS_*_INTR_SOURCE interrupt mux sources, as defined in soc/soc.h, or one of the internal ETS_INTERNAL_*_INTR_SOURCE sources as defined in this header.

  • flags -- An ORred mask of the ESP_INTR_FLAG_* defines. These restrict the choice of interrupts that this routine can choose from. If this value is 0, it will default to allocating a non-shared interrupt of level 1, 2 or 3. If this is ESP_INTR_FLAG_SHARED, it will allocate a shared interrupt of level 1. Setting ESP_INTR_FLAG_INTRDISABLED will return from this function with the interrupt disabled.

  • intrstatusreg -- The address of an interrupt status register

  • intrstatusmask -- A mask. If a read of address intrstatusreg has any of the bits that are 1 in the mask set, the ISR will be called. If not, it will be skipped.

  • handler -- The interrupt handler. Must be NULL when an interrupt of level >3 is requested, because these types of interrupts aren't C-callable.

  • arg -- Optional argument for passed to the interrupt handler

  • 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.

返回

ESP_ERR_INVALID_ARG if the combination of arguments is invalid. ESP_ERR_NOT_FOUND No free interrupt found with the specified flags ESP_OK otherwise

esp_err_t esp_intr_free(intr_handle_t handle)

Disable and free an interrupt.

Use an interrupt handle to disable the interrupt and release the resources associated with it. If the current core is not the core that registered this interrupt, this routine will be assigned to the core that allocated this interrupt, blocking and waiting until the resource is successfully released.

备注

When the handler shares its source with other handlers, the interrupt status bits it's responsible for should be managed properly before freeing it. see esp_intr_disable for more details. Please do not call this function in esp_ipc_call_blocking.

参数

handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

返回

ESP_ERR_INVALID_ARG the handle is NULL ESP_FAIL failed to release this handle ESP_OK otherwise

int esp_intr_get_cpu(intr_handle_t handle)

Get CPU number an interrupt is tied to.

参数

handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

返回

The core number where the interrupt is allocated

int esp_intr_get_intno(intr_handle_t handle)

Get the allocated interrupt for a certain handle.

参数

handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

返回

The interrupt number

esp_err_t esp_intr_disable(intr_handle_t handle)

Disable the interrupt associated with the handle.

备注

  1. For local interrupts (ESP_INTERNAL_* sources), this function has to be called on the CPU the interrupt is allocated on. Other interrupts have no such restriction.

  2. When several handlers sharing a same interrupt source, interrupt status bits, which are handled in the handler to be disabled, should be masked before the disabling, or handled in other enabled interrupts properly. Miss of interrupt status handling will cause infinite interrupt calls and finally system crash.

参数

handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

返回

ESP_ERR_INVALID_ARG if the combination of arguments is invalid. ESP_OK otherwise

esp_err_t esp_intr_enable(intr_handle_t handle)

Enable the interrupt associated with the handle.

备注

For local interrupts (ESP_INTERNAL_* sources), this function has to be called on the CPU the interrupt is allocated on. Other interrupts have no such restriction.

参数

handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

返回

ESP_ERR_INVALID_ARG if the combination of arguments is invalid. ESP_OK otherwise

esp_err_t esp_intr_set_in_iram(intr_handle_t handle, bool is_in_iram)

Set the "in IRAM" status of the handler.

备注

Does not work on shared interrupts.

参数
  • handle -- The handle, as obtained by esp_intr_alloc or esp_intr_alloc_intrstatus

  • is_in_iram -- Whether the handler associated with this handle resides in IRAM. Handlers residing in IRAM can be called when cache is disabled.

返回

ESP_ERR_INVALID_ARG if the combination of arguments is invalid. ESP_OK otherwise

void esp_intr_noniram_disable(void)

Disable interrupts that aren't specifically marked as running from IRAM.

void esp_intr_noniram_enable(void)

Re-enable interrupts disabled by esp_intr_noniram_disable.

void esp_intr_enable_source(int inum)

enable the interrupt source based on its number

参数

inum -- interrupt number from 0 to 31

void esp_intr_disable_source(int inum)

disable the interrupt source based on its number

参数

inum -- interrupt number from 0 to 31

static inline int esp_intr_flags_to_level(int flags)

Get the lowest interrupt level from the flags.

参数

flags -- The same flags that pass to esp_intr_alloc_intrstatus API

static inline int esp_intr_level_to_flags(int level)

Get the interrupt flags from the supplied level (priority)

参数

level -- The interrupt priority level

esp_err_t esp_intr_dump(FILE *stream)

Dump the status of allocated interrupts.

参数

stream -- The stream to dump to, if NULL then stdout is used

返回

ESP_OK on success

Macros

ESP_INTR_FLAG_LEVEL1

Interrupt allocation flags.

These flags can be used to specify which interrupt qualities the code calling esp_intr_alloc* needs. Accept a Level 1 interrupt vector (lowest priority)

ESP_INTR_FLAG_LEVEL2

Accept a Level 2 interrupt vector.

ESP_INTR_FLAG_LEVEL3

Accept a Level 3 interrupt vector.

ESP_INTR_FLAG_LEVEL4

Accept a Level 4 interrupt vector.

ESP_INTR_FLAG_LEVEL5

Accept a Level 5 interrupt vector.

ESP_INTR_FLAG_LEVEL6

Accept a Level 6 interrupt vector.

ESP_INTR_FLAG_NMI

Accept a Level 7 interrupt vector (highest priority)

ESP_INTR_FLAG_SHARED

Interrupt can be shared between ISRs.

ESP_INTR_FLAG_EDGE

Edge-triggered interrupt.

ESP_INTR_FLAG_IRAM

ISR can be called if cache is disabled.

ESP_INTR_FLAG_INTRDISABLED

Return with this interrupt disabled.

ESP_INTR_FLAG_LOWMED

Low and medium prio interrupts. These can be handled in C.

ESP_INTR_FLAG_HIGH

High level interrupts. Need to be handled in assembly.

ESP_INTR_FLAG_LEVELMASK

Mask for all level flags.

ETS_INTERNAL_TIMER0_INTR_SOURCE

Platform timer 0 interrupt source.

The esp_intr_alloc* functions can allocate an int for all ETS_*_INTR_SOURCE interrupt sources that are routed through the interrupt mux. Apart from these sources, each core also has some internal sources that do not pass through the interrupt mux. To allocate an interrupt for these sources, pass these pseudo-sources to the functions.

ETS_INTERNAL_TIMER1_INTR_SOURCE

Platform timer 1 interrupt source.

ETS_INTERNAL_TIMER2_INTR_SOURCE

Platform timer 2 interrupt source.

ETS_INTERNAL_SW0_INTR_SOURCE

Software int source 1.

ETS_INTERNAL_SW1_INTR_SOURCE

Software int source 2.

ETS_INTERNAL_PROFILING_INTR_SOURCE

Int source for profiling.

ETS_INTERNAL_UNUSED_INTR_SOURCE

Interrupt is not assigned to any source.

ETS_INTERNAL_INTR_SOURCE_OFF

Provides SystemView with positive IRQ IDs, otherwise scheduler events are not shown properly

ESP_INTR_ENABLE(inum)

Enable interrupt by interrupt number

ESP_INTR_DISABLE(inum)

Disable interrupt by interrupt number