# SPI0/1 上 Flash 的并发约束

[中文]

SPI0/1 总线在缓存和 SPI1 外设（由包括此 SPI Flash 驱动在内的驱动程序控制）之间共享。对 SPI1 的操作可能会对缓存以及整个系统造成重大影响。连接到其他 SPI 总线的 flash 芯片没有此类约束和影响，不在本文档的讨论范围中。

SPI0/1 总线上可能发生三种活动：

- Flash 写入操作（通过 SPI1）。例如，擦除、页面编程或状态寄存器写入命令（例如，`SE`、`PP` 和 `WRSR`）。在这些命令期间，flash 处于不可读状态。CPU 和缓存必须等待直到写入命令完成。以下 API 可以触发写入命令：

  > - 调用非加密 SPI flash 写入 API（`esp_flash_write()`、`esp_flash_erase_region()` 等）

  > - 调用 `esp_flash_write_encrypted()`

- 短操作（通过 SPI1，包括非写入 flash 命令）。以下 API 可以触发短操作：

  > - 调用非加密 SPI flash 读取 API（`esp_flash_read()` 等）

- 缓存读取（通过 SPI0）。以下 API 和操作可以触发缓存读取：

  > - 从 SPI Flash 或 PSRAM 执行代码

  > - 从 SPI Flash 或 PSRAM 获取 .data/.rodata/.bss 段的静态数据

  > - 通过堆或 esp_himem 对 PSRAM 的所有其他读/写操作

  > - 从映射到 SPI Flash 的区域读取，包括：

  >   > - 类似 mmap 的函数：`spi_flash_mmap()`、`spi_flash_mmap_pages()`、`esp_mmu_map()`、`bootloader_mmap()` 和 `esp_partition_mmap()`。

  >   > - 依赖 `spi_flash_mmap()` 的函数：`esp_partition_find()`、`esp_partition_register_external()`。

  >   > - 加密 flash 读/写 API：`esp_flash_read_encrypted()` 和 `esp_flash_write_encrypted()` （在 esp32 上，或用于数据验证）。

所有 SPI flash API 通过驱动程序提供的某些内部互斥锁实现互斥访问。

对于所有 SPI1 操作（读/写），默认情况下在这些操作期间缓存会被禁用，因此无法访问 Flash/PSRAM，大多数任务将被禁用。有关更多详细信息，请参阅 缓存禁用（默认）。

有一些选项可以帮助减轻缓存禁用的影响。写入操作的影响在不同模式下是不同的。

- Auto Suspend：在此模式下，当 flash 区域发生缓存未命中时，允许暂停 flash 写入以透明地从中读取，但会有一些延迟。因此，缓存保持启用状态，代码在写入操作期间影响不会很大。

  > 这是一个可选功能，依赖于特殊的 SPI Flash 型号，因此默认禁用。有关更多详细信息，请参阅 flash 的可选功能 和 flash 自动暂停功能。

有关软件实现的详细信息，请参阅 OS 函数 和 SPI 总线锁。

## 缓存禁用（默认）

默认情况下，在 SPI1 操作期间缓存会被禁用。所有 SPI1 操作将自动透明地禁用缓存。

当禁用缓存时，所有非 IRAM 安全的中断将被禁用，所有其他任务将被暂停。只有 IRAM 安全的中断处理程序将被执行。这些将在 Flash 操作完成时恢复。

有关如何在禁用缓存时防止中断处理程序被禁用的信息，请参阅 IRAM 安全中断处理程序。

当禁用缓存时，所有 CPU 应该只从内部 RAM 执行代码和访问数据。有关内部 RAM（例如，IRAM、DRAM）和 flash 缓存之间的差异，请参阅 应用程序内存布局 文档。

### IRAM 安全中断处理程序

如果需要在 flash 操作期间运行中断处理程序（比如低延迟操作），请在 注册中断处理程序 时设置 `ESP_INTR_FLAG_IRAM`。

请确保中断处理程序访问的所有数据和函数（包括其调用的数据和函数）都存储在 IRAM 或 DRAM 中。参见 如何将代码放入 IRAM。

在未将函数或符号正确放入 IRAM/DRAM 的情况下，在 flash 操作期间，中断处理程序从 flash cache 中读取数据时，会导致程序崩溃。这可能是因为代码未正确放入 IRAM，产生了非法指令异常，也可能是因为常数未正确放入 DRAM，读取到了垃圾数据。

备注

在 ISRs 中处理字符串时，不建议使用 `printf` 和其他输出函数。为了方便调试，在从 ISRs 中获取数据时，请使用 `ESP_DRAM_LOGE()` 和类似的宏。请确保 `TAG` 和格式字符串都放置于 `DRAM` 中。

### 非 IRAM 安全中断处理程序

如果在注册时没有设置 `ESP_INTR_FLAG_IRAM` 标志，当禁用 cache 时，将不会执行中断处理程序。一旦 cache 恢复，非 IRAM 安全的中断将重新启用，中断处理程序随即再次正常运行。这意味着，只要禁用了 cache，就不会发生相应的硬件事件。

## flash 自动暂停功能

重要

1. 应使用支持暂停/恢复功能的 flash。

2. 应使用支持自动暂停功能的 MSPI 硬件，即硬件支持自动发送暂停命令。

如果在不支持自动暂停的 flash 上使用该功能，可能会造成严重的程序崩溃。因此，强烈建议提前阅读规格书，确保 flash 至少满足如下条件：

1. 基于当前软件的实现限制，flash 状态寄存器中的 SUS 位应当位于 SR2 bit7 或 SR bit15。

2. 基于当前软件的实现限制，flash 暂停命令应为 75H，恢复命令为 7AH。

3. flash 成功暂停后，除已擦除的段或块外，flash 地址中的任何内容都支持读取。也可以在此种状态下立即下达恢复命令。

4. flash 从暂停模式恢复后，支持立即下达另一个暂停命令。

启用 CONFIG_SPI_FLASH_AUTO_SUSPEND 后，缓存将保持启用状态，禁用该选项即可禁用缓存。SPI0 和 SPI1 之间的仲裁由硬件决定。当 SPI1 进行读取等耗时较短的操作时，CPU 和缓存将等待至 SPI1 操作完成。然而，在擦除、页面写入或状态寄存器写入等过程中，如 `SE`、`PP` 和 `WRSR`，自动暂停功能将中断正在进行的 flash 操作，使 CPU 得以在有限的时间内读取缓存及 flash 中的数据。

基于此功能，部分的代码及变量现可存放在 flash/PSRAM 中，同时仍能保证在 flash 擦除期间的正常执行，减少了 IRAM/DRAM 的消耗。

需注意，使用此功能暂停/恢复 flash 时，会导致额外开销。在 flash 擦除期间，频繁触发中断将显著延长擦除时间。为避免这种情况，建议调整 FreeRTOS 任务优先级，仅将实时关键的任务排在擦除操作前，从而确保在合理时间内完成 flash 擦除操作。

代码可以分为以下三类：

1. 关键代码：存放在 IRAM/DRAM 中。这类代码通常有较高的性能要求，与 cache/flash/PSRAM 相关，或者被频繁调用。

2. 缓存访问的代码：存放在 flash/PSRAM 中。这类代码的性能要求较低，或者较少被调用。flash 擦除时将执行这类代码，导致一定的开销。

3. 低优先级代码：存放在 flash/PSRAM 中，且在 flash 擦除期间禁止运行。这类代码的任务优先级应低于擦除操作，避免影响 flash 擦除的速度。

关于 flash 自动暂停功能的用法和对应的响应时间延迟，请参考 system/flash_suspend 示例。

备注

由于 flash 自动暂停功能极度依赖时序，其多用作为一种优化方案，而非万能解法。例如 LCD 刷新、蓝牙、Wi-Fi 等场景对实时系统要求较高、或需要频繁触发中断，所以此方案并不适用。建议参照以下步骤，选择搭配 flash 自动暂停功能使用的 ISR 类型：

需注意图中的两个关键值：

1. ISR 时间 (ISR time)：ISR 时间不能过长，需小于 `IWDT` 中设置的值。ISR 时间会显著增加擦除/写入操作的完成时间。

2. ISR 间隔时间 (ISR interval)：由于不能频繁触发 ISR，需格外注意 ISR 间隔时间减去 ISR 时间后的剩余时间 （图中 b 点至 c 点的距离）。在此期间，SPI1 会发送恢复命令重新启动操作，所需准备时间 `tsus` 的典型值约为 40 us。如果在 SPI1 完成恢复操作前接收到了新的暂停命令，可能导致 CPU 饥饿，触发 `TWDT`。

对于第 2 点中所提到的 `tsus` 时间可以通过翻阅 flash datasheets 查找，通常在 AC CHARACTERISTICS 章节中。用户需要保证从 datasheets 获得的 `tsus` 值不大于 CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US 值。

此外，flash 暂停可能延迟。CPU 和缓存通过 SPI0 频繁访问 flash，且 SPI1 频繁发送暂停命令时，会导致 MSPI 数据传输效率下降。可以通过在内部使用 锁 来避免此种情况。当 SPI1 发送暂停命令时，SPI0 将接管内存 SPI 总线并启用锁。SPI0 完成数据传输后，在锁延迟时间结束前，都将保有对内存 SPI 总线的控制权。在此锁延迟期间，如果接收到其他 SPI0 事务，则该 SPI0 事务将正常进行，并开启新一轮锁延迟周期。如无其他 SPI0 事务，则 SPI0 释放内存总线并启动 SPI0/1 仲裁。

### suspend-resume 进阶用法

在默认配置下，flash 暂停由硬件自动发出，恢复也由硬件在合适的时机自动完成，并采用基于固定时间的延迟来等待暂停命令生效。这种实现已经可以满足绝大多数场景。然而在某些对实时性、性能或暂停粒度有更高要求的场景下，ESP-IDF 还提供了进阶配置项，用于精细化控制 suspend-resume 行为。

1. 通过 flash 寄存器自动判断暂停是否生效

通常情况下，硬件在发出暂停命令后，会按 `tsus` 设定的时间延迟一段时间，再开放内存总线给 SPI0/CPU 使用。该延迟必须按 datasheet 中的最坏情况进行设置，因此在大多数情况下都偏保守。

通过启用 CONFIG_SPI_FLASH_AUTO_CHECK_SUSPEND_STATUS 后，硬件将通过读取 flash 状态寄存器中的 `WIP` 位，来判断暂停命令是否真正生效，而不再依据 CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US 给出的固定时间进行等待。由于实际的暂停建立时间通常远小于 datasheet 给出的最大值，所以这种方式可以显著降低暂停过程中的开销，提升整体性能。

重要

使用前需确认所用 flash 在暂停后 `WIP` 位的行为符合预期。绝大多数支持 suspend/resume 的 flash 都满足该条件，但仍建议查阅相应 datasheet 进行确认。

2. 软件控制的 flash resume

通常情况下，硬件在 flash 被暂停后，会自动安排合适的时机发送 resume 命令，让 flash 继续完成原有的擦除/写入操作。这种实现对软件透明，但存在一个副作用：在高优先级任务/中断仍在运行时，硬件可能再次发起 suspend/resume 流程，从而打断这些任务，影响其执行的连续性与时序。

通过启用 CONFIG_SPI_FLASH_SOFTWARE_RESUME 后，硬件自动 resume 功能将被关闭，flash 的恢复操作改由软件在合适的时机主动发出。此时，flash 在被暂停后将一直保持暂停状态，直到软件显式恢复。在 SPI1 的等待空闲流程中，软件会主动检查 flash 的 suspend 状态，若发现处于暂停状态则调用驱动的 resume 接口让 flash 重新开始原有操作。这意味着只有当高优先级任务或中断真正完成、软件回到 SPI1 操作上下文时，才会发出 resume，从而避免 suspend-resume 行为在高优先级路径上反复抢占总线。

由于该机制依赖软件层在确定的执行点完成 resume，且当前实现没有针对多核做相应保护，因此该选项有以下限制：

- 仅支持单核场景，需要使能 CONFIG_FREERTOS_UNICORE。

- 属于实验性功能，需要使能 CONFIG_IDF_EXPERIMENTAL_FEATURES 后才能可见。

- 该功能会提升中断响应的连续性，提升应用的性能。但同时，单次操作 flash 的耗时会上升。

此文档对您有帮助吗？

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

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