本章是"工程能力"章:①固件体积怎么优化(含本书真实账单);②崩溃
(Guru Meditation)到底是什么机制;③用 addr2line 把崩溃地址翻译成
代码行的完整方法(含本书真实案例:fade 安装顺序错误的崩溃排查全过程)。
11.1 体积账单:先看我们项目的真实数字
idf.py size(当前发布版):
text data bss dec hex 118639 53598 362939 535176 82a88| 段 | 大小 | 存放 | 含义 |
|---|---|---|---|
| text | 118639 B | Flash | 代码指令 |
| data | 53598 B | Flash 初值 + SRAM 运行 | 有初值数据 |
| bss | 362939 B | SRAM(大头是 IDF 预建的内存池/缓冲区,不是我们的变量) | 无初值数据 |
(最后两列dec/hex:把 text+data+bss 加在一起的总大小,dec 是十进制、
hex 是同一个数的十六进制写法——535176 = 0x82a88,两种记数是同一个数,
第 0/1 章讲过互转。)
bin 大小(烧录报告):
l298n.bin binary size 0x2a150 bytes = 172368 B 0xd5eb0 bytes (84%) free = 分区剩 876208 B体积变化记录(我们的真实验证):
| 版本 | bin 大小 | 原因 |
|---|---|---|
| 初始 -Og | 176336 B | 基线(不优化) |
| 改 -Os | 162288 B | 优化等级省 14KB |
| +PSRAM 配置 | 170768 B | 启动代码增加 |
| 当前(含 fade 修复) | 172368 B | 最终;相对 170768 增加约 1.6KB |
11.2 优化等级到底在改什么
优化器是"只改代码、不改语义"的转换器。常见招式:
- 常量折叠:
x = 100 * 2→x = 200; - 死代码删除:
if (0) {...}整段删; - 内联:小函数展开到调用处(省压栈/跳转);
- 强度削减:
x * 2→x << 1; - 删除未用变量/函数:没被引用的直接不进固件。
| 选项 | 目标 | 体积 | 速度 | 调试 |
|---|---|---|---|---|
| -O0 | 无 | 大 | 慢 | 最好 |
| -Og | 调试友好 | 中 | 中 | 好 |
| -Os | 体积 | 小 | 中 | 一般 |
| -O2 | 速度 | 大 | 快 | 差 |
⚠️ 优化会影响崩溃回溯:代码被重排/内联后,PC 地址和源码行
对不齐。先 -Og 查 bug,发布再 -Os。
11.3 减体积五招
- -Os:
CONFIG_COMPILER_OPTIMIZATION_SIZE=y(省得最多); - 关不需要的组件:menuconfig 里蓝牙/WiFi/调试组件按需关;
- 别用 double:浮点库整体很大(
printf("%f")等); - 日志等级 INFO:DEBUG 字符串不进固件;
- 定位大头:
idf.py size-components、size-files找出最占地方的。
11.4 崩溃机制:Guru Meditation 是什么
芯片检测到非法操作 → 进入panic(恐慌)状态:
Guru Meditation Error: Core 0 panic'ed (LoadProhibited)恐慌来源(底层):
- CPU 执行了禁止的访存(读/写地址越界、访问保护区域);
- 内核级异常(栈溢出、非法指令);
- 软件主动 abort(如 ESP_ERROR_CHECK,第 6 章)。
LoadProhibited的机制:地址空间被分成很多区,各自有访问权限。
CPU 去读一个"不允许读"的地址(如 0x00000000,或未映射区)
→ MMU(内存管理单元,芯片里查"这个地址准不准访问"的门卫)/总线保护
→ 异常 → panic。
LoadProhibited + EXCVADDR: 0x00000000 = 访问了地址 0 = 大概率空指针(指针的值是 0,解引用它)Backtrace(回溯)怎么读
Backtrace: 0x4200942f:0x3fca0000 0x4037c123:0x3fca0010 0x42000056:0x3fca0030格式:地址:栈指针依次排列,第一个是当前卡住的位置,后面是
“谁调用了它”(调用链)。栈指针帮我们确认每一层的栈帧
(栈帧:每次函数调用在栈上占用的那一格,装着返回地址和局部变量)。
11.5 addr2line:把地址翻译成代码行
原理
.elf 里带调试信息(DWARF):记录"每条指令对应源码哪一行"。
addr2line 用地址查这张表。
xtensa-esp32s3-elf-addr2line.exe-ebuild\l298n.elf-f-C0x4200942f-e:指定 .elf(必须有调试信息,别删 build);-f:显示函数名;-C:C++ 名字还原成可读形式。
在哪跑:IDF 终端(跑过 export 的窗口,2.3);提示"找不到命令"就是
工具链没进 PATH,先执行 export 再试。
习惯:每次发布前备份 .elf——把build\l298n.elf复制一份、按固件
版本改名存档。旧崩溃日志只有配对的那份 .elf 才翻得动,build 一删就
永远翻不出来了(11.9 有对应的坑)。
更省事的路:idf.py monitor会在崩溃发生时自动把 Backtrace 里的
地址翻译成函数名和源码行打印出来,日常排查直接看 monitor 输出就行;
addr2line 是用来理解原理和处理离线日志的。
11.6 真实案例:fade 顺序崩溃的全排查过程
症状(第 7 章坑 2):
Guru Meditation Error: Core 0 panic'ed (LoadProhibited) EXCVADDR: 0x00000000 Backtrace: 0x4200942f:0x3fca0000 0x4037c123:0x3fca0010 0x42000056:0x3fca0030排查步骤(完整方法演示):
- 读类型:LoadProhibited = 非法读;EXCVADDR=0 = 空指针;
- 翻译 3 个地址(addr2line):
0x4200942f → ledc_ll_get_fade_end_intr_addr (ledc 驱动内部) 0x4037c123 → ledc_fade_func_install (我们调用的安装函数) 0x42000056 → DCMotor::init (我们的代码)(注意:这些地址来自修复前那次崩溃时的固件,代码改过之后重新编译,
当前 build 里同样的行号/地址已经对不上了——翻译历史崩溃日志时,
必须用产生该日志的那一份 .elf,别拿最新 build 去翻旧地址。)
- 看调用链:DCMotor::init → 调用 ledc_fade_func_install →
内部访问"通道对象表"——表还是空的 → 空指针; - 原因:install 放在 timer/channel 配置之前;
- 修复:把
ledc_fade_func_install(0)移到配置之后; - 验证:重编译 → 烧录 → 日志正常。
方法论:翻译地址 → 看调用链 → 想"为什么访问空/非法地址" → 修 → 验证。
不要"瞎试"。
11.6b 动手实验:亲手复现一次崩溃,再亲手翻译它
光读一百遍不如亲手崩一次。这个实验把 11.6 的案例"故意"重演一遍
(前提是你手上已有完整工程:8.6 全文复现的motor_control.*两个文件 +
12.2 的main.cpp,也可用随书源码包;还没搭好就先去做 12.2 再回来)。
【动手框】
① 在哪执行:你工程的main/motor_control.cpp(改文件);
之后回到 l298n 工程根目录的 IDF 终端敲命令
② 改什么、敲什么:在init()里找到以if (!s_ledc_fade_installed) {开头的整块 fade 安装(正确位置在
通道配置之后),把整块挪到ledc_timer_config_t timer_cfg = {};
之前——还原当年"装错顺序"的错误;然后idf.py -p COM3 build flash monitor(COM3 换你的端口)
③ 预期看到:崩溃前照常有一行I (xxx) l298n: L298N 直流电机演示启动
——app_main 的开场日志打在 init() 之前,所以一定能看到;紧接着
“就绪:” 日志永远不出现,取而代之:Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled. Core 0 register dump: PC : 0x420123a4 PS : 0x00060830 A0 : 0x800d1e9c A1 : 0x3fcee3d0 (下面还有十几行寄存器,是标准的案发现场快照,此处节选略过) SAR : 0x0000001e EXCVADDR: 0x00000000 Backtrace: 0x420123a4:0x3fcee3d0 0x4037d4a2:0x3fcee3f0 0x4200f6b8:0x3fcee420 ELF file SHA256:(这个值每次重新编译都会变) Rebooting...注意:打印完回溯会自动重启(
Rebooting...就是重启预告),
重启后再崩一次,日志从头循环刷屏——Guru Meditation ≠ 板子坏了,
第 6 章就说过。两个读法细节:Backtrace 第一组的"地址:栈指针"就是
寄存器表里的PC和A1,可以互相对照;你看到的地址和上面这组
一定不相同(固件不是同一份),"类型 + EXCVADDR=0"对上就够了。
电机完全不运动、崩溃也发生在任何硬件动作之前,所以这个实验不必
接驱动板和电机,开发板单独插 USB 就能做。
④ 没看到:
- 照常打出"就绪"没崩 → 安装块没真正挪到定时器配置之前,检查位置;
- monitor 乱码刷屏 → 波特率/端口问题,回 2.11;
- flash 失败 → 端口被别的 monitor 占用,或按住 BOOT→点按 EN→松 BOOT
手动进下载模式(第 2 章三步)。
接下来就是 11.6 六步的跟做:monitor 已经自动把 Backtrace 翻译好了
(11.5 说的便利),三个地址的函数名与 11.6 那次是同一族——
ledc 驱动内部函数 ←ledc_fade_func_install←DCMotor::init;
你也可以把手上最新 build 的 .elf 拿去跑 addr2line(11.5 的命令),
照样翻得动——因为你烧的就是这份固件。这恰好和 11.6 括号里的陷阱
互为镜像:新日志配新 .elf 能翻,旧日志只能配旧 .elf,所以才要
“每次发布前备份 .elf”(11.5)。做完把整块挪回原处,重烧,“就绪”
日志回来,实验闭环。
11.7 其他崩溃类型
| 类型 | 机制 | 常见原因 |
|---|---|---|
| LoadProhibited | 读了禁止的地址 | 空指针、悬垂指针(指向已归还内存的指针,像攥着退掉的房卡) |
| StoreProhibited | 写了禁止的地址 | 越界写、只读区写 |
| IllegalInstruction | 执行了非法指令 | 栈坏 → 跳到垃圾地址 |
| Stack canary | 栈溢出检测 | 递归/大局部变量 |
| Task watchdog | 任务饿死 | 死循环/优先级反转(第 13 章) |
看门狗的原理、怎么喂狗、怎么避免饿死,第 13 章有专门一节。
11.8 调试流程(背下来)
1. 看日志(是否有前因) 2. 看崩溃类型 + EXCVADDR 3. addr2line 翻译 Backtrace 所有地址 4. 还原调用链,找"我们自己的代码层" 5. 想清楚为什么(空指针?顺序?越界?) 6. 修复 → 重编译 → 烧录 → 验证优化等级太高导致回溯不准时,先改 -Og 重编译再查。
11.9 常见问题速查
| 问题 | 方法 |
|---|---|
| 固件太大 | -Os + 关组件 + 别用 double |
| 崩溃看不懂 | 类型 → EXCVADDR → addr2line |
| 翻译出系统库 | 往调用链上层找自己的代码 |
| 换 -O 就不崩 | 未定义行为,用 -Og 查 |
| build 目录被删 | 重编译可恢复构建,但旧崩溃地址从此无法翻译——每次发布前备份 .elf(11.5) |
11.10 想一想 + 小测验
想一想:
- 为什么 -O2 下 Backtrace 可能不准?
- EXCVADDR=0 说明什么?请用"访问权限"解释 LoadProhibited。
- 排查崩溃的完整步骤是什么?
小测验:
- 当前固件大小?(A. 172368 B. 162288 C. 176336)
- LoadProhibited 的含义是?(A. 非法读 B. 非法写 C. 非法指令)
- 翻译崩溃地址用什么工具?(A. addr2line B. esptool C. idf.py size)
- 想固件最小用哪个优化?
11.11 本章总结
- 体积:-Os 影响最大;
size-components/files定位大头; - 崩溃 = 非法操作触发 panic;LoadProhibited + 地址 0 = 空指针;
- Backtrace = 调用链;addr2line 用 DWARF 调试信息翻译地址;
- 方法论:类型 → 地址 → 调用链 → 想清楚 → 修 → 验证。
下一章:把全部知识串起来,完成综合实战。