3、脚本化烧录:使用Python脚本实现一键烧录与校验

说实话,很多工程师做了好几年嵌入式,烧录固件还是老一套——打开IDE,点下载,等进度条跑完,然后手动重启看灯闪没闪。一次两次还行,一天烧几十次板子,手都点酸了。

我个人习惯是:凡是重复超过三次的操作,必须用脚本干掉。烧录这件事,太适合自动化了。今天我们就聊聊怎么用Python写一个靠谱的一键烧录脚本,顺便把校验也带上。

核心目标:一条命令完成“编译→烧录→校验→日志归档”全流程,中间不需要人盯着。

3.1 烧录脚本的基本架构

先搭个架子。一个成熟的烧录脚本,至少需要四个模块:

模块 职责 我常用的工具/库
参数解析 接收目标芯片、固件路径、烧录器类型等 argparse
烧录引擎 调用底层烧录工具(OpenOCD、JLink、ST-Link CLI) subprocess
校验模块 读回Flash内容,与原始固件比对 hashlib + 读寄存器
日志与状态 记录每次烧录结果,方便追溯 logging + JSON

你想想看,这四个模块拆开写,每个都不复杂。但合在一起,就是一个能跑在生产环境里的自动化工具。

3.2 核心实现:调用烧录工具

底层烧录工具通常都提供命令行接口。以STM32 + OpenOCD为例,烧录一条命令就够了:

openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
        -c "program firmware.bin 0x08000000 verify reset exit"

Python里用subprocess.run()包一下:

import subprocess
import sys

def flash_openocd(firmware_path, chip_config):
    cmd = [
        "openocd",
        "-f", f"interface/{chip_config['interface']}.cfg",
        "-f", f"target/{chip_config['target']}.cfg",
        "-c", f"program {firmware_path} {chip_config['base_addr']} verify reset exit"
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
    if result.returncode != 0:
        raise RuntimeError(f"烧录失败: {result.stderr}")
    return result.stdout

小技巧:timeout=60 一定要加。我曾经遇到过烧录器接触不良,脚本卡在那里半小时没反应。设个超时,至少能快速失败。

3.3 校验:不只是读回来比一下

很多人觉得校验就是“读Flash vs 原始bin”,其实没那么简单。我遇到过三种情况:

  1. 全片校验:读回全部Flash内容,逐字节比对。最可靠,但慢。
  2. CRC校验:芯片硬件支持CRC计算,读回CRC值比对。快,但依赖芯片。
  3. 关键段校验:只校验向量表、启动代码等关键区域。折中方案。

我个人习惯用全片校验 + 快速CRC双重确认。脚本里这样写:

import hashlib

def verify_flash(firmware_path, readback_path):
    with open(firmware_path, "rb") as f:
        original = f.read()
    with open(readback_path, "rb") as f:
        readback = f.read()

    if len(original) != len(readback):
        return False

    orig_hash = hashlib.sha256(original).hexdigest()
    read_hash = hashlib.sha256(readback).hexdigest()

    return orig_hash == read_hash

注意:读回Flash时,要确保读回的地址范围和长度与固件完全一致。偏移一个字节,校验必失败。我曾经踩过这个坑,排查了半天才发现是读回时地址写错了。

3.4 一键烧录的完整流程

把上面这些拼起来,就是一条命令搞定的事。我画了个流程图,你看看:

一键烧录与校验流程图 解析参数 检查固件完整性 执行烧录 读回Flash内容 SHA256校验 ✅ 成功 → 归档日志 ❌ 失败 → 告警重试 校验失败时自动重试1次,仍失败则退出并保留现场日志

嗯,这里要注意:校验失败不要直接放弃。我一般让脚本自动重试一次,因为有时候是烧录器接触问题,重插一下就好了。如果重试还失败,再报错。

3.5 实战:一个完整的烧录脚本骨架

下面这个脚本,我项目里一直在用。你改改芯片参数就能跑:

#!/usr/bin/env python3
"""一键烧录脚本 - 支持校验与重试"""

import argparse
import subprocess
import hashlib
import logging
import json
from pathlib import Path

logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("flasher")

def parse_args():
    parser = argparse.ArgumentParser(description="一键烧录工具")
    parser.add_argument("firmware", type=Path, help="固件路径 (.bin)")
    parser.add_argument("--chip", default="stm32f4", help="芯片型号")
    parser.add_argument("--retry", type=int, default=1, help="失败重试次数")
    return parser.parse_args()

def flash(fw_path, chip):
    # 这里调用实际烧录命令
    log.info(f"烧录 {fw_path} 到 {chip}...")
    # 示例:subprocess.run(["openocd", ...], check=True)
    return True

def verify(fw_path, readback_path):
    if not readback_path.exists():
        return False
    fw_hash = hashlib.sha256(fw_path.read_bytes()).hexdigest()
    rb_hash = hashlib.sha256(readback_path.read_bytes()).hexdigest()
    return fw_hash == rb_hash

def main():
    args = parse_args()
    if not args.firmware.exists():
        log.error("固件文件不存在")
        return 1

    for attempt in range(args.retry + 1):
        try:
            flash(args.firmware, args.chip)
            # 假设读回文件为 readback.bin
            if verify(args.firmware, Path("readback.bin")):
                log.info("✅ 烧录成功,校验通过")
                return 0
            else:
                log.warning("⚠️ 校验失败")
        except Exception as e:
            log.error(f"烧录异常: {e}")

        if attempt < args.retry:
            log.info(f"重试第 {attempt+1} 次...")

    log.error("❌ 烧录失败,请检查硬件连接")
    return 1

if __name__ == "__main__":
    exit(main())

避坑指南:我曾经在生产线上遇到过一个问题——固件路径里有中文,OpenOCD直接报错。后来我统一用Path.resolve()转成绝对路径,再也不用担心编码问题。

3.6 多芯片支持与配置管理

项目多了,不可能每个芯片写一个脚本。我习惯用JSON配置文件管理:

{
  "stm32f4": {
    "interface": "stlink",
    "target": "stm32f4x",
    "base_addr": "0x08000000",
    "flash_size": "1M",
    "verify_method": "sha256"
  },
  "nrf52840": {
    "interface": "jlink",
    "target": "nrf52840",
    "base_addr": "0x00000000",
    "flash_size": "1M",
    "verify_method": "crc"
  },
  "esp32": {
    "interface": "esptool",
    "target": "esp32",
    "base_addr": "0x10000",
    "flash_size": "4M",
    "verify_method": "sha256"
  }
}

脚本启动时读这个配置,--chip参数一传,自动匹配烧录参数。说白了,这就是把硬编码变成了可配置,维护成本直线下降。

3.7 日志与追溯

每次烧录的结果,我建议都记下来。格式很简单:

{
  "timestamp": "2025-03-15T14:32:10",
  "firmware": "app_v2.1.bin",
  "chip": "stm32f4",
  "status": "success",
  "hash": "a1b2c3d4...",
  "duration_sec": 12.5
}

为什么要记?因为出问题的时候,你能回溯——是哪次烧录、哪个版本、哪个芯片。我在客户现场排查问题时,靠这个日志半小时就定位到了问题,而不是靠猜。

总结一下:脚本化烧录的核心就三件事——调工具、做校验、记日志。把这三件事用Python串起来,你就拥有了一条可靠的一键烧录流水线。下次烧录的时候,喝杯咖啡,让脚本干活。