Files

222 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_*.batzip 内含工程+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. 打印固件 CRC0x30 下发 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()