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()等)
短操作(通过 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,大多数任务将被禁用。有关更多详细信息,请参阅 缓存禁用(默认)。
有一些选项可以帮助减轻缓存禁用的影响。写入操作的影响在不同模式下是不同的。
XIP from PSRAM:在此模式下,所有过去从 Flash 执行的段都改为从 PSRAM 加载和执行。因此,缓存能在 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,就不会发生相应的硬件事件。
当 DMA 从 Flash 读取数据时
Flash 器件不允许在擦除/编程时读取,即使数据不在正在被擦除/编程的区域中。
当 flash 正在被擦除/编程时,DMA 读取的 Flash 数据是不可预测的。建议在擦除或写入之前停止 DMA 对 Flash 的访问。如果无法停止 DMA(例如,LCD 需要持续刷新存储在 Flash 中的图像数据),建议将此类数据复制到 PSRAM 或内部 SRAM。
flash 自动暂停功能
重要
应使用支持暂停/恢复功能的 flash。
应使用支持自动暂停功能的 MSPI 硬件,即硬件支持自动发送暂停命令。
如果在不支持自动暂停的 flash 上使用该功能,可能会造成严重的程序崩溃。因此,强烈建议提前阅读规格书,确保 flash 至少满足如下条件:
基于当前软件的实现限制,flash 状态寄存器中的 SUS 位应当位于 SR2 bit7 或 SR bit15。
基于当前软件的实现限制,flash 暂停命令应为 75H,恢复命令为 7AH。
flash 成功暂停后,除已擦除的段或块外,flash 地址中的任何内容都支持读取。也可以在此种状态下立即下达恢复命令。
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 擦除操作。
代码可以分为以下三类:
关键代码:存放在 IRAM/DRAM 中。这类代码通常有较高的性能要求,与 cache/flash/PSRAM 相关,或者被频繁调用。
缓存访问的代码:存放在 flash/PSRAM 中。这类代码的性能要求较低,或者较少被调用。flash 擦除时将执行这类代码,导致一定的开销。
低优先级代码:存放在 flash/PSRAM 中,且在 flash 擦除期间禁止运行。这类代码的任务优先级应低于擦除操作,避免影响 flash 擦除的速度。
关于 flash 自动暂停功能的用法和对应的响应时间延迟,请参考 system/flash_suspend 示例。
备注
由于 flash 自动暂停功能极度依赖时序,其多用作为一种优化方案,而非万能解法。例如 LCD 刷新、蓝牙、Wi-Fi 等场景对实时系统要求较高、或需要频繁触发中断,所以此方案并不适用。建议参照以下步骤,选择搭配 flash 自动暂停功能使用的 ISR 类型:
需注意图中的两个关键值:
ISR 时间 (ISR time):ISR 时间不能过长,需小于
IWDT中设置的值。ISR 时间会显著增加擦除/写入操作的完成时间。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 行为。
通过 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 进行确认。
软件控制的 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 的耗时会上升。
从 PSRAM 执行代码功能
选择 CONFIG_SPIRAM_XIP_FROM_PSRAM 配置以启用此模式。在此模式下,代码从 PSRAM 执行,在大多数情况下,缓存不会在写入 API 期间被禁用。
在此模式下,flash .text 段(用于指令)和 flash .rodata 段(用于只读数据)将在启动时加载到 PSRAM。相应的虚拟地址将映射到 PSRAM。您无需确保在 flash 被擦除/编程时执行的代码/数据位于 IRAM 中。
例外:当缓存映射 flash 中的区域时
由于 SPI Nor Flash 器件的限制,在 flash 被擦除/写入时,仍然不允许访问 Flash 中的缓存映射区域(通过 spi_flash_mmap 等 API 映射),无论擦写区域和映射区域是否重合。在这种情况下,在擦写 Flash 时仍应禁用缓存以防止从缓存读取错误的数据。
为了防止缓存禁用,在 SPI Flash 驱动程序中实现了一个锁,以确保缓存映射的存在和 Flash 写入是互斥的,大多数进行了 Flash 映射的 ESP-IDF API 都使用了该标志。如果您自己调用类似 mmap 的 API,可以指定此标志 SPI_FLASH_MMAP_FLAG_BLOCKS_WRITE 以防止缓存禁用。您不能在 spi_flash_mmap 和 spi_flash_munmap 之间使用 esp_flash_erase_* 或者 esp_flash_write 的任务中使用此标志(无论写入区域和映射区域是否重叠),否则会造成死锁。有关该标志的详细信息,请参阅 关于 SPI_FLASH_MMAP_FLAG_BLOCKS_WRITE 标志。
如果在没有此标志的情况下调用类似 mmap 的 API,当 Flash 发生擦除或者写入时,缓存仍将被禁用。