125 lines
4.1 KiB
Python
125 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
"""生成写入W25Q80BV的出厂固件合并镜像。
|
||
|
||
合并镜像布局如下:
|
||
0x000000:主程序裸二进制固件
|
||
0x002000:出厂固件元数据
|
||
|
||
脚本会自动读取主程序BIN、计算CRC-16/MODBUS,并把版本号写入输出文件名
|
||
和出厂元数据。CH341A打开生成的文件后,从W25Q80BV的0x000000地址开始烧录。
|
||
"""
|
||
|
||
from pathlib import Path
|
||
import re
|
||
|
||
|
||
# 当前汇编主程序Makefile生成的裸二进制文件,已经去除了Bootloader地址空间。
|
||
# 该文件从应用链接地址0x8500对应的第一个字节开始,不包含Bootloader。
|
||
FIRMWARE_PATH = Path("STM8_app_asm/build/stm8_app_asm.bin")
|
||
|
||
# 主程序版本定义文件,脚本会自动读取其中的FIRMWARE_VERSION宏。
|
||
VERSION_HEADER_PATH = Path("include/app_config.h")
|
||
|
||
# 出厂固件元数据在W25Q80BV中的固定地址。
|
||
FACTORY_METADATA_ADDRESS = 0x002000
|
||
|
||
|
||
def read_firmware_version() -> int:
|
||
"""从主程序配置头文件读取FIRMWARE_VERSION宏。"""
|
||
|
||
if not VERSION_HEADER_PATH.is_file():
|
||
raise SystemExit(f"找不到版本配置文件:{VERSION_HEADER_PATH}")
|
||
|
||
content = VERSION_HEADER_PATH.read_text(encoding="utf-8")
|
||
match = re.search(
|
||
r"^\s*#define\s+FIRMWARE_VERSION\s+((?:0[xX][0-9A-Fa-f]+)|(?:[0-9]+))",
|
||
content,
|
||
flags=re.MULTILINE,
|
||
)
|
||
if match is None:
|
||
raise SystemExit(
|
||
f"在{VERSION_HEADER_PATH}中找不到FIRMWARE_VERSION宏定义"
|
||
)
|
||
|
||
version = int(match.group(1), 0)
|
||
if not 0 <= version <= 0xFFFF:
|
||
raise SystemExit(f"FIRMWARE_VERSION超出16位范围:{version}")
|
||
return version
|
||
|
||
|
||
def calculate_crc16_modbus(data: bytes) -> int:
|
||
"""计算一段数据的CRC-16/MODBUS校验值。
|
||
|
||
Args:
|
||
data: 需要计算CRC的完整固件数据。
|
||
|
||
Returns:
|
||
16位CRC结果。发送和存储时由调用方按高字节、低字节写入。
|
||
"""
|
||
|
||
crc = 0xFFFF
|
||
for value in data:
|
||
crc ^= value
|
||
for _ in range(8):
|
||
if crc & 1:
|
||
crc = (crc >> 1) ^ 0xA001
|
||
else:
|
||
crc >>= 1
|
||
return crc & 0xFFFF
|
||
|
||
|
||
def build_factory_metadata(firmware: bytes, crc16: int, version: int) -> bytes:
|
||
"""按照Bootloader固定布局生成18字节出厂元数据。"""
|
||
|
||
metadata = bytearray(18)
|
||
|
||
# FACTORY_METADATA_MAGIC=0x46414331。
|
||
# 当前Bootloader兼容的魔数字节布局为31 43 41 46。
|
||
metadata[0:4] = bytes([0x31, 0x43, 0x41, 0x46])
|
||
|
||
# 固件长度、版本号和CRC16统一使用高字节在前。
|
||
metadata[4:8] = len(firmware).to_bytes(4, "big")
|
||
metadata[8:10] = version.to_bytes(2, "big")
|
||
metadata[10:12] = crc16.to_bytes(2, "big")
|
||
|
||
# 0x0C~0x0D为保留字段,保持0x00。
|
||
metadata[14] = 0xA5 # 镜像有效标志。
|
||
metadata[15] = 0x01 # 请求Bootloader处理出厂镜像。
|
||
metadata[16] = 0x00 # 初始升级结果。
|
||
metadata[17] = 0x00 # 保留字段。
|
||
return bytes(metadata)
|
||
|
||
|
||
def main() -> None:
|
||
"""读取固件、生成出厂镜像并输出烧录文件信息。"""
|
||
|
||
if not FIRMWARE_PATH.is_file():
|
||
raise SystemExit(
|
||
f"找不到固件文件:{FIRMWARE_PATH}\n"
|
||
"请先执行 make -C STM8_app_asm。"
|
||
)
|
||
|
||
firmware = FIRMWARE_PATH.read_bytes()
|
||
firmware_version = read_firmware_version()
|
||
crc16 = calculate_crc16_modbus(firmware)
|
||
metadata = build_factory_metadata(firmware, crc16, firmware_version)
|
||
|
||
# 输出文件名包含4位十六进制版本号,例如factory_flash_v0100.bin。
|
||
output_path = Path(f"factory_flash_v{firmware_version:04X}.bin")
|
||
|
||
# 文件覆盖到元数据末尾,空白区域填充0xFF,符合NOR Flash擦除状态。
|
||
image = bytearray([0xFF] * (FACTORY_METADATA_ADDRESS + len(metadata)))
|
||
image[0:len(firmware)] = firmware
|
||
image[FACTORY_METADATA_ADDRESS:] = metadata
|
||
output_path.write_bytes(image)
|
||
|
||
print(f"固件长度:{len(firmware)} 字节")
|
||
print(f"固件版本:0x{firmware_version:04X}")
|
||
print(f"固件CRC16:0x{crc16:04X}")
|
||
print(f"输出文件:{output_path}")
|
||
print(f"输出大小:{len(image)} 字节")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|