Added JTAG interface with testbench

This commit is contained in:
2026-02-23 15:37:49 +01:00
parent 20cfece6e3
commit 8f4e887b9d
12 changed files with 731 additions and 24 deletions

167
scripts/jtag_write_user_impact.py Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Write USER JTAG data via iMPACT using SVF + play flow."""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
DEFAULT_IMPACT = "/opt/Xilinx/14.7/ISE_DS/ISE/bin/lin64/impact"
DEFAULT_SETTINGS = "/opt/Xilinx/14.7/ISE_DS/settings64.sh"
def _parse_int(value: str) -> int:
return int(value, 0)
def _resolve_impact(binary: str | None) -> str | None:
if binary:
return binary if os.path.isfile(binary) else shutil.which(binary)
if os.path.isfile(DEFAULT_IMPACT):
return DEFAULT_IMPACT
return shutil.which("impact")
def _fmt_hex_for_bits(value: int, bits: int) -> str:
masked = value & ((1 << bits) - 1)
nibbles = (bits + 3) // 4
return f"{masked:0{nibbles}X}"
def main() -> int:
parser = argparse.ArgumentParser(
description="Write USER JTAG data to Spartan-6 via iMPACT play (SVF)."
)
parser.add_argument(
"--input",
required=True,
help="Input file to stream over USER JTAG (written byte-by-byte).",
)
parser.add_argument("--output", type=str, default=None)
parser.add_argument("--dr-bits", type=int, default=8, help="DR width in bits (default: 8).")
parser.add_argument("--ir-value", type=_parse_int, default=0x02, help="IR opcode (default USER1: 0x02).")
parser.add_argument("--ir-bits", type=int, default=6, help="IR width in bits (default: 6).")
parser.add_argument("--serial", default=None, help="Cable ESN/serial (example: D306180FABCD).")
parser.add_argument("--impact", default=None, help=f"Path/name of impact binary (default: {DEFAULT_IMPACT}).")
parser.add_argument(
"--settings",
default=DEFAULT_SETTINGS,
help=f"Xilinx settings script sourced for this run only (default: {DEFAULT_SETTINGS}).",
)
parser.add_argument("--dry-run", action="store_true", help="Print generated SVF/CMD and exit.")
args = parser.parse_args()
if args.dr_bits <= 0 or args.ir_bits <= 0:
print("error: --dr-bits and --ir-bits must be > 0", file=sys.stderr)
return 2
if args.dr_bits != 8:
print("error: this file-stream mode currently supports only --dr-bits 8", file=sys.stderr)
return 2
impact = _resolve_impact(args.impact)
if impact is None:
print("error: impact binary not found", file=sys.stderr)
return 127
if not os.path.isfile(args.input):
print(f'error: input file not found: "{args.input}"', file=sys.stderr)
return 2
ir_hex = _fmt_hex_for_bits(args.ir_value, args.ir_bits)
with open(args.input, "rb") as f:
payload = f.read()
if len(payload) == 0:
print("error: input file is empty", file=sys.stderr)
return 2
svf_lines = [
"! Auto-generated by jtag_write_user_impact.py",
f"! Source file: {args.input} ({len(payload)} bytes)",
"TRST ABSENT;",
"ENDIR IDLE;",
"ENDDR IDLE;",
"STATE RESET;",
"STATE IDLE;",
f"SIR {args.ir_bits} TDI ({ir_hex});",
]
for byte in payload:
svf_lines.append(f"SDR 8 TDI ({byte:02X});")
svf_lines += [
"RUNTEST 16 TCK;",
"STATE IDLE;",
"",
]
svf_text = "\n".join(svf_lines)
cable_cmd = "setCable -port auto"
if args.serial:
cable_cmd += f" -esn {args.serial}"
if args.dry_run:
print("### SVF ###")
preview_limit = 64
if len(payload) <= preview_limit:
print(svf_text, end="")
else:
preview = "\n".join(svf_lines[:8 + preview_limit])
print(preview)
print(f"... ({len(payload) - preview_limit} more SDR lines omitted)")
print("### iMPACT CMD ###")
print("setMode -bs")
print(cable_cmd)
print("addDevice -p 1 -file <generated.svf>")
print("play")
print("quit")
return 0
svf_path = None
cmd_path = None
try:
with tempfile.NamedTemporaryFile("w", suffix=".svf", delete=False) as sf:
sf.write(svf_text)
svf_path = sf.name
if args.output is not None:
with open(args.output, "w") as sf:
sf.write(svf_text)
cmd_text = "\n".join(
[
"setMode -bs",
cable_cmd,
f'addDevice -p 1 -file "{svf_path}"',
"play",
"quit",
"",
]
)
with tempfile.NamedTemporaryFile("w", suffix=".cmd", delete=False) as cf:
cf.write(cmd_text)
cmd_path = cf.name
shell_cmd = (
f'source "{args.settings}" >/dev/null 2>&1 && '
f'"{impact}" -batch "{cmd_path}"'
)
proc = subprocess.run(["bash", "-lc", shell_cmd], text=True, capture_output=True)
output = (proc.stdout or "") + (proc.stderr or "")
print(output, end="")
return proc.returncode
finally:
if cmd_path is not None:
try:
os.unlink(cmd_path)
except OSError:
pass
if svf_path is not None:
try:
os.unlink(svf_path)
except OSError:
pass
if __name__ == "__main__":
raise SystemExit(main())