故障定位与升级 / 崩溃诊断与故障门控发布 内容更新

ESP32 死机怎么看?从 backtrace 到 watchdog 和栈溢出

保留同次ELF与原日志,用安全默认、显式故障开关和合成地址夹具练习离线解析;区分abort、任务看门狗与栈问题,真实panic与恢复仍待实板。

读完能做什么
保存可复验的故障报告,按匹配ELF将PC解码,并明确尚未执行的三种实板故障。
开始之前
先读所列前置,能使用固定IDF环境、保存同次源码/ELF/配置与原log;本文下载README含完整命令与恢复步骤。
所需器材
ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8、USB数据线与电脑、ESP-IDF5.5.5 / 固定Xtensa工具;本例不接外部负载、仅独立实验板,正常固件及原日志/数据备份
版本组合
固件基线:ESP-IDF5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4f目标:DevKitC-1 v1.1 / WROOM-1-N8R8
适用范围

固定S3-DevKitC-1 v1.1 / WROOM-1-N8R8;IDF5.5.5,Flash8MiB/DIO80,不初始化PSRAM,桥接UART115200;无外接负载。默认不注入错误。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/maintenance-diagnostics-sources.json;29份固定SDK/子模块来源鲜取且与安装字节一致,具体阅读范围另列。验证日期:
构建 / 配置已通过
docs/esp32-evidence/maintenance-diagnostics-final-builds.json;本例两最终配置实际Exit0,输入快照、配置、app/ELF/map及日志SHA保留。对象:ESP32-S3 / N8R8计划版本:ESP-IDF5.5.5验证日期:
仿真不适用
仅执行适用主机逻辑、离线符号或电脑TLS;没有运行ESP32 panic/无线/TLS仿真。
本站实板待验证
没有本例实体烧录/运行/测量/恢复结果;observations.csv只有表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 崩溃诊断与故障门控

先固定任务、器材与本轮证据范围

原计划R02的三种受控故障路径均保留在完整例程:abort、已订阅任务不喂狗、独立小栈递归。没有板时不能宣称分别触发过panic;本轮交付的是实际构建、严格门控、真实ELF的合成PC符号化,以及逐项实板操作与恢复表。

E04已解释队列/协作;E06提供原生USB/JTAG方法;R01分清欠压与复位线索。本篇着重让日志、固件与ELF来自同一次构建,且不把符号命中直接当根因。固定DevKitC-1 v1.1 N8R8和USB-to-UART,先断开其他外设,保留正常固件与数据备份。

两个最终配置实际构建Exit0:安全app192304B、启用门控app194112B。合成SP不是实栈;未执行烧录、abort/TWDT/stack故障或复位恢复,observations.csv仅表头。

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

02 / 崩溃诊断与故障门控

构建与运行基线

安装官方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/r02-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 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

03 / 崩溃诊断与故障门控

先观察安全版本

使用SDK默认single-app分区(factory槽1MiB,板Flash仍8MiB),无OTA。默认 CONFIG_HX_ENABLE_FAULTS=n,包括所有ARM字符串都不执行注入。启动打印reset_reason、IDF和应用版本,heartbeat每500ms通过noinline检查点更新计数;串口一行输入后打印esp_timer微秒、free/min heap和主任务stack high-water。这里high-water单位是 字节(本版IDF API),是已观察最小剩余量,不是所有调用路径安全证明。任务创建失败会明确报告,不将“没崩溃”当测试通过。

esp_reset_reason()只是分类线索:上电/掉电/看门狗/软件reset的上下文不同,且不是每次reset都有可解码panic。供电不稳先回R01,不因没有backtrace就排除电源。E06讲连真实JTAG/GDB;本例只做日志、离线符号与显式故障路径。

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

04 / 崩溃诊断与故障门控

显式注入门控与退出

故障版本必须单独build目录、明确加载config/faults.defaults,再手工输入完全匹配的一行。不要把错误版本装到控制设备或存在不可丢数据的板上。 编译开关和串口口令都要满足,启动不会自动触发,复位也不保留ARM状态。

故障后RESET,确认启动不再注入;最终刷回安全版本。栈溢出可能破坏上下文,不能为了更漂亮backtrace反复增加递归深度。TWDT与IWDT分别关注任务进展和中断/调度阻塞;本例 没有 制造关中断的IWDT故障。OpenOCD断点会关闭IWDT/TWDT且离开断点不重新打开,做看门狗实验时应断开调试会话并复位,不能把JTAG环境的“未触发”当喂狗正确。

精确命令代码动作能证明什么 / 尚不能承诺
ARM ABORTnoinline fault_abort调用abort仅实板实际执行后才能记录abort panic;abort路径可能不打印寄存器转储
ARM TWDT显式配置2s/panic,订阅当前任务,yield10s但故意不feed“让出CPU”不等于“订阅任务喂狗”;若没出现,保留TWDT_NOT_OBSERVED,不能改成成功
ARM STACK创建2048字节任务,以384字节局部数组递归、每层yield故意制造栈耗尽风险,实际可能先触发canary/watchpoint/异常;不保证固定panic文本或有效所有帧
代码 / 配置
# POSIX shell · 配合本节前提逐项执行
export LAB_FAULT_BUILD="$HOME/esp32-labs/r02-faults"
python "$IDF_PATH/tools/idf.py" -C "$LAB_PROJECT" -B "$LAB_FAULT_BUILD" -D SDKCONFIG="$LAB_FAULT_BUILD/sdkconfig" -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/faults.defaults' build
# 按前述要求自行flash/monitor该目录;一次只做一项并保存完整原log。

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

05 / 崩溃诊断与故障门控

真实ELF的离线解析

Xtensa日志 Backtrace: PC:SP ... 中只有PC送addr2line;SP用于保留栈上下文,不能作为指令地址。工具先校验操作者提供的ELF SHA256,最多1MiB日志、1..32帧;它不能认证日志来源,也不替代原日志或证明堆栈回溯有效。ROM、跨ELF/损坏地址可返回??,原样保留,不凭猜测补函数名。中断上下文或双核panic可混合不同任务帧,要连同core/task、异常原因、EXCCAUSE/EXCVADDR和寄存器一起读。

hardware是操作者声明,工具不能将普通文本认证为实板。检测到SYNTHETIC标记时只允许synthetic;不要删标记换名。默认addr2line需来自已激活同一Xtensa工具链。

代码 / 配置
# POSIX shell · 配合本节前提逐项执行
python -c 'import hashlib,pathlib; p=pathlib.Path("/ABS/PATH/to/esp32_crash_watchdog_backtrace.elf"); print(hashlib.sha256(p.read_bytes()).hexdigest())'
python tools/decode_trace.py --elf /ABS/PATH/to/esp32_crash_watchdog_backtrace.elf --trace /ABS/PATH/raw-panic.log --expected-elf-sha YOUR_64_LOWERCASE_HEX --provenance hardware

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

06 / 崩溃诊断与故障门控

无板也能执行的合成夹具

生成器用真实nm符号选择PC,却人为填写SP;真实addr2line可映射本次ELF的fault_abort/stack_recurse/diagnostic_checkpoint。这证明离线符号链路可用,没有执行ESP32 abort/TWDT/栈溢出,也没有验证真实unwind。串口接收由同源FaultLine逐字节组帧,必须出现CR或LF才识别;超长(超过79个可打印ASCII字符)或控制字符行整行拒绝到下一行,CRLF不会触发第二次命令。同源host检查原16个字符串输入和8个分批/整行边界断言;额外检查错误ELF SHA、合成冒充hardware及空/损坏/过多帧的拒绝。任意输入被拒绝时工具Exit2,不能当解析成功。

还可在已激活Xtensa环境运行 python host/check_decode.py --elf /ABS/PATH/to/faults/esp32_crash_watchdog_backtrace.elf,生成临时合成帧、解析真实ELF并验证两类来源/SHA拒绝和三类帧语法拒绝。

代码 / 配置
# POSIX shell · 配合本节前提逐项执行
cc -std=c11 -Wall -Wextra -Werror -Imain host/policy_test.c -o /tmp/r02-policy
/tmp/r02-policy
python tools/make_synthetic_trace.py --elf /ABS/PATH/to/faults/esp32_crash_watchdog_backtrace.elf --out /tmp/r02-synthetic.log
python tools/decode_trace.py --elf /ABS/PATH/to/faults/esp32_crash_watchdog_backtrace.elf --trace /tmp/r02-synthetic.log --expected-elf-sha YOUR_64_LOWERCASE_HEX --provenance synthetic

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

07 / 崩溃诊断与故障门控

故障恢复与记录

先固定触发输入、固件SHA和第一次失败日志,再一次改一项;从第一条有效异常线索向前找原因,不把最后一个打印函数自动当根因。若stack high-water持续缩小,检查数组/递归/日志格式和任务栈预算;若TWDT,找订阅任务最后一次进展、阻塞API和feed所有权,不靠无限加大超时掩盖死锁。实际日志采集/解码和复位恢复填写observations.csv;无板时保持只有表头。源、构建、合成符号检查与硬件状态分别记录,未执行DUT故障不得填“实板通过”。

固定依据:IDF5.5.5 docs/en/api-guides/fatal-errors.rst、docs/en/api-reference/system/wdts.rst、esp_task_wdt.h/task_wdt.c、FreeRTOS task.h与Xtensa panic_arch.c;详见站点文末来源和作者证据的固定URL/SHA。版本变化时先核API、栈单位和panic策略再迁移。

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

08 / 崩溃诊断与故障门控

完整诊断主程序:默认安全、显式门控

下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。

代码 / 配置
// ---- main/main.c ----
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_app_desc.h"
#include "esp_timer.h"
#include "esp_task_wdt.h"
#include "fault_policy.h"
static volatile uint32_t probe_sink;
__attribute__((noinline)) void diagnostic_checkpoint(uint32_t n) { probe_sink=n; }
static void heartbeat(void *unused) {
 (void)unused;
 for (;;) { diagnostic_checkpoint(probe_sink+1); vTaskDelay(pdMS_TO_TICKS(500)); }
}
#if CONFIG_HX_ENABLE_FAULTS
static volatile uint32_t stack_sink;
__attribute__((noinline)) void fault_abort(void) { abort(); }
__attribute__((noinline)) void stack_recurse(unsigned depth) {
 volatile unsigned char pad[384]; pad[0]=(unsigned char)depth; pad[383]=(unsigned char)(depth+1);
 vTaskDelay(1); /* Let the scheduler check the guard; outcome still target-specific. */
 if (depth<32) stack_recurse(depth+1);
 stack_sink+=pad[0]+pad[383]; /* Prevent tail-call elimination. */
}
static void stack_task(void *unused) { (void)unused; stack_recurse(0); vTaskDelete(NULL); }
static void deliberate_twdt(void) {
 esp_task_wdt_config_t c={.timeout_ms=2000,.idle_core_mask=(1U<<portNUM_PROCESSORS)-1,.trigger_panic=true};
 esp_err_t e=esp_task_wdt_reconfigure(&c);
 if (e==ESP_ERR_INVALID_STATE) e=esp_task_wdt_init(&c);
 ESP_ERROR_CHECK(e);
 if (esp_task_wdt_status(NULL)!=ESP_OK) ESP_ERROR_CHECK(esp_task_wdt_add(NULL));
 puts("INJECTION TWDT: this subscribed task intentionally will not feed; yields for10s");
 fflush(stdout); vTaskDelay(pdMS_TO_TICKS(10000));
 puts("TWDT_NOT_OBSERVED: retain log, do not claim panic; reset lab board");
}
#endif
void app_main(void) {
 printf("R02 reset_reason=%d idf=%s firmware=%s; no fault auto-start\n",esp_reset_reason(),esp_get_idf_version(),esp_app_get_description()->version);
 if (xTaskCreate(heartbeat,"heartbeat",3072,NULL,2,NULL)!=pdPASS) { puts("TASK_ALLOC_FAILED"); return; }
#if CONFIG_HX_ENABLE_FAULTS
 puts("FAULT LAB COMPILED: exact ARM ABORT / ARM TWDT / ARM STACK can reset/corrupt this isolated run. No peripherals.");
 const int enabled=1;
#else
 puts("SAFE BUILD: ARM commands are disabled. Build explicit faults.defaults for isolated experiment only.");
 const int enabled=0;
#endif
 FaultLine input={0};
 for (;;) {
  int c=getchar();
  if(c==EOF){clearerr(stdin);vTaskDelay(pdMS_TO_TICKS(20));continue;}
  bool ready=false;Fault f=fault_feed(&input,(unsigned char)c,enabled,&ready);
  if(!ready)continue;
  printf("COMMAND t_us=%"PRId64" fault=%u free=%lu min=%lu main_stack_low_bytes=%u\n",esp_timer_get_time(),(unsigned)f,(unsigned long)esp_get_free_heap_size(),(unsigned long)esp_get_minimum_free_heap_size(),(unsigned)uxTaskGetStackHighWaterMark(NULL)); fflush(stdout);
#if CONFIG_HX_ENABLE_FAULTS
  if (f==FAULT_ABORT) fault_abort();
  else if (f==FAULT_TWDT) deliberate_twdt();
  else if (f==FAULT_STACK) { if(xTaskCreate(stack_task,"intentional_stack",2048,NULL,3,NULL)!=pdPASS) puts("FAULT_TASK_ALLOC_FAILED; no overflow executed"); }
#else
  (void)f;
#endif
 }
}

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

09 / 崩溃诊断与故障门控

串口命令识别与默认拒绝的同源函数

下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。

代码 / 配置
// ---- main/fault_policy.h ----
/* SPDX-License-Identifier: MIT */
#pragma once
#include <string.h>
typedef enum { FAULT_NONE, FAULT_ABORT, FAULT_TWDT, FAULT_STACK } Fault;
static inline Fault fault_command(const char *text, int enabled) {
 if (!enabled || !text) return FAULT_NONE;
 if (!strcmp(text,"ARM ABORT")) return FAULT_ABORT;
 if (!strcmp(text,"ARM TWDT")) return FAULT_TWDT;
 if (!strcmp(text,"ARM STACK")) return FAULT_STACK;
 return FAULT_NONE;
}

#include <stdbool.h>
typedef struct { char bytes[80]; unsigned used; bool rejected; } FaultLine;
/* Assemble one full CR/LF terminated line, including across UART read batches.
 * Reject overflow/control bytes until the next terminator, never parse chunks. */
static inline Fault fault_feed(FaultLine *line,unsigned char c,int enabled,bool *ready) {
 *ready=false;
 if(c=='\r'||c=='\n') {
  *ready=line->used>0||line->rejected;
  line->bytes[line->used]=0;
  Fault f=line->rejected?FAULT_NONE:fault_command(line->bytes,enabled);
  line->used=0;line->rejected=false;return f;
 }
 if(c<32||c>126)line->rejected=true;
 if(!line->rejected) {
  if(line->used<sizeof(line->bytes)-1)line->bytes[line->used++]=(char)c;
  else line->rejected=true;
 }
 return FAULT_NONE;
}

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

10 / 崩溃诊断与故障门控

完整离线解析工具:校验ELF、保留未知帧

下列是下载包中的完整同名文件;其余CMake、配置、锁、工具和空表在独立工程内,按README的相同版本构建。

代码 / 配置
# ---- tools/decode_trace.py ----
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Symbolication only. Does not prove PC origin, unwind or real ESP32 panic."""
import argparse,pathlib,hashlib,json,re,subprocess

def frames(text):
    result=[]
    for block in re.findall(r'Backtrace:\s*([^\r\n]*)',text):
        for pc,sp in re.findall(r'(0x[0-9a-fA-F]{8}):(0x[0-9a-fA-F]{8})',block):result.append((pc,sp))
    if not result or len(result)>32:raise ValueError('need1..32 PC:SP pairs from Backtrace lines; preserve raw/corrupted suffix separately')
    return result

def decode(elf,trace,expected,tool,provenance):
    if not re.fullmatch('[0-9a-f]{64}',expected) or hashlib.sha256(elf.read_bytes()).hexdigest()!=expected:raise ValueError('ELF SHA mismatch; do not decode against a different build')
    if trace.stat().st_size>1048576:raise ValueError('trace exceeds1MiB cap')
    text=trace.read_text()
    if 'SYNTHETIC' in text and provenance!='synthetic':raise ValueError('synthetic fixture cannot be labeled hardware/simulation')
    pairs=frames(text);items=[]
    for pc,sp in pairs:
        r=subprocess.run([tool,'-a','-f','-C','-i','-e',str(elf),pc],capture_output=True,text=True,check=True)
        items.append({'pc':pc,'sp':sp,'symbolication':r.stdout,'unresolved': '??' in r.stdout})
    return {'provenanceProvidedByOperator':provenance,'elfSHA256':expected,'traceSHA256':hashlib.sha256(trace.read_bytes()).hexdigest(),'scope':'symbolication only; synthetic PCs/SPs do not prove execution/unwind. ROM/corrupted/outside-ELF frames may remain unresolved. Preserve complete raw log.','frames':items}
if __name__=='__main__':
    p=argparse.ArgumentParser();p.add_argument('--elf',type=pathlib.Path,required=True);p.add_argument('--trace',type=pathlib.Path,required=True);p.add_argument('--expected-elf-sha',required=True);p.add_argument('--addr2line',default='xtensa-esp32s3-elf-addr2line');p.add_argument('--provenance',choices=['hardware','synthetic','simulation'],required=True);a=p.parse_args()
    try:print(json.dumps(decode(a.elf,a.trace,a.expected_elf_sha,a.addr2line,a.provenance),indent=2))
    except (ValueError,OSError,subprocess.SubprocessError) as e:p.exit(2,str(e)+'\n')

参考:[1] IDF5.5.5 Fatal Errors:panic/backtrace/abort边界[2] IDF5.5.5 看门狗:订阅、panic策略、JTAG行为[3] 固定esp_task_wdt.h:init/reconfigure/status/add

BACK TO THE SOURCE

资料来源与版本说明

保持原计划ID/slug/阶段和用途。固定上游与实际构建、适用主机/QEMU验证留证;没有把本机结果升级为实体运行结论。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. IDF5.5.5 Fatal Errors:panic/backtrace/abort边界
  2. IDF5.5.5 看门狗:订阅、panic策略、JTAG行为
  3. 固定esp_task_wdt.h:init/reconfigure/status/add
  4. 固定任务看门狗实现
  5. 固定FreeRTOS task.h:栈单位与high-water语义
  6. 固定Xtensa panic_arch.c