本章从底层讲清:
①日志是怎么通过串口"喊"出来的(UART 帧、格式化);②日志五级的语义;
③esp_err_t错误码体系;④ESP_ERROR_CHECK出错时芯片内部发生了什么
(打印 → abort → 默认配置下自动重启)。这些是嵌入式调试的核心工具。
6.1 日志的底层链路:字符怎么从芯片到屏幕
ESP_LOGI(TAG, "格式串", 各个参数) │ ① 按格式串把参数拼成文本(printf 家族:C 语言最老的"按格式拼装 │ 并输出"函数们;ESP_LOG 就是加了"等级 + 署名"的 printf 变体。 │ 逐个字符处理) ▼ 一长串字节(就是那行日志的每个字符的 ASCII 码,一字一字节) │ ② 交给 UART 驱动 ▼ 按帧格式一个个字节发出去(起始位+8数据位+停止位,第 0 章) │ ③ 走 UART 口经 USB 转串口芯片,或走原生 USB 口(2.5 的两条路) ▼ 电脑串口程序(monitor)按 115200 波特率收帧、还原文字、显示- 波特率:双方约定每秒传多少位;不一致 = 乱码;
- 日志默认是同步阻塞的:谁调用
ESP_LOGI,谁就停下手里的事,
把这条日志的字节一个个塞进 UART,塞完才继续跑下一行代码。
所以正常情况下一条都不会丢,代价是打印得多、程序就慢——
嵌入式里"加点日志时序就变了"就是这个原因。 - 只有主动换成非阻塞方案才可能出现"迟到"或丢行:日志先进缓冲区、
由后台任务(FreeRTOS 里的一个任务,第 13 章)慢慢搬运,缓冲区塞满
时高频打印确实会丢行。那是模式选择的代价,不是日志系统的日常。
(本书用的 IDF v6.0.1 经典日志 V1 就是全同步;非阻塞打印模式CONFIG_LOG_PRINT_MODE是另一些 IDF 版本/日志实现提供的选项,
没有特意去开,就不要假设日志是异步的。)
一句话:丢日志不是正常现象。"屏幕上一个字都没有"先查 COM 口、
波特率、监视器占用;"少了几行中间"才考虑是不是自己开了缓冲方案。
6.2 日志五级:从"正常"到"完蛋"
| 宏 | 等级 | 语义 | 典型用途 |
|---|---|---|---|
ESP_LOGE | ERROR (E) | 出错,程序可能无法继续 | 致命错误 |
ESP_LOGW | WARN (W) | 有问题但还能跑 | 边界情况 |
ESP_LOGI | INFO (I) | 正常进度 | 主要流程 |
ESP_LOGD | DEBUG (D) | 调试细节 | 排查时开 |
ESP_LOGV | VERBOSE (V) | 最琐碎 | 极少用 |
过滤机制:编译时按CONFIG_LOG_DEFAULT_LEVEL决定保留哪些等级
(低于阈值的调用被直接删掉——不进固件)。
这组开关在 Kconfig 里是一个choice 互斥组(呼应 3.6 讲过的全名陷阱):CONFIG_LOG_DEFAULT_LEVEL_NONE / _ERROR / _WARN / _INFO / _DEBUG / _VERBOSE
六选一,sdkconfig 里同时只会有一行=y,其余全是# ... is not set。
本书工程真实 sdkconfig 生效的就是:
CONFIG_LOG_DEFAULT_LEVEL_INFO=y # 只保留 INFO 及以上(实际生效值) # CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set想看 DEBUG 日志,去 menuconfig 把这一组选到 DEBUG(工具会自动把
INFO 那行改成 “is not set”),而不是两行并排都写=y——defaults 里
这么写只会让后处理的值纠结半天,事与愿违。
为什么"调试完要关 DEBUG"?因为 DEBUG 的字符串本身会进固件
(增加体积),而且打印过程耗时(拖慢时序)。发布用 INFO。
格式串与参数
ESP_LOGI(TAG,"速度=%d 占空比=%u 十六进制=%x",speed,duty,val);%d有符号整数、%u无符号、%x十六进制、%s字符串、%f浮点;- 类型必须匹配:参数在内存里的解释方式由格式符决定,不匹配就乱打;
- 想输出一个
%要写%%(格式化器把%当转义起点)。
6.3 TAG 的作用:日志的"分区"
TAG 是日志字符串里的"名牌"(如l298n、DCMotor、ledc)。
作用:
- 一眼看出这条日志是谁打的(哪一层);
- 可以按 TAG 开关日志(
esp_log_level_set(TAG, ESP_LOG_DEBUG))。
排查时看完整链路:从系统驱动(如 ledc)到我们的代码(DCMotor)
到main——层级递进的日志能还原"事故经过"(6.6 有实例)。
6.4 esp_err_t:统一的"体检报告"
typedefintesp_err_t;// 本质是 int// typedef:给已有类型起个"更有意义的别名"——// esp_err_t 就是"错误码"这个用途下的 intESP_OK=0// 成功ESP_ERR_*=负数// 失败(每个负数值是一种错误)每个返回 esp_err_t 的函数,返回的不是"结果数据",而是成功/失败+原因。
配合esp_err_to_name(err)可以打印错误的名字(如ESP_FAIL)。
三种处理姿势(按场景选)
姿势 1:检查 + 自己处理(灵活)
esp_err_t err=motor.init();if(err!=ESP_OK){ESP_LOGE("l298n","init 失败: %s",esp_err_to_name(err));returnerr;// 向上层报告}注意这段外面套的是一个返回esp_err_t的函数才有return err可言。
如果就是在app_main里检查——app_main返回void,那里只能是ESP_LOGE+ 光秃秃的return;。本书真实main.cpp就是这么写的:
constesp_err_t err=motor.init();if(err!=ESP_OK){ESP_LOGE(TAG,"电机初始化失败: %s,程序终止",esp_err_to_name(err));return;}姿势 2:ESP_ERROR_CHECK(出错就"崩退":打印后自动重启)
ESP_ERROR_CHECK(motor.init());宏展开后大致是:
esp_err_t err=motor.init();if(err!=ESP_OK){// __FILE__/__LINE__:编译内置宏,编译时自动替换成"本文件名/当前行号"// (宏见 5.1)——所以它不用你手写,就能精确报出出错位置ESP_LOGE(TAG,"ESP_ERROR_CHECK failed: %s at %s:%d",esp_err_to_name(err),__FILE__,__LINE__);abort();// 主动"崩退"!不是安静退出,见下}abort 内部发生了什么(底层):打印错误信息 → 触发 panic(芯片的
崩溃应急机制)→ 转储寄存器和调用回溯(Backtrace,第 11 章教你翻译)→
默认配置下自动重启,开机日志从头再滚一遍。程序绝不会停在崩溃那一步
继续跑错误逻辑。所以:看到 Guru Meditation / “rebooting” 不等于板子坏了——
那正是程序"自杀重启"的标准流程;真正要读的是重启前的最后几行。
适合:启动阶段,出错了继续跑只会更糟。
姿势 3:ESP_RETURN_ON_ERROR(出错返回,程序继续活)
ESP_RETURN_ON_ERROR(motor.init(),TAG,"初始化失败");出错 → 打印日志 →return err(不崩不退,由上层决定怎么办)。
适合:出错后可以优雅降级继续的场合。
6.5 动手:让 DEBUG 日志亲手出现、再亲手让它消失
6.2 说过"低于阈值的日志编译期直接被删掉"。这一节就用你自己写的
程序验证这句话——不需要新电路,把 4.8 的点灯程序改三行。
第 1 步:给 4.8 程序加署名和两行日志(片段,接在 4.8 完整程序上,
不能单独编译——TAG那行放在app_main之前,两行ESP_LOG插在gpio_set_level(LED_PIN, 1);之后):
staticconstchar*TAG="blink";// 日志署名(它的作用见 6.3)ESP_LOGW(TAG,"马上点亮");// WARN:默认阈值下一定看得到ESP_LOGD(TAG,"点亮:DEBUG 细节行");// DEBUG:默认阈值下看不到【动手框】把 DEBUG 调出来,再调回去
① 在哪执行:4.8 点灯工程的目录里,用 2.3 打开的 IDF 终端
(VS Code 用户用它的 ESP-IDF 终端)。
② 敲什么:先idf.py build flash monitor(确认只看得到 W 那行);
再idf.py menuconfig,方向键进入Component config → Log output →
Default log level,回车选Debug log level,按s保存、q
退出;然后idf.py build flash monitor重来一遍——必须重新编译
并烧录:阈值是编译期裁进固件的(6.2),只重启监视器没有用。
③ 预期看到:第一次跑,串口每隔 1 秒滚出W (1055) blink: 马上点亮(D 行不见踪影);
改成 Debug 重烧后,每次亮灯变成两行:W (1055) blink: 马上点亮+D (1055) blink: 点亮:DEBUG 细节行。
再调回 Info 重烧一次,D 行消失——你亲手"编译掉"了它。
④ 没看到:① D 行不出现 → 重开 menuconfig 看*号是不是真停在
Debug 上;② 只改了配置没重编译烧录 →idf.py build flash monitor
整串重来;③ 一个日志都没有或满屏乱码 → 回 2.11 查端口和波特率。
退出 monitor 按 Ctrl+]。
6.6 真实案例:三层日志还原事故(本书实际踩过)
固件烧录成功(Calling app_main()已出现),但电机初始化失败。
下面这段是修复前的历史日志——括号里的init(80)是当时代码版本的
行号,现在的motor_control.cpp行号已经不同,对照时别被行号绕晕。
这个坑本书后来已经修好,现在照抄本书代码无法复现这段日志——
这一节的目的不是让你制造事故,而是先学会"读法"(第 11 章教翻译工具):
E (749) ledc: Fade service not installed, call ledc_fade_func_install E (759) ledc: ledc_set_duty_and_update(1650): LEDC fade channel init error, not enough memory or service not installed E (769) DCMotor: init(80): 占空比初始化失败 E (779) l298n: 电机初始化失败: ESP_FAIL,程序终止逐层解读:
| 行 | 谁打的 | 告诉我们 |
|---|---|---|
| 1 | ledc 驱动 | 根因:fade 服务没装 |
| 2 | ledc 驱动 | 具体函数返回了错误 |
| 3 | DCMotor | 我们的封装层收到错误并上报 |
| 4 | main | 最上层决定:程序终止 |
这就是日志的价值:从驱动层到应用层,把"哪一步、为什么失败"完整还原。
修复方法在第 7 章(先配置 timer/channel 再装 fade 服务)。
另外记住:中断服务函数里不能打日志(printf 太慢且非中断安全),
原因与替代方案见第 15 章。
6.7 日志与错误处理的选择表
| 场景 | 用 |
|---|---|
| 正常流程汇报 | ESP_LOGI |
| 可疑但继续 | ESP_LOGW |
| 必须停机 | ESP_LOGE + ESP_ERROR_CHECK |
| 可降级继续 | ESP_RETURN_ON_ERROR / 手动检查 |
| 排查细节 | ESP_LOGD(用完调回) |
6.8 常见问题速查
| 现象 | 原因 | 解决 |
|---|---|---|
| 日志一个不显示 | 串口没连对/波特率错 | 检查 COM、115200 |
| 乱码 | 波特率不一致 | 统一波特率 |
| %d 打出巨大数 | 类型不匹配 | 检查格式符与实参 |
| DEBUG 看不到 | 阈值是 INFO | 调 CONFIG_LOG_DEFAULT_LEVEL(操作见 6.5) |
| ESP_ERROR_CHECK 打出 Guru Meditation 后重启 | 确实出错:panic 默认自动重启(6.4 姿势 2) | 读重启前最后几行日志找根因,板子没坏 |
6.9 想一想 + 小测验
想一想:
- 为什么波特率不一致会乱码?(从帧格式角度回答)
ESP_ERROR_CHECK和ESP_RETURN_ON_ERROR的本质区别是什么?- 为什么驱动层的日志比应用层日志更能揭示根因?
小测验:
- 日志
I (779) l298n: 电机初始化失败: ESP_FAIL,程序终止中I是什么等级? ESP_OK的值是多少?(A. 0 B. 1 C. -1)- 想输出"占空比 50%",格式串怎么写?
ESP_ERROR_CHECK出错时内部会调用什么函数?(A. abort B. reboot C. exit)
6.10 本章总结
- 日志 = 字符 → UART 帧 → 电脑显示;波特率一致才不乱码;
- 五级:E/W/I/D/V,编译期按阈值裁剪,DEBUG 用完关掉(6.5 亲手试过);
- esp_err_t = int:0 成功,负数失败,
esp_err_to_name翻译; - ESP_ERROR_CHECK = 出错打印 + abort:panic 后默认配置自动重启,
不是死机不动(适合启动必停场景); - 看日志要读完整链路:驱动层根因 → 封装层 → 应用层。
下一章,主角登场:PWM 和 LEDC——让引脚输出可调速的信号。