先区分四种大小
固定ESP32-S3-DevKitC-1 v1.1 / WROOM-1-N8R8,IDF5.5.5,Flash8MiB/DIO80MHz;PSRAM8MiB不计入Flash,也未初始化。完整分区表、只读运行清单程序、离线预算工具、错误表fixture与空实板表都在包内。无本站真实flash_id、刷分区、保存配置、OTA切换或恢复结果。
8MiB=8388608字节是计划物理Flash容量;厂商N8R8编码只能作选型线索,实板还须flash_id及运行esp_flash_get_physical_size核物理ID探测值、esp_flash_get_size核镜像头可用值;后者不能当物理容量。idf.py size中的RAM/Flash链接段报告不是app.bin文件长度;app.bin是一个应用镜像;完整刷写计划还含bootloader、partition-table、otadata等,合并整包可带地址填充。OTA槽需要容纳app镜像及你声明的保留余量,不能把整包放OTA槽,也不能拿PSRAM抵扣槽容量。
- 做到这里,你应该看到
- 先核固定板型、版本与本例结果范围;将尚未做的实体步骤保留空白。
参考:[1] 固定SDK分区表格式、对齐与OTA布局[3] Flash物理容量与初始化后可用容量API不同[4] 物理容量探测与可用size实现[5] 镜像头与Flash初始化容量规则
这张分区表分给谁
最后一分区恰到0x800000。表前bootloader/0x8000分区表、0x12000..0x20000对齐空隙不算全部可随意写的free空间。模型分区存在不表示已放模型、模型能运行或RAM足够;当前模型文件=0字节。没有OTA下载器、云升级或回滚完成结果,仅提供正确容量的双槽和运行识别方法。
| 区域 | 偏移 | 容量 | 用途 |
|---|---|---|---|
| nvs | 0x9000 | 0x6000=24KiB | 配置,当前程序只读不自动擦除 |
| otadata | 0xf000 | 0x2000=8KiB | OTA选择元数据的两页,不是第三份app |
| phy_init | 0x11000 | 0x1000=4KiB | 预留PHY数据区域;本例使用SDK默认初始化 |
| ota_0 | 0x20000 | 0x200000=2MiB | 首个应用槽 |
| ota_1 | 0x220000 | 0x200000=2MiB | 另一个应用槽;没有factory槽 |
| model | 0x420000 | 0x180000=1.5MiB | 自定义data subtype0x40,原始模型文件占位 |
| logs | 0x5a0000 | 0x260000=2.375MiB | SPIFFS类型空间;本例未格式化、挂载或写日志 |
参考:[1] 固定SDK分区表格式、对齐与OTA布局[2] 固定SDK分区二进制与MD5生成器[6] OTA API与运行/候选槽含义
从独立配置构建,再读真实产物
预算工具从实际project_description.json和flasher_args.json解析应用文件及0x20000/0x8000偏移,不让你误传merged.bin。读取真实partition-table.bin,核magic/MD5/终止、4KiB数据对齐、64KiB应用起点、重叠/越界、两个不同OTA槽和otadata两页,再按实际app文件字节数计算。固定未加密、未签名实验范围,未知分区flags拒绝;不是通用安全固件验证器。
默认65536字节是本例人为维护余量,不是签名固定开销或IDF硬性要求。当前源码实际构建app211232字节,两槽各2097152字节,扣保留后headroom1820384字节;这是离线构建文件事实,不是实板Flash容量测量。若修改源码必须重新算,不能长期抄此数字。--model-bin可计你自己真实文件字节,但不证明格式/推理/RAM合格。
idf.py -B build -D SDKCONFIG=build/sdkconfig -D SDKCONFIG_DEFAULTS=sdkconfig.defaults build
idf.py -B build size
python3 tools/budget.py --build build --flash-bytes 8388608 --flash-source planned-N8R8-not-measured --reserve-bytes 65536
python3 host/budget_test.py
用明确错误证明检查在起作用
partitions.bad.csv故意让ota_1从0x210000开始,而ota_0到0x220000,重叠64KiB。单独配置与构建目录执行(只构建,绝不刷写):
本站实际Exit2,原始日志保留官方生成器CSV Error/Partitions overlap;它没有可用最终分区镜像,不写成“错误布局烧板通过”。12组合成输入另核MD5损坏、对齐/重叠、容量边界、重复OTA、otadata不足、app+保留恰好/超1B、模型过大、负余量、截断/缺MD5。工具成功只说明所声明输入预算自洽。
idf.py -B build-bad -D SDKCONFIG=build-bad/sdkconfig -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.bad.defaults' build
换分区前先保存可恢复的一组文件
只在自己的无加密/无安全启动实验板使用下列步骤;现有产品的保护策略不在此例迁移范围。本机固定SDK环境实际esptool为4.12.0,命令是flash_id/read_flash/write_flash/verify_flash下划线形式,不能机械抄新版5.x连字符写法。
先保存旧项目源/锁/SDK配置、ELF、全部刷写文件和flasher_args.json、应用导出的设置及所用端口/板号。保持电源和端口明确,先做只读查询与完整备份,以下REPLACE_PORT必须换成自己确认的接口:
保留私有备份与摘要,不公开含Wi-Fi/应用配置的原始Flash。按旧CSV读真实NVS偏移/大小,不能因为本例新NVS=0x9000就假定旧数据也在那里。只保留旧NVS字节不等于应用schema已经迁移;不同表位置/密钥/格式须走应用自己的导出导入。本站未读取任何板Flash,命令是待执行模板,不用合成文件填“备份成功”。
python -m esptool --chip esp32s3 -p REPLACE_PORT flash_id
# 只有已核对实际8MiB才读这个长度;其它容量换用对应计划
mkdir -p backups
python -m esptool --chip esp32s3 -p REPLACE_PORT read_flash 0x0 0x800000 backups/flash-original.bin
python -m esptool --chip esp32s3 -p REPLACE_PORT read_flash 0x8000 0x1000 backups/partitions-original.bin
python "$IDF_PATH/components/partition_table/gen_esp32part.py" backups/partitions-original.bin backups/partitions-original.csv
python -c 'import hashlib,pathlib; p=pathlib.Path("backups/flash-original.bin"); print(p.stat().st_size,hashlib.sha256(p.read_bytes()).hexdigest())'
参考:[1] 固定SDK分区表格式、对齐与OTA布局[2] 固定SDK分区二进制与MD5生成器[7] 官方esptool release-v4:read_flash/write_flash/verify_flash
部署与运行应分别记录
备份/恢复办法已验证后,才使用本次build对应的完整idf.py flash计划;不要只刷新partition-table就以为两个app和数据一起迁好了。这个例子会列detected_physical_flash_bytes与available_header_flash_bytes、当前运行槽、另一个候选槽与每个区域容量;两值不是8MiB则STOP;物理小于镜像头时SDK可能在app_main前先拒绝启动,应保留boot日志。物理API也依赖厂商ID容量约定,应与模块手册/flash_id交叉核对。它只读e09命名空间marker,缺命名空间/键给明确提示,不自动写默认值或erase NVS;NVS初始化失败也原样报错。
空otadata时的首次启动、实际running=ota_0、重启、旧设置导入、断电恢复、两槽真实下载/选择/回滚都要各自验收。本项目没有升级器,所以“next_slot=ota_1”只是分区查询,不是OTA成功或备用槽已含可启动固件。data/spiffs标签本身不表示存在文件系统镜像。
参考:[1] 固定SDK分区表格式、对齐与OTA布局[3] Flash物理容量与初始化后可用容量API不同[4] 物理容量探测与可用size实现[5] 镜像头与Flash初始化容量规则[6] OTA API与运行/候选槽含义
恢复原布局不靠猜偏移
保存的旧全Flash与原刷写计划来自同一块已核对的无保护实验板时,按该计划恢复。完整8MiB备份的恢复模板如下,默认保留原头的模式/频率/容量,不更改eFuse:
之后重启核旧应用与导出设置,保留原始成功/失败日志;写回相同字节不代替应用功能验收。不要把不同板备份、加密镜像或只一段app当整Flash恢复。本文没有执行任何硬件写入/擦除。
python -m esptool --chip esp32s3 -p REPLACE_PORT write_flash --flash_mode keep --flash_freq keep --flash_size keep 0x0 backups/flash-original.bin
python -m esptool --chip esp32s3 -p REPLACE_PORT verify_flash 0x0 backups/flash-original.bin
参考:[1] 固定SDK分区表格式、对齐与OTA布局[7] 官方esptool release-v4:read_flash/write_flash/verify_flash
许可与结果范围
原创预算解析器/应用/fixture MIT,分区二进制格式及MD5依据固定SDK5.5.5 gen_esp32part.py;SDK各原许可保留。正常编译、受控失败、12合成与实际文件预算分别留原日志/SHA,physicalFlash/备份/重启/OTA/模型/文件系统均pending。器材仅固定开发板/数据线,无外设接线或额外Flash元件。
参考:[1] 固定SDK分区表格式、对齐与OTA布局[2] 固定SDK分区二进制与MD5生成器[3] Flash物理容量与初始化后可用容量API不同[4] 物理容量探测与可用size实现[7] 官方esptool release-v4:read_flash/write_flash/verify_flash
运行清单程序与两张分区表
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <inttypes.h>
#include "esp_flash.h"
#include "esp_partition.h"
#include "esp_ota_ops.h"
#include "esp_app_desc.h"
#include "nvs_flash.h"
#include "nvs.h"
void app_main(void){
uint32_t physical=0,available=0;esp_err_t e=esp_flash_get_physical_size(NULL,&physical);
if(e!=ESP_OK){printf("E09 STOP flash size read: %s\n",esp_err_to_name(e));return;}
e=esp_flash_get_size(NULL,&available);if(e!=ESP_OK){printf("E09 STOP available/header size read: %s\n",esp_err_to_name(e));return;}
printf("E09 detected_physical_flash_bytes=%"PRIu32" available_header_flash_bytes=%"PRIu32" plan_bytes=8388608\n",physical,available);
if(physical!=8388608||available!=8388608){puts("E09 STOP actual chip is not this 8MiB plan; do not infer capacity from PSRAM or a module nickname");return;}
const esp_partition_t *running=esp_ota_get_running_partition(),*next=esp_ota_get_next_update_partition(NULL);
const esp_app_desc_t *app=esp_app_get_description();
printf("E09 version=%s project=%s IDF=%s running=%s next_slot=%s; this example does NOT perform OTA\n",app->version,app->project_name,app->idf_ver,running?running->label:"NONE",next?next->label:"NONE");
esp_partition_iterator_t it=esp_partition_find(ESP_PARTITION_TYPE_ANY,ESP_PARTITION_SUBTYPE_ANY,NULL);
while(it){const esp_partition_t *p=esp_partition_get(it);printf("E09 PART,%s,type=%u,subtype=%u,offset=0x%08"PRIx32",capacity_bytes=%"PRIu32"\n",p->label,p->type,p->subtype,p->address,p->size);it=esp_partition_next(it);}
/* Iterator automatically released when next() returns NULL. Do not erase NVS
* on init failure: this tutorial is about preserving existing device data. */
e=nvs_flash_init();if(e!=ESP_OK){printf("E09 NVS_INIT=%s; no automatic erase\n",esp_err_to_name(e));return;}
nvs_handle_t handle;e=nvs_open("e09",NVS_READONLY,&handle);
if(e==ESP_ERR_NVS_NOT_FOUND){puts("E09 namespace missing: expected on clean flash; no write performed");return;}
if(e!=ESP_OK){printf("E09 nvs_open=%s\n",esp_err_to_name(e));return;}
uint32_t marker=0;e=nvs_get_u32(handle,"marker",&marker);nvs_close(handle);
if(e==ESP_ERR_NVS_NOT_FOUND)puts("E09 marker missing; no default write or migration performed");
else printf("E09 marker_read=%s marker=%"PRIu32"; compare only with your saved pre-migration record\n",esp_err_to_name(e),marker);
}
# ---- partitions.csv ----
# Name,Type,SubType,Offset,Size,Flags
nvs,data,nvs,0x9000,0x6000,
otadata,data,ota,0xf000,0x2000,
phy_init,data,phy,0x11000,0x1000,
ota_0,app,ota_0,0x20000,0x200000,
ota_1,app,ota_1,0x220000,0x200000,
model,data,0x40,0x420000,0x180000,
logs,data,spiffs,0x5a0000,0x260000,
# ---- partitions.bad.csv ----
# Deliberate overlap fixture; NEVER flash
# Name,Type,SubType,Offset,Size,Flags
nvs,data,nvs,0x9000,0x6000,
otadata,data,ota,0xf000,0x2000,
ota_0,app,ota_0,0x20000,0x200000,
ota_1,app,ota_1,0x210000,0x200000,
参考:[1] 固定SDK分区表格式、对齐与OTA布局[2] 固定SDK分区二进制与MD5生成器[3] Flash物理容量与初始化后可用容量API不同[4] 物理容量探测与可用size实现[5] 镜像头与Flash初始化容量规则[6] OTA API与运行/候选槽含义[7] 官方esptool release-v4:read_flash/write_flash/verify_flash[8] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电
直接读取实际构建产物的完整预算工具
以下为下载工程中对应文件的全文;带文件名的注释只是网页分隔,构建时使用独立原文件。其它构建配置、锁、许可与夹具在完整示例中。
# ---- tools/budget.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Read actual build outputs and compute byte capacity, never flash anything.
Scope: this example's unencrypted/unsigned S3 table at0x8000, MD5 enabled.
A fitting image is not a valid OTA transaction, valid model, or available RAM.
"""
import argparse,hashlib,json,pathlib,struct
def read_table(path,flash_bytes):
raw=pathlib.Path(path).read_bytes();parts=[];md5=hashlib.md5();verified=False;ended=False
if len(raw)%32:raise ValueError('table length must be a multiple of32')
for at in range(0,len(raw),32):
record=raw[at:at+32]
if record==b'\xff'*32:
if raw[at:]!=b'\xff'*(len(raw)-at):raise ValueError('nonempty data after table terminator')
ended=True;break
if record[:2]==b'\xeb\xeb':
if verified or record[:16]!=b'\xeb\xeb'+b'\xff'*14 or record[16:]!=md5.digest():raise ValueError('partition MD5 mismatch/duplicate')
verified=True;continue
if verified:raise ValueError('partition record after MD5')
magic,kind,subtype,offset,size,label,flags=struct.unpack('<2sBBII16sI',record)
if magic!=b'\xaa\x50':raise ValueError('invalid partition magic')
label=label.split(b'\0',1)[0].decode('ascii')
if not label or offset<0x9000 or offset%4096 or not size or size%4096:raise ValueError('invalid example partition offset/size/alignment')
if kind==0 and offset%65536:raise ValueError('app must be64KiB aligned')
if offset+size>flash_bytes:raise ValueError('partition exceeds declared Flash')
if flags:raise ValueError('this unsigned/unencrypted budget does not accept partition flags')
if any(x['name']==label for x in parts):raise ValueError('duplicate label')
parts.append({'name':label,'type':kind,'subtype':subtype,'offset_bytes':offset,'capacity_bytes':size,'end_bytes':offset+size});md5.update(record)
if not parts or not verified or not ended:raise ValueError('missing entries/MD5/end marker')
for a,b in zip(sorted(parts,key=lambda p:p['offset_bytes']),sorted(parts,key=lambda p:p['offset_bytes'])[1:]):
if a['end_bytes']>b['offset_bytes']:raise ValueError('partition overlap')
return parts
def assess(parts,app_bytes,model_bytes,reserve_bytes):
if min(app_bytes,reserve_bytes)<0 or app_bytes==0 or model_bytes<0:raise ValueError('positive app and nonnegative reserve/model required')
slots=[p for p in parts if p['type']==0 and p['subtype'] in [16,17]]
if len(slots)!=2 or {p['subtype'] for p in slots}!={16,17}:raise ValueError('two distinct ota_0/ota_1 slots required')
ota=[p for p in parts if p['type']==1 and p['subtype']==0]
if len(ota)!=1 or ota[0]['capacity_bytes']<8192:raise ValueError('otadata requires at least two4KiB sectors')
model=next((p for p in parts if p['name']=='model' and p['type']==1),None)
if model is None or model_bytes>model['capacity_bytes']:raise ValueError('model asset exceeds dedicated data partition')
for p in slots:
if app_bytes+reserve_bytes>p['capacity_bytes']:raise ValueError('app+declared reserve exceeds OTA slot '+p['name'])
return {'app_image_bytes':app_bytes,'declared_reserve_bytes':reserve_bytes,'minimum_ota_capacity_bytes':min(p['capacity_bytes'] for p in slots),'minimum_ota_headroom_after_reserve_bytes':min(p['capacity_bytes'] for p in slots)-app_bytes-reserve_bytes,'model_asset_bytes':model_bytes,'model_capacity_bytes':model['capacity_bytes'],'model_headroom_bytes':model['capacity_bytes']-model_bytes,'scope':'file/partition byte budget only; no OTA download/switch/rollback, no model validity/RAM guarantee'}
def inspect(build,flash_bytes,flash_source,reserve,model_bin=None):
build=pathlib.Path(build).resolve()
if flash_bytes!=8388608:raise ValueError('this fixed N8R8 plan requires8388608 declared bytes; do not change the physical part by editing this number')
description=json.loads((build/'project_description.json').read_text());flasher=json.loads((build/'flasher_args.json').read_text())
app=(build/description['app_bin']).resolve();application=(build/flasher['app']['file']).resolve()
if app!=application or int(flasher['app']['offset'],0)!=0x20000:raise ValueError('app metadata mismatch; do not pass a merged/full Flash image as an app')
table=(build/flasher['partition-table']['file']).resolve()
if int(flasher['partition-table']['offset'],0)!=0x8000:raise ValueError('this example supports table offset0x8000 only')
parts=read_table(table,flash_bytes);model_bytes=pathlib.Path(model_bin).stat().st_size if model_bin else 0
if app.read_bytes()[:1]!=b'\xe9':raise ValueError('not the expected app image header')
result=assess(parts,app.stat().st_size,model_bytes,reserve)
result.update(declared_flash_bytes=flash_bytes,flash_capacity_basis=flash_source,partitions=parts,app_path=str(app),app_sha256=hashlib.sha256(app.read_bytes()).hexdigest(),table_sha256=hashlib.sha256(table.read_bytes()).hexdigest(),outside_app_data_partition_bytes=flash_bytes-sum(p['capacity_bytes'] for p in parts),outside_partition_note='includes bootloader/table and alignment gaps; NOT all free or writable',model_path=str(model_bin) if model_bin else None)
return result
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--build',required=True);p.add_argument('--flash-bytes',required=True,type=int);p.add_argument('--flash-source',choices=['planned-N8R8-not-measured','measured-by-user-esptool-flash-id'],required=True);p.add_argument('--reserve-bytes',type=int,default=65536);p.add_argument('--model-bin');a=p.parse_args()
try:print(json.dumps(inspect(a.build,a.flash_bytes,a.flash_source,a.reserve_bytes,a.model_bin),indent=2))
except (ValueError,KeyError,OSError) as e:p.exit(2,'BUDGET_REJECTED: '+str(e)+'\n')
- 做到这里,你应该看到
- 按README保存自己的原始日志和SHA;下载包的observations.csv实测列仍为空,不能用合成输入补填。
参考:[1] 固定SDK分区表格式、对齐与OTA布局[2] 固定SDK分区二进制与MD5生成器[3] Flash物理容量与初始化后可用容量API不同[4] 物理容量探测与可用size实现[5] 镜像头与Flash初始化容量规则[6] OTA API与运行/候选槽含义[7] 官方esptool release-v4:read_flash/write_flash/verify_flash[8] 固定官方DevKitC-1 v1.1:N8R8、双USB接口、GPIO与供电
资料来源与版本说明
保留原计划E09用途与slug;固定上游、实际本机构建和适用的逻辑检查独立留证。未补造无线、采样、屏幕、真实Flash或掉电结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。