187 lines
7.4 KiB
Python
187 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the quickboot _update.mcs for the SMOD ATSC exciter.
|
|
|
|
Runs exactly the three scripts the project has always used, in order:
|
|
|
|
1. tcl_1/post_bitstream.tcl impl_1/*.bit,.bin,.ltx -> bitstream/
|
|
2. tcl_1/runScript_GenRu.tcl write_cfgmem -size 64 -interface SPIx1
|
|
-> bitstream/atsc_exciter.mcs
|
|
3. tcl_1/MakeSpiFlashProgrammerMcsFiles_backup.pl -imagesize 64 atsc_exciter.mcs
|
|
-> bitstream/atsc_exciter_update.mcs
|
|
|
|
Step 2 does ``glob *.bit`` inside bitstream/, so every stale .bit sitting there gets
|
|
concatenated into one giant image. bitstream/ accumulates iteration bits, so this
|
|
script moves the old ones aside for the duration and puts them back afterwards.
|
|
|
|
The resulting _update.mcs is ONLY for the network remote update (scripts/fpga_ru.py).
|
|
It cannot be JTAG-flashed -- see references/quickboot.md.
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
def _env_lookup(key, default=None):
|
|
"""Machine-local values (paths, IPs) live in ~/skills_env.md, never in git.
|
|
An OS environment variable of the same name overrides the file."""
|
|
v = os.environ.get(key)
|
|
if v:
|
|
return v
|
|
path = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
|
try:
|
|
with open(path, encoding="utf-8") as f:
|
|
for line in f:
|
|
m = re.match(rf"\s*{re.escape(key)}\s*=\s*(.+?)\s*(?:#.*)?$", line)
|
|
if m:
|
|
val = m.group(1).strip()
|
|
if val and not val.startswith("<"):
|
|
return val
|
|
except OSError:
|
|
pass
|
|
return default
|
|
|
|
|
|
# The my_exciter checkout root differs per machine; default keeps this machine's
|
|
# historical path so behaviour is unchanged when the key is absent.
|
|
REPO = _env_lookup("SMOD_REPO_ROOT", r"E:\Xilinx_design\smod\SMOD_ATSC\my_exciter")
|
|
TCL_DIR = os.path.join(REPO, "my_exciter.srcs", "tcl_1")
|
|
BITSTREAM = os.path.join(REPO, "bitstream")
|
|
IMPL = os.path.join(REPO, "my_exciter.runs", "impl_1")
|
|
|
|
POST_BITSTREAM = os.path.join(TCL_DIR, "post_bitstream.tcl")
|
|
GEN_RU = os.path.join(TCL_DIR, "runScript_GenRu.tcl")
|
|
PERL_SCRIPT = os.path.join(TCL_DIR, "MakeSpiFlashProgrammerMcsFiles_backup.pl")
|
|
|
|
|
|
def resolve_vivado():
|
|
"""Vivado's install path differs per machine -- never hardcode it."""
|
|
exe = _env_lookup("VIVADO_PATH")
|
|
if exe:
|
|
return exe
|
|
root = _env_lookup("VIVADO_ROOT")
|
|
if root:
|
|
return os.path.join(root, "bin", "vivado.bat")
|
|
sys.exit(
|
|
"Vivado path not configured. Add `VIVADO_PATH = <...>/bin/vivado.bat` to "
|
|
"~/skills_env.md, set it as an env var, or pass --vivado."
|
|
)
|
|
|
|
|
|
# runScript_GenRu.tcl hardcodes this name.
|
|
BASE_MCS = "atsc_exciter.mcs"
|
|
UPDATE_MCS = "atsc_exciter_update.mcs"
|
|
|
|
# The perl script emits these alongside the one we want.
|
|
BYPRODUCTS = ("atsc_exciter_initial.mcs", "atsc_exciter_golden.mcs", "atsc_exciter_total.mcs",
|
|
"atsc_exciter_update2.mcs", "atsc_exciter_simple.mcs")
|
|
|
|
# ":02000004HHHH" is the Intel-HEX extended-linear-address record.
|
|
ELA_RE = re.compile(rb"^:02000004([0-9A-Fa-f]{4})")
|
|
EXPECT_ELA = 0x800000
|
|
|
|
|
|
def sh(cmd, cwd=None):
|
|
print(f" $ {' '.join(cmd)}", flush=True)
|
|
if subprocess.run(cmd, cwd=cwd).returncode != 0:
|
|
sys.exit("[FAIL] command failed; see output above")
|
|
|
|
|
|
def first_ela(path):
|
|
with open(path, "rb") as fh:
|
|
for line in fh:
|
|
m = ELA_RE.match(line)
|
|
if m:
|
|
return int(m.group(1), 16) << 16
|
|
return None
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--vivado", default=None,
|
|
help="path to vivado.bat; defaults to VIVADO_PATH from ~/skills_env.md")
|
|
ap.add_argument("--tag", help="rename the result to <tag>_update.mcs so it survives the next run")
|
|
ap.add_argument("--keep-byproducts", action="store_true")
|
|
args = ap.parse_args()
|
|
args.vivado = args.vivado or resolve_vivado()
|
|
|
|
for path, what in ((args.vivado, "vivado"), (POST_BITSTREAM, "post_bitstream.tcl"),
|
|
(GEN_RU, "runScript_GenRu.tcl"), (PERL_SCRIPT, "perl script")):
|
|
if not os.path.exists(path):
|
|
sys.exit(f"[FAIL] {what} not found: {path}")
|
|
|
|
impl_bits = glob.glob(os.path.join(IMPL, "*.bit"))
|
|
if len(impl_bits) != 1:
|
|
sys.exit(f"[FAIL] expected exactly one .bit in {IMPL}, found {len(impl_bits)}: "
|
|
f"{[os.path.basename(b) for b in impl_bits]}")
|
|
print(f"[1/4] implementation bit: {os.path.basename(impl_bits[0])}")
|
|
|
|
# Park every stale .bit so runScript_GenRu.tcl's `glob *.bit` can only see the fresh one.
|
|
stash = tempfile.mkdtemp(prefix="smod_bit_stash_")
|
|
parked = glob.glob(os.path.join(BITSTREAM, "*.bit"))
|
|
for b in parked:
|
|
shutil.move(b, os.path.join(stash, os.path.basename(b)))
|
|
if parked:
|
|
print(f"[2/4] parked {len(parked)} stale .bit files from bitstream/ (restored at the end)")
|
|
|
|
try:
|
|
# One vivado launch, both tcl scripts: each re-derives its own path from [info script].
|
|
wrapper = os.path.join(stash, "run_both.tcl")
|
|
with open(wrapper, "w") as fh:
|
|
fh.write(f'source {{{POST_BITSTREAM}}}\n')
|
|
fh.write(f'source {{{GEN_RU}}}\n')
|
|
print(f"[3/4] vivado: post_bitstream.tcl + runScript_GenRu.tcl -> {BASE_MCS}")
|
|
sh([args.vivado, "-mode", "batch", "-nojournal", "-notrace", "-source", wrapper], cwd=stash)
|
|
|
|
base = os.path.join(BITSTREAM, BASE_MCS)
|
|
if not os.path.exists(base):
|
|
sys.exit(f"[FAIL] {base} was not produced")
|
|
|
|
print(f"[4/4] perl MakeSpiFlashProgrammerMcsFiles_backup.pl -imagesize 64 {BASE_MCS}")
|
|
sh(["perl", PERL_SCRIPT, "-imagesize", "64", BASE_MCS], cwd=BITSTREAM)
|
|
finally:
|
|
for name in os.listdir(stash):
|
|
if not name.endswith(".bit"):
|
|
continue
|
|
dest = os.path.join(BITSTREAM, name)
|
|
# post_bitstream.tcl just copied the fresh impl_1 bit here under this same
|
|
# name; putting the parked (stale) one back would silently undo that.
|
|
if not os.path.exists(dest):
|
|
shutil.move(os.path.join(stash, name), dest)
|
|
shutil.rmtree(stash, ignore_errors=True)
|
|
|
|
update = os.path.join(BITSTREAM, UPDATE_MCS)
|
|
if not os.path.exists(update):
|
|
sys.exit(f"[FAIL] {UPDATE_MCS} was not produced")
|
|
|
|
# -imagesize 64 must place the image at 0x800000. If it doesn't, the payload is not
|
|
# 8 MB and the FPGA's CRC check over 0x400000..0xC00000 will fail after a long upload.
|
|
ela = first_ela(update)
|
|
if ela != EXPECT_ELA:
|
|
got = "none" if ela is None else f"0x{ela:X}"
|
|
sys.exit(f"[FAIL] {UPDATE_MCS} starts at {got}, expected 0x{EXPECT_ELA:X}. Do not upload it.")
|
|
|
|
if not args.keep_byproducts:
|
|
for name in BYPRODUCTS:
|
|
p = os.path.join(BITSTREAM, name)
|
|
if os.path.exists(p):
|
|
os.remove(p)
|
|
|
|
if args.tag:
|
|
tagged = os.path.join(BITSTREAM, f"{args.tag}_update.mcs")
|
|
shutil.move(update, tagged)
|
|
update = tagged
|
|
|
|
print(f"\n[OK] {update} ({os.path.getsize(update)/1e6:.1f} MB)")
|
|
print(f" base mcs kept at {os.path.join(BITSTREAM, BASE_MCS)}")
|
|
print("\nNext: python scripts/fpga_ru.py --mcs \"%s\"" % update)
|
|
print(" Turn DPD off first -- see references/protocol.md.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|