222 lines
9.0 KiB
Python
222 lines
9.0 KiB
Python
#!/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()
|