添加普冉 PY32F040 OTA 双工程代码生成模板

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 18:59:56 +08:00
co-authored by Cursor
commit 331864dc64
496 changed files with 329040 additions and 0 deletions
+221
View File
@@ -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_*.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()