Files

253 lines
10 KiB
Python

#!/usr/bin/env python3
"""
GD32E230 固件工具:整页补齐 + 合成烧录镜像,二合一。
模式(互斥):
-m / --merge 合成完整 Flash 镜像 (bootloader + APP + FLAG)
→ 用于 OpenOCD / J-Link 整片烧录,不补齐
-f / --fill 仅补齐 APP 到整页边界
→ 用于 Bootloader Ymodem 升级
Flash 布局:
0x08000000 ┌──────────────┐
│ Bootloader │ 8KB
APP_ADDR ├──────────────┤
│ Application │ 最大 54KB (F8) / 22KB (F6) / 6KB (F4)
│ ... │
CAL_ADDR │ 校准数据 │ 1KB (预留)
FLAG_ADDR │ 0xEEEE 标志 │ 1KB
FLASH_END └──────────────┘
"""
import argparse
import struct
import sys
import os
# ═══════════════════════════════════════════════════════════
# 常量
# ═══════════════════════════════════════════════════════════
FLASH_BASE = 0x08000000
BOOTLOADER_SIZE = 8 * 1024 # 8KB
CAL_PAGE_SIZE = 1 * 1024 # 1KB 校准数据预留
FLAG_PAGE_SIZE = 1 * 1024 # 1KB Flag 页
PAGE_SIZE = 1024 # Flash 页大小 (1KB)
FILL_BYTE = 0xFF # 补齐字节
FLAG_VALUE = 0xEEEE # 与 bootloader 统一
CHIP_CONFIG = {
"F4": {"size": 16 * 1024, "label": "GD32E230F4 (16KB)"},
"F6": {"size": 32 * 1024, "label": "GD32E230F6 (32KB)"},
"F8": {"size": 64 * 1024, "label": "GD32E230F8 (64KB)"},
}
DEFAULT_CHIP = "F8"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_BOOTLOADER = os.path.join(SCRIPT_DIR, "gd32e230f8_bootloader_hulk.bin")
# ═══════════════════════════════════════════════════════════
# 地址计算
# ═══════════════════════════════════════════════════════════
def calc_layout(flash_size):
app_addr = FLASH_BASE + BOOTLOADER_SIZE # 0x08002000
cal_addr = FLASH_BASE + flash_size - CAL_PAGE_SIZE - FLAG_PAGE_SIZE
flag_addr = FLASH_BASE + flash_size - 4 # 最后一页末尾 - 4B
app_max = cal_addr - app_addr
return {
"flash_size": flash_size,
"flash_end": FLASH_BASE + flash_size,
"app_addr": app_addr,
"app_max": app_max,
"cal_addr": cal_addr,
"flag_addr": flag_addr,
}
# ═══════════════════════════════════════════════════════════
# 补齐
# ═══════════════════════════════════════════════════════════
def pad_to_page(data, page_size=PAGE_SIZE, fill_byte=FILL_BYTE):
remainder = len(data) % page_size
if remainder == 0:
return data, 0
pad_len = page_size - remainder
return data + bytes([fill_byte]) * pad_len, pad_len
# ═══════════════════════════════════════════════════════════
# 合成模式
# ═══════════════════════════════════════════════════════════
def merge(bootloader_path, app_path, output_path, layout):
# 读取
with open(bootloader_path, "rb") as f:
bl = f.read()
with open(app_path, "rb") as f:
app = f.read()
# 校验
app_offset = layout["app_addr"] - FLASH_BASE
if len(bl) > app_offset:
print(f"错误:Bootloader ({len(bl)}B) 超过 APP 偏移 ({app_offset}B)", file=sys.stderr)
return False
if len(app) > layout["app_max"]:
print(
f"错误:APP ({len(app)}B / {len(app)/1024:.1f}KB) 超过最大容量 "
f"({layout['app_max']}B / {layout['app_max']/1024:.0f}KB)\n"
f" 芯片 {layout['flash_size']//1024}KB: "
f"{layout['flash_size']//1024}KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) "
f"= {layout['app_max']//1024}KB",
file=sys.stderr,
)
return False
# 构建镜像
image = bytearray(b'\xFF' * layout["flash_size"])
image[0:len(bl)] = bl
image[app_offset:app_offset + len(app)] = app
flag_offset = layout["flag_addr"] - FLASH_BASE
struct.pack_into("<I", image, flag_offset, FLAG_VALUE)
# 输出
with open(output_path, "wb") as f:
f.write(image)
print(f"\n合成完成: {output_path}")
print(f" 芯片 : {layout['flash_size']//1024}KB")
print(f" Bootloader : {len(bl):>6}B @ 0x{FLASH_BASE:08X}")
print(f" Application: {len(app):>6}B @ 0x{layout['app_addr']:08X}")
print(f" Flag : 0x{FLAG_VALUE:04X} @ 0x{layout['flag_addr']:08X}")
print(f" 校准预留 : 1KB @ 0x{layout['cal_addr']:08X}")
print(f" APP 剩余 : {layout['app_max'] - len(app):>6}B")
print(f" 总大小 : {layout['flash_size']:>6}B ({layout['flash_size']//1024}KB)")
return True
# ═══════════════════════════════════════════════════════════
# 补齐模式
# ═══════════════════════════════════════════════════════════
def fill_only(input_path, output_path, layout):
with open(input_path, "rb") as f:
data = f.read()
padded, pad_len = pad_to_page(data)
# 校验补齐后不超过 APP 最大容量
if len(padded) > layout["app_max"]:
print(
f"错误:补齐后 ({len(padded)}B / {len(padded)/1024:.1f}KB) 超过 APP 最大容量 "
f"({layout['app_max']}B / {layout['app_max']/1024:.0f}KB)\n"
f" 芯片 {layout['flash_size']//1024}KB: "
f"{layout['flash_size']//1024}KB - 8KB(BL) - 1KB(校准) - 1KB(Flag) "
f"= {layout['app_max']//1024}KB",
file=sys.stderr,
)
sys.exit(1)
with open(output_path, "wb") as f:
f.write(padded)
print(f"补齐完成: {output_path}")
print(f" 原始大小: {len(data)}B")
print(f" 页大小 : {PAGE_SIZE}B (1KB)")
print(f" 补齐字节: {pad_len}B")
print(f" 输出大小: {len(padded)}B")
print(f" APP 上限: {layout['app_max']}B ({layout['app_max']//1024}KB) → "
f"剩余 {layout['app_max'] - len(padded)}B")
if pad_len == 0:
print(f" 说明 : 已是整页对齐,原样输出")
# ═══════════════════════════════════════════════════════════
# 入口
# ═══════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(
description="GD32E230 固件工具 — 整页补齐 & 合成烧录镜像",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python %(prog)s -m app.bin # F8 合成镜像 (默认)
python %(prog)s -m app.bin -s F6 # F6 合成镜像
python %(prog)s -f app.bin # F8 补齐升级包 (默认)
python %(prog)s -f app.bin -s F4 # F4 补齐升级包
python %(prog)s -m app.bin -b my_bl.bin -o out.bin # 自定义 bootloader/输出
""",
)
# 位置参数
parser.add_argument(
"app", help="Application 的 .bin 文件路径"
)
# 模式 (互斥)
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument(
"-m", "--merge",
action="store_true",
help="合成完整 Flash 镜像 (bootloader + APP + FLAG),用于整片烧录",
)
mode.add_argument(
"-f", "--fill",
action="store_true",
help="仅补齐 APP 到整页边界,用于 Bootloader Ymodem 升级",
)
# 选项
parser.add_argument(
"-s", "--flash-size",
default=DEFAULT_CHIP,
choices=list(CHIP_CONFIG.keys()),
help=f"芯片型号: F4(16K) F6(32K) F8(64K) (默认: {DEFAULT_CHIP})",
)
parser.add_argument(
"-b", "--bootloader",
default=DEFAULT_BOOTLOADER,
help=f"Bootloader bin 路径 (默认: {DEFAULT_BOOTLOADER})",
)
parser.add_argument(
"-o", "--output",
default=None,
help="输出文件路径 (默认: APP名_BL.bin 或 APP名_UPDATE.bin)",
)
args = parser.parse_args()
chip = CHIP_CONFIG[args.flash_size]
layout = calc_layout(chip["size"])
print(f"芯片: {chip['label']}")
# 输出文件名
if args.output is None:
base, ext = os.path.splitext(os.path.basename(args.app))
suffix = "_BL" if args.merge else "_UPDATE"
args.output = f"{base}{suffix}{ext}"
# 输入文件检查
if not os.path.isfile(args.app):
print(f"错误:找不到 APP 文件 {args.app}", file=sys.stderr)
sys.exit(1)
if args.merge:
if not os.path.isfile(args.bootloader):
print(f"错误:找不到 Bootloader 文件 {args.bootloader}", file=sys.stderr)
sys.exit(1)
if not merge(args.bootloader, args.app, args.output, layout):
sys.exit(1)
else:
fill_only(args.app, args.output, layout)
if __name__ == "__main__":
main()