# 中断分配

[English]

## 概述

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

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

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

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

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

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

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

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

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

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

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

## 多核问题

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

> - 外部外设，属于 ESP32，但不属于 Xtensa 核。大多数 ESP32 外设都属于此类型。

> - 内部外设，是 Xtensa CPU 的一部分。

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

### 内部外设中断

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

> - 三个定时器比较器

> - 一个性能监视器

> - 两个软件中断

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

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

### 外部外设中断

其余中断源来自外部外设。

## IRAM 安全中断处理程序

在执行 SPI flash 的写入和擦除操作时，ESP32 会禁用 cache，中断处理程序将无法访问 SPI flash 和 SPIRAM。因此，ESP-IDF 中存在两种中断处理程序，它们各有优缺点：

IRAM 安全中断处理程序 - 只能访问内部内存中的代码和数据（代码存储在 IRAM 中，数据存储在 DRAM 中）。

- + 延迟：flash 写入和擦除操作相对缓慢，例如擦除可能需要几十或几百毫秒才能完成，但这些中断处理程序不受影响，执行速度较快且延迟较低，能够保证最小执行延迟。

- - 占用内部内存：中断处理程序占用了宝贵的内部内存，这些内存本可以用于其他目的。

- + cache 未命中：中断处理程序不依赖 cache, 因此就不会出现 cache 未命中带来的不确定性，因为代码和数据已存储在内部存储器中了。

- 使用场景：请使用 `ESP_INTR_FLAG_IRAM` 标志，通过中断分配器 API 注册此类中断。

非 IRAM 安全中断处理程序 - 可能会访问 flash 中的代码和（只读）数据。

- - 延迟：在进行 flash 操作时，中断处理程序会被推迟，因此平均延迟较高且难以预测。

- + 占用内部内存：该中断处理程序不使用内部 RAM，或者使用的内存比 IRAM 安全中断要少。

- 使用场景：通过中断分配器 API 注册此类中断时，请 不要 使用 `ESP_INTR_FLAG_IRAM` 标志。

请注意，没有任何显式标记将中断处理程序标识为 IRAM 安全。 当且仅当要访问的代码和数据存储在内部内存中时，中断处理程序才被隐式标记为 IRAM 安全。“IRAM 安全”这个术语实际上有点误导性，因为除了将处理程序的代码放在 IRAM 中之外，还有更多其他要求。以下是 不属于 IRAM 安全的中断处理程序示例：

- 部分代码放置在 flash 中。

- 放置在 IRAM 中但调用了存储在 flash 里的函数。

- 代码放置在 IRAM 中但访问了位于 flash 中的只读变量。

关于如何将代码和数据放置在 IRAM 或 DRAM 中，请参见 如何将代码放入 IRAM。

有关 SPI flash 操作及其与中断处理程序交互的更多详细信息，请参见 SPI flash API 文档。

备注

如果不能 100% 确定中断处理程序访问的所有代码和数据都位于 IRAM（代码）或 DRAM（数据）中，切勿使用 `ESP_INTR_FLAG_IRAM` 标志注册中断处理程序。忽略这一点将导致（有时是偶发性的）:ref:cache 错误 <cache_error>。通过调用函数间接访问代码和数据时也需要注意这点。

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

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

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

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

私有共享中断 (`ESP_INTR_FLAG_SHARED_PRIVATE`) 在行为上与 `ESP_INTR_FLAG_SHARED` 类似，允许多个中断源共享同一条 CPU 中断线。但与常规共享中断不同的是，中断分配器永远不会自动选择这些中断线。

换言之，即使使用 `ESP_INTR_FLAG_SHARED`，任何分配函数也不会选择由 `ESP_INTR_FLAG_SHARED_PRIVATE` 创建的中断线。这些中断线实际上是保留专用的，只能通过显式绑定新的中断源来使用（例如通过 `esp_intr_alloc_bind()` 或相关 API）。

因此，可以预先定义一组固定的中断源来共享同一条 CPU 中断线（例如多个 GPIO 中断源），同时避免该中断线被分配器复用于无关中断的情况。

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

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

备注

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

调用 `esp_intr_alloc()` 或 `esp_intr_alloc_intrstatus()` 时，中断分配器会选择第一个满足电平要求的中断为指定的源映射中断，而不会考虑已经映射到共享中断线上的其他源。然而，通过使用 `esp_intr_alloc_bind()` 或 `esp_intr_alloc_intrstatus_bind()` 函数，可以显式地指定中断处理程序与给定的中断源共享。

对于私有共享中断，首次分配使用 `ESP_INTR_FLAG_SHARED_PRIVATE` （通过 `esp_intr_alloc()` 或 `esp_intr_alloc_intrstatus()`），后续的源通过相同的函数进行附加，仍然使用 `ESP_INTR_FLAG_SHARED_PRIVATE` 标志和返回的句柄。

### 命名分组

无论是公共还是私有，共享中断组均可通过名称定义。具体通过 `esp_intr_alloc_info_t` 结构体的 `bind_by.name` 字段实现。若已存在指定名称的共享中断组（由先前使用相同名称的调用创建），则新中断源会附加到该组；否则，会分配一个新的共享中断组并打上该名称标记。 `bind_by.handle` 与 `bind_by.name` 只能设置其一。常规的 flags 规则（例如中断优先级、SHARED 与 SHARED_PRIVATE）仍然适用。这不会影响公共共享中断的行为：中断分配函数仍可在不指定名称或句柄的情况下，将新中断源附加到现有的公共共享中断线上。

## 排除中断分配故障

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()` 分配，但很少使用，因为大多数外设中断是电平触发的。

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

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

- 找到可接受更高延迟的中断，并用 `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_LEVEL2` 或 `ESP_INTR_FLAG_LEVEL3`。

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

## API 参考

### Header File

- components/esp_hw_support/include/esp_intr_types.h

- This header file can be included with:

  >     #include "esp_intr_types.h"

### 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

- components/esp_hw_support/include/esp_intr_alloc.h

- This header file can be included with:

  >     #include "esp_intr_alloc.h"

### Structures

struct esp_intr_alloc_info_t

  Structure used to specify all the information needed to allocate an interrupt. This structure can be passed to the function esp_intr_alloc_info.

  Public Members

  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

  intr_handle_t handle

    Handle of the interrupt to share with

  const char *name

    Name of the interrupt to share with

  struct esp_intr_alloc_info_t 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`.

此文档对您有帮助吗？

- 反馈已收到，谢谢！
- 如果您有其他意见，欢迎填写乐鑫文档反馈表。

- 我们重视您的反馈。
- 您可以填写乐鑫文档反馈表告诉我们如何改进该文档。