4. 脚本调试与日志:日志系统设计、断点调试技巧、异常捕获与处理、性能分析

烧录脚本跑起来容易,但跑得稳、跑得快,那是另一回事。我见过太多同事,脚本写了两百行,一跑就崩,然后对着黑乎乎的终端窗口发呆。说实话,调试和日志这块,才是区分「能用」和「好用」的分水岭。

这一章,我们就把烧录脚本的调试体系搭起来。从日志怎么设计,到断点怎么打,再到异常怎么抓、性能怎么分析,一条线走通。

4.1 日志系统设计:别让信息石沉大海

日志不是简单的 print。你想想看,烧录一次可能要几分钟,中间出了错,你总不能重跑一遍盯着屏幕看吧?日志系统要解决三个问题:记什么、记多细、记到哪。

4.1.1 日志分级

我个人习惯分五级,够用又不啰嗦:

级别 关键字 使用场景
DEBUG [DBG] 变量值、寄存器读写、流程分支选择
INFO [INF] 烧录开始、校验通过、步骤完成
WARN [WRN] 校验和轻微偏差、重试次数接近上限
ERROR [ERR] 通信超时、校验失败、设备断开
FATAL [FTL] 脚本崩溃、硬件不可恢复故障
我的习惯:生产环境只开 INFO 及以上。DEBUG 日志太多,会拖慢烧录速度。调试时才开 DEBUG,跑完一轮关掉。

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 天,避免磁盘撑爆。

避坑:我曾经遇到过日志文件写满磁盘,导致烧录脚本直接崩溃。后来加了个检查:每次写日志前,先看磁盘剩余空间,低于 50MB 就自动清理旧日志。

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,就能远程打断点
要点:远程调试时,代码版本必须一致。我踩过坑:工控机上的脚本是 v2.1,我本地是 v2.2,断点行号对不上,调试了个寂寞。

4.3 异常捕获与处理:别让脚本裸奔

烧录脚本最怕什么?最怕跑到一半,啪,异常了,连个错误信息都没留下。异常捕获不是简单的 try-except,要分场景处理。

4.3.1 分层捕获

我习惯分三层:

  1. 底层捕获:硬件通信层。捕获超时、CRC 错误、设备断开。重试 3 次,不行就抛上层。
  2. 业务层捕获:烧录流程层。捕获校验失败、地址越界。记录日志,决定是否跳过或终止。
  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)
我的经验:异常信息一定要包含上下文。别只写 "Error: timeout",要写 "Error: timeout when erasing sector 0x0800C000, retry 2/3"。这样看日志的人一眼就知道问题在哪。

4.3.2 异常恢复

有些异常是可以恢复的。比如通信超时,重连一下就好。我设计了一个恢复机制:

异常类型 恢复策略 最大重试次数
通信超时 重新初始化通信接口 3
校验和错误 重新读取并比对 2
设备断开 等待设备重新枚举 5
写入失败 擦除后重写 1
注意:重试不是万能的。如果连续重试都失败,说明问题不是偶发的。我一般设个全局重试计数器,超过阈值就直接报 FATAL,避免死循环。

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 本章知识体系

下面这张图,把本章的核心内容串起来了。你可以把它当作调试体系的「地图」:

脚本调试与日志知识体系 脚本调试与日志知识体系 日志系统设计 断点调试技巧 异常捕获与处理 性能分析 调试体系 日志分级 (DBG/INF/WRN/ERR/FTL) 格式: 时间戳 + 级别 + 模块 + 消息 输出: 控制台 + 文件双通道 条件断点 (IDE / pdb) 远程调试 (debugpy) 代码版本一致性 分层捕获 (底层/业务/顶层) 异常恢复与重试策略 上下文信息 + 崩溃转储 打点计时 (装饰器) 火焰图 (py-spy) 内存 / I/O 监控 核心原则:日志留痕、断点定位、异常兜底、性能量化 四者配合,才能让烧录脚本从「能跑」进化到「可靠」

调试和日志,说白了就是给脚本装上一双眼睛和一副听诊器。眼睛看日志,听诊器听性能。两者都到位了,脚本出问题你就能快速定位,而不是靠猜。