内存优化
固件应用程序的可用 RAM 在某些情况下可能处于低水平,甚至完全耗尽。为此,应调整这些情况下固件应用程序的内存使用情况。
固件通常需要为内部 RAM 保留备用空间,用于应对非常规情况,或在后续版本的更新中,适应 RAM 使用需求的变化。
背景
在进行 ESP-IDF 的内存优化前,应了解有关 ESP32-C6 内存类型的基础知识、C 语言中静态和动态内存使用的区别、以及 ESP-IDF 中栈和堆的使用方式。以上信息均可参阅 堆内存分配。
测量静态内存使用情况
测量动态内存使用情况
ESP-IDF 包含一系列堆 API,可以在运行时测量空闲堆内存,请参阅 堆内存调试。
备注
在嵌入式系统中,除 RAM 使用总量外,也应重点关注堆碎片化问题。堆测量 API 提供了一些方法,可以测量最大空闲内存块。通过监测最大空闲内存块和总空闲字节数,可以快速了解是否存在堆碎片化问题。
静态内存优化
降低应用程序的静态内存使用,会增加运行时堆的可用 RAM 空间,反之亦然。
优化静态内存使用通常需要监测
.data
和.bss
的大小,有关工具请参阅 测量静态数据大小。在 C 语言中,ESP-IDF 内部函数不会占用大量静态 RAM。在多数情况下(例如 Wi-Fi 库、蓝牙控制器和 IEEE 802.15.4 库),静态缓冲区仍从堆中分配。然而,这些分配只在功能初始化阶段进行一次,并在功能去初始化时释放,从而在应用程序生命周期中,优化不同阶段的可用内存。
要实现静态内存优化,请执行以下操作:
由于常量数据可以存储在 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_ESP_SYSTEM_HW_STACK_GUARD 选项启用硬件栈保护。
栈末尾监视点
栈末尾监视点将 CPU 监视点放置在当前栈的末尾。如果该字被覆盖(例如栈溢出),则会立即触发紧急情况提示。在未使用调试器的监视点时,可以设置 CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK 选项,启用栈末尾监视点功能。
栈 canary 字节
栈 canary 字节功能在每个任务的栈末尾添加一组魔术字节,并在每次上下文切换时检查这些字节是否已更改。如果这些魔术字节被覆盖,则会触发紧急情况提示。可以通过 CONFIG_FREERTOS_CHECK_STACKOVERFLOW 选项启用栈 canary 字节功能。
备注
使用栈末尾监视点或栈 canary 字节时,栈指针可能在栈溢出时跳过监视点或 canary 字节,损坏 RAM 的其他区域。因此,上述方法并不能检测所有的栈溢出。
推荐启用默认选项 CONFIG_ESP_SYSTEM_HW_STACK_GUARD,避免这个缺点。
任务运行时确定栈内存大小的方法
调用
uxTaskGetStackHighWaterMark()
会返回任务整个生命周期中空闲栈内存的最小值,从而较好地显示出任务未使用的栈内存量。从任务本身内部调用
uxTaskGetStackHighWaterMark()
是调用该函数最容易的方式:在任务达到其栈内存使用峰值后,调用uxTaskGetStackHighWaterMark(NULL)
获取当前任务的高水位标记,换言之,如果有主循环,请多次执行主循环来覆盖各种状态,随后调用uxTaskGetStackHighWaterMark()
。通常可以用任务的栈内存总大小减去调用
uxTaskGetStackHighWaterMark()
的返回值,计算任务实际使用的栈内存大小,但应留出一定的安全余量,应对运行时栈内存使用量的小幅意外增长。
调用
uxTaskGetSystemState()
来获取系统中所有任务的摘要,包括各栈内存的高水位标记值。
减少栈内存大小
避免占用过多栈内存的函数。字符串格式化函数(如
printf()
)会使用大量栈内存,如果任务不调用这类函数,通常可以减小其占用的栈内存。使用实验性的选项 使用 Picolibc 替代 Newlib 可以显著减少
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 大小造成的影响,请参阅配置项的帮助文本。