From 826faba032dfa2516493bf421c2fc84c17724e53 Mon Sep 17 00:00:00 2001 From: Michael Ossmann Date: Mon, 22 Jul 2024 23:00:34 -0400 Subject: [PATCH 1/4] cli: Force FPGA offline before reading flash UID Fix #102 --- apollo_fpga/commands/cli.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apollo_fpga/commands/cli.py b/apollo_fpga/commands/cli.py index 8dfa510..855a456 100755 --- a/apollo_fpga/commands/cli.py +++ b/apollo_fpga/commands/cli.py @@ -84,10 +84,12 @@ def print_device_info(device, args): logging.info(f"\tSerial number: {device.serial_number}") logging.info(f"\tFirmware version: {device.get_firmware_version()}") logging.info(f"\tUSB API version: {device.get_usb_api_version_string()}") - with device.jtag as jtag: - programmer = device.create_jtag_programmer(jtag) - flash_uid = programmer.read_flash_uid() - logging.info(f"\tFlash UID: {flash_uid:016x}") + if args.force_offline: + device.force_fpga_offline() + with device.jtag as jtag: + programmer = device.create_jtag_programmer(jtag) + flash_uid = programmer.read_flash_uid() + logging.info(f"\tFlash UID: {flash_uid:016x}") def print_chain_info(device, args): """ Command that prints information about devices connected to the scan chain to the console. """ From 3e3a16309320ba10bee2794fed14c0c7279f1a74 Mon Sep 17 00:00:00 2001 From: Michael Ossmann Date: Mon, 22 Jul 2024 23:04:00 -0400 Subject: [PATCH 2/4] cli: Always force FPGA offline for flash-info It is not safe to access FPGA configuration flash via JTAG if the FPGA is online and may be trying to use the flash. --- apollo_fpga/commands/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apollo_fpga/commands/cli.py b/apollo_fpga/commands/cli.py index 855a456..e83f319 100755 --- a/apollo_fpga/commands/cli.py +++ b/apollo_fpga/commands/cli.py @@ -241,7 +241,7 @@ def read_back_flash(device, args): def print_flash_info(device, args): """ Command that prints information about the currently connected FPGA's configuration flash. """ - ensure_unconfigured(device) + device.force_fpga_offline() serial_number = device.serial_number with device.jtag as jtag: @@ -351,7 +351,7 @@ def jtag_debug_spi_register(device, args): help="Print device info.", ), Command("jtag-scan", handler=print_chain_info, help="Prints information about devices on the onboard JTAG chain."), - Command("flash-info", handler=print_flash_info, args=[(("--force-offline",), dict(action='store_true'))], + Command("flash-info", handler=print_flash_info, help="Prints information about the FPGA's attached configuration flash."), # Flash commands From 0c8d013e1b09476c858d230263e2110499d533bc Mon Sep 17 00:00:00 2001 From: Michael Ossmann Date: Tue, 23 Jul 2024 12:47:55 -0400 Subject: [PATCH 3/4] cli: Print info for multiple devices Fix #81 --- apollo_fpga/__init__.py | 172 +++++++++++++++++++++++++++++------- apollo_fpga/commands/cli.py | 9 +- 2 files changed, 149 insertions(+), 32 deletions(-) diff --git a/apollo_fpga/__init__.py b/apollo_fpga/__init__.py index 60d71c5..29bd19b 100644 --- a/apollo_fpga/__init__.py +++ b/apollo_fpga/__init__.py @@ -9,6 +9,7 @@ import usb.core import platform import errno +import sys from .jtag import JTAGChain from .spi import DebugSPIConnection @@ -18,6 +19,10 @@ from .onboard_jtag import * +import importlib.metadata +__version__ = importlib.metadata.version(__package__) + + class DebuggerNotFound(IOError): pass @@ -84,12 +89,15 @@ class ApolloDebugger: 0xFE: "Amalthea" } + backend = None + - def __init__(self, force_offline=False): + def __init__(self, force_offline=False, device=None): """ Sets up a connection to the debugger. """ # Try to create a connection to our Apollo debug firmware. - device = self._find_device(self.APOLLO_USB_IDS, custom_match=self._device_has_apollo_id) + if device is None: + device = self._find_device(self.APOLLO_USB_IDS, custom_match=self._device_has_apollo_id) # If Apollo VID/PID is not found, try to find a gateware VID/PID with a valid Apollo stub # interface. If found, request the gateware to liberate the USB port. In devices with a @@ -99,10 +107,9 @@ def __init__(self, force_offline=False): # First, find the candidate device... fpga_device = self._find_device(self.LUNA_USB_IDS, custom_match=self._device_has_stub_iface) if fpga_device is None: - raise DebuggerNotFound("No Apollo device or stub interface found.") + raise DebuggerNotFound("No Apollo debugger or stub interface found.") elif not force_offline: - raise DebuggerNotFound("Apollo stub interface found. " - "Switch the device to Apollo mode or add the `--force-offline` option.") + raise DebuggerNotFound("Apollo stub interface found but not requested to be forced offline.") # ... and now request a USB handoff to Apollo try: @@ -113,7 +120,7 @@ def __init__(self, force_offline=False): # Wait for Apollo to enumerate and try again device = self._find_device(self.APOLLO_USB_IDS, custom_match=self._device_has_apollo_id, timeout=5000) if device is None: - raise DebuggerNotFound("Handoff was requested, but Apollo is not available") + raise DebuggerNotFound("Handoff was requested, but Apollo debugger not found.") self.device = device self.major, self.minor = self.get_hardware_revision() @@ -147,30 +154,35 @@ def _request_handoff(cls, device): request_type = usb.ENDPOINT_OUT | usb.RECIP_INTERFACE | usb.TYPE_VENDOR device.ctrl_transfer(request_type, REQUEST_APOLLO_ADV_STOP, wIndex=intf_number, timeout=5000) - @staticmethod - def _find_device(ids, custom_match=None, timeout=0): - import usb.backend.libusb1 - - # In Windows, we need to specify the libusb library location to create a backend. - if platform.system() == "Windows": - # Determine the path to libusb-1.0.dll. - try: - from importlib_resources import files # <= 3.8 - except: - from importlib.resources import files # >= 3.9 - libusb_dll = os.path.join(files("usb1"), "libusb-1.0.dll") - - # Create a backend by explicitly passing the path to libusb_dll. - backend = usb.backend.libusb1.get_backend(find_library=lambda x: libusb_dll) - else: - # On other systems we can just use the default backend. - backend = usb.backend.libusb1.get_backend() + @classmethod + def _init_backend(cls): + """Initialize the USB backend.""" + if cls.backend is None: + import usb.backend.libusb1 + + # In Windows, we need to specify the libusb library location to create a backend. + if platform.system() == "Windows": + # Determine the path to libusb-1.0.dll. + try: + from importlib_resources import files # <= 3.8 + except: + from importlib.resources import files # >= 3.9 + libusb_dll = os.path.join(files("usb1"), "libusb-1.0.dll") + + # Create a backend by explicitly passing the path to libusb_dll. + cls.backend = usb.backend.libusb1.get_backend(find_library=lambda x: libusb_dll) + else: + # On other systems we can just use the default backend. + cls.backend = usb.backend.libusb1.get_backend() - # Find the device. + @classmethod + def _find_device(cls, ids, custom_match=None, timeout=0): + """Find a USB device matching a list of VID/PIDs.""" + cls._init_backend() wait = 0 while True: for vid, pid in ids: - device = usb.core.find(backend=backend, idVendor=vid, idProduct=pid, custom_match=custom_match) + device = usb.core.find(backend=cls.backend, idVendor=vid, idProduct=pid, custom_match=custom_match) if device is not None: return device # Should we wait and try again? @@ -178,12 +190,33 @@ def _find_device(ids, custom_match=None, timeout=0): break time.sleep(0.5) wait += 500 - return None + @classmethod + def _find_all_devices(cls, ids, custom_match=None): + """Find all USB devices matching a list of VID/PIDs.""" + cls._init_backend() + devices = [] + candidates = [] + + for vid, pid in ids: + candidates.extend(usb.core.find(True, cls.backend, idVendor=vid, idProduct=pid)) + + for device in candidates: + try: + if custom_match is None or custom_match(device): + devices.append(device) + except (usb.USBError, NotImplementedError): + # A permissions error or NotImplementedError is likely on + # Windows if the device is not the expected type of device. + # Other typical errors are transient conditions shortly after + # device enumeration of a device that is not yet ready. + pass + return devices + @staticmethod def _device_has_stub_iface(device, return_iface=False): - """ Checks if a device has an Apollo stub interface present. + """Check if a device has an Apollo stub interface present. Optionally return the interface itself. """ @@ -195,12 +228,10 @@ def _device_has_stub_iface(device, return_iface=False): @staticmethod def _device_has_apollo_id(device): - """ Checks if a device identifies itself as Apollo.""" + """Check if a device identifies itself as an Apollo debugger.""" request_type = usb.ENDPOINT_IN | usb.RECIP_DEVICE | usb.TYPE_VENDOR try: response = device.ctrl_transfer(request_type, ApolloDebugger.REQUEST_GET_ID, data_or_wLength=256, timeout=500) - apollo_id = bytes(response).decode('utf-8').split('\x00')[0] - return True if "Apollo" in apollo_id else False except usb.USBError as e: if e.errno == errno.EPIPE: # A pipe error occurs when the device does not implement a @@ -209,6 +240,10 @@ def _device_has_apollo_id(device): return False else: raise + finally: + usb.util.dispose_resources(device) + apollo_id = bytes(response).decode('utf-8').split('\x00')[0] + return True if "Apollo" in apollo_id else False def detect_connected_version(self): """ Attempts to determine the revision of the connected hardware. @@ -376,3 +411,78 @@ def get_usb_api_version(self): def get_usb_api_version_string(self): (api_major, api_minor) = self.get_usb_api_version() return (f"{api_major}.{api_minor}") + + @classmethod + def print_info(cls, ids=None, stub_ids=None, force_offline=False, timeout=5000, out=print): + """ Print information about Apollo and all connected Apollo devices. + + Return True if any connected device is found. + """ + out(f"Apollo version: {__version__}") + out(f"Python version: {sys.version}\n") + + found_device = False + if ids is None: + ids = cls.APOLLO_USB_IDS + if stub_ids is None: + stub_ids = cls.LUNA_USB_IDS + + # Look for devices with stub interfaces. + stub_devs = cls._find_all_devices(stub_ids, cls._device_has_stub_iface) + + for device in stub_devs: + found_device = True + out("Found Apollo stub interface!") + out(f"\tBitstream: {device.product} ({device.manufacturer})") + out(f"\tVendor ID: {device.idVendor:04x}") + out(f"\tProduct ID: {device.idProduct:04x}") + out(f"\tbcdDevice: {device.bcdDevice:04x}") + out(f"\tBitstream serial number: {device.serial_number}") + out("") + + count = 0 + if force_offline: + apollo_devs = cls._find_all_devices(ids, cls._device_has_apollo_id) + count = len(apollo_devs) + len(stub_devs) + for device in apollo_devs: + usb.util.dispose_resources(device) + if count > 0: + out(f"Forcing offline.\n") + for device in stub_devs: + try: + cls._request_handoff(device) + except usb.USBError as e: + raise DebuggerNotFound(f"Handoff request failed: {e.strerror}") + + # Look for Apollo debuggers. + start = time.time() + while True: + apollo_devs = cls._find_all_devices(ids, cls._device_has_apollo_id) + if len(apollo_devs) >= count: + break; + if ((time.time() - start) * 1000) >= timeout: + raise DebuggerNotFound("Handoff was requested, but Apollo debugger not found.") + time.sleep(0.1) + + for device in apollo_devs: + found_device = True + debugger = ApolloDebugger(device=device) + out(f"Found {debugger.get_compatibility_string()} device!") + out(f"\tHardware: {debugger.get_hardware_name()}") + out(f"\tManufacturer: {device.manufacturer}") + out(f"\tProduct: {device.product}") + out(f"\tSerial number: {device.serial_number}") + out(f"\tVendor ID: {device.idVendor:04x}") + out(f"\tProduct ID: {device.idProduct:04x}") + out(f"\tbcdDevice: {device.bcdDevice:04x}") + out(f"\tFirmware version: {debugger.get_firmware_version()}") + out(f"\tUSB API version: {debugger.get_usb_api_version_string()}") + if force_offline: + debugger.force_fpga_offline() + with debugger.jtag as jtag: + programmer = debugger.create_jtag_programmer(jtag) + flash_uid = programmer.read_flash_uid() + out(f"\tFlash UID: {flash_uid:016x}") + out("") + + return found_device diff --git a/apollo_fpga/commands/cli.py b/apollo_fpga/commands/cli.py index e83f319..ea22dea 100755 --- a/apollo_fpga/commands/cli.py +++ b/apollo_fpga/commands/cli.py @@ -416,11 +416,18 @@ def main(): # Force the FPGA offline by default in most commands to force Apollo mode if needed. force_offline = args.force_offline if "force_offline" in args else True - device = ApolloDebugger(force_offline=force_offline) # Set up python's logging to act as a simple print, for now. logging.basicConfig(level=logging.INFO, format="%(message)-s") + if args.command == "info": + if ApolloDebugger.print_info(force_offline=force_offline, out=logging.info): + if not force_offline: + logging.info(f"For additional device information use the --force-offline option.") + return + + device = ApolloDebugger(force_offline=force_offline) + # Execute the relevant command. args.func(device, args) From dc29b155282c7e9c3444f4d48520e5dc4bd39959 Mon Sep 17 00:00:00 2001 From: Michael Ossmann Date: Tue, 23 Jul 2024 12:48:23 -0400 Subject: [PATCH 4/4] cli: Deprecate print_device_info() --- apollo_fpga/commands/cli.py | 6 +++++- pyproject.toml | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apollo_fpga/commands/cli.py b/apollo_fpga/commands/cli.py index ea22dea..74e4f88 100755 --- a/apollo_fpga/commands/cli.py +++ b/apollo_fpga/commands/cli.py @@ -18,8 +18,9 @@ from collections import namedtuple import xdg.BaseDirectory from functools import partial +import deprecation -from apollo_fpga import ApolloDebugger +from apollo_fpga import ApolloDebugger, __version__ from apollo_fpga.jtag import JTAGChain, JTAGPatternError from apollo_fpga.ecp5 import ECP5_JTAGProgrammer, ECP5FlashBridgeProgrammer from apollo_fpga.onboard_jtag import * @@ -76,6 +77,9 @@ } +@deprecation.deprecated(deprecated_in="1.1.0", removed_in="2.0.0", + current_version=__version__, + details="Use ApolloDebugger.print_info() instead.") def print_device_info(device, args): """ Command that prints information about devices connected to the scan chain to the console. """ diff --git a/pyproject.toml b/pyproject.toml index 1bdc1d3..97cc8c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "pyvcd>=0.2.4", "prompt-toolkit>3.0.16", "pyxdg>=0.27", + "deprecation>=2.1.0", ] dynamic = ["version"]