警告
This document is not updated for ESP32C61 yet, so some of the content may not be correct.
This warning was automatically inserted due to the source file being in the add_warnings_pages list.
内存优化
固件应用程序的可用 RAM 在某些情况下可能处于低水平,甚至完全耗尽。为此,应调整这些情况下固件应用程序的内存使用情况。
固件应用程序通常需要为内部 RAM 保留备用空间,用于应对非常规情况,或在后续版本的更新中,适应 RAM 使用需求的变化。
背景
在进行 ESP-IDF 的内存优化前,应了解有关 ESP32-C61 内存类型的基础知识、C 语言中静态和动态内存使用的区别、以及 ESP-IDF 中栈和堆的使用方式。以上信息均可参阅 堆内存分配。
测量静态内存使用情况
测量动态内存使用情况
ESP-IDF 包含一系列堆 API,可以在运行时测量空闲堆内存,请参阅 堆内存调试。
备注
在嵌入式系统中,除 RAM 使用总量外,也应重点关注堆碎片化问题。堆测量 API 提供了一些方法,可以测量最大空闲内存块。通过监测最大空闲内存块和总空闲字节数,可以快速了解是否存在堆碎片化问题。
静态内存优化
降低应用程序的静态内存使用,会增加运行时堆的可用 RAM 空间,反之亦然。
优化静态内存使用通常需要监测
.data
和.bss
的大小,有关工具请参阅 测量静态数据大小。在 C 语言中,ESP-IDF 内部函数不会占用大量静态 RAM。在多数情况下(例如 Wi-Fi 库和蓝牙控制器),静态缓冲区仍从堆中分配。然而,这些分配只在功能初始化阶段进行一次,并在功能去初始化时释放,从而在应用程序生命周期中,优化不同阶段的可用内存。
要实现静态内存优化,请执行以下操作:
由于常量数据可以存储在 flash 中,不占用 RAM,建议尽量将结构体、缓冲区或其他变量声明为
const
。为此,可能需要修改固件参数,使其接收const *
参数而非可变指针参数。以上更改还可以减少某些函数的栈内存使用。若使用 Bluedroid,请设置 CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY 选项,Bluedroid 将在初始化时分配内存,并在去初始化时释放内存。这并不一定会降低内存使用峰值,但可以将使用静态内存改为运行时使用动态内存。
若使用 OpenThread,请设置 CONFIG_OPENTHREAD_PLATFORM_MSGPOOL_MANAGEMENT 选项,OpenThread 将从外部 PSRAM 中分配消息池缓冲区,从而减少对内部静态内存的使用。
确定栈内存大小
在 FreeRTOS 操作系统中,任务栈通常从堆中分配。每个任务的栈大小固定,且会作为参数传递给 xTaskCreate()
。每个任务可用的栈内存不得超过为其分配的栈内存大小,否则将导致栈内存溢出或堆内存损坏,使原本可用的程序崩溃。
因此,确定每个任务栈内存的最佳大小、最小化每个任务栈内存大小、以及最小化任务栈内存的整体数量,都可以大幅减少 RAM 的使用。
栈溢出检测的配置选项
栈末尾监视点
栈末尾监视点将 CPU 监视点放置在当前栈的末尾。如果该字被覆盖(例如栈溢出),则会立即触发紧急情况提示。在未使用调试器的监视点时,可以设置 CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK 选项,启用栈末尾监视点功能。
栈金丝雀字节
栈金丝雀字节功能在每个任务的栈末尾添加一组魔术字节,并在每次上下文切换时检查这些字节是否已更改。如果这些魔术字节被覆盖,则会触发紧急情况提示。可以通过 CONFIG_FREERTOS_CHECK_STACKOVERFLOW 选项启用栈金丝雀字节功能。
备注
使用栈末尾监视点或栈金丝雀字节时,栈指针可能在栈溢出时跳过监视点或金丝雀字节,损坏 RAM 的其他区域。因此,上述方法并不能检测所有的栈溢出。
任务运行时确定栈内存大小的方法
调用
uxTaskGetStackHighWaterMark()
会返回任务整个生命周期中空闲栈内存的最小值,从而较好地显示出任务未使用的栈内存量。从任务本身内部调用
uxTaskGetStackHighWaterMark()
是调用该函数最容易的方式:在任务达到其栈内存使用峰值后,调用uxTaskGetStackHighWaterMark(NULL)
获取当前任务的高水位标记,换言之,如果有主循环,请多次执行主循环来覆盖各种状态,随后调用uxTaskGetStackHighWaterMark()
。通常可以用任务的栈内存总大小减去调用
uxTaskGetStackHighWaterMark()
的返回值,计算任务实际使用的栈内存大小,但应留出一定的安全余量,应对运行时栈内存使用量的小幅意外增长。
调用
uxTaskGetSystemState()
来获取系统中所有任务的摘要,包括各栈内存的高水位标记值。
减少栈内存大小
避免占用过多栈内存的函数。字符串格式化函数(如
printf()
)会使用大量栈内存,如果任务不调用这类函数,通常可以减小其占用的栈内存。启用 Newlib Nano 格式化,可以在任务调用
printf()
或其他 C 语言字符串格式化函数时,减少这类任务的栈内存使用量。
避免在栈上分配大型变量。在 C 语言声明的默认作用域中,任何分配为自动变量的大型结构体或数组都会占用栈内存。要优化这些变量占用的栈内存大小,可以使用静态分配,或仅在需要时从堆中动态分配。
避免调用深度递归函数。尽管调用单个递归函数并不一定会占用大量栈内存,但若每个函数都包含大量基于栈的变量,那么调用这些函数的开销将会很高。
减少任务数量
合并任务。如果从未创建某个特定任务,就不会分配该任务的栈内存,从而极大减少 RAM 使用。如果某些任务可以与另一个任务合并,通常可以将不必要的任务删除。在应用程序中,如果满足以下条件,通常可以合并或删除任务:
任务所执行的内容可以按顺序分解为多个函数调用。
任务所执行的内容可以分解为较小的工作,这些工作可以通过 FreeRTOS 队列或类似机制串行化,并由工作任务执行。
内部任务栈内存大小
为进行系统维护,或操作系统功能,ESP-IDF 分配了许多内部任务,一部分在启动过程中创建,一部分在初始化特定功能时创建。
为了确保支持所有常见的使用模式,这些任务栈内存的默认设置值较大。ESP-IDF 支持配置栈内存大小,因此可以减小任务栈内存,匹配其实际运行时的栈内存使用情况。
重要
如果内部任务的栈内存设置得过小,可能会导致 ESP-IDF 发生无法预测的崩溃。即使任务栈内存溢出是导致崩溃的根本原因,在调试过程中也很难确定具体原因。因此,建议特别关注任务在负载高时的高水位标记,在必要情况下,谨慎减小内部任务的栈内存大小。如果在减小内部任务堆内存大小后,仍遇到问题,请在报告中提供以下信息,以及正在使用的具体配置。
运行主任务 的栈内存大小为 CONFIG_ESP_MAIN_TASK_STACK_SIZE。
系统任务 ESP 定时器(高分辨率定时器) 用于执行回调函数,其栈内存大小为 CONFIG_ESP_TIMER_TASK_STACK_SIZE。
部分 FreeRTOS 定时器任务用于处理 FreeRTOS 定时器回调,其栈内存大小为 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH。
系统任务 事件循环库 用于执行默认系统事件循环回调,其栈内存大小为 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE。
TCP/IP 任务 lwIP 的栈内存大小为 CONFIG_LWIP_TCPIP_TASK_STACK_SIZE。
蓝牙 API 的栈内存大小为 CONFIG_BT_BTC_TASK_STACK_SIZE,CONFIG_BT_BTU_TASK_STACK_SIZE。
NimBLE-based Host APIs 的栈内存大小为 CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE。
以太网驱动程序会创建任务,用于使 MAC 接收以太网帧,在默认配置
ETH_MAC_DEFAULT_CONFIG
下,任务栈内存大小为 4 KB。在初始化以太网 MAC 时,传递自定义eth_mac_config_t
结构体可以更改此设置。FreeRTOS 空闲任务栈内存大小由 CONFIG_FREERTOS_IDLE_TASK_STACKSIZE 配置。
使用 ESP-MQTT 组件时会创建一个任务,其栈内存大小由 CONFIG_MQTT_TASK_STACK_SIZE 配置。MQTT 栈内存大小也可以使用
esp_mqtt_client_config_t
结构体中的task_stack
字段配置。有关使用
mDNS
时内存优化的详细信息,请参阅 优化内存使用。
备注
除 ESP 定时器等内置系统功能外,若固件应用程序没有初始化 ESP-IDF 中特定功能,则不会创建相关任务。此时,相关任务的栈内存使用量为零,而这些功能没有与之关联的任务,因此无需考虑其栈内存大小配置。
堆内存优化
有关分析运行时堆内存使用的函数,请参阅 堆内存调试。
通常,堆内存优化包含以下几个方面:分析堆内存使用情况、撤回未使用的 malloc()
调用、缩小相应的内存使用大小、或提早释放先前分配的缓冲区。
以下是一些 ESP-IDF 配置选项,有助于在运行时实现堆内存优化:
lwIP 文档中的有关章节介绍了如何配置 最小内存使用。
Wi-Fi 缓冲区使用情况 中介绍了一些选项,这些选项可以减少对静态缓冲区的使用,或减少运行时动态缓冲区的最大数量,从而最小化内存使用,但可能会影响性能。注意,Wi-Fi 初始化时,仍会从堆中分配静态 Wi-Fi 缓冲区,并在 Wi-Fi 去初始化时释放这些缓冲区。
部分 Mbed TLS 配置选项也可用于堆内存优化,详情请参阅 减少内存使用 的 Mbed TLS 部分。
备注
如果将某些配置选项更改为非默认值,也会增加运行时的堆内存使用。这类选项未在上文中列出,但配置选项的帮助文档中给出了相应说明。
IRAM 优化
程序运行时,由于使用了静态 IRAM,用于堆内存使用的 DRAM 会相应减少。反之,可以通过减少 IRAM 使用,增加可用 DRAM。
如果应用程序分配的静态 IRAM 超过可用上限,应用程序将无法构建,并出现链接器错误,如 section '.iram0.text' will not fit in region 'iram0_0_seg'
、IRAM0 segment data does not fit
以及 region 'iram0_0_seg' overflowed by 84-bytes
。如果发生这种情况,应找到减少静态 IRAM 使用的方法,链接应用程序。
要分析固件应用程序二进制文件中的 IRAM 使用情况,请使用 测量静态数据大小。如果固件应用程序链接失败,请参阅 Showing Size When Linker Fails 中的步骤,分析失败原因。
要对某些 ESP-IDF 功能进行 IRAM 优化,请使用以下选项:
启用 CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH。只要没有从 ISR 中错误地调用这些函数,就可以在所有配置中安全启用此选项。
启用 CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH。只要没有从 ISR 中错误地调用这些函数,就可以在所有配置中安全启用此选项。
启用 CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH。如果从 IRAM 中的中断上下文中使用 ISR ringbuf 函数,例如启用了 CONFIG_UART_ISR_IN_IRAM,则无法安全使用此选项。在此情况下,安装 ESP-IDF 相关驱动程序时,将在运行时报错。
禁用 Wi-Fi 选项 CONFIG_ESP_WIFI_IRAM_OPT 和/或 CONFIG_ESP_WIFI_RX_IRAM_OPT 会释放可用 IRAM,但会牺牲部分 Wi-Fi 性能。
启用 CONFIG_SPI_FLASH_ROM_IMPL 选项可以释放一些 IRAM,但此时 esp_flash 错误修复程序及新的 flash 芯片支持将失效,详情请参阅 SPI Flash API ESP-IDF 版本与芯片 ROM 版本的对比。
禁用 CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR 可以防止从 IRAM 安全中断处理程序 中发布
esp_event
事件,节省 IRAM 空间。禁用 CONFIG_SPI_MASTER_ISR_IN_IRAM 可以防止在写入 flash 时发生 spi_master 中断,节省 IRAM 空间,但可能影响 spi_master 的性能。
禁用 CONFIG_SPI_SLAVE_ISR_IN_IRAM 可以防止在写入 flash 时发生 spi_slave 中断,节省 IRAM 空间。
设置 CONFIG_HAL_DEFAULT_ASSERTION_LEVEL 为禁用 HAL 组件的断言,可以节省 IRAM 空间,对于经常调用
HAL_ASSERT
且位于 IRAM 中的 HAL 代码尤为如此。要禁用不需要的 flash 驱动程序,节省 IRAM 空间,请参阅 sdkconfig 菜单中的
Auto-detect Flash chips
选项。启用 CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH。只要未启用 CONFIG_SPI_MASTER_ISR_IN_IRAM 选项,且没有从 ISR 中错误地调用堆函数,就可以在所有配置中安全启用此选项。
备注
将常用函数从 IRAM 移动到 flash,可能会增加函数的执行时间。
备注
部分配置选项可以将一些功能移动到 IRAM 中,从而提高性能,但这类选项默认不进行配置,因此未在此列出。了解启用上述选项对 IRAM 大小造成的影响,请参阅配置项的帮助文本。