先固定任务、器材与本轮证据范围
相机、语音与屏幕会同时争用内部RAM、外部RAM与特定DMA能力。打开PSRAM不会把所有需要内部RAM的对象搬走,也不会保证任意大块请求都成功。先核请求的caps、大小、对齐与生命周期,再解释free数值。
保留原计划R03/前置E07与E09:本例不重复组件锁和Flash槽预算,专门检查运行期能力域、连续块与受限碎片。实体固定N8R8/OPI80MHz;普通启动只观察,不做全局耗尽压力。
实际S3 QEMU运行固定SDK的32位私有分配器:31块,交错释放后free33952B/largest3072B、4096失败,释放其余后成功且integrity1。它使用内部arena,不是PSRAM或DMA测试。OPI与QEMU两个最终构建分别203104B/191728B;实板表为空。
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
构建与运行基线
安装官方IDF5.5.5并在新终端激活 export.sh / EIM 对应环境。作者复用已安装SDK,没有修改SDK源码。POSIX终端进入本例解压目录;PowerShell用等效变量/换行,不能直接照搬export语法。构建目录使用绝对路径,避免把对象写入网站仓库。
同一build保留app.bin、未剥离ELF、map、sdkconfig、dependencies.lock、源文件SHA和完整串口日志,不能仅保存一张panic截图或只下载来历不明bin。idf.py monitor退出Ctrl+]。若误刷或实验卡住,先停止输入、按RESET;必要时在BOOT配合RESET进入ROM下载模式并刷回已备份且分区匹配的正常固件。不要为排查修改eFuse或盲目erase_flash,NVS和其他分区可能有数据。
# POSIX shell · 配合本节前提逐项执行
export LAB_PROJECT="$PWD"
export LAB_BUILD="$HOME/esp32-labs/r03-safe"
python "$IDF_PATH/tools/idf.py" -C "$LAB_PROJECT" -B "$LAB_BUILD" -D SDKCONFIG="$LAB_BUILD/sdkconfig" build
# 确认板型、串口和备份后才自行执行;本文没有执行烧录。
python "$IDF_PATH/tools/idf.py" -C "$LAB_PROJECT" -B "$LAB_BUILD" -p YOUR_SERIAL_PORT flash monitor参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
固定硬件与默认配置
使用SDK默认single-app分区(factory槽1MiB,无OTA,板Flash8MiB)。实体版本要求N8R8的8MiB Octal PSRAM80MHz,并启用SPIRAM_USE_MALLOC。PSRAM在启动阶段初始化;错误板型可能在app_main之前失败,不以SPIRAM_IGNORE_NOTFOUND隐瞒。app_main仍核esp_psram_get_size()==8388608,不符则停止pool命令。普通版本只打印能力域快照,等待snapshot或pool,不周期性耗尽系统堆。
默认将HEAP_TLSF_USE_ROM_IMPL关闭,使用同版SDK的C实现作可核查实验;这会改变IRAM/固件占用,不是生产内存优化推荐,不能把本例free数值与ROM实现直接比较。任务/外设使用的栈、描述符、内部保留池仍有独立限制,8MiB PSRAM不等于能放8MiB的任意对象。
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
五个域不是五份互斥内存
每行HEAP打印total/free/largest/min_sum,单位 字节,t_us是单调微秒。internal8、external8、internal_dma8、external_dma8、general8按capability查询,有重叠,禁止相加。各字段及各行不保证是同一原子瞬间,其他任务可能同时分配。min_sum是符合域的多个heap各自历史minimum之和,未必曾在某一时刻同时达到,不能冒称应用在某一时刻的真实总最低free。
| 单次4096字节请求的快照 | 分类 | 下一步 |
|---|---|---|
| total=0 | 无匹配能力域 | 先核配置/域是否被注册,不猜碎片 |
| free<4096 | 总量压力 | 找所有权、生命周期、保留池及泄漏 |
| free>=4096但largest<4096 | 连续块限制 | 复查小块寿命与相邻占用;元数据/对齐另算 |
| largest>=4096 | 快照看似可容纳 | 不构成分配保留,实际调用仍需判NULL |
| largest>free、free>total或request=0 | 输入不一致 | 拒绝分析,不绘制虚假比例 |
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
受限私有池实验
pool先申请256字节internal DMA缓冲和按64字节对齐的256字节external DMA缓冲,判断region API后释放;没有启动任何DMA、描述符或外设事务。S3具备外部EDMA支持,不能把老ESP32限制泛化为“所有PSRAM绝不能DMA”;仍须按具体驱动核cache维护、对齐、最大传输、带宽、描述符在内部RAM等约束。64仅为本实验选择,不是任意外设正确对齐的保证。
随后一次申请64KiB外部arena,multi_heap_register只建立本应用私有分配器,不把已分配缓冲再heap_caps_add_region注册成全局堆。最多64个2048字节块,填到失败、释放交错块、试4096、释放其余后重试,检查integrity,最后把arena归还全局堆。arena申请失败就保留前后快照并跳过,不为了“复现碎片”把系统全局堆耗尽。私有池内部metadata/对齐也占空间,不能用64KiB/2048推断一定32块。
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
原生S3 QEMU的适用范围
独立config/qemu.defaults禁用PSRAM,启用HX_POOL_SELFTEST,改为在 内部64KiB arena 运行同一个pool_probe.h与SDK分配器,然后打印完成标记。普通OPI版本和QEMU版本用两个绝对build目录,禁止拿禁PSRAM配置的固件当实体PSRAM验收。
本轮实际QEMU9.2.2 esp_develop_9.2.2_20260417(官方macarm64 SHA在证据中)执行S3 32位SDK:31块,交错释放后free33952B/largest3072B,4096失败;释放其余后4096成功、integrity1、failures0。结果属于特定配置的私有池,不是实板PSRAM测量或DMA/缓存一致性证明。QEMU不提供本文所需物理PSRAM、外设电气与目标时延结论。
作者曾尝试在macARM64直接链接SDK allocator,Darwin alias/弱符号和布局不适配后出现链接/运行失败,保留原日志,最终没有将它交付为通过方案。上游host Makefile显式-m32;TLSF本身含64位分支,不能据此泛称TLSF不支持64位。成品只在主机运行纯分类函数,在实际S3 QEMU运行原SDK allocator;没有修改SDK去“修绿”测试。
# POSIX shell · 配合本节前提逐项执行
export LAB_QEMU_BUILD="$HOME/esp32-labs/r03-qemu"
python "$IDF_PATH/tools/idf.py" -C "$LAB_PROJECT" -B "$LAB_QEMU_BUILD" -D SDKCONFIG="$LAB_QEMU_BUILD/sdkconfig" -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/qemu.defaults' build
# 通过本版官方工具清单选择对应OS/CPU的QEMU;不安装另一套SDK。
python "$IDF_PATH/tools/idf_tools.py" install qemu-xtensa
. "$IDF_PATH/export.sh"
python host/run_qemu.py --build "$LAB_QEMU_BUILD" --log /tmp/r03-qemu.log --timeout 90参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
同源主机分类与离线快照
heap-snapshots.csv默认只有表头,最后命令会Exit2提示没有测量;亲自收集后按域逐行填写label,total/free/largest/request,纯十进制字节,不写KB/百分比。工具最多1MiB/1000行,每行调用与固件相同mem_model.h的C分类器;不相加能力域、不推算物理PSRAM。合成测试包含五行有效类别/边界,及负数、溢出、错误单位、形状、零请求和不一致数据的拒绝。合成输入不填进observations.csv。
# POSIX shell · 配合本节前提逐项执行
cc -std=c11 -Wall -Wextra -Werror -Imain host/heap_test.c -o /tmp/r03-test
/tmp/r03-test
cc -std=c11 -Wall -Wextra -Werror -Imain host/classify.c -o /tmp/r03-classifier
python host/check_csv.py --classifier /tmp/r03-classifier
python tools/analyse_heap.py --csv heap-snapshots.csv --classifier /tmp/r03-classifier参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
现场排查和恢复
若OPI启动失败先核模块标记、供电和80MHz配置,不立即更换heap参数。若分配失败,保存请求大小/caps/对齐/生命周期、前后largest与free,再查buffer是否被别的任务持有;缓存禁用期间不可随意访问PSRAM,Flash写入路径尤其需要按固定SDK限制核对。integrity失败先停止该实验、保存日志、RESET并回安全程序,不继续解引用可疑指针。完成后在同一配置下做多周期与内存归还检查;这里只提供步骤,实体值、泄漏趋势、DMA正确性均pending,observations.csv空表保留。
固定依据:IDF5.5.5 mem_alloc.rst、external-ram.rst的S3条件段、heap_caps/multi_heap API、esp_memory_utils.h、heap Kconfig和固定tlsf子模块2867f6883a12920b1969ff9624c0ab0e4185c2ce。详见作者证据,不混用老ESP32文档或不同板型数字。
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
完整内存诊断主程序:实体OPI与QEMU分支
下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "esp_heap_caps.h"
#include "esp_psram.h"
#include "esp_memory_utils.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mem_model.h"
#include "pool_probe.h"
static void snapshot(const char *name,uint32_t caps) {
multi_heap_info_t i;heap_caps_get_info(&i,caps);size_t total=heap_caps_get_total_size(caps);
printf("HEAP name=%s t_us=%"PRId64" caps=%"PRIu32" total=%zu free=%zu largest=%zu min_sum=%zu request=4096 class=%d\n",name,esp_timer_get_time(),caps,total,i.total_free_bytes,i.largest_free_block,i.minimum_free_bytes,mem_classify(total,i.total_free_bytes,i.largest_free_block,4096));
}
static void snapshots(void){snapshot("internal8",MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);snapshot("external8",MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);snapshot("internal_dma8",MALLOC_CAP_INTERNAL|MALLOC_CAP_DMA|MALLOC_CAP_8BIT);snapshot("external_dma8",MALLOC_CAP_SPIRAM|MALLOC_CAP_DMA|MALLOC_CAP_8BIT);snapshot("general8",MALLOC_CAP_8BIT);}
#if CONFIG_HX_POOL_SELFTEST
void app_main(void) {
void *arena=heap_caps_malloc(65536,MALLOC_CAP_INTERNAL|MALLOC_CAP_8BIT);
if(!arena){puts("R03_POOL_TEST_COMPLETE failures=1 allocation");return;}
PoolResult r=pool_probe(arena,65536);heap_caps_free(arena);
unsigned fail=!(r.blocks>4 && r.free_bytes>=4096 && r.largest<4096 && r.failed4096 && r.coalesced);
printf("R03_POOL_TEST_COMPLETE failures=%u; actual32-bit SDK allocator, no PSRAM or peripheral test\n",fail);
}
#else
void app_main(void) {
printf("R03 physical PSRAM reported_bytes=%zu; require8388608 for this N8R8 lab; no measured values supplied in article\n",esp_psram_get_size());
snapshots(); if(esp_psram_get_size()!=8388608) { puts("PSRAM_SCOPE_MISMATCH; stop pool experiment");return; }
puts("Commands: snapshot / pool. Default only metadata, no stress loop. Caps overlap: DO NOT SUM rows.");char line[40];
for(;;) {
if(!fgets(line,sizeof line,stdin)){clearerr(stdin);vTaskDelay(pdMS_TO_TICKS(20));continue;}
line[strcspn(line,"\r\n")]=0;
if(!strcmp(line,"snapshot"))snapshots();
else if(!strcmp(line,"pool")) {
void *dma=heap_caps_malloc(256,MALLOC_CAP_INTERNAL|MALLOC_CAP_DMA|MALLOC_CAP_8BIT);
if(dma){printf("DMA pointer internal_capable=%u ext_dma_region=%u; peripheral contract/alignment still required\n",esp_ptr_dma_capable(dma),esp_ptr_dma_ext_capable(dma));heap_caps_free(dma);}else puts("DMA_256_ALLOC_FAILED");
void *extdma=heap_caps_aligned_alloc(64,256,MALLOC_CAP_SPIRAM|MALLOC_CAP_DMA|MALLOC_CAP_8BIT);
if(extdma){printf("EXTERNAL_DMA region=%u; no DMA transfer/descriptors tested\n",esp_ptr_dma_ext_capable(extdma));heap_caps_free(extdma);}else puts("EXTERNAL_DMA_256_UNAVAILABLE");
void *arena=heap_caps_malloc(65536,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);
if(!arena){puts("EXTERNAL_ARENA_ALLOC_FAILED: keep before/after snapshots");snapshots();continue;}
printf("ARENA external=%u; private pool not global capability registration\n",esp_ptr_external_ram(arena));pool_probe(arena,65536);heap_caps_free(arena);snapshots();
} else puts("UNKNOWN_COMMAND; no allocation");
}
}
#endif
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
固件与主机共用的字节快照分类
下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。
// ---- main/mem_model.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdint.h>
typedef enum { INVALID_SNAPSHOT, NO_CAPABILITY, TOTAL_PRESSURE, CONTIGUOUS_LIMIT, FITS_SNAPSHOT } MemState;
static inline MemState mem_classify(uint64_t total,uint64_t free_bytes,uint64_t largest,uint64_t request) {
if(!request || largest>free_bytes || free_bytes>total) return INVALID_SNAPSHOT;
if(!total) return NO_CAPABILITY;
if(free_bytes<request) return TOTAL_PRESSURE;
if(largest<request) return CONTIGUOUS_LIMIT;
return FITS_SNAPSHOT; /* Not a reservation; metadata/alignment/concurrent users still matter. */
}
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
完整受限私有池分配、失败、合并与释放
下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。
// ---- main/pool_probe.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include "multi_heap.h"
#include "mem_model.h"
typedef struct { size_t free_bytes,largest; bool failed4096,coalesced; unsigned blocks; } PoolResult;
/* Private allocator only; NEVER heap_caps_add_region over an existing allocation. */
static inline PoolResult pool_probe(void *arena,size_t bytes) {
PoolResult r={0}; multi_heap_handle_t pool=multi_heap_register(arena,bytes); if(!pool)return r;
void *blocks[64]={0};
for(unsigned i=0;i<64;i++) { blocks[i]=multi_heap_malloc(pool,2048); if(!blocks[i])break; r.blocks++; }
for(unsigned i=0;i<r.blocks;i+=2) { multi_heap_free(pool,blocks[i]); blocks[i]=NULL; }
multi_heap_info_t info; multi_heap_get_info(pool,&info);r.free_bytes=info.total_free_bytes;r.largest=info.largest_free_block;
void *trial=multi_heap_malloc(pool,4096);r.failed4096=trial==NULL;
if(trial)multi_heap_free(pool,trial);
for(unsigned i=0;i<r.blocks;i++)if(blocks[i])multi_heap_free(pool,blocks[i]);
trial=multi_heap_malloc(pool,4096);r.coalesced=trial!=NULL;if(trial)multi_heap_free(pool,trial);
printf("PRIVATE_POOL bytes=%zu blocks=%u free=%zu largest=%zu class=%d request4096_failed=%u after_free4096_ok=%u integrity=%u\n",bytes,r.blocks,r.free_bytes,r.largest,mem_classify(bytes,r.free_bytes,r.largest,4096),r.failed4096,r.coalesced,multi_heap_check(pool,false));
return r;
}
参考:[1] IDF5.5.5 Heap Memory Allocation:能力域与DMA[2] IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制[3] 固定esp_heap_caps.h:free/largest/minimum与查询接口
资料来源与版本说明
保持原计划ID/slug/阶段和用途。固定上游与实际构建、适用主机/QEMU验证留证;没有把本机结果升级为实体运行结论。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。
- IDF5.5.5 Heap Memory Allocation:能力域与DMA
- IDF5.5.5 external RAM:S3 DMA、描述符、缓存与启动限制
- 固定esp_heap_caps.h:free/largest/minimum与查询接口
- 固定multi_heap.h:私有池与info/check
- 固定heap Kconfig:ROM/C allocator选择
- 固定IDF gitlink对应espressif/tlsf:2867f688…
- 固定官方QEMU动作与S3目标支持
- 固定SDK host heap Makefile:显式-m32
- 固定esp_memory_utils.h:内部/外部DMA区域查询