forked from M-Labs/artiq
355 lines
13 KiB
Python
Executable File
355 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import shutil
|
|
import re
|
|
import atexit
|
|
from functools import partial
|
|
from collections import defaultdict
|
|
|
|
from sipyco import common_args
|
|
|
|
from artiq import __version__ as artiq_version
|
|
from artiq.remoting import SSHClient, LocalClient
|
|
from artiq.frontend.bit2bin import bit2bin
|
|
|
|
|
|
def get_argparser():
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description="ARTIQ flashing/deployment tool",
|
|
epilog="""\
|
|
Valid actions:
|
|
|
|
* gateware: write main gateware bitstream to flash
|
|
* bootloader: write bootloader to flash
|
|
* storage: write storage image to flash
|
|
* firmware: write firmware to flash
|
|
* load: load main gateware bitstream into device (volatile but fast)
|
|
* erase: erase flash memory
|
|
* start: trigger the target to (re)load its gateware bitstream from flash
|
|
|
|
Prerequisites:
|
|
|
|
* Connect the board through its/a JTAG adapter.
|
|
* Have OpenOCD installed and in your $PATH.
|
|
* Have access to the JTAG adapter's devices. Udev rules from OpenOCD:
|
|
'sudo cp openocd/contrib/99-openocd.rules /etc/udev/rules.d'
|
|
and replug the device. Ensure you are member of the
|
|
plugdev group: 'sudo adduser $USER plugdev' and re-login.
|
|
""")
|
|
|
|
parser.add_argument("--version", action="version",
|
|
version="ARTIQ v{}".format(artiq_version),
|
|
help="print the ARTIQ version number")
|
|
|
|
common_args.verbosity_args(parser)
|
|
|
|
parser.add_argument("-n", "--dry-run",
|
|
default=False, action="store_true",
|
|
help="only show the openocd script that would be run")
|
|
parser.add_argument("-H", "--host", metavar="HOSTNAME",
|
|
type=str, default=None,
|
|
help="SSH host where the board is located")
|
|
parser.add_argument("-J", "--jump",
|
|
type=str, default=None,
|
|
help="SSH host to jump through")
|
|
parser.add_argument("-t", "--target", default="kasli",
|
|
help="target board, default: %(default)s, one of: "
|
|
"kasli efc kc705")
|
|
parser.add_argument("-I", "--preinit-command", default=[], action="append",
|
|
help="add a pre-initialization OpenOCD command. "
|
|
"Useful for selecting a board when several are connected.")
|
|
parser.add_argument("-f", "--storage", help="write file to storage area")
|
|
parser.add_argument("-d", "--dir", default=None, help="look for board binaries in this directory")
|
|
parser.add_argument("--srcbuild", help="board binaries directory is laid out as a source build tree",
|
|
default=False, action="store_true")
|
|
parser.add_argument("action", metavar="ACTION", nargs="*",
|
|
default=[],
|
|
help="actions to perform, default: flash everything")
|
|
return parser
|
|
|
|
def openocd_root():
|
|
openocd = shutil.which("openocd")
|
|
if not openocd:
|
|
raise FileNotFoundError("OpenOCD is required but was not found in PATH. Is it installed?")
|
|
return os.path.dirname(os.path.dirname(openocd))
|
|
|
|
|
|
def scripts_path():
|
|
p = ["share", "openocd", "scripts"]
|
|
if os.name == "nt":
|
|
p.insert(0, "Library")
|
|
return os.path.abspath(os.path.join(openocd_root(), *p))
|
|
|
|
|
|
def proxy_path():
|
|
return os.path.abspath(os.path.join(openocd_root(), "share", "bscan-spi-bitstreams"))
|
|
|
|
|
|
def find_proxy_bitfile(filename):
|
|
for p in [proxy_path(), os.path.expanduser("~/.migen"),
|
|
"/usr/local/share/migen", "/usr/share/migen"]:
|
|
full_path = os.path.join(p, filename)
|
|
if os.access(full_path, os.R_OK):
|
|
return full_path
|
|
raise FileNotFoundError("Cannot find proxy bitstream {}"
|
|
.format(filename))
|
|
|
|
|
|
def add_commands(script, *commands, **substs):
|
|
script += [command.format(**substs) for command in commands]
|
|
|
|
|
|
class Programmer:
|
|
def __init__(self, client, preinit_script):
|
|
self._client = client
|
|
self._board_script = []
|
|
self._preinit_script = [
|
|
"gdb_port disabled",
|
|
"tcl_port disabled",
|
|
"telnet_port disabled"
|
|
] + preinit_script
|
|
self._loaded = defaultdict(lambda: None)
|
|
self._script = [
|
|
"set error_msg \"Trying to use configured scan chain anyway\"",
|
|
"if {[string first $error_msg [capture \"init\"]] != -1} {",
|
|
"puts \"Found error and exiting\"",
|
|
"exit}"
|
|
]
|
|
|
|
def _transfer_script(self, script):
|
|
if isinstance(self._client, LocalClient):
|
|
return "[find {}]".format(script)
|
|
|
|
def rewriter(content):
|
|
def repl(match):
|
|
return self._transfer_script(match.group(1).decode()).encode()
|
|
return re.sub(rb"\[find (.+?)\]", repl, content, re.DOTALL)
|
|
|
|
script = os.path.join(scripts_path(), script)
|
|
return self._client.upload(script, rewriter)
|
|
|
|
def add_flash_bank(self, name, tap, index):
|
|
add_commands(self._board_script,
|
|
"target create {tap}.{name}.proxy testee -chain-position {tap}.tap",
|
|
"flash bank {name} jtagspi 0 0 0 0 {tap}.{name}.proxy {ir:#x}",
|
|
tap=tap, name=name, ir=0x02 + index)
|
|
|
|
def erase_flash(self, bankname):
|
|
self.load_proxy()
|
|
add_commands(self._script,
|
|
"flash probe {bankname}",
|
|
"flash erase_sector {bankname} 0 last",
|
|
bankname=bankname)
|
|
|
|
def load(self, bitfile, pld):
|
|
os.stat(bitfile) # check for existence
|
|
|
|
if self._loaded[pld] == bitfile:
|
|
return
|
|
self._loaded[pld] = bitfile
|
|
|
|
bitfile = self._client.upload(bitfile)
|
|
add_commands(self._script,
|
|
"pld load {pld} {{{filename}}}",
|
|
pld=pld, filename=bitfile)
|
|
|
|
def load_proxy(self):
|
|
raise NotImplementedError
|
|
|
|
def write_binary(self, bankname, address, filename):
|
|
self.load_proxy()
|
|
|
|
size = os.path.getsize(filename)
|
|
filename = self._client.upload(filename)
|
|
add_commands(self._script,
|
|
"flash probe {bankname}",
|
|
"flash erase_sector {bankname} {firstsector} {lastsector}",
|
|
"flash write_bank {bankname} {{{filename}}} {address:#x}",
|
|
"flash verify_bank {bankname} {{{filename}}} {address:#x}",
|
|
bankname=bankname, address=address, filename=filename,
|
|
firstsector=address // self._sector_size,
|
|
lastsector=(address + size - 1) // self._sector_size)
|
|
|
|
def read_binary(self, bankname, address, length, filename):
|
|
self.load_proxy()
|
|
|
|
filename = self._client.prepare_download(filename)
|
|
add_commands(self._script,
|
|
"flash probe {bankname}",
|
|
"flash read_bank {bankname} {{{filename}}} {address:#x} {length}",
|
|
bankname=bankname, filename=filename, address=address, length=length)
|
|
|
|
def start(self):
|
|
raise NotImplementedError
|
|
|
|
def script(self):
|
|
return [
|
|
*self._board_script,
|
|
*self._preinit_script,
|
|
*self._script,
|
|
"exit"
|
|
]
|
|
|
|
def run(self):
|
|
cmdline = ["openocd"]
|
|
if isinstance(self._client, LocalClient):
|
|
cmdline += ["-s", scripts_path()]
|
|
cmdline += ["-c", "; ".join(self.script())]
|
|
|
|
cmdline = [arg.replace("{", "{{").replace("}", "}}") for arg in cmdline]
|
|
self._client.run_command(cmdline)
|
|
self._client.download()
|
|
|
|
self._script = []
|
|
|
|
|
|
class ProgrammerXC7(Programmer):
|
|
_sector_size = 0x10000
|
|
|
|
def __init__(self, client, preinit_script, board, proxy):
|
|
Programmer.__init__(self, client, preinit_script)
|
|
self._proxy = proxy
|
|
|
|
if board != "efc":
|
|
add_commands(self._board_script,
|
|
"source {boardfile}",
|
|
boardfile=self._transfer_script("board/{}.cfg".format(board)))
|
|
else:
|
|
add_commands(self._board_script,
|
|
# OpenOCD does not have the efc board file so custom script is included.
|
|
# To be used with Digilent-HS2 Programming Cable but the config in digilent-hs2.cfg is wrong
|
|
# See digilent_jtag_smt2_nc.cfg for details
|
|
"source [find interface/ftdi/digilent_jtag_smt2_nc.cfg]",
|
|
|
|
"ftdi tdo_sample_edge falling",
|
|
|
|
"reset_config none",
|
|
"transport select jtag",
|
|
"adapter speed 25000",
|
|
|
|
"source [find cpld/xilinx-xc7.cfg]",
|
|
"source [find cpld/jtagspi.cfg]",
|
|
"source [find fpga/xilinx-xadc.cfg]",
|
|
"source [find fpga/xilinx-dna.cfg]"
|
|
)
|
|
self.add_flash_bank("spi0", "xc7", index=0)
|
|
|
|
add_commands(self._script, "xadc_report xc7.tap")
|
|
|
|
def load_proxy(self):
|
|
self.load(find_proxy_bitfile(self._proxy), pld=0)
|
|
|
|
def start(self):
|
|
add_commands(self._script,
|
|
"xc7_program xc7.tap")
|
|
|
|
|
|
def main():
|
|
args = get_argparser().parse_args()
|
|
common_args.init_logger_from_args(args)
|
|
|
|
config = {
|
|
"kasli": {
|
|
"programmer": partial(ProgrammerXC7, board="kasli", proxy="bscan_spi_xc7a100t.bit"),
|
|
"gateware": ("spi0", 0x000000),
|
|
"bootloader": ("spi0", 0x400000),
|
|
"storage": ("spi0", 0x440000),
|
|
"firmware": ("spi0", 0x450000),
|
|
},
|
|
"efc": {
|
|
"programmer": partial(ProgrammerXC7, board="efc", proxy="bscan_spi_xc7a100t.bit"),
|
|
"gateware": ("spi0", 0x000000),
|
|
"bootloader": ("spi0", 0x400000),
|
|
"storage": ("spi0", 0x440000),
|
|
"firmware": ("spi0", 0x450000),
|
|
},
|
|
"kc705": {
|
|
"programmer": partial(ProgrammerXC7, board="kc705", proxy="bscan_spi_xc7k325t.bit"),
|
|
"gateware": ("spi0", 0x000000),
|
|
"bootloader": ("spi0", 0xaf0000),
|
|
"storage": ("spi0", 0xb30000),
|
|
"firmware": ("spi0", 0xb40000),
|
|
},
|
|
}[args.target]
|
|
|
|
if not args.action:
|
|
args.action = "gateware bootloader firmware start".split()
|
|
needs_artifacts = any(
|
|
action in args.action
|
|
for action in ["gateware", "bootloader", "firmware", "load"])
|
|
if needs_artifacts and args.dir is None:
|
|
raise ValueError("the directory containing the binaries need to be specified using -d.")
|
|
|
|
binary_dir = args.dir
|
|
|
|
if args.host is None:
|
|
client = LocalClient()
|
|
else:
|
|
client = SSHClient(args.host, args.jump)
|
|
|
|
programmer = config["programmer"](client, preinit_script=args.preinit_command)
|
|
|
|
def artifact_path(this_binary_dir, *path_filename):
|
|
if args.srcbuild:
|
|
# source tree - use path elements to locate file
|
|
return os.path.join(this_binary_dir, *path_filename)
|
|
else:
|
|
# flat tree - all files in the same directory, discard path elements
|
|
*_, filename = path_filename
|
|
return os.path.join(this_binary_dir, filename)
|
|
|
|
def convert_gateware(bit_filename):
|
|
bin_handle, bin_filename = tempfile.mkstemp(
|
|
prefix="artiq_", suffix="_" + os.path.basename(bit_filename))
|
|
with open(bit_filename, "rb") as bit_file, open(bin_handle, "wb") as bin_file:
|
|
bit2bin(bit_file, bin_file)
|
|
atexit.register(lambda: os.unlink(bin_filename))
|
|
return bin_filename
|
|
|
|
for action in args.action:
|
|
if action == "gateware":
|
|
gateware_bin = convert_gateware(
|
|
artifact_path(binary_dir, "gateware", "top.bit"))
|
|
programmer.write_binary(*config["gateware"], gateware_bin)
|
|
elif action == "bootloader":
|
|
bootloader_bin = artifact_path(binary_dir, "software", "bootloader", "bootloader.bin")
|
|
programmer.write_binary(*config["bootloader"], bootloader_bin)
|
|
elif action == "storage":
|
|
storage_img = args.storage
|
|
programmer.write_binary(*config["storage"], storage_img)
|
|
elif action == "firmware":
|
|
firmware_fbis = []
|
|
for firmware in "satman", "runtime":
|
|
filename = artifact_path(binary_dir, "software", firmware, firmware + ".fbi")
|
|
if os.path.exists(filename):
|
|
firmware_fbis.append(filename)
|
|
if not firmware_fbis:
|
|
raise FileNotFoundError("no firmware found")
|
|
if len(firmware_fbis) > 1:
|
|
raise ValueError("more than one firmware file, please clean up your build directory. "
|
|
"Found firmware files: {}".format(" ".join(firmware_fbis)))
|
|
programmer.write_binary(*config["firmware"], firmware_fbis[0])
|
|
elif action == "load":
|
|
gateware_bit = artifact_path(binary_dir, "gateware", "top.bit")
|
|
programmer.load(gateware_bit, 0)
|
|
elif action == "start":
|
|
programmer.start()
|
|
elif action == "erase":
|
|
programmer.erase_flash("spi0")
|
|
else:
|
|
raise ValueError("invalid action", action)
|
|
|
|
if args.dry_run:
|
|
print("\n".join(programmer.script()))
|
|
else:
|
|
programmer.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|