ESP32 内存分析—案例研究

本文翻译自:https://medium.com/the-esp-journal/esp32-memory-analysis-case-study-eacc75fe5431

内存对硅片成本以及芯片尺寸具有重大影响,因此从硬件角度看,优化内存尺寸很重要,从软件角度来看,能够充分利用内存资源也至关重要。

在本文中,我们将讨论 ESP-IDF 中的一些即将推出的功能和常用配置选项(可调项)(译者注:原文发表于 2020.06.02),以允许 终端应用程序 以最佳方式利用各个内部存储区域。

重要提示

  • 这里我们将重点介绍 ESP32 的单核模式,因为该模式下可以适用更多内存优化功能;

  • 我们将在这里考虑典型的 IoT 用例,在该用例下牺牲性能获得内存是可以接受的准则;

  • 我们将以典型的云应用程序为研究用例,该应用需要具有相互认证支持的 TLS 连接;

  • 这里使用的 ESP-IDF 功能分支可查阅 https://github.com/mahavirj/esp-idf/tree/feature/memory_optimizations

ESP32:内部存储器分解

ESP32 内存布局

ESP32 内存布局
  • 从上面的存储器布局可以看出,芯片内部存在各种具有不同时钟速度的存储器区域;

  • 对于单核用例,我们获得了额外的 32K 指令存储空间(IRAM),否则(双核模式)该区域会作为 APP CPU 内核的 cache;

  • 对指令 RAM 的访问,地址和空间大小应始终 32 位对齐;

  • 对于终端应用程序的业务逻辑来说,总是希望有更多的 DRAM,它是访问速度最快的内存且没有任何访问限制。

案例研究— AWS IoT 示例应用程序

  • 我们将使用来自 ESP-AWS-IoT 的 subscribe_publish 示例作为研究案例,来分析内存利用率;

  • ESP-IDF 提供了一个 API ,可以使用 heap_caps_get_minimum_free_size() 获取最小空闲堆或者说系统中可用的动态内存大小。我们的目标是最大化这个数字(进行相对分析),从而增加终端应用程序特定业务逻辑的可用内存数量(特别是 DRAM 区域)

默认内存利用率

我们将在 subscribe_publish 示例之上添加以下代码补丁来记录动态内存的统计信息。

  • 首先,我们将分别记录 DRAM 和 IRAM 区域如上所述的系统最小空闲堆大小;

  • 其次,我们将使用堆任务跟踪功能,该功能提供了基于每个任务的动态内存使用信息。修改此功能后,还可以记录每个任务 DRAM 和 IRAM 区域的峰值使用量;

  • 我们将分别为 aws_iot_tasktiT (tcpip)wifi 任务记录这些信息 (因为这些任务定义了从应用层到物理层的数据传输通道,反之亦然)。还应该注意的是,网络任务的峰值内存使用量会受到环境因素(如 Wi-Fi 连接,网络等待时间)的影响而变化。

注意 : 任务创建过程中对 core-id 的更改(代码补丁如下)是针对单核的配置,这里我们仅用于此特殊示例。

diff --git examples/subscribe_publish/main/subscribe_publish_sample.c examples/subscribe_publish/main/subscribe_publish_sample.c
index c5b48ae..1982375 100644
--- examples/subscribe_publish/main/subscribe_publish_sample.c
+++ examples/subscribe_publish/main/subscribe_publish_sample.c
@@ -157,6 +157,28 @@ void disconnectCallbackHandler(AWS_IoT_Client *pClient, void *data) {
     }
 }
 
+#include "esp_heap_task_info.h"
+static void esp_dump_per_task_heap_info(void)
+{
+    heap_task_stat_t tstat = {};
+    bool begin = true;
+    printf("Task Heap Utilisation Stats:\n");
+    printf("||\tTask\t\t|\tPeak DRAM\t|\tPeak IRAM\t|| \n");
+    while (1) {
+        size_t ret = heap_caps_get_next_task_stat(&tstat, begin);
+        if (ret == 0) {
+            printf("\n");
+            break;
+        }
+        const char *task_name = tstat.task ? pcTaskGetTaskName(tstat.task) : "Pre-Scheduler allocs";
+        if (!strcmp(task_name, "wifi") || !strcmp(task_name, "tiT") || !strcmp(task_name, "aws_iot_task")) {
+            printf("||\t%-12s\t|\t%-5d\t\t|\t%-5d\t\t|| \n",
+                        task_name, tstat.peak[0], tstat.peak[1]);
+        }
+        begin = false;
+    }
+}
+
 void aws_iot_task(void *param) {
     char cPayload[100];
 
@@ -278,6 +300,16 @@ void aws_iot_task(void *param) {
         }
 
         ESP_LOGI(TAG, "Stack remaining for task '%s' is %d bytes", pcTaskGetTaskName(NULL), uxTaskGetStackHighWaterMark(NULL));
+
+        const int min_free_8bit_cap = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);
+        const int min_free_32bit_cap = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL|MALLOC_CAP_32BIT);
+
+        esp_dump_per_task_heap_info();
+        printf("System Heap Utilisation Stats:\n");
+        printf("||   Miniumum Free DRAM\t|   Minimum Free IRAM\t|| \n");
+        printf("||\t%-6d\t\t|\t%-6d\t\t||\n",
+                    min_free_8bit_cap, (min_free_32bit_cap - min_free_8bit_cap));
+
         vTaskDelay(1000 / portTICK_RATE_MS);
         sprintf(cPayload, "%s : %d ", "hello from ESP32 (QOS0)", i++);
         paramsQOS0.payloadLen = strlen(cPayload);
@@ -328,5 +360,5 @@ void app_main()
     ESP_ERROR_CHECK( err );
 
     initialise_wifi();
-    xTaskCreatePinnedToCore(&aws_iot_task, "aws_iot_task", 9216, NULL, 5, NULL, 1);
+    xTaskCreatePinnedToCore(&aws_iot_task, "aws_iot_task", 9216, NULL, 5, NULL, 0);
 }
在订阅发布示例之上打补丁

使用默认配置(并启用了堆任务跟踪功能),我们得到以下堆利用率统计信息(所有值以字节为单位):

任务的内存使用率统计:
 || 任务名       | 峰值 DRAM |峰值 IRAM||
 || aws_iot_task | 63124   |     0   ||
 || tiT          | 3840    |     0   ||
 || wifi         | 31064   |     0   ||
 
 系统的内存使用率统计:
 || 最小可用 DRAM | 最小可用 IRAM ||
 || 152976        | 40276        ||

单核配置

如前所述,我们将在所有实验中使用单核配置。请注意,即使在单核模式下,ESP32 仍具有足够的处理能力(接近 300 DMIPS),足以满足典型的 IoT 用例的需求。

在应用程序中启用相应的配置:

CONFIG_FREERTOS_UNICORE = y

重新运行应用程序之后更新的堆利用率统计信息如下所示:

任务的内存使用率统计:
 || 任务名       | 峰值 DRAM |峰值 IRAM||
 || aws_iot_task | 63124   |     0   ||
 || tiT          | 3892    |     0   ||
 || wifi         | 31192   |     0   ||
 
 系统的内存使用率统计:
 || 最小可用DRAM | 最小可用IRAM ||
 || 162980      | 76136       ||

从上面可以看出,我们在 DRAM 中多获得了约 10KB 的内存,这是由于不再需要第二个 CPU 内核的某些服务(例如,idle, esp_timer 任务等)。此外,也不再需要用于处理器间通信的 IPC 服务,因此我们可以从该服务的堆栈和动态内存中获得额外内存。 IRAM 的增加是由于释放了第二个 CPU 内核的 32KB cache,并且由于禁用了上述服务节省了一些代码空间。

TLS 特定(优化)

非对称 TLS 内容长度

此功能从 v4.0 开始已成为 ESP-IDF 的一部分。此功能允许为 TLS IN/OUT 缓冲区启用非对称的内容长度。因此,应用程序能够将 TLS OUT 缓冲区从默认值 16KB (每个规范的最大 TLS 片段长度) 减小到 2KB,从而可以节省 14KB 的动态内存空间。

请注意,不太可能将 TLS IN 的缓冲区长度从默认值 16KB 减小,除非您可以直接控制服务器配置项,或者确保服务器不会出现发送超过某个阈值的入站数据的行为(在握手或实际数据传输阶段)。

在程序中启用相应的配置:

#启用 TLS 非对称 IN/OUT 内容长度
 CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN = y
 CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN = 2048

重新运行应用程序以更新堆利用率统计信息,如下所示:

任务的内存使用率统计:
 || 任务         | 峰值 DRAM | 峰值 IRAM ||
 || aws_iot_task | 48784     | 0        ||
 || tiT          | 3892      | 0        ||
 || wifi         | 30724     | 0        ||

系统的内存使用率统计:
 || 最小可用 DRAM | 最小可用 IRAM ||
 || 177972        | 76136        ||

从上面可以看出,我们从 aws_iot_task 任务中获得了约 14KB 的内存,因此最小可用 DRAM 数量也相应的增加了。

动态缓冲区分配特性

在 TLS 连接期间,mbedTLS 堆栈从初始握手阶段开始在整个会话期间保持动态分配功能的开启。这些分配包括 TLS IN/OUT 缓冲区,对端证书,客户端证书,私钥等。在此特性中(即将成为 ESP-IDF 的一部分),mbedTLS 内部 API 已被粘合(使用 SHIM 层),因此可以确保只要资源使用(包括数据缓冲区)完成,就会立即释放相关的动态内存。

这大大有助于减少 TLS 连接时堆内存峰值利用率。由于频繁进行动态内存操作(按需资源使用策略),因此对性能有微小的影响。此外,由于与身份验证凭据(证书,密钥等)有关的内存已被释放,因此在 TLS 尝试重新连接(如果需要)期间,应用程序需要确保再次填充 mbedTLS SSL 上下文。

应用程序中启用相应的配置:

#允许对 mbedTLS 使用动态缓冲策略
 CONFIG_MBEDTLS_DYNAMIC_BUFFER = y
 CONFIG_MBEDTLS_DYNAMIC_FREE_PEER_CERT = y
 CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA = y

重新运行应用程序以更新堆利用率统计信息,如下所示:

任务的内存使用率统计:
 || 任务         | 峰值 DRAM | 峰值 IRAM||
 || aws_iot_task | 26268     | 0        ||
 || tiT          | 3648      | 0        ||
 || wifi         | 30724     | 0        ||

系统的内存使用率统计:
 || 最少可用DRAM | 最小可用IRAM ||
 || 203648       | 76136        ||

从上面可以看出,我们从 aws_iot_task 任务中获得了约 22KB 的内存,因此最小可用 DRAM 数量也相应增加。

网络特定(优化)

Wi-Fi / LwIP 配置

我们可以进一步优化 Wi-Fi 和 LwIP 配置以减少内存使用,但以牺牲一些性能为代价。首先,我们将减少 Wi-Fi TX 和 RX 缓冲区,并且通过将一些关键代码路径从网络子系统迁移到指令存储器(IRAM)来尝试平衡二者。

​从性能方面考虑,在默认网络配置下,平均 TCP 吞吐量接近 〜20Mbps,但在下面的配置下,它将接近 〜4.5Mbps,这仍然足以满足典型的 IoT 用例。

应用程序中启用相应的配置:

#最小的 Wi-Fi / lwIP 的配置
 CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM = 4
 CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM = 16
 CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM = 8
 CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED =
 CONFIG_LWIP_TCPIP_RECVMBOX_SIZE = 16
 CONFIG_LWIP_TCP_SND_BUF_DEFAULT = 6144
 CONFIG_LWIP_TCP_WND_DEFAULT = 6144
 CONFIG_LWIP_TCP_RECVMBOX_SIZE = 8
 CONFIG_LWIP_UDP_RECVMBOX_SIZE = 8
 CONFIG_ESP32_WIFI_IRAM_OPT = Y
 CONFIG_ESP32_WIFI_RX_IRAM_OPT = Y
 CONFIG_LWIP_IRAM_OPTIMIZATION = Y

重新运行应用程序以更新堆利用率统计信息,如下所示:

任务的内存使用率统计:
 || 任务         | 峰值 DRAM | 峰值 IRAM||
 || aws_iot_task | 26272     | 0        ||
 || tiT          | 4108      | 0        ||
 || wifi         | 19816     | 0        ||

 系统的内存使用率统计:
 || 最少可用DRAM | 最小可用 IRAM ||
 || 213712       | 62920        ||

从上面的日志可以看出,我们获得了大约 9KB 的额外 DRAM 供应用程序使用。对总 IRAM 的影响(减少)是因为我们将关键代码路径从网络子系统移到了该区域。

系统特定(优化)

使用 RTC(快速)内存(仅单核)

从前述的内存分解图可以看到,有一个有用的 8KB RTC 快速内存(相当快),它一直处于空闲状态并且没有被充分利用。ESP-IDF 很快将具有启用 RTC 快速内存以进行动态分配的功能。该选项存在于单核配置中,因为 RTC 快速内存只能由 PRO CPU 访问。

已经确定的是 RTC 快速内存将用作第一个动态存储范围,并且大多数启动,预调度程序代码/服务都将占据该范围。这样就不会因为内存的时钟速度(稍微慢一点)而影响应用程序代码的性能。

由于对该区域没有访问限制,因此从功能上我们以后将其称为 DRAM 。

让我们使用此功能重新运行我们的应用程序并收集内存数据。

应用程序中启用相应的配置:

#将RTC内存添加到系统堆中
 CONFIG_ESP32_ALLOW_RTC_FAST_MEM_AS_HEAP = y

重新运行应用程序以更新堆利用率统计信息,如下所示:

任务的内存使用率统计:
 || 任务         | 峰值 DRAM | 峰值 IRAM||
 || aws_iot_task | 26272     | 0 ||
 || tiT          | 4096      | 0 ||
 || wifi         | 19536     | 0 ||
 
 系统的内存使用率统计:
 || 最少可用 DRAM | 最小可用 IRAM ||
 || 221792        | 62892         ||

从上面的日志可以看出,我们获得了 8KB 的额外 DRAM 供应用程序使用。

使用指令存储器(IRAM,仅单核)

​到目前为止,我们已经看到了允许终端应用程序从 DRAM(数据内存)区域获得更多内存的不同配置选项。沿着类似的路线继续,应该注意到的是 IRAM(指令内存)还剩余充足的空间,但是由于 32 位地址和大小对齐的限制,它不能被用作通用目的。

  • 如果访问(加载或存储)来自 IRAM 区域且大小未按字对齐,则处理器将生成 LoadStoreError(3) exception 异常;

  • 如果访问(加载或存储)来自 IRAM 区域且地址未字对齐,则处理器将生成 LoadStoreAlignmentError(9) exception 异常。

在 ESP-IDF 的此特殊特性中,上面提到的未对齐访问异常已通过相应的异常处理程序进行了修复,因此程序能够正确的执行。但是,对于每个(受限制的)加载或存储操作,这些异常处理程序最多可能消耗 167 个 CPU 周期。因此,使用此功能时可能会导致性能显著下降(与 DRAM 访问相比)。

可以按以下方式使用此内存区域:

  • 首先通过使用堆分配器中称为MALLOC_CAP_IRAM_8BIT 的 API,以使用特殊功能区域;

  • 其次使用提供的链接器属性 IRAM_DATA_ATTRIRAM_BSS_ATTR 将 DATA/BSS 重定向到此区域。

局限性:

  • 该存储区预不能用于 DMA 目的;

  • 该内存区域不能用于分配任务堆栈。

​在讨论该内存区域的使用以及理解性能损失的同时,发现 TLS IN/OUT(根据我们的配置值缓冲区可以是 16KB/2KB)缓冲区是从该区域分配的潜在候选对象之一。在其中一项实验中,将 TLS IN/OUT 缓冲区移至 IRAM 后,通过 TLS 连接传输 1 MB 文件的时间从约 3 秒增加到 5.2 秒。

也可以将所有 TLS 分配重定向到 IRAM 区域,但这可能会对性能产生更大的影响,因此此功能仅重定向大小大于或等于 TLS IN 或 OUT 最小缓冲区的缓冲区(在我们的示例中,阈值为 2 KB)。

让我们使用此功能重新运行并收集内存数据

应用程序中启用相应的配置:

#允许将 IRAM 用作 8 位可访问区域
 CONFIG_ESP32_IRAM_AS_8BIT_ACCESSIBLE_MEMORY = y
 CONFIG_MBEDTLS_IRAM_8BIT_MEM_ALLOC = y

重新运行应用程序中更新的堆利用率统计信息,如下所示:

任务的内存使用率统计:
 || 任务         | 峰值 DRAM | 峰值 IRAM||
 || aws_iot_task | 17960     | 21216    ||
 || tiT          | 3640      | 0        ||
 || wifi         | 19536     | 0        ||
 
系统的内存使用率统计:
 || 最少可用DRAM | 最小可用IRAM ||
 || 228252       | 40432        ||

从上面的日志可以看出,我们为应用程序又增加了约 7KB 的额外 DRAM 。请注意,即使我们已将所有超过 2KB 阈值的分配重定向到 IRAM ,但 DRAM 区域(另一个局部最大值)仍发生许多较小的(同时发生的)分配,因此有效增益低于实际值。如果附加的性能影响是可以接受的,则可以将所有 TLS 分配重定向到 IRAM ,并从 DRAM 区域获得至少 10-12KB 的内存。

总结

  • 通过配置选项对各种功能进行选择从而实现对应用程序的完全控制,是 ESP-IDF 的重要功能之一;

  • 通过上述实践,我们系统地评估了 ESP-IDF 中的各种特性和配置选项,从而将终端应用程序的 DRAM (最快的内存)预算提高了63 KB (最小空闲 DRAM 大小从 ~160KB 增加到 ~223KB);

  • 其中一些配置选项仅适用于单核配置(在标题本身中已进行了标记),但即使在双核配置中,其余选项也可用于节省内存;

  • 对性能无严格要求的模块(如终端应用程序的日志记录和诊断),还可以将指令存储器(IRAM)用作 8 位可访问区域;

  • 一旦实现了所需的系统特性后,建议禁用上面实践中使用的某些调试功能,例如堆任务跟踪,以减少元数据开销(并进一步增加内存预算)。

引用

  • 修改后的 subscription_publish 示例以及最终 sdkconfig.defaults 文件可以在 这里 找到;

  • 这个应用程序应该基于此处 ESP-IDF 副本和特性分支来构建。
    所需的系统特性后,建议禁用上面实践中使用的某些调试功能,例如堆任务跟踪,以减少元数据开销(并进一步增加内存预算)。

引用

  • 修改后的 subscription_publish 示例以及最终 sdkconfig.defaults 文件可以在 这里 找到;

  • 这个应用程序应该基于此处 ESP-IDF 副本和特性分支来构建。

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值