#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 一键构建 PY32F040 开发框架 OTA 固件(两个独立工程:Bootloader + App): 1) Bootloader (0x08000000, 17KB 内,行为由 Shared/ota_config.h 宏决定) 2) App (0x08003000, 运行区,单份固件,三模式通用) 三模式(Shared/ota_config.h 的 OTA_BACKUP_MODE / OTA_SWAP_STRATEGY)均只需一份 App: · 单备份(SINGLE) :主区运行,BAK 下载→拷贝 · 双备份 + RAM 交换 :A 区运行,B 区备份,RAM 缓冲交换 · 双备份 + 暂存区交换 :A 区运行,B 区备份,scratch 交换 ★ 工程已解耦为两个互独立工程 + 顶层共享层: - App/ : APP 工程(引用顶层共享层) - Bootloader/ : Bootloader 工程(引用顶层共享层) - Shared/ : OTA 契约单一真源(ota_config.h / flash.c / crc32.c,h / log.h) - Drivers/ : 厂商 HAL/CMSIS(两工程共用,只读) 两个 .uvprojx 通过 ..\\..\\Shared / ..\\..\\Drivers 引用顶层共享层, 彼此零依赖;发给别人用 tools/pack_*.bat(zip 内含工程+Drivers+Shared)。 产物输出到 <框架>/Output/ : bootloader.hex / bootloader.bin app.hex / app.bin (运行区固件,链接 0x08003000) ★ 关键: Keil 命令行构建的链接地址由 Target 对话框 ROM1 () 决定, 而非散列文件(ScatterFile)。因此切换链接地址直接改写 OCR_RVCT4 / Cpu / TextAddressRange。 """ import os import re import io import sys import time import zlib import shutil import subprocess def find_keil(): """自动探测 Keil MDK 安装目录(含 UV4.exe)。 顺序:注册表 HKLM 两处 -> 环境变量 KEIL_MDK_ROOT -> 常见路径 -> 原硬编码兜底。 返回 (keil_root, uv4_path);找不到返回 (None, None)。 """ roots = [] try: import winreg for hive, key in ( (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Keil\Products\MDK"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Keil\Products\MDK"), ): try: with winreg.OpenKey(hive, key) as k: p, _ = winreg.QueryValueEx(k, "Path") if p: roots.append(p) except OSError: pass except ImportError: pass roots += [os.environ.get("KEIL_MDK_ROOT", ""), r"E:\Software\Keil_v537", r"C:\Keil_v5", r"C:\Keil"] for r in roots: r = (r or "").strip().rstrip("\\/") if not r: continue uv4 = os.path.join(r, "UV4", "UV4.exe") if os.path.isfile(uv4): return r, uv4 return None, None def find_fromelf(keil_root): """探测 fromelf.exe(不同版本 Keil 安装位置不同)。""" if not keil_root: return None for sub in (r"ARM\ARMCLANG\bin\fromelf.exe", r"ARM\ARM_Compiler_5.06u7\bin\fromelf.exe", r"ARM\ARMCC\bin\fromelf.exe"): p = os.path.join(keil_root, sub) if os.path.isfile(p): return p return None KEIL_ROOT, KEIL_UV4 = find_keil() FROMELF = find_fromelf(KEIL_ROOT) if not KEIL_UV4: print("[ERR] 未找到 Keil (UV4.exe)。请安装 Keil MDK,或设置环境变量 KEIL_MDK_ROOT 指向 Keil 安装目录后重试。") sys.exit(1) if not FROMELF: print("[WARN] 未找到 fromelf.exe,将跳过 .bin 生成(仅输出 .hex)") else: print("[OK] Keil: %s" % KEIL_ROOT) # ROOT = 本脚本所在目录的上一级(框架根) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) APP_UVPROJX = os.path.join(ROOT, "App", "MDK-ARM", "project.uvprojx") BL_UVPROJX = os.path.join(ROOT, "Bootloader", "MDK-ARM", "Bootloader.uvprojx") APP_OBJ_DIR = os.path.join(ROOT, "App", "MDK-ARM", "Objects") BL_OBJ_DIR = os.path.join(ROOT, "Bootloader", "MDK-ARM", "Output") OUT_DIR = os.path.join(ROOT, "Output") # 运行区链接配置(三模式统一):OCR_RVCT4 Start/Size, Cpu IROM, TextAddressRange # ★ 必须与 Shared/ota_config.h 的 OTA_RUN_ADDR_BASE(0x08003000) 保持一致! # Size 取两模式最小槽 57K(0xE400):SINGLE(58K)/AB(57K) 均不侵入 BAK 区。 # 曾误设为 0x08004800 → 固件按 0x4800 链接而 Bootloader 跳 0x3000 执行,必崩。 SLOT_RUN = ("0x8003000", "0xe400", "IROM(0x08003000,0x0000E400)", "0x08003000") def read_text(path): with io.open(path, "r", encoding="utf-8-sig") as f: return f.read() def write_text(path, text): with io.open(path, "w", encoding="utf-8", newline="\r\n") as f: f.write(text) def wait_uv4_exit(timeout_s=25): deadline = time.time() + timeout_s while time.time() < deadline: q = subprocess.run( ["powershell", "-NoProfile", "-Command", "if (Get-Process UV4 -ErrorAction SilentlyContinue) { 'RUN' } else { 'DONE' }"], capture_output=True, text=True) if "DONE" in q.stdout: return True time.sleep(1) return False def set_run_slot(uvprojx): """把 App 工程链接地址设为运行区 0x08003000 / 57KB(与 ota_config.h 契约一致)""" start, size, cpu_irom, text_range = SLOT_RUN t = read_text(uvprojx) # 1) Target 对话框 ROM1 (OCR_RVCT4) —— 命令行构建实际生效的配置 t2, n = re.subn( r"(\s*1\s*)0x[0-9A-Fa-f]+(\s*)0x[0-9A-Fa-f]+()", lambda m: m.group(1) + start + m.group(2) + size + m.group(3), t, count=1) if n == 0: print("[ERR] OCR_RVCT4 未找到: %s" % uvprojx) sys.exit(1) # 2) Cpu 行 IROM(保持一致) t2 = re.sub(r"IROM\(0x[0-9A-Fa-f]+,0x[0-9A-Fa-f]+\)", cpu_irom, t2, count=1) # 3) LDads TextAddressRange(保持一致) t2 = re.sub(r".*?", "%s" % text_range, t2, count=1) write_text(uvprojx, t2) print("[set] App link @ %s (size %s)" % (text_range, size)) def build(uvprojx, tag): log = os.path.join(OUT_DIR, "build_%s_log.txt" % tag) # 用 -r 强制重建:确保链接配置(地址)变更一定生效 r = subprocess.run([KEIL_UV4, "-j0", "-r", uvprojx, "-o", log]) wait_uv4_exit() print("[%s] UV4 exit=%d" % (tag, r.returncode)) with io.open(log, "r", encoding="utf-8", errors="ignore") as f: content = f.read() errs = [l for l in content.splitlines() if "Error:" in l] if errs: print("[ERR] %s 编译错误:" % tag) for e in errs[:20]: print(" " + e) return False warns = [l for l in content.splitlines() if "warning" in l.lower() and "0 warning" not in l.lower()] for w in warns[:10]: print("[WARN] " + w) return True def collect_artifacts(hex_path, name): if not os.path.exists(hex_path): print("[ERR] 未找到 %s" % hex_path) return False shutil.copy(hex_path, os.path.join(OUT_DIR, name + ".hex")) axf = hex_path.replace(".hex", ".axf") if os.path.exists(axf) and os.path.exists(FROMELF): subprocess.run([FROMELF, "--bin", "--output=%s" % os.path.join(OUT_DIR, name + ".bin"), axf]) print("[OK] 产物: %s.hex / %s.bin" % (name, name)) return True def main(): os.makedirs(OUT_DIR, exist_ok=True) # 1. Bootloader(默认 0x08000000 链接,行为由 Shared/ota_config.h 宏决定) print("=== 1/2 编译 Bootloader ===") if not build(BL_UVPROJX, "bl"): sys.exit(1) collect_artifacts(os.path.join(BL_OBJ_DIR, "Project.hex"), "bootloader") # 2. App(运行区 0x08003000,单份固件,三模式通用) print("=== 2/2 编译 App (0x08003000) ===") set_run_slot(APP_UVPROJX) if not build(APP_UVPROJX, "a"): sys.exit(1) collect_artifacts(os.path.join(APP_OBJ_DIR, "Project.hex"), "app") # 3. 打印固件 CRC(0x30 下发 fwCrc32 用,CRC-32/IEEE,与 MCU crc32.c 一致) app_bin = os.path.join(OUT_DIR, "app.bin") if os.path.exists(app_bin): with open(app_bin, "rb") as f: data = f.read() crc = zlib.crc32(data) & 0xFFFFFFFF print("[OK] 文件: %s" % app_bin) print(" 固件长度 fwLength : %u (0x%X)" % (len(data), len(data))) print(" CRC32 fwCrc32 : %08X" % crc) print(" 大端下发字节 : %02X %02X %02X %02X" % ( (crc >> 24) & 0xFF, (crc >> 16) & 0xFF, (crc >> 8) & 0xFF, crc & 0xFF)) print("\n[DONE] 全部构建完成,产物在: %s" % OUT_DIR) print(" 出厂烧录: bootloader.hex (0x08000000) + app.hex (0x08003000)") print(" OTA 下发: 每次升级上传同一份 app.bin,BLE 原协议流程不变(0x30/0x32/0x34)") print(" 固件 CRC: 上方 fwCrc32 即为 0x30 请求携带值") print(" 模式切换: 修改 Shared/ota_config.h 的 OTA_BACKUP_MODE / OTA_SWAP_STRATEGY 后重新构建") print(" 单独发包: 运行 tools/pack_bootloader.bat 或 tools/pack_app.bat 生成可发送压缩包") if __name__ == "__main__": main()