4. 脚本调试与日志:日志系统设计、断点调试技巧、异常捕获与处理、性能分析
烧录脚本跑起来容易,但跑得稳、跑得快,那是另一回事。我见过太多同事,脚本写了两百行,一跑就崩,然后对着黑乎乎的终端窗口发呆。说实话,调试和日志这块,才是区分「能用」和「好用」的分水岭。
这一章,我们就把烧录脚本的调试体系搭起来。从日志怎么设计,到断点怎么打,再到异常怎么抓、性能怎么分析,一条线走通。
4.1 日志系统设计:别让信息石沉大海
日志不是简单的 print。你想想看,烧录一次可能要几分钟,中间出了错,你总不能重跑一遍盯着屏幕看吧?日志系统要解决三个问题:记什么、记多细、记到哪。
4.1.1 日志分级
我个人习惯分五级,够用又不啰嗦:
| 级别 | 关键字 | 使用场景 |
|---|---|---|
| DEBUG | [DBG] | 变量值、寄存器读写、流程分支选择 |
| INFO | [INF] | 烧录开始、校验通过、步骤完成 |
| WARN | [WRN] | 校验和轻微偏差、重试次数接近上限 |
| ERROR | [ERR] | 通信超时、校验失败、设备断开 |
| FATAL | [FTL] | 脚本崩溃、硬件不可恢复故障 |
4.1.2 日志格式与输出
每条日志最好包含时间戳、级别、模块名、消息。我常用的格式:
[2025-03-18 14:32:17.456] [INF] [FlashWriter] 开始擦除扇区 0x0800C000
[2025-03-18 14:32:17.789] [DBG] [FlashWriter] 发送擦除命令: 0x20 0x00 0xC0 0x08
[2025-03-18 14:32:18.012] [ERR] [FlashWriter] 擦除超时,重试第 1 次
输出目标我建议双通道:控制台 + 文件。控制台实时看进度,文件留底备查。文件日志按天滚动,保留最近 7 天,避免磁盘撑爆。
4.2 断点调试技巧:让脚本停下来等你
日志是事后分析,断点是现场抓人。烧录脚本跑在 PC 上,调试手段比嵌入式端丰富得多。但很多人只会用 print 大法,其实有更高效的方式。
4.2.1 条件断点
不是所有地方都需要停。比如循环烧录 1000 片芯片,你只关心第 500 片出问题那次。条件断点就派上用场了:
# Python 示例:使用 pdb 的条件断点
import pdb
for chip_id in range(1000):
# 只在 chip_id == 500 时触发断点
if chip_id == 500:
pdb.set_trace()
result = flash_chip(chip_id)
log_result(chip_id, result)
我个人更推荐用 IDE 的断点功能。VS Code 或 PyCharm 里,右键断点就能设条件,不用改代码。调试完直接删断点,干净利落。
4.2.2 远程调试
有时候烧录脚本跑在工控机上,你坐在办公室。这时候远程调试就香了。Python 的 pydevd 或 debugpy 都能搞定:
# 工控机上运行
import debugpy
debugpy.listen(("0.0.0.0", 5678))
debugpy.wait_for_client() # 等待 IDE 连接
# 你的 IDE 连接工控机的 IP:5678,就能远程打断点
4.3 异常捕获与处理:别让脚本裸奔
烧录脚本最怕什么?最怕跑到一半,啪,异常了,连个错误信息都没留下。异常捕获不是简单的 try-except,要分场景处理。
4.3.1 分层捕获
我习惯分三层:
- 底层捕获:硬件通信层。捕获超时、CRC 错误、设备断开。重试 3 次,不行就抛上层。
- 业务层捕获:烧录流程层。捕获校验失败、地址越界。记录日志,决定是否跳过或终止。
- 顶层捕获:脚本入口。兜底,捕获所有未处理异常。保证至少能输出错误信息和堆栈。
def flash_chip():
try:
# 底层:硬件通信
connect_device()
try:
# 业务层:烧录流程
erase_flash()
write_flash()
verify_flash()
except VerifyError as e:
log_error(f"校验失败: {e}")
# 根据策略决定重试还是跳过
if retry_count < 3:
retry()
else:
raise
except Exception as e:
# 顶层兜底
log_fatal(f"未捕获异常: {e}")
save_crash_dump()
sys.exit(1)
4.3.2 异常恢复
有些异常是可以恢复的。比如通信超时,重连一下就好。我设计了一个恢复机制:
| 异常类型 | 恢复策略 | 最大重试次数 |
|---|---|---|
| 通信超时 | 重新初始化通信接口 | 3 |
| 校验和错误 | 重新读取并比对 | 2 |
| 设备断开 | 等待设备重新枚举 | 5 |
| 写入失败 | 擦除后重写 | 1 |
4.4 性能分析:找到烧录的瓶颈
烧录脚本慢,有时候不是硬件慢,是脚本写得烂。我见过一个脚本,每次写数据前都重新打开文件,结果 90% 的时间花在文件 I/O 上。性能分析就是帮你找到这种「假慢」。
4.4.1 打点计时
最简单的性能分析,就是自己打点计时。我常用一个装饰器:
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
log_debug(f"{func.__name__} 耗时: {elapsed:.3f}s")
return result
return wrapper
@timer
def erase_flash():
# 擦除逻辑
pass
@timer
def write_flash():
# 写入逻辑
pass
跑完一轮,看日志就能知道哪个函数最慢。我一般把耗时超过总时间 10% 的函数列为优化对象。
4.4.2 火焰图分析
如果打点计时不够细,可以用火焰图。Python 的 py-spy 工具很好用,不用改代码:
# 在另一个终端运行
py-spy record -o flame.svg --pid <烧录脚本的PID> --duration 30
生成的 SVG 火焰图,横轴是时间,纵轴是调用栈。哪个函数占的「火焰」最宽,哪个就是瓶颈。我在项目中用这招抓出过一个隐藏很深的 time.sleep(0.1),它在循环里被调了 1000 次,白白浪费了 100 秒。
4.4.3 内存与 I/O 监控
烧录脚本如果内存泄漏,跑久了会越来越慢。我习惯在关键节点打印内存占用:
import psutil
import os
def log_memory_usage(tag=""):
process = psutil.Process(os.getpid())
mem_mb = process.memory_info().rss / 1024 / 1024
log_debug(f"[{tag}] 内存占用: {mem_mb:.1f} MB")
I/O 监控也一样。如果发现磁盘读写频繁,考虑用内存缓存或批量写入。烧录脚本的 I/O 瓶颈,多半是日志写太勤或者文件读太碎。
4.5 本章知识体系
下面这张图,把本章的核心内容串起来了。你可以把它当作调试体系的「地图」:
调试和日志,说白了就是给脚本装上一双眼睛和一副听诊器。眼睛看日志,听诊器听性能。两者都到位了,脚本出问题你就能快速定位,而不是靠猜。