Skip to content

Commit

Permalink
Merge pull request #107 from mossmann/info
Browse files Browse the repository at this point in the history
Print info for multiple devices
  • Loading branch information
mossmann authored Jul 24, 2024
2 parents ec9d7c6 + dc29b15 commit cf5d358
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 39 deletions.
172 changes: 141 additions & 31 deletions apollo_fpga/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import usb.core
import platform
import errno
import sys

from .jtag import JTAGChain
from .spi import DebugSPIConnection
Expand All @@ -18,6 +19,10 @@

from .onboard_jtag import *

import importlib.metadata
__version__ = importlib.metadata.version(__package__)


class DebuggerNotFound(IOError):
pass

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -147,43 +154,69 @@ 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?
if wait >= timeout:
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.
"""
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
29 changes: 21 additions & 8 deletions apollo_fpga/commands/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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. """

Expand All @@ -84,10 +88,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. """
Expand Down Expand Up @@ -239,7 +245,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:
Expand Down Expand Up @@ -349,7 +355,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
Expand Down Expand Up @@ -414,11 +420,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)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies = [
"pyvcd>=0.2.4",
"prompt-toolkit>3.0.16",
"pyxdg>=0.27",
"deprecation>=2.1.0",
]
dynamic = ["version"]

Expand Down

0 comments on commit cf5d358

Please sign in to comment.