Files
seat-firmware/firmware/gcc/make_factory_image.py
T

149 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Build the one-time WildFire image from server-built firmware artifacts."""
from __future__ import annotations
import argparse
import struct
import zlib
from pathlib import Path
BOOT_BASE = 0x10000000
APP_BASE = 0x10010000
APP_END = 0x10080000
METADATA_ADDRESS = 0x14000000
METADATA_MAGIC = 0x4F544131
METADATA_VERSION = 1
def parse_ihex(path: Path) -> tuple[dict[int, int], int | None]:
memory: dict[int, int] = {}
upper = 0
start_linear: int | None = None
for line_number, raw in enumerate(path.read_text(encoding="ascii").splitlines(), 1):
line = raw.strip()
if not line:
continue
if not line.startswith(":"):
raise ValueError(f"{path}:{line_number}: invalid Intel HEX record")
record = bytes.fromhex(line[1:])
if len(record) < 5 or sum(record) & 0xFF:
raise ValueError(f"{path}:{line_number}: invalid record checksum")
length = record[0]
if len(record) != length + 5:
raise ValueError(f"{path}:{line_number}: invalid record length")
offset = int.from_bytes(record[1:3], "big")
kind = record[3]
payload = record[4 : 4 + length]
if kind == 0:
address = upper + offset
for index, value in enumerate(payload):
absolute = address + index
previous = memory.get(absolute)
if previous is not None and previous != value:
raise ValueError(f"overlapping data at 0x{absolute:08X}")
memory[absolute] = value
elif kind == 2:
if length != 2:
raise ValueError("invalid extended segment record")
upper = int.from_bytes(payload, "big") << 4
elif kind == 4:
if length != 2:
raise ValueError("invalid extended linear record")
upper = int.from_bytes(payload, "big") << 16
elif kind == 5:
if length != 4:
raise ValueError("invalid start linear address record")
start_linear = int.from_bytes(payload, "big")
elif kind in (1, 3):
continue
else:
raise ValueError(f"unsupported Intel HEX record type {kind}")
if not memory:
raise ValueError("bootloader HEX contains no data")
return memory, start_linear
def record(address: int, kind: int, payload: bytes) -> str:
body = bytes((len(payload), (address >> 8) & 0xFF, address & 0xFF, kind)) + payload
checksum = (-sum(body)) & 0xFF
return ":" + (body + bytes((checksum,))).hex().upper()
def write_ihex(path: Path, memory: dict[int, int], start_linear: int | None) -> None:
lines: list[str] = []
current_upper: int | None = None
addresses = sorted(memory)
index = 0
while index < len(addresses):
start = addresses[index]
upper = start >> 16
if upper != current_upper:
lines.append(record(0, 4, upper.to_bytes(2, "big")))
current_upper = upper
low = start & 0xFFFF
payload = bytearray((memory[start],))
index += 1
while index < len(addresses) and len(payload) < 16:
next_address = addresses[index]
if next_address != start + len(payload) or next_address >> 16 != upper:
break
payload.append(memory[next_address])
index += 1
lines.append(record(low, 0, bytes(payload)))
if start_linear is not None:
lines.append(record(0, 5, start_linear.to_bytes(4, "big")))
lines.append(record(0, 1, b""))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines) + "\n", encoding="ascii", newline="\n")
def add_region(memory: dict[int, int], address: int, data: bytes, label: str) -> None:
for index, value in enumerate(data):
absolute = address + index
if absolute in memory:
raise ValueError(f"{label} overlaps existing data at 0x{absolute:08X}")
memory[absolute] = value
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--bootloader", type=Path, required=True)
parser.add_argument("--calibration", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
memory, start_linear = parse_ihex(args.bootloader)
if min(memory) < BOOT_BASE or max(memory) >= APP_BASE:
raise ValueError("bootloader data is outside the immutable 64 KiB region")
calibration = args.calibration.read_bytes()
if len(calibration) < 8 or APP_BASE + len(calibration) > APP_END:
raise ValueError("calibration firmware size is outside the application region")
crc32 = zlib.crc32(calibration) & 0xFFFFFFFF
metadata = struct.pack(
"<8I",
METADATA_MAGIC,
METADATA_VERSION,
len(calibration),
crc32,
(~len(calibration)) & 0xFFFFFFFF,
(~crc32) & 0xFFFFFFFF,
APP_BASE,
(~APP_BASE) & 0xFFFFFFFF,
)
add_region(memory, APP_BASE, calibration, "calibration firmware")
add_region(memory, METADATA_ADDRESS, metadata, "boot metadata")
write_ihex(args.output, memory, start_linear)
print(
"FACTORY_IMAGE_OK "
f"boot_bytes={sum(BOOT_BASE <= value < APP_BASE for value in memory)} "
f"app_bytes={len(calibration)} crc32=0x{crc32:08X} output={args.output}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())