添加普冉 PY32F040 OTA 双工程代码生成模板
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
#!/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 (<OCR_RVCT4>) 决定,
|
||||
而非散列文件(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"(<OCR_RVCT4>\s*<Type>1</Type>\s*<StartAddress>)0x[0-9A-Fa-f]+(</StartAddress>\s*<Size>)0x[0-9A-Fa-f]+(</Size>)",
|
||||
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"<TextAddressRange>.*?</TextAddressRange>",
|
||||
"<TextAddressRange>%s</TextAddressRange>" % 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()
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
合并 bootloader.hex + app.hex 为出厂烧录用的 merged_single.hex。
|
||||
用法:
|
||||
python tools/merge_hex.py [--out OUT] [--bl bootloader.hex] [--app app.hex]
|
||||
默认输入/输出均位于 PY32F040开发框架/Output/ 下。
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 默认相对本脚本: ../Output (脚本位于 框架/tools/)
|
||||
_DEF_OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"Output")
|
||||
|
||||
|
||||
def parse_hex(path):
|
||||
"""解析 Intel HEX -> dict[abs_addr] = byte。完整支持 0x04 扩展线性地址记录,
|
||||
得到 32 位绝对地址。本工程基址 0x08000000 即由 0x04 记录(:020000040800F2)给出。"""
|
||||
mem = {}
|
||||
upper = 0 # 0x04 记录的扩展线性地址(左移到高 16 位)
|
||||
with open(path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line.startswith(":"):
|
||||
continue
|
||||
n = int(line[1:3], 16)
|
||||
addr = int(line[3:7], 16)
|
||||
rec = int(line[7:9], 16)
|
||||
if rec == 0x00:
|
||||
data = bytes.fromhex(line[9:9 + n * 2])
|
||||
base = (upper << 16) | addr
|
||||
for i, b in enumerate(data):
|
||||
mem[base + i] = b
|
||||
elif rec == 0x01: # EOF
|
||||
break
|
||||
elif rec == 0x04: # 扩展线性地址
|
||||
upper = int(line[9:13], 16)
|
||||
# 0x02/0x03/0x05 等本工程不涉及,忽略
|
||||
return mem
|
||||
|
||||
|
||||
def _emit_ela(upper):
|
||||
"""0x04 扩展线性地址记录。"""
|
||||
body = "02000004%04X" % upper
|
||||
cs = 0
|
||||
for i in range(0, len(body), 2):
|
||||
cs += int(body[i:i + 2], 16)
|
||||
cs = (0x100 - (cs & 0xFF)) & 0xFF
|
||||
return ":%s%02X" % (body, cs)
|
||||
|
||||
|
||||
def _emit(buf, addr):
|
||||
body = "%02X%04X00" % (len(buf), addr) + "".join("%02X" % b for b in buf)
|
||||
cs = 0
|
||||
for i in range(0, len(body), 2):
|
||||
cs += int(body[i:i + 2], 16)
|
||||
cs = (0x100 - (cs & 0xFF)) & 0xFF
|
||||
return ":%s%02X" % (body, cs)
|
||||
|
||||
|
||||
def write_hex(path, mem):
|
||||
"""按 32 位绝对地址写出;每当高 16 位(upper)变化时插入 0x04 扩展记录,
|
||||
保证 0x08000000 / 0x08004800 等基址被烧录器正确识别。"""
|
||||
addrs = sorted(mem)
|
||||
lines = []
|
||||
buf = []
|
||||
cur_upper = None
|
||||
cur = None
|
||||
next_addr = None
|
||||
|
||||
def flush():
|
||||
if buf:
|
||||
lines.append(_emit(buf, cur))
|
||||
buf.clear()
|
||||
|
||||
for a in addrs:
|
||||
up = (a >> 16) & 0xFFFF
|
||||
off = a & 0xFFFF
|
||||
if cur_upper is None:
|
||||
cur_upper = up
|
||||
lines.append(_emit_ela(up))
|
||||
cur = off
|
||||
next_addr = a + 1
|
||||
elif up != cur_upper:
|
||||
flush()
|
||||
cur_upper = up
|
||||
lines.append(_emit_ela(up))
|
||||
cur = off
|
||||
next_addr = a + 1
|
||||
elif a != next_addr:
|
||||
flush()
|
||||
cur = off
|
||||
next_addr = a + 1
|
||||
buf.append(mem[a])
|
||||
next_addr = a + 1
|
||||
if len(buf) == 16:
|
||||
flush()
|
||||
cur = next_addr & 0xFFFF
|
||||
flush()
|
||||
lines.append(":00000001FF")
|
||||
with open(path, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--bl", default=os.path.join(_DEF_OUT_DIR, "bootloader.hex"))
|
||||
ap.add_argument("--app", default=os.path.join(_DEF_OUT_DIR, "app.hex"))
|
||||
ap.add_argument("--out", default=os.path.join(_DEF_OUT_DIR, "merged_single.hex"))
|
||||
args = ap.parse_args()
|
||||
|
||||
for p in (args.bl, args.app):
|
||||
if not os.path.isfile(p):
|
||||
sys.exit("ERROR: 找不到 %s" % p)
|
||||
|
||||
bl = parse_hex(args.bl)
|
||||
app = parse_hex(args.app)
|
||||
print("bootloader: %d bytes, range 0x%X-0x%X" % (len(bl), min(bl), max(bl)))
|
||||
print("app: %d bytes, range 0x%X-0x%X" % (len(app), min(app), max(app)))
|
||||
|
||||
overlap = set(bl) & set(app)
|
||||
if overlap:
|
||||
sys.exit("ERROR: 地址重叠 %d 处 (例如 0x%X)" % (len(overlap), min(overlap)))
|
||||
|
||||
merged = dict(bl)
|
||||
merged.update(app)
|
||||
write_hex(args.out, merged)
|
||||
nlines = sum(1 for _ in open(args.out))
|
||||
print("merged: %d bytes -> %s (%d 行)" % (len(merged), args.out, nlines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Pack a project (Bootloader/ or App/) together with the shared layers
|
||||
(top-level Drivers/ + Shared/) into a sendable zip.
|
||||
|
||||
Structure change (2026-08-18): Drivers/ and Shared/ now live at the repo
|
||||
top level and are shared by both projects. The zip reproduces the same
|
||||
layout, so the receiver unzips anywhere and the ..\\..\\Drivers /
|
||||
..\\..\\Shared references in the .uvprojx resolve correctly.
|
||||
|
||||
Usage:
|
||||
python pack.py bl -> Output/bootloader_package.zip
|
||||
python pack.py app -> Output/app_package.zip
|
||||
|
||||
Intermediate build artifacts (.o/.d/.crf/.axf/... and MDK Objects/Output/Listings)
|
||||
are excluded to keep the package small and clean.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# intermediate build artifacts to exclude from the zip
|
||||
EXCLUDE_EXT = {'.o', '.d', '.crf', '.axf', '.lst', '.dep', '.htm', '.map', '.bak', '.uvguix'}
|
||||
# MDK build-output dirs (case-insensitive) - never descend into them
|
||||
EXCLUDE_DIRS = {'objects', 'output', 'listings'}
|
||||
|
||||
|
||||
def add_dir(zf, src_dir, arc_base):
|
||||
"""Add src_dir tree into zip under arc_base, skipping build artifacts."""
|
||||
n = 0
|
||||
for base, dirs, files in os.walk(src_dir):
|
||||
rel = os.path.relpath(base, src_dir)
|
||||
parts = [p.lower() for p in rel.split(os.sep) if p not in ('', '.')]
|
||||
if any(p in EXCLUDE_DIRS for p in parts):
|
||||
dirs[:] = [] # do not descend into MDK build-output dirs
|
||||
continue
|
||||
for f in files:
|
||||
if os.path.splitext(f)[1].lower() in EXCLUDE_EXT:
|
||||
continue
|
||||
src = os.path.join(base, f)
|
||||
dst = os.path.join(arc_base, rel, f) if rel != '.' else os.path.join(arc_base, f)
|
||||
zf.write(src, dst)
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def pack(proj, zip_path):
|
||||
"""zip 内布局与仓库一致: <Proj>/ + Drivers/ + Shared/(共享层来自顶层)"""
|
||||
n = 0
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
n += add_dir(zf, os.path.join(ROOT, proj), proj)
|
||||
n += add_dir(zf, os.path.join(ROOT, 'Drivers'), 'Drivers')
|
||||
n += add_dir(zf, os.path.join(ROOT, 'Shared'), 'Shared')
|
||||
return n
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print('usage: python pack.py bl|app')
|
||||
sys.exit(1)
|
||||
mode = sys.argv[1].lower()
|
||||
if mode == 'bl':
|
||||
proj, zipname, uv = 'Bootloader', 'bootloader_package.zip', 'Bootloader.uvprojx'
|
||||
elif mode == 'app':
|
||||
proj, zipname, uv = 'App', 'app_package.zip', 'project.uvprojx'
|
||||
else:
|
||||
print('unknown mode: %s (use bl or app)' % mode)
|
||||
sys.exit(1)
|
||||
|
||||
out_dir = os.path.join(ROOT, 'Output')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
zip_path = os.path.join(out_dir, zipname)
|
||||
# 注:不预先删除旧 zip('w' 模式会直接截断覆盖,避免依赖删除旧文件的权限/回收站)
|
||||
|
||||
n = pack(proj, zip_path)
|
||||
print('[OK] %d files -> %s' % (n, zip_path))
|
||||
print(' zip layout: %s/ + Drivers/ + Shared/ (same as repo, unzip anywhere)' % proj)
|
||||
print(' receiver: unzip, open %s\\MDK-ARM\\%s in Keil and build.' % (proj, uv))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
REM Pack App project into Output\app_package.zip
|
||||
REM App/ is fully self-contained (own Drivers/ + Shared/).
|
||||
REM Keep this file pure ASCII - cmd parses .bat in GBK.
|
||||
setlocal
|
||||
cd /d "%~dp0\.."
|
||||
set PY=python
|
||||
where python >nul 2>nul || set PY=py
|
||||
%PY% tools\pack.py app
|
||||
if errorlevel 1 (
|
||||
echo [ERR] pack failed - python not found in PATH?
|
||||
)
|
||||
endlocal
|
||||
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
REM Pack Bootloader project into Output\bootloader_package.zip
|
||||
REM Bootloader/ is fully self-contained (own Drivers/ + Shared/).
|
||||
REM Keep this file pure ASCII - cmd parses .bat in GBK.
|
||||
setlocal
|
||||
cd /d "%~dp0\.."
|
||||
set PY=python
|
||||
where python >nul 2>nul || set PY=py
|
||||
%PY% tools\pack.py bl
|
||||
if errorlevel 1 (
|
||||
echo [ERR] pack failed - python not found in PATH?
|
||||
)
|
||||
endlocal
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Verify zip package contents for the two project packages (shared-layer layout).
|
||||
|
||||
Checks:
|
||||
1. zip top-level layout: <Proj>/ + Drivers/ + Shared/ (must be siblings)
|
||||
2. uvprojx ..\\..\\Drivers / ..\\..\\Shared references resolve inside the zip
|
||||
(simulates the receiver unzipping the archive anywhere and opening Keil)
|
||||
3. no junk build artifacts (.o/.d/.crf/...)
|
||||
"""
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
EXCLUDE_EXT = ('.o', '.d', '.crf', '.axf', '.lst', '.dep', '.htm', '.map', '.bak', '.uvguix')
|
||||
|
||||
ok = True
|
||||
for z, proj in [('bootloader_package.zip', 'Bootloader'),
|
||||
('app_package.zip', 'App')]:
|
||||
names = zipfile.ZipFile(z).namelist()
|
||||
print('===== %s (%d files) =====' % (z, len(names)))
|
||||
|
||||
# 1) 顶层布局
|
||||
tops = sorted(set(n.split('/')[0] for n in names))
|
||||
print('top dirs:', tops)
|
||||
expect = {proj, 'Drivers', 'Shared'}
|
||||
if not expect.issubset(tops):
|
||||
print(' [FAIL] 缺少共享层目录: %s' % (expect - set(tops)))
|
||||
ok = False
|
||||
|
||||
# 2) uvprojx 的 ..\\..\\ 引用在 zip 内可解析
|
||||
uv = [n for n in names if n.endswith('.uvprojx')]
|
||||
if not uv:
|
||||
print(' [FAIL] 无 uvprojx')
|
||||
ok = False
|
||||
continue
|
||||
data = zipfile.ZipFile(z).read(uv[0]).decode('utf-8', errors='ignore')
|
||||
tree = ET.fromstring(data)
|
||||
base = os.path.dirname(uv[0]) # 工程内 MDK-ARM 目录在 zip 中的路径
|
||||
missing_f = []
|
||||
for fp in tree.iter('FilePath'):
|
||||
v = (fp.text or '').strip()
|
||||
if not v:
|
||||
continue
|
||||
# zip 条目用 '/', uvprojx 用 '\' —— 统一为 posix 形式比较
|
||||
full = posixpath.normpath(posixpath.join(base, v.replace('\\', '/')))
|
||||
if full not in names:
|
||||
missing_f.append(v)
|
||||
missing_i = []
|
||||
for ip in tree.iter('IncludePath'):
|
||||
for seg in (ip.text or '').split(';'):
|
||||
seg = seg.strip()
|
||||
if not seg:
|
||||
continue
|
||||
# IncludePath 是目录:zip 无目录条目,用「目录下有文件条目」判断存在
|
||||
d = posixpath.normpath(posixpath.join(base, seg.replace('\\', '/')))
|
||||
if not any(n.startswith(d + '/') for n in names):
|
||||
missing_i.append(seg)
|
||||
print('uvprojx: %s' % uv[0])
|
||||
print(' FilePath 缺失: %d, IncludePath 缺失: %d' % (len(missing_f), len(missing_i)))
|
||||
for m in (missing_f + missing_i)[:8]:
|
||||
print(' MISSING: %s' % m)
|
||||
ok = False
|
||||
|
||||
# 3) 中间产物
|
||||
bad = [n for n in names if n.lower().endswith(EXCLUDE_EXT)]
|
||||
print('junk files:', len(bad))
|
||||
if bad:
|
||||
ok = False
|
||||
|
||||
print('has startup:', any(n.lower().endswith('startup_py32f040xx.s') for n in names))
|
||||
print('has RTE:', any('/RTE/' in n for n in names))
|
||||
|
||||
print('\n%s' % ('=== 全部通过 ✓ 接收方解压即编译 ===' if ok else '=== 存在问题 ✗ ==='))
|
||||
sys.exit(0 if ok else 1)
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""验证共享层结构:uvprojx XML 合法性 + 所有 FilePath/IncludePath 相对解析存在。"""
|
||||
import os, sys, re, xml.etree.ElementTree as ET
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJS = [
|
||||
('App', 'MDK-ARM/project.uvprojx', 'App'),
|
||||
('Bootloader', 'MDK-ARM/Bootloader.uvprojx', 'Bootloader'),
|
||||
]
|
||||
|
||||
ok = True
|
||||
for name, rel, proj in PROJS:
|
||||
p = os.path.join(ROOT, proj, rel)
|
||||
d = os.path.dirname(p)
|
||||
print('===== %s (%s) =====' % (name, rel))
|
||||
# 1) XML 合法性
|
||||
try:
|
||||
tree = ET.parse(p)
|
||||
print(' XML: 合法')
|
||||
except Exception as e:
|
||||
print(' XML: 非法 -> %s' % e)
|
||||
ok = False
|
||||
continue
|
||||
# 2) FilePath 存在性
|
||||
missing = []
|
||||
for fp in tree.iter('FilePath'):
|
||||
v = fp.text or ''
|
||||
if not v.strip():
|
||||
continue
|
||||
full = os.path.normpath(os.path.join(d, v))
|
||||
if not os.path.isfile(full):
|
||||
missing.append(v)
|
||||
print(' FilePath: %d 个,缺失 %d 个' % (
|
||||
len(list(tree.iter('FilePath'))), len(missing)))
|
||||
for m in missing[:10]:
|
||||
print(' MISSING: %s' % m)
|
||||
ok = False
|
||||
# 3) IncludePath 存在性(目录)
|
||||
inc_missing = []
|
||||
for ip in tree.iter('IncludePath'):
|
||||
v = ip.text or ''
|
||||
for seg in v.split(';'):
|
||||
if not seg.strip():
|
||||
continue
|
||||
full = os.path.normpath(os.path.join(d, seg))
|
||||
if not os.path.isdir(full):
|
||||
inc_missing.append(seg)
|
||||
print(' IncludePath 段: 缺失 %d 个' % len(inc_missing))
|
||||
for m in inc_missing[:10]:
|
||||
print(' MISSING: %s' % m)
|
||||
ok = False
|
||||
# 4) 是否引用对方工程(除 Shared/Drivers 外)
|
||||
cross = []
|
||||
for fp in tree.iter('FilePath'):
|
||||
v = fp.text or ''
|
||||
low = v.lower()
|
||||
if ('..\\..\\' in low) and ('shared' not in low) and ('drivers' not in low):
|
||||
cross.append(v)
|
||||
print(' 跨工程引用(非 Shared/Drivers): %d 个' % len(cross))
|
||||
for c in cross[:10]:
|
||||
print(' CROSS: %s' % c)
|
||||
if cross:
|
||||
ok = False
|
||||
|
||||
print('\n%s' % ('=== 全部通过 ✓ ===' if ok else '=== 存在缺失 ✗ ==='))
|
||||
sys.exit(0 if ok else 1)
|
||||
Reference in New Issue
Block a user