每次按下复位键,ESP32 都会先喷出三十来行字。它们滚得太快,又长得像乱码, 所以几乎所有人的第一反应都是「噪音,忽略」。我看它们看了两年,也是这么想的—— 直到有一天,一块板子开始每隔几分钟自己重启,而我手上没有任何调试器。
那一次我才发现:这三十行里藏着芯片型号、晶振频率、内存余量、Flash 分区和编译参数, 全部是硬件层面的信息,不用写一行代码就能读到。它是这台设备失控之前,唯一愿意开口说话的时刻。
下面按出现的顺序,逐段拆开。
01第一行就是个陷阱
ets Jul 29 2019 12:21:46
2019 年 7 月 29 日。看到这行的人常有两个误解:以为是固件的编译日期,或者以为自己不小心烧录了一个三年前的旧版本。
都不是。这是 bootloader 的编译时间戳——这一段代码由 Espressif 出厂时就固化在芯片里, 他们多年没有改动过它,所以这个日期在你的板子上、在我的板子上、在别人刚拆封的新板子上,完全一样。 它不是一个会变化的信息,看到它就当没看到。
真正的固件编译时间是后面 Software Info 里的那一行,我们等会儿会走到那里。
02复位原因与启动模式
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
rst: 后面这个十六进制数是复位原因。
0x1 是上电复位,也就是你拔插电源、或者按下复位键时会看到的那种;除此之外还有软件复位、
看门狗复位、欠压复位等等。这一行是排查「设备莫名其妙重启」的起点:
如果是看门狗,说明你的某段代码把 CPU 卡住了;如果指向欠压,那就去查电源——
我在另一篇笔记里就是靠这个思路找到电源问题的。
完整的码表在 ESP-IDF 的 esp_reset_reason() 文档里,不用背。
boot:0x13 是启动模式,括号里的 SPI_FAST_FLASH_BOOT 才是给人看的部分——
意思是「从 SPI Flash 里快速启动」,这是正常工作时的取值。
如果你把它做成产品,客户不小心把某个引脚拉低了,这里会变成别的模式,
然后就表现为「板子通电没反应」。
中间那两行 configsip、clk_drv 之类,是 bootloader 读到的 Flash 配置
和各信号线的驱动能力。它们平时是固定的,只有在换了 Flash 芯片或者改了硬件之后才会变——
所以它们的用途是「记住正常时的样子」,将来出问题了拿来对比。
最后 mode:DIO, clock div:1 值得注意:bootloader 阶段这里用的是 DIO(双线),
而几分钟后你会看到 Flash Info 里写的是 QIO(四线)。
这不矛盾——ROM 阶段还没能力协商,只好用最保守的方式先把代码读进来,之后才切到更快的模式。
03三段 load:镜像有没有坏
load:0x3fff0030,len:4876
load:0x40078000,len:16560
load:0x40080400,len:3500
entry 0x400805b4
这是 bootloader 把自己从 Flash 搬进内存的过程:地址、长度、三段。 平时完全不用管,但有一种情况例外——如果这三行里出现校验失败或者长度异常,说明 Flash 里的镜像损坏了, 常见原因是烧录到一半拔线、或者供电不稳写坏了页。
entry 是跳转到应用程序入口的地址。看到它出现,就意味着「bootloader 干完了,接下来是固件的事」。
所以判断一个「通电没反应」的板子卡在哪一层,看日志停在哪一行就知道了:
停在 rst: 附近是 bootloader 阶段的问题,停在 entry 之后就是固件的问题。
04芯片信息:你手上到底是什么
Model : ESP32
Package : D0WD-Q5
Revision : 3.01
Cores : 2
CPU Frequency : 240 MHz
XTAL Frequency : 40 MHz
Embedded Flash : No
Embedded PSRAM : No
2.4GHz WiFi : Yes
Classic BT : Yes
Package: D0WD-Q5 给出的是这颗 die 的型号和封装代号。
它有用是因为——模组厂商在同一个「ESP32-WROOM-32」的名字下,装过不同批次的芯片,
而某些低功耗特性、蓝牙行为在修订版之间有差异。Revision: 3.01 就是芯片的修订版号。
XTAL Frequency: 40 MHz 是外部晶振。往下拉几行你会看到固件启动时打印的一句:
[ 3][D][esp32-hal-cpu.c:324] setCpuFrequencyMhz(): PLL: 480 / 2 = 240 Mhz, APB: 80000000 Hz
240 MHz 是怎么来的,日志直接告诉你了:480 MHz 的 PLL 二分频。
方括号里的 3 是开机后的毫秒数——三毫秒,它已经把主频设好了。
另外 Embedded Flash: No 和 Embedded PSRAM: No 这两行,
是回答「我这颗芯片到底带不带内置 Flash / PSRAM」的最快方式,
比查数据手册快得多——尤其是当你手上只有一块来源不明的开发板时。
05内存:为什么不是 520 KB
Total Size : 376480 B ( 367.7 KB)
Free Bytes : 333984 B ( 326.2 KB)
Allocated Bytes : 34744 B ( 33.9 KB)
Minimum Free Bytes: 328368 B ( 320.7 KB)
Largest Free Block: 110580 B ( 108.0 KB)
ESP32 的 SRAM 标称 520 KB,这里却只有 367.7 KB。差额不是被谁偷了—— 它是 ROM 里的代码、启动时的静态分配、以及协议栈占掉的部分。 所以这一行反映的是「系统初始化之后,留给动态分配的堆总量」,比标称值更接近你能用的现实。
这五个数字里,我认为最该盯的是最后两个,而不是 Free Bytes:
- Minimum Free Bytes 是历史最低水位。设备跑了一整天之后回来看这个值, 就知道它最紧张的时候剩下多少——这个数字比「当前还剩多少」有意义得多。
- Largest Free Block 是最大的一块连续空闲。总共剩 326 KB,
但最大连续块只有 108 KB,意味着你想
malloc一个 200 KB 的缓冲区会失败, 尽管「总空闲」看起来完全够。内存碎片就是这么咬人的。
那一次我遇到的「跑几小时就自己重启」,最后就是靠这两行定位的: Free 一直在 300 KB 以上,看着很健康,但 Largest Free Block 从 108 KB 一路缩到 30 多 KB, 最后某个需要连续内存的操作失败,触发了复位。
06分区表:一本地址账本
这是我后来觉得最值得看的一段。它把整块 Flash 按地址切给了不同用途:
nvs : addr: 0x00009000, size: 20.0 KB, type: DATA, subtype: NVS
otadata : addr: 0x0000E000, size: 8.0 KB, type: DATA, subtype: OTA
app0 : addr: 0x00010000, size: 1280.0 KB, type: APP, subtype: OTA_0
app1 : addr: 0x00150000, size: 1280.0 KB, type: APP, subtype: OTA_1
spiffs : addr: 0x00290000, size: 1408.0 KB, type: DATA, subtype: SPIFFS
coredump : addr: 0x003F0000, size: 64.0 KB, type: DATA, subtype: COREDUMP
把地址当成账目来核对一遍,你会发现它严丝合缝:
app0从0x010000(64 KB)开始,占 1280 KB,结束于 64 + 1280 = 1344 KB =0x150000;- 这正好是
app1的起始地址。app1 再加 1280 KB,到 2624 KB =0x290000; - 这正好是
spiffs的起始地址。spiffs 加 1408 KB,到 4032 KB =0x3F0000; - 这正好是
coredump的起始地址。再加 64 KB ——刚好 4096 KB,也就是 4 MB。
一个字节不多,一个字节不少。这说明分区表不是随便填的,它是按芯片容量精算出来的。 看懂这本账,你就能回答两个很实际的问题:
第一,我的固件还能长多大? app0 和 app1 各 1280 KB,
意味着你的固件必须同时塞进这两个槽(OTA 需要备份槽),所以上限就是 1280 KB。
编译时如果报 Sketch too big,数字就是从这里来的。
第二,文件存储还剩多少? spiffs 的 1408 KB 就是你能用来存图片、
日志、配置文件的全部空间。它不会随着你的固件变大而变大——它是固定的。
顺带一提,coredump 那 64 KB 是留给崩溃现场快照的。
如果它在账本里被删掉了,你的设备崩溃时就不会留下可分析的转储。
07最后几行:找出日志为什么这么多
Compile Date/Time : Aug 21 2026 01:37:10
Compile Host OS : windows
ESP-IDF Version : v5.5.4
Arduino Version : 3.3.10
Arduino FQBN : esp32:esp32:esp32:UploadSpeed=921600,CPUFreq=240,
FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=default,
DebugLevel=verbose,PSRAM=disabled,LoopCore=1,EventsCore=1,EraseFlash=none
Compile Date/Time 才是真正的固件编译时间。
「我明明改了代码,怎么行为没变?」——先看这一行。它没变,说明你烧的不是你以为的那个固件。
而 FQBN 那一长串,是整个启动日志里最实用的一行,因为它把你的编译选项全部抄了一遍: 上传波特率、主频、Flash 频率与模式、Flash 容量、分区方案,一个不落。
如果你好奇「为什么我的串口刷得这么快、全是 [V] 开头的行」——
答案就在里面:DebugLevel=verbose。这是 Arduino IDE 里
Tools → Core Debug Level 被设成了 verbose 的结果。改成 None,
你的串口立刻安静下来,只剩你自己打印的东西:
[ 529][V][esp32-hal-uart.c:807] uartSetPins(): UART0: Driver not yet installed,
storing pins for later attachment (RX:3, TX:1)
[ 666][D][esp32-hal-uart.c:882] uartSetPins(): Attaching pin 3 to UART0 as RX
[ 673][V][esp32-hal-periman.c:173] perimanSetPinBus(): Pin 3 successfully set to
type UART_RX (2) with bus 0x3ffbdb68
这些行在说「把 GPIO3 接到 UART0 的 RX 上」,对调试外设冲突其实有用—— 它能告诉你某个引脚被谁占用了。但如果你不需要,它就是纯占地方。
08现在我会怎么看这三十行
按顺序扫四个地方就够了:
rst:—— 设备是为什么醒过来的。这是排查重启类问题的唯一入口。Largest Free Block—— 内存还有没有可能分配大块。别只看 Free。Partitions Info—— 固件和数据还有多少空间。算一遍地址账。FQBN与Compile Date/Time—— 现在跑的到底是哪一个固件、带什么编译选项。
这四件事都不需要额外的工具,插上 USB 就能读。 对一个没有调试器、板子又开始不听话的下午来说,这已经是很好的起点了。
- 关于本文
- 整理自 2026 年 8 月的一段串口记录与当时的排查笔记。日志原文来自 ESP32-WROOM-32E(ESP-IDF v5.5.4 / Arduino 3.3.10),未作删改。
- 2026.09.20 整理
- 补上了 0x3F0000 + 64 KB = 0x400000 的地址核对过程——当初只是觉得「数字对得上」,没有真的算过。
留言
正在载入留言…