Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print info for multiple devices #107

Merged
merged 4 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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