乐鑫科技代理公司蓝牙wifi二合一模块ESP32内存分析案例研究,内存对硅片成本以及芯片尺寸具有重大影响,因此从硬件角度看,优化内存尺寸很重要,从软件角度来看,能够充分利用内存资源也至关重要。
在本文中,我们将讨论 ESP-IDF 中的一些即将推出的功能和常用配置选项(可调项),以允许 终端应用程序 以佳方式利用各个内部存储区域。
重要提示
这里我们将重点介绍乐鑫科技代理公司蓝牙wifi二合一模块ESP32 的单核模式,因为该模式下可以适用更多内存优化功能;
我们将在这里考虑典型的 IoT 用例,在该用例下牺牲性能获得内存是可以接受的准则;
我们将以典型的云应用程序为研究用例,该应用需要具有相互认证支持的 TLS 连接;
这里使用的 ESP-IDF 功能分支可查阅 https://github.com/mahavirj/esp-idf/tree/feature/memory_optimizations 。
ESP32:内部存储器分解
从存储器布局可以看出,芯片内部存在各种具有不同时钟速度的存储器区域;
对于单核用例,我们获得了额外的 32K 指令存储空间(IRAM),否则(双核模式)该区域会作为 APP CPU 内核的 cache;
对指令 RAM 的访问,地址和空间大小应始终 32 位对齐;
对于终端应用程序的业务逻辑来说,总是希望有更多的 DRAM,它是访问速度快的内存且没有任何访问限制。
案例研究— AWS IoT 示例应用程序
我们将使用来自 ESP-AWS-IoT 的 subscribe_publish 示例作为研究案例,来分析内存利用率;
乐鑫科技代理公司蓝牙wifi二合一模块ESP-IDF 提供了一个 API ,可以使用 heap_caps_get_minimum_free_size() 获取小空闲堆或者说系统中可用的动态内存大小。我们的目标是大化这个数字(进行相对分析),从而增加终端应用程序特定业务逻辑的可用内存数量(特别是 DRAM 区域)。
默认内存利用率
我们将在 subscribe_publish 示例之上添加以下代码补丁来记录动态内存的统计信息。
开始,我们将分别记录 DRAM 和 IRAM 区域如上所述的系统小空闲堆大小;
其次,我们将使用堆任务跟踪功能,该功能提供了基于每个任务的动态内存使用信息。修改此功能后,还可以记录每个任务 DRAM 和 IRAM 区域的峰值使用量;
我们将分别为 aws_iot_task ,tiT (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 ||
单核配置
如前所述,我们将在所有实验中使用单核配置。请注意,即使在单核模式下,乐鑫科技代理公司蓝牙wifi二合一模块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 缓冲区,对端证书,客户端证书,私钥等。在此特性中(即将成为乐鑫科技代理公司蓝牙wifi二合一模块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_ATTR 和 IRAM_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 的重要功能之一;
通过上述实践,我们系统地评估了乐鑫科技代理公司蓝牙wifi二合一模块ESP-IDF 中的各种特性和配置选项,从而将终端应用程序的 DRAM (快的内存)预算提高了63 KB (小空闲 DRAM 大小从 ~160KB 增加到 ~223KB);
其中一些配置选项仅适用于单核配置(在标题本身中已进行了标记),但即使在双核配置中,其余选项也可用于节省内存;
对性能无严格要求的模块(如终端应用程序的日志记录和诊断),还可以将指令存储器(IRAM)用作 8 位可访问区域;
一旦实现了所需的系统特性后,建议禁用上面实践中使用的某些调试功能,例如堆任务跟踪,以减少元数据开销(并进一步增加内存预算)。
引用
修改后的 subscription_publish 示例以及终 sdkconfig.defaults 文件可以在 这里 找到;
这个应用程序应该基于此处 ESP-IDF 副本和特性分支来构建。