☰
第 6 章 日志与错误处理:串口输出、错误码与断言机制
2026/9/25 6:00:42 网站建设 项目流程

本章从底层讲清:
①日志是怎么通过串口"喊"出来的(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_LOGEERROR (E)出错,程序可能无法继续致命错误
ESP_LOGWWARN (W)有问题但还能跑边界情况
ESP_LOGIINFO (I)正常进度主要流程
ESP_LOGDDEBUG (D)调试细节排查时开
ESP_LOGVVERBOSE (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)。
作用:

  1. 一眼看出这条日志是谁打的(哪一层);
  2. 可以按 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,程序终止

逐层解读:

行谁打的告诉我们
1ledc 驱动根因:fade 服务没装
2ledc 驱动具体函数返回了错误
3DCMotor我们的封装层收到错误并上报
4main最上层决定:程序终止

这就是日志的价值:从驱动层到应用层,把"哪一步、为什么失败"完整还原。
修复方法在第 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 想一想 + 小测验

想一想:

  1. 为什么波特率不一致会乱码?(从帧格式角度回答)
  2. ESP_ERROR_CHECK和ESP_RETURN_ON_ERROR的本质区别是什么?
  3. 为什么驱动层的日志比应用层日志更能揭示根因?

小测验:

  1. 日志I (779) l298n: 电机初始化失败: ESP_FAIL,程序终止中I是什么等级?
  2. ESP_OK的值是多少?(A. 0 B. 1 C. -1)
  3. 想输出"占空比 50%",格式串怎么写?
  4. 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——让引脚输出可调速的信号。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询