From a72df48eb6b90f1ecda5b0d1fa6bc09693308dc5 Mon Sep 17 00:00:00 2001 From: Aki Van Ness Date: Thu, 7 Nov 2024 07:16:40 -0800 Subject: [PATCH] squishy: big-renovation-wip --- setup.py | 2 +- squishy/__init__.py | 10 +- squishy/actions/__init__.py | 489 +++++++---- squishy/actions/applet.py | 308 +++---- squishy/actions/cache.py | 114 --- squishy/actions/provision.py | 197 ++--- squishy/applets/__init__.py | 169 ++-- squishy/applets/analyzer/__init__.py | 35 +- squishy/applets/taperipper/__init__.py | 190 ---- squishy/applets/taperipper/fat32.py | 47 - squishy/applets/taperipper/gpt.py | 63 -- squishy/applets/taperipper/tape.py | 29 - squishy/cli.py | 175 ++-- squishy/config.py | 30 - squishy/core/__init__.py | 9 +- .../bootloader => core}/bitstream.py | 84 +- squishy/core/cache.py | 154 ++-- squishy/core/collect.py | 101 --- squishy/core/config.py | 300 +++++++ squishy/core/device.py | 540 ------------ squishy/core/{dfu_types.py => dfu.py} | 9 +- squishy/core/exceptions.py | 24 - squishy/core/flash.py | 163 +++- squishy/core/reflection.py | 78 ++ squishy/device.py | 817 ++++++++++++++++++ squishy/gateware/__init__.py | 125 +-- squishy/gateware/applet/__init__.py | 54 +- squishy/gateware/applet/elaboratable.py | 51 -- squishy/gateware/bootloader/__init__.py | 236 ++++- squishy/gateware/bootloader/dfu.py | 436 ---------- squishy/gateware/bootloader/rev1.py | 252 +++--- squishy/gateware/bootloader/rev2.py | 80 ++ squishy/gateware/core/__init__.py | 35 +- squishy/gateware/core/pll.py | 148 ---- squishy/gateware/core/scsi.py | 233 ----- squishy/gateware/core/uart.py | 108 --- .../gateware/peripherals}/__init__.py | 9 +- .../gateware/{core => peripherals}/flash.py | 26 +- .../{ => peripherals}/scsi/__init__.py | 82 +- .../peripherals/scsi/quirks/__init__.py | 9 + squishy/gateware/{core => peripherals}/spi.py | 9 +- squishy/gateware/peripherals/usb/__init__.py | 9 + squishy/gateware/peripherals/usb/dfu.py | 392 +++++++++ .../peripherals/usb/quirks/__init__.py | 13 + .../usb => peripherals/usb/quirks}/windows.py | 41 +- squishy/gateware/platform/__init__.py | 175 +++- squishy/gateware/platform/mixins.py | 84 -- squishy/gateware/platform/platform.py | 87 -- .../gateware/platform/resources/__init__.py | 12 +- squishy/gateware/platform/resources/scsi.py | 324 ------- squishy/gateware/platform/rev1.py | 297 +++++-- squishy/gateware/platform/rev2.py | 262 +++++- squishy/gateware/quirks/__init__.py | 15 - squishy/gateware/quirks/usb/__init__.py | 13 - squishy/gateware/scsi/common/__init__.py | 59 -- squishy/gateware/scsi/device.py | 20 - squishy/gateware/scsi/initiator.py | 20 - squishy/gateware/scsi/scsi1/__init__.py | 134 --- squishy/gateware/scsi/scsi2/__init__.py | 43 - squishy/gateware/scsi/scsi3/__init__.py | 43 - squishy/gateware/usb/__init__.py | 13 - squishy/gateware/usb/dfu.py | 213 ----- squishy/gateware/usb/rev1.py | 195 ----- squishy/gateware/usb/rev2.py | 25 - squishy/paths.py | 83 ++ squishy/scsi/__init__.py | 18 +- squishy/scsi/command.py | 9 +- squishy/scsi/commands/__init__.py | 7 +- squishy/scsi/commands/common.py | 9 +- squishy/scsi/commands/direct.py | 6 +- squishy/scsi/commands/printer.py | 5 +- squishy/scsi/commands/processor.py | 2 +- squishy/scsi/commands/ro_direct.py | 2 +- squishy/scsi/commands/sequential.py | 5 +- squishy/scsi/commands/worm.py | 3 +- squishy/scsi/common.py | 16 +- squishy/scsi/device.py | 6 +- squishy/scsi/messages.py | 8 +- squishy/scsi/vid.py | 2 +- squishy/support/__init__.py | 9 + .../support/test.py | 77 +- tests/gateware/{quirks => applet}/__init__.py | 0 .../{quirks/usb => peripherals}/__init__.py | 0 .../{ => peripherals}/scsi/__init__.py | 0 .../scsi/quirks}/__init__.py | 0 .../{core => peripherals}/test_flash.py | 62 +- .../{core => peripherals}/test_spi.py | 8 +- .../scsi1 => peripherals/usb}/__init__.py | 0 .../usb/quirks}/__init__.py | 0 .../usb/quirks}/test_windows.py | 26 +- .../usb}/test_dfu.py | 92 +- tests/gateware/usb/test_dfu.py | 51 -- .../{gateware/scsi/scsi3 => scsi}/__init__.py | 0 93 files changed, 4150 insertions(+), 4875 deletions(-) delete mode 100644 squishy/actions/cache.py delete mode 100644 squishy/applets/taperipper/__init__.py delete mode 100644 squishy/applets/taperipper/fat32.py delete mode 100644 squishy/applets/taperipper/gpt.py delete mode 100644 squishy/applets/taperipper/tape.py delete mode 100644 squishy/config.py rename squishy/{gateware/bootloader => core}/bitstream.py (66%) delete mode 100644 squishy/core/collect.py create mode 100644 squishy/core/config.py delete mode 100644 squishy/core/device.py rename squishy/core/{dfu_types.py => dfu.py} (99%) delete mode 100644 squishy/core/exceptions.py create mode 100644 squishy/core/reflection.py create mode 100644 squishy/device.py delete mode 100644 squishy/gateware/applet/elaboratable.py delete mode 100644 squishy/gateware/bootloader/dfu.py create mode 100644 squishy/gateware/bootloader/rev2.py delete mode 100644 squishy/gateware/core/pll.py delete mode 100644 squishy/gateware/core/scsi.py delete mode 100644 squishy/gateware/core/uart.py rename {tests/gateware/usb => squishy/gateware/peripherals}/__init__.py (60%) rename squishy/gateware/{core => peripherals}/flash.py (93%) rename squishy/gateware/{ => peripherals}/scsi/__init__.py (58%) create mode 100644 squishy/gateware/peripherals/scsi/quirks/__init__.py rename squishy/gateware/{core => peripherals}/spi.py (90%) create mode 100644 squishy/gateware/peripherals/usb/__init__.py create mode 100644 squishy/gateware/peripherals/usb/dfu.py create mode 100644 squishy/gateware/peripherals/usb/quirks/__init__.py rename squishy/gateware/{quirks/usb => peripherals/usb/quirks}/windows.py (94%) delete mode 100644 squishy/gateware/platform/mixins.py delete mode 100644 squishy/gateware/platform/platform.py delete mode 100644 squishy/gateware/platform/resources/scsi.py delete mode 100644 squishy/gateware/quirks/__init__.py delete mode 100644 squishy/gateware/quirks/usb/__init__.py delete mode 100644 squishy/gateware/scsi/common/__init__.py delete mode 100644 squishy/gateware/scsi/device.py delete mode 100644 squishy/gateware/scsi/initiator.py delete mode 100644 squishy/gateware/scsi/scsi1/__init__.py delete mode 100644 squishy/gateware/scsi/scsi2/__init__.py delete mode 100644 squishy/gateware/scsi/scsi3/__init__.py delete mode 100644 squishy/gateware/usb/__init__.py delete mode 100644 squishy/gateware/usb/dfu.py delete mode 100644 squishy/gateware/usb/rev1.py delete mode 100644 squishy/gateware/usb/rev2.py create mode 100644 squishy/paths.py create mode 100644 squishy/support/__init__.py rename tests/gateware_test.py => squishy/support/test.py (73%) rename tests/gateware/{quirks => applet}/__init__.py (100%) rename tests/gateware/{quirks/usb => peripherals}/__init__.py (100%) rename tests/gateware/{ => peripherals}/scsi/__init__.py (100%) rename tests/gateware/{scsi/common => peripherals/scsi/quirks}/__init__.py (100%) rename tests/gateware/{core => peripherals}/test_flash.py (85%) rename tests/gateware/{core => peripherals}/test_spi.py (88%) rename tests/gateware/{scsi/scsi1 => peripherals/usb}/__init__.py (100%) rename tests/gateware/{scsi/scsi2 => peripherals/usb/quirks}/__init__.py (100%) rename tests/gateware/{quirks/usb => peripherals/usb/quirks}/test_windows.py (83%) rename tests/gateware/{bootloader => peripherals/usb}/test_dfu.py (59%) delete mode 100644 tests/gateware/usb/test_dfu.py rename tests/{gateware/scsi/scsi3 => scsi}/__init__.py (100%) diff --git a/setup.py b/setup.py index 2258cfd3..166a5846 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def doc_ver() -> str: author_email = 'nya@catgirl.link', description = 'SCSI Multitool and Torii HDL Library', license = 'BSD-3-Clause', - python_requires = '~=3.10', + python_requires = '~=3.11', zip_safe = True, url = 'https://github.com/squishy-scsi/squishy', diff --git a/squishy/__init__.py b/squishy/__init__.py index 5906348b..428c489c 100644 --- a/squishy/__init__.py +++ b/squishy/__init__.py @@ -3,8 +3,8 @@ from sys import version_info # Bounce out if python is too old -if version_info < (3, 10): - raise RuntimeError('Python version 3.10 or newer is required to use Squishy') +if version_info < (3, 11): + raise RuntimeError('Python version 3.11 or newer is required to use Squishy') try: from importlib import metadata @@ -12,11 +12,9 @@ except ImportError: __version__ = 'unknown' # :nocov: -__all__ = ( +__all__ = () -) - -'''\ +''' ╭─────────────────────────────────────╮ │ │ │ !!! WARNING !!! │ diff --git a/squishy/actions/__init__.py b/squishy/actions/__init__.py index 28fc80d2..8ddea214 100644 --- a/squishy/actions/__init__.py +++ b/squishy/actions/__init__.py @@ -1,20 +1,26 @@ # SPDX-License-Identifier: BSD-3-Clause -import logging as log -from abc import ABCMeta, abstractmethod -from argparse import ArgumentParser, Namespace -from pathlib import Path +''' -from rich.progress import ( - Progress, SpinnerColumn, BarColumn, - TextColumn -) +''' + +import logging as log +import json + +from abc import ABCMeta, abstractmethod +from argparse import ArgumentParser, Namespace +from pathlib import Path + +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn -from ..core.device import SquishyHardwareDevice -from ..gateware.platform.platform import SquishyPlatform -from ..gateware.platform import AVAILABLE_PLATFORMS -from ..config import SQUISHY_BUILD_DIR +from torii import Elaboratable +from torii.build.run import BuildPlan, LocalBuildProducts +from ..device import SquishyDevice +from ..paths import SQUISHY_BUILD_DIR +from ..core.config import USB_VID, USB_APP_PID, USB_DFU_PID +from ..core.cache import SquishyCache +from ..gateware import AVAILABLE_PLATFORMS, SquishyPlatformType __all__ = ( 'SquishyAction', @@ -23,74 +29,66 @@ class SquishyAction(metaclass = ABCMeta): ''' - Squishy action base class + Base class for all invocable actions from the Squishy CLI. - This is the abstract base class that is used - to implement any possible action for the squishy - command line interface. + This defines a common interface that the main CLI, or any other + consumer of Squishy can use to reliably invoke actions. Attributes ---------- - pretty_name : str - The pretty name of the action to show. - - short_help : str - A short help string for the action. - - help : str - A more comprehensive help string for the action. + name : str + The name used to invoke the action and display in the help documentation. description : str - The description of the action. + A short description of what this action does, used in the help. requires_dev : bool - If this action requires a Squishy to be attached to the machine. + Whether or not this action requires physical Squishy hardware. + + Note + ---- + Actions should be sure to also overload the doc comments when derived + as to allow for :py:class:`HelpAction` to generate appropriate long-form + documentation when invoked. ''' - @property - @abstractmethod - def pretty_name(self) -> str: - ''' The pretty name of the action ''' - raise NotImplementedError('Actions must implement this property') @property @abstractmethod - def short_help(self) -> str: - ''' A short help description for the action ''' + def name(self) -> str: + ''' The name of the action. ''' raise NotImplementedError('Actions must implement this property') @property - def help(self) -> str: - ''' A longer help message for the action ''' - return '' - - @property + @abstractmethod def description(self) -> str: - ''' A description for the action ''' - return '' + ''' Short description of the action. ''' + raise NotImplementedError('Actions must implement this property') @property @abstractmethod def requires_dev(self) -> bool: - ''' Does this action require a squishy device to be attached ''' + ''' Whether or not this action requires a physical hardware device. ''' raise NotImplementedError('Actions must implement this property') - def __init__(self): + def __init__(self) -> None: pass @abstractmethod def register_args(self, parser: ArgumentParser) -> None: ''' - Register action arguments. + Register action argument parsers. - When an action instance is initialized this method is - called so when :py:func:`run` is called any needed - arguments can be passed to the action. + After initialization, but prior to being invoked with :py:func:`.run` + this method will be called to allow the action to register any wanted + command line options. + + This is also used when displaying help. Parameters ---------- parser : argparse.ArgumentParser - The argument parser to register commands with. + The Squishy CLI argument parser group to register arguments into. Raises ------ @@ -98,23 +96,22 @@ def register_args(self, parser: ArgumentParser) -> None: The abstract method must be implemented by the action. ''' - raise NotImplementedError('Actions must implement this method') @abstractmethod - def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: + def run(self, args: Namespace, dev: SquishyDevice | None = None) -> int: ''' - Run the action. + Invoke the action. - Run the action instance, passing the parsed - arguments and the selected device if any. + This method is run when the Squishy CLI has determined that this action + was to be called. Parameters ---------- args : argsparse.Namespace - Any command line arguments passed. + The parsed arguments from the Squishy CLI - dev : Optional[squishy.core.device.SquishyHardwareDevice] + dev : squishy.device.SquishyDevice | None The device this action was invoked on if any. Returns @@ -128,245 +125,355 @@ def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: The abstract method must be implemented by the action. ''' - raise NotImplementedError('Actions must implement this method') - class SquishySynthAction(SquishyAction): ''' - This class is a sub-type of :py:class:`SquishyAction` that is dedicated to actions - that deal with building gateware for Squishy hardware platforms. + Common base class derived from :py:`SquishyAction` for all Squishy CLI + actions that synthesize gateware. + + This lets us abstract away needed common command line argument setup and + parsing, and having all the needed machinery for it be self-contained. + + There are three additional methods that this provides that are for use + by synthesis based actions. - It centralizes the needed arguments for gateware Synthesis as well as Place and Routing, - allowing for it to be updated without needing duplicated effort. + The first is :py:meth:`.get_platform`, this will return the appropriate + :py:class:`squishy.gateware.SquishyPlatform` for the given hardware device + that is attached, or ``None`` if it's not able to determine the platform or + a device is not attached + + The next is :py:meth:`.register_synth_args`, this provides the registration + mechanism to populate the action with all relevant command line arguments + related to the synthesis of the gateware for the target device. + + Finally there is :py:meth:`.run_synth` which does the actual invocation of + the synthesis run. ''' def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) + self._cache = SquishyCache() + + def get_platform(self, args: Namespace, dev: SquishyDevice | None) -> type[SquishyPlatformType] | None: + ''' + Get the platform to synthesize for, either from the selected device or the `--platform` cli option. + Parameters + ---------- + args : argsparse.Namespace + The parsed arguments from the action invocation. + + dev : SquishyDevice | None + The optional device to extract the platform from - def get_hw_platform( - self, args: Namespace, dev: SquishyHardwareDevice | None - ) -> tuple[SquishyPlatform, str, SquishyHardwareDevice] | None: - ''' Acquire the connected or specified hardware platform ''' - if not args.build_only and dev is None: - dev = SquishyHardwareDevice.get_device(serial = args.device) + Returns + ------- + SquishyPlatformType | None + The extracted platform if possible, otherwise None + ''' - if dev is None: - log.error('No device selected, unable to continue.') - return None + # TODO(aki): If we are in `--build-only` mode, should we override the resulting platform if we *do* have a dev? + # This means we need to change `--platform` so it doesn't have a default. - hardware_platform = f'rev{dev.rev}' - if hardware_platform not in AVAILABLE_PLATFORMS.keys(): - log.error(f'Unknown hardware revision \'{hardware_platform}\'') - log.error(f'Expected one of {", ".join(AVAILABLE_PLATFORMS.keys())}') - return None + # If we were passed a device, pull the platform from that + if dev is not None: + plat = dev.get_platform() + if plat is None: + log.error(f'Attempted to get platform for device {dev.serial}, but failed?') + # Otherwise, get it the long way else: - hardware_platform = args.hardware_platform - - log.info(f'Targeting platform \'{hardware_platform}\'') - return (AVAILABLE_PLATFORMS[hardware_platform](), hardware_platform, dev) + plat = AVAILABLE_PLATFORMS.get(args.platform, None) + if plat is None: + log.error(f'Unknown platform {args.platform}') + return plat def run_synth( - self, args: Namespace, plat: SquishyPlatform, elab, elab_name: str, cacheable: bool = False - ): # -> tuple[str, LocalBuildProducts]: - ''' Run Synthesis and Place and Route ''' + self, args: Namespace, platform: SquishyPlatformType, elaboratable: Elaboratable, name: str, cacheable: bool = False + ) -> LocalBuildProducts: + ''' + Run gateware synthesis, place-and-route, and bitstream packing in a cache-aware manner. + + Parameters + ---------- + args : argsparse.Namespace + The parsed arguments from the action invocation. + + platform : SquishyPlatformType + The target Squishy platform we are synthesizing for. + + elaboratable : torii.Elaboratable + The root/'top' gateware module to synthesize. + + name : str + The root/'top' gateware module name. + + cacheable : bool + Whether or not to process cache-related options. + + Returns + ------- + LocalBuildProducts + The resulting built artifacts. + ''' synth_opts: list[str] = [] pnr_opts: list[str] = [] - pack_ops: list[str] = [] + pack_opts: list[str] = [] + script_pre_synth = '' script_post_synth = '' - build_dir = Path(args.build_dir) + # TODO(aki): We might want to split the applet and bootloader build dirs into their own sub-dirs? + # Default to the typical build directory unless otherwise specified + build_dir = SQUISHY_BUILD_DIR + if args.build_dir is not None: + build_dir = Path(args.build_dir) if not build_dir.exists(): - log.debug(f'Making build directory {args.build_dir}') - build_dir.mkdir() - else: - log.debug(f'Using build directory {args.build_dir}') + log.debug(f'Creating build directory {build_dir}') + build_dir.mkdir(parents = True) - # Build Options + # By default skip cache + skip_cache = not cacheable if cacheable: - skip_cache = args.skip_cache - else: - skip_cache = True + skip_cache: bool = args.skip_cache # Synthesis Options if not args.no_abc9: synth_opts.append('-abc9') - if args.aggressive_mapping: + + if not args.no_aggressive_mapping: if args.no_abc9: - log.error('Can not spcify `--aggressive-mapping` with ABC9 disabled, remove `--no-abc9`') + log.warning('option `--no_aggressive_mapping` passed along with `--no-abc9`, ignoring') else: script_pre_synth += 'scratchpad -copy abc9.script.flow3 abc9.script\n' - # Place and Route Options - if args.use_router2: - pnr_opts.append('--router router2') - else: - pnr_opts.append('--router router1') - - if args.tmg_ripup: - pnr_opts.append('--tmg-ripup') - - if args.detailed_timing_report: - pnr_opts.append('--report timing.json') + # Place-and-Route Options + pnr_opts.append(f'--report {name}.pnr.json') + if args.detailed_report: pnr_opts.append('--detailed-timing-report') - if args.routed_svg is not None: - svg_path = args.routed_svg.resolve() - log.info(f'Writing PnR output svg to {svg_path}') - pnr_opts.append(f'--routed-svg {svg_path}') + if args.routed_netlist is not None: + netlist_out: Path = args.routed_netlist + pnr_opts.append(f'--write {netlist_out.resolve()}') - if args.routed_json is not None: - json_path = args.routed_json.resolve() - log.info(f'Writing PnR output json to {json_path}') - pnr_opts.append(f'--write {json_path}') - - if args.pnr_seed is not None: + # If the seed is negative, use a random seed + if args.pnr_seed > 0: + pnr_opts.append('-r') + else: pnr_opts.append(f'--seed {args.pnr_seed}') - # Bitstream packing options - if args.compress: - pack_ops.append('--compress') + # Packing Options + if not args.dont_compress: + pack_opts.append('--compress') - # Actually do the build + log.info(f'Using platform version: {platform.revision_str}') + log.info(f' Device: {platform.device}-{platform.package}') + + # Run the synth, pnr, et. al. with Progress( SpinnerColumn(), TextColumn('[progress.description]{task.description}'), BarColumn(bar_width = None), transient = True ) as progress: - name, prod = plat.build( - elab, - name = elab_name, + # First we run a `prepare` which will do RTL generation + + task = progress.add_task('Elaborating Bitstream', start = False) + + plan: BuildPlan = platform.prepare( + elaboratable, + name = name, build_dir = build_dir, - do_build = True, - do_program = False, synth_opts = synth_opts, nextpnr_opts = pnr_opts, - ecppack_opts = pack_ops, - verbose = args.loud, - skip_cache = skip_cache, - progress = progress, + ecppack_opts = pack_opts, + verbose = args.build_verbose, debug_verilog = cacheable and not skip_cache, script_after_read = script_pre_synth, script_after_synth = script_post_synth ) - return (name, prod) + # If we are not skipping the cache, try to get the built result + prod = None + if not skip_cache: + prod = self._cache.get(name, plan) + + # Run the build + if prod is None: + log.info('Bitstream was not cached, this might take [yellow][i]a while[/][/]', extra = { 'markup': True }) + progress.update(task, description = 'Building bitstream') + prod = plan.execute_local(build_dir) + + progress.remove_task(task) + + # If we're allowed to, cache the products and then return that cached version + if not skip_cache: + prod = self._cache.store(name, prod) + + # If we're in verbose logging mode, go the extra step and print out the utilization report + if args.verbose: + self.dump_utilization(name, prod) + + return prod + + def register_synth_args(self, parser: ArgumentParser, cacheable: bool = False) -> None: - ''' Register the common gateware options ''' + ''' + Register common Synthesis, Place and Route, and Bitstream packing options. + + Parameters + ---------- + parser : argsparse.ArgumentParser + The root action argument parser to register the options into. + + cacheable : bool + Whether or not to show cache-related options. + ''' parser.add_argument( '--platform', '-p', - dest = 'hardware_platform', type = str, - default = list(AVAILABLE_PLATFORMS.keys())[-1], + default = list(AVAILABLE_PLATFORMS.keys())[-1], # Always pick the latest platform as the default choices = list(AVAILABLE_PLATFORMS.keys()), - help = 'The target hardware platform if using --build-only', + help = 'The target hardware platform to synthesize for.' ) - gateware_options = parser.add_argument_group('Gateware Options') - - synth_options = parser.add_argument_group('Synthesis Options') - pnr_options = parser.add_argument_group('Place and Route Options') - pack_options = parser.add_argument_group('Packing Options') + generic_options = parser.add_argument_group('Generic Options') - gateware_options.add_argument( - '--build-only', + # TODO(aki): Should this be the default w/ needing to pass `--program` to program instead? + generic_options.add_argument( + '--build-only', '-B', action = 'store_true', - help = 'Only build the gateware, skip device programming' + help = 'Only build and pack the gateware, skip device programming.' + ) + + generic_options.add_argument( + '--build-dir', '-b', + type = Path, + help = 'The output directory for the intermediate and final build artifacts.' ) if cacheable: - gateware_options.add_argument( - '--skip-cache', + generic_options.add_argument( + '--skip-cache', '-C', action = 'store_true', - help = 'Skip gateware cache lookup and subsequent caching of resultant gateware' + help = 'Skip artifact cache lookup, and don\'t cache the resulting gateware artifact once built.' ) - gateware_options.add_argument( - '--build-dir', '-b', - type = str, - default = SQUISHY_BUILD_DIR, - help = 'The output directory for Squishy binaries and firmware images' - ) - - gateware_options.add_argument( - '--loud', + # TODO(aki): Should this be rather tied into `-v`, and if we pass 2 it flips this switch? + generic_options.add_argument( + '--build-verbose', action = 'store_true', - help = 'Enables verbose output of Synthesis and PnR runs' + help = 'Enable verbose output during build (very noisy)' ) - # Synthesis Options + synth_options = parser.add_argument_group('Synthesis Options') + synth_options.add_argument( '--no-abc9', action = 'store_true', - help = 'Disable use of Yosys\' ABC9' + help = 'Disable the use of `abc9` during synth.' ) synth_options.add_argument( - '--aggressive-mapping', - action = 'store_true', - help = 'Run multiple ABC9 mapping more than once to improve performance in exchange for longer synth time' - ) - - # Place and Route Options - pnr_options.add_argument( - '--use-router2', + '--no-aggressive-mapping', action = 'store_true', - help = 'Use nextpnr\'s \'router2\' routing engine rather than \'router1\'' + help = 'Disable multiple `abc9` mapping passes, resulting in faster synth time but worse overall gateware performance.' ) - pnr_options.add_argument( - '--tmg-ripup', - action = 'store_true', - help = 'Use the timing-driven ripup router' - ) + pnr_options = parser.add_argument_group('Place-and-Route Options') pnr_options.add_argument( - '--detailed-timing-report', + '--detailed-report', action = 'store_true', - help = 'Have nextpnr output a detailed net timing report' - ) - - pnr_options.add_argument( - '--routed-svg', - type = Path, - default = None, - help = 'Write a render of the routing to an SVG' + help = 'Have nextpnr output a detailed timing report' ) pnr_options.add_argument( - '--routed-json', + '--routed-netlist', type = Path, default = None, - help = 'Write the PnR output json for viewing in nextpnr after PnR' + help = 'Write out the netlist with embedded routing information for later inspection.' ) pnr_options.add_argument( '--pnr-seed', type = int, default = 0, - help = 'Specify the PnR seed to use' + help = 'The place and route RNG seed to use.' ) - pnr_options.add_argument( - '--hunt-n-peck', - action = 'store_true', - help = 'If PnR fails with given seed, try to find one that passes timing' - ) - - # Bitstream packing options + pack_options = parser.add_argument_group('Bitstream Packing Options') pack_options.add_argument( - '--compress', - action = 'store_true', - help = 'Compress resulting bitstream (Only for ECP5 based Squishy Platforms)' + '--dont-compress', + action = 'store_true', + help = 'Disable bitstream compression if viable for target platform.' ) + + def dfu_util_msg(self, args: Namespace, name: str, slot: int, dev: SquishyDevice | None = None) -> str: + ''' + Build up a message that accuratly displays how to flash a built artifact to the given Squishy device. + + Parameters + ---------- + args : argsparse.Namespace + The parsed args from the invocation of the action, used to extract the set build directory if done so. + + name : str + The name of the artifact that was generated. + + slot : int + The DFU slot/alt-mode to specify. + + dev : SquishyDevice | None + If attached, a Squishy device to pull the serial number from + + Returns + ------- + str + The appropriate help message for flashing the given built artifact to the Squishy device. + ''' + + artifact_dir = Path(args.build_dir) if args.build_dir is not None else SQUISHY_BUILD_DIR + artifact_file = (artifact_dir / f'{name}.bin') + + serial = '' + if dev is not None: + serial = f' -S {dev.serial}' + + msg = f'Use \'dfu-util\' to flash \'{artifact_file}\' into slot {slot}\n' + msg += f'e.g. \'dfu-util -d {USB_VID:04X}:{USB_APP_PID:04X},:{USB_DFU_PID:04X}{serial} -a {slot} -R -D {artifact_file}\'\n' + + return msg + + def dump_utilization(self, name: str, products: LocalBuildProducts) -> None: + ''' + Print out resource utilization and fmax timing info from the build. + + Parameters + ---------- + name : str + The name of the built resource. + + products : LocalBuildProducts + The build products + ''' + + pnr_rpt = json.loads(products.get(f'{name}.pnr.json', 't')) + + log.debug('Clock network Fmax:') + for net, fmax in pnr_rpt['fmax'].items(): + log.debug(f' \'{net}\': {fmax["achieved"]:.2f}MHz (min: {fmax["constraint"]:.2f}MHz)') + + log.debug('Resource Utilization:') + for name, util in pnr_rpt['utilization'].items(): + used: int = util['used'] + available: int = util['available'] + log.debug(f' {name:>15}: {used:>5}/{available:>5} ({(used/available) * 100.0:>6.2f}%)') diff --git a/squishy/actions/applet.py b/squishy/actions/applet.py index d2206a7c..24ed70cb 100644 --- a/squishy/actions/applet.py +++ b/squishy/actions/applet.py @@ -1,213 +1,167 @@ # SPDX-License-Identifier: BSD-3-Clause -import logging as log -from pathlib import Path -from argparse import ArgumentParser, Namespace -from rich.progress import ( - Progress, SpinnerColumn, BarColumn, - TextColumn +''' + +''' + +import logging as log +from pathlib import Path +from argparse import ArgumentParser, Namespace + +from rich.prompt import Confirm +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn + +from . import SquishySynthAction +from ..applets import SquishyApplet +from ..paths import SQUISHY_APPLETS +from ..device import SquishyDevice +from ..core.reflection import collect_members, is_applet +from ..gateware import Squishy as SquishyGateware +from ..gateware import AVAILABLE_PLATFORMS + +__all__ = ( + 'AppletAction', ) -from ..applets import SquishyApplet -from ..config import SQUISHY_APPLETS -from ..core.collect import collect_members, predicate_applet -from ..core.device import SquishyHardwareDevice -from ..gateware import Squishy -from . import SquishySynthAction +class AppletAction(SquishySynthAction): + ''' + Build and Run Squishy Applets + + This action implements all the machinery needed to build and run :py:class:`SquishyApplet`'s, on + both gateware-side and host-side, along with setting up a the optional communication channel + between then if needed. + Note + ---- + Not all applets will have a host-side invocation runtime, some might be gateware only and implement + something such as a SCSI disk endpoint over USB and let th host OS drivers deal with it. -class Applet(SquishySynthAction): - pretty_name = 'Squishy Applets' - short_help = 'Squishy applet subsystem' + ''' + + name = 'applet' description = 'Build and run Squishy applets' - requires_dev = True + # TODO(aki): We /technically/ want this, but it would be nice to be able to build them w/o a device attached + requires_dev = False + + def _collect_applets(self, external: bool = True) -> list[SquishyApplet]: + ''' + Try to collect known applets. + + Parameters + ---------- + external : bool + Also try to collect applets located in the ``SQUISHY_APPLETS`` directory where + users can drop their own or third-party applets. + + Returns + ------- + ''' - def _collect_all_applets(self) -> list[dict[str, str | SquishyApplet]]: from .. import applets return [ *collect_members( Path(applets.__path__[0]), - predicate_applet, - f'{applets.__name__}.' + is_applet, + f'{applets.__name__}.', + make_instance = True ), - *collect_members( + # BUG(aki): This is likely entirely busted + *(collect_members( SQUISHY_APPLETS, - predicate_applet, - '' - ) + is_applet, + make_instance = True, + ) if external else ()) ] - def __init__(self): + def __init__(self) -> None: super().__init__() - self.applets = self._collect_all_applets() + self._applets = self._collect_applets() def register_args(self, parser: ArgumentParser) -> None: - # actions = parser.add_subparsers(dest = 'gateware_action') - - # do_verify = actions.add_parser('verify', help = 'Run formal verification') - # verify_options = do_verify.add_argument_group('Verification options') - - # do_simulation = actions.add_parser('simulate', help = 'Run simulation test cases') - # sim_options = do_simulation.add_argument_group('Simulation Options') - self.register_synth_args(parser, cacheable = True) - usb_options = parser.add_argument_group('USB Options') - uart_options = parser.add_argument_group('Debug UART Options') - scsi_options = parser.add_argument_group('SCSI Options') - - - # USB Options - usb_options.add_argument( - '--enable-webusb', + parser.add_argument( + '--noconfirm', '-Y', action = 'store_true', - help = 'Enable the experimental WebUSB descriptors' - ) - - usb_options.add_argument( - '--webusb-url', - type = str, - default = 'https://localhost', - help = 'The location URL to encode in the device descriptor' - ) - - # SCSI Options - scsi_options.add_argument( - '--scsi-did', - type = int, - default = 0x01, - help = 'The SCSI Device ID to use' - ) - - scsi_options.add_argument( - '--scsi-arbitrating', - default = False, - action = 'store_true', - help = 'Enable SCSI Bus arbitration' - ) - - scsi_options.add_argument( - '--scsi-device', - default = False, - action = 'store_true', - help = 'Set the SCSI bus to be a device rather than an initiator', + help = 'Do not ask for confirmation if the target applet is in preview.' ) - # UART Options - uart_options.add_argument( - '--enable-uart', '-U', - default = False, - action = 'store_true', - help = 'Enable the debug UART', - ) - - uart_options.add_argument( - '--baud', '-B', - type = int, - default = 9600, - help = 'The rate at which to run the debug UART' - ) - - uart_options.add_argument( - '--data-bits', '-D', - type = int, - default = 8, - help = 'The data bits to use for the UART' + parser.add_argument( + '--flash', '-f', + action = 'store_true', + help = 'Flash the gateware into persistent flash rather than doing an ephemeral load' ) - uart_options.add_argument( - '--parity', '-c', - type = str, - choices = [ - 'none', 'mark', 'space' - 'even', 'odd' - ], - default = 'none', - help = 'The parity mode for the debug UART' - ) + # TODO(aki): Peripheral options and the like applet_parser = parser.add_subparsers( dest = 'applet', required = True ) - if len(self.applets) > 0: - for apl in self.applets: - applet = apl['instance'] - p = applet_parser.add_parser( - apl['name'], - help = applet.short_help, - ) - applet.register_args(p) - - def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: - plt = self.get_hw_platform(args, dev) - if plt is None: - return 1 + for applet in self._applets: + p = applet_parser.add_parser(applet.name, help = applet.description) + applet.register_args(p) - platform, hardware_platform, dev = plt + def run(self, args: Namespace, dev: SquishyDevice) -> int: + # Get the platform + platform_type = self.get_platform(args, dev) + if platform_type is None: + # the call to `get_platform` will have already printed an error message + return 1 - apl = list(filter(lambda a: a['name'] == args.applet, self.applets))[0] + # Initialize the platform + plat = platform_type() - name: str = apl['name'] - applet: SquishyApplet = apl['instance'] + # Pull out the selected applet + applet: SquishyApplet = next(filter(lambda applet: applet.name == args.applet, self._applets), None) - if not applet.supported_platform(hardware_platform): - log.error(f'Applet {name} does not support platform {hardware_platform}') - log.error(f'Supported platform(s) {applet.hardware_rev}') + # Check to make sure we support this platform + if not applet.is_supported(plat): + log.error(f'Applet \'{applet.name}\' does not support revision {plat.revision_str} hardware') return 1 + # Warn the user if this applet is unstable if applet.preview: - log.warning('This applet is a preview, it may be buggy or not work at all') - - - applet_elaboratable = applet.init_applet(args) - - uart_config = { - 'enabled' : args.enable_uart, - 'baud' : args.baud, - 'parity' : args.parity, - 'data_bits': args.data_bits, - } - - usb_config = { - 'vid': platform.usb_vid, - 'pid': platform.usb_pid_app, - 'manufacturer': platform.usb_mfr, - 'serial_number': SquishyHardwareDevice.make_serial() if dev is None else dev.serial, - 'product': platform.usb_prod[platform.usb_pid_app], - 'webusb': { - 'enabled': args.enable_webusb, - 'url' : args.webusb_url, - } - } - - scsi_config = { - 'version' : applet_elaboratable.scsi_version, - 'vid' : platform.scsi_vid, - 'did' : args.scsi_did, - 'arbitrating': args.scsi_arbitrating, - 'is_device' : args.scsi_device, - } - - - gateware = Squishy( - revision = platform.revision, - uart_config = uart_config, - usb_config = usb_config, - scsi_config = scsi_config, - applet = applet_elaboratable + log.warning(f'The {applet.name} applet is a preview, it may be buggy or not work at all') + if not args.noconfirm: + if not Confirm.ask('Are you sure you would like to use this applet?'): + return 0 + + # Try to initialize the applet gateware + applet_elab = applet.initialize(args) + if applet_elab is None: + log.error('Failure initializing applet elaboratable, aborting') + return 1 + + # TODO(aki): Construct gateware superstructure peripherals and the like + + # Get the target slot, ephemeral or otherwise + slot: int | None = plat.ephemeral_slot + if slot is None or args.flash: + slot = 1 + + # Construct the gateware + gateware = SquishyGateware( + revision = plat.revision, + applet = applet_elab ) + # TODO(aki): This should be made unique to the applet being made? + applet_name = 'squishy_applet' + + # Actually build the gateware log.info('Building applet gateware') - name, prod = self.run_synth(args, platform, gateware, 'squishy_applet', cacheable = True) + prod = self.run_synth(args, plat, gateware, applet_name, cacheable = True) + # if on the off chance the user only built the gateware, display how to use dfu-util to flash it if args.build_only: - log.info(f'Use \'dfu-util\' to flash \'{args.build_dir / name}.bin\' into slot 1 to update the applet') - log.info(f'e.g. \'dfu-util -d 1209:ca70,:ca71 -a 1 -R -D {args.build_dir / name}.bin\'') + log.info(self.dfu_util_msg(args, applet_name, slot, dev)) return 0 + + # If we *are* programming the device, then with Progress( SpinnerColumn(), TextColumn('[progress.description]{task.description}'), @@ -215,17 +169,29 @@ def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: transient = True ) as progress: - file_name = name - if not file_name.endswith('.bin'): - file_name += '.bin' + # TODO(aki): If we get products back from the cache, the name is different, + + fname = applet_name + if not fname.endswith('.bin'): + fname += '.bin' + + # TODO(aki): We don't cache the packed artifact, we re-pack it each time + # should we cache it? + # Pack the bitstream artifact in a way the platform wants + packed = plat.pack_artifact(prod.get(fname)) + + # Make sure there is actually a device attached + if dev is None: + log.error('No device specified, however we were asked to program the device, aborting') + return 1 - log.info(f'Programming applet with {file_name}') - if dev.upload(prod.get(file_name), 1, progress): - log.info('Resetting Device') + log.info(f'Programming device with \'{fname}\'') + if dev.upload(packed, slot, progress): + log.info('Resetting device') dev.reset() else: - log.error('Device upload failed!') + log.error('Device upload failed') return 1 log.info('Running applet...') - return applet.run(dev, args) + return applet.run(args, dev) diff --git a/squishy/actions/cache.py b/squishy/actions/cache.py deleted file mode 100644 index 43f564d0..00000000 --- a/squishy/actions/cache.py +++ /dev/null @@ -1,114 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -import logging as log -from argparse import ArgumentParser, Namespace - -from torii.util.units import iec_size - -from ..core.cache import SquishyBitstreamCache -from ..core.device import SquishyHardwareDevice -from ..config import SQUISHY_CACHE, SQUISHY_APPLET_CACHE, SQUISHY_BUILD_DIR -from . import SquishyAction - -class Cache(SquishyAction): - pretty_name = 'Squishy Cache Utility' - short_help = 'Manage the Squishy cache' - description = 'Manages the Squishy cache' - requires_dev = False - - def _list_cache(self, args: Namespace) -> int: - applet_size = 0 - build_size = 0 - - applet_items = list(SQUISHY_APPLET_CACHE.rglob('*.*')) - build_items = list(SQUISHY_BUILD_DIR.rglob('*.*')) - - for i in applet_items: - applet_size += i.stat().st_size - - for i in build_items: - build_size += i.stat().st_size - - total = applet_size + build_size - - log.info(f'Squishy applet cache contains {len(applet_items)} bitstream files totaling {iec_size(applet_size)}') - log.info(f'Squishy build cache contains {len(build_items)} files totaling {iec_size(build_size)}') - - log.info(f'Total cache size is {iec_size(total)}') - - if args.list_cache_items: - log.warning('Printing cache tree, as --list-cache-items was passed') - log.warning('This might be very long') - from rich.tree import Tree - from rich import print - - cache_tree = Tree( - f'[green][link file://{str(SQUISHY_CACHE)}]{str(SQUISHY_CACHE)}[/][/]', - guide_style = 'blue' - ) - - applet_tree = cache_tree.add('[bright_red]applets[/]') - - segments = dict() - - for item in applet_items: - s = str(item.parent).split("/")[-1] - if s not in segments: - segments[s] = applet_tree.add(f'[magenta]{s}[/]') - segments[s].add(f'{item.name}') - - build_tree = cache_tree.add('[bright_red]build[/]') - - for item in build_items: - build_tree.add(f'{item.name}') - - print(cache_tree) - - return 0 - - def _clear_cache(self, args: Namespace) -> int: - from rich.prompt import Confirm - from shutil import rmtree - - if Confirm.ask('Are you sure you want to clear the cache?'): - bc = SquishyBitstreamCache(False) - bc.flush() - log.info('Flushing build cache') - rmtree(SQUISHY_BUILD_DIR) - SQUISHY_BUILD_DIR.mkdir() - return 0 - else: - log.info('Aborted') - return 1 - - def __init__(self): - super().__init__() - - self._dispatch = { - 'list': self._list_cache, - 'clear': self._clear_cache, - } - - def register_args(self, parser: ArgumentParser) -> None: - actions = parser.add_subparsers( - dest = 'cache_action', - required = True - ) - - cache_list = actions.add_parser( - 'list', - help = 'list cache contents and size' - ) - - cache_list.add_argument( - '--list-cache-items', - action = 'store_true', - help = 'List each item in the cache (WARNING, THIS CAN BE LARGE)' - ) - - cache_clear = actions.add_parser( # noqa: F841 - 'clear', - help = 'clear cache' - ) - - def run(self, args: Namespace, _: SquishyHardwareDevice | None = None) -> int: - return self._dispatch.get(args.cache_action, lambda _: 1)(args) diff --git a/squishy/actions/provision.py b/squishy/actions/provision.py index d032fd55..e5b7b0ec 100644 --- a/squishy/actions/provision.py +++ b/squishy/actions/provision.py @@ -1,139 +1,123 @@ # SPDX-License-Identifier: BSD-3-Clause -import logging as log -from pathlib import Path -from argparse import ArgumentParser, Namespace +''' -from torii.build.run import LocalBuildProducts +''' -from rich.progress import ( - Progress, SpinnerColumn, BarColumn, - TextColumn -) - -from ..core.device import SquishyHardwareDevice -from ..core.flash import FlashGeometry - -from . import SquishySynthAction - - -class Provision(SquishySynthAction): - pretty_name = 'Squishy Provision' - short_help = 'Squishy first-time provisioning' - description = 'Build squishy bootloader flash image' - requires_dev = False - - def _build_slots(self, flash_geometry: FlashGeometry) -> bytes: - ''' ''' - from ..gateware.bootloader.bitstream import iCE40BitstreamSlots - - slot_data = bytearray(flash_geometry.erase_size) - slots = iCE40BitstreamSlots(flash_geometry).build() - - slot_data[0:len(slots)] = slots - - for byte in range(len(slots), flash_geometry.erase_size): - slot_data[byte] = 0xFF - - return bytes(slot_data) - - def _build_multiboot(self, - build_dir: str, name: str, boot_products: tuple[str, LocalBuildProducts], - flash_geometry: FlashGeometry - ) -> Path: - - build_path = Path(build_dir) / name - - log.debug(f'Building multiboot bitstream in \'{build_path}\'') - - boot_name = boot_products[0] - if not boot_name.endswith('.bin'): - boot_name = boot_name + '.bin' - - log.debug(f'Bootloader bitstream name: \'{boot_name}\'') - - with build_path.open('wb') as multiboot: - slot_data = self._build_slots(flash_geometry) +import logging as log +from argparse import ArgumentParser, Namespace +from pathlib import Path - log.debug('Writing slot data') - multiboot.write(slot_data) - log.debug('Writing bootloader bitstream') - multiboot.write(boot_products[1].get(boot_name)) +from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn - start = multiboot.tell() - end = flash_geometry.partitions[1]['start_addr'] +from . import SquishySynthAction +from ..paths import SQUISHY_BUILD_DIR +from ..device import SquishyDevice +from ..gateware import AVAILABLE_PLATFORMS, SquishyBootloader - log.debug('Padding bitstream') - for _ in range(start, end): - multiboot.write(b'\xFF') +__all__ = ( + 'ProvisionAction', +) - # Stuff in a copy of the bootloader entry - log.debug('Copying bootloader entry to active slot') - multiboot.write(slot_data[32:64]) +class ProvisionAction(SquishySynthAction): + ''' + Provision Squishy Hardware. - return build_path + This action is for provisioning actions, such as building full-device flash images, or + just the bootloader. - def __init__(self): - super().__init__() + ''' + name = 'provision' + description = 'Provision Squishy hardware' + requires_dev = False # We need one to provision a live device, but not to build the image def register_args(self, parser: ArgumentParser) -> None: self.register_synth_args(parser, cacheable = False) - provision_opts = parser.add_argument_group('Provisioning Options') + prov_opts = parser.add_argument_group('Provisioning Options') - # Provisioning Options - provision_opts.add_argument( + prov_opts.add_argument( '--serial-number', '-S', type = str, default = None, - help = 'Specify the device serial number rather than automatically generating it' + help = 'Directly specify the device serial number rather than automatically generating it' ) - provision_opts.add_argument( + prov_opts.add_argument( '--whole-device', '-W', action = 'store_true', - default = False, - help = 'Program the whole device, not just the bootloader' + help = 'Generate a whole-device flash image, not just the bootloader.' ) - def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: - plt = self.get_hw_platform(args, dev) - if plt is None: + def run(self, args: Namespace, dev: SquishyDevice | None) -> int: + # Get the platform + platform_type = self.get_platform(args, dev) + if platform_type is None: + # the call to `get_platform` will have already printed an error message return 1 - device, _, dev = plt - - if device.bootloader_module is None: - log.error('Unable to provision for platform, no bootloader module!') - return 1 + # Initialize the platform + plat = platform_type() - # Provisioning Options + # If we were passed a serial number, then use that if args.serial_number is not None: - serial_number = args.serial_number - if dev is not None: - serial_number = dev.serial + serial: str = args.serial_number + # Otherwise, if we have an attached device, use it's existing serial + elif dev is not None: + serial = dev.serial + # Otherwise otherwise, generate a brand new one else: - serial_number = SquishyHardwareDevice.make_serial() + serial = SquishyDevice.generate_serial() + + log.info(f'Assigning device serial number \'{serial}\'') - log.info(f'Assigning device serial number \'{serial_number}\'') - bootloader = device.bootloader_module(serial_number = serial_number) + # TODO(aki): Booloader opts etc + bootloader = SquishyBootloader( + serial_number = serial, revision = plat.revision + ) + boot_name = 'squishy_boot' log.info('Building bootloader gateware') - name, prod = self.run_synth(args, device, bootloader, 'squishy_bootloader', cacheable = False) + prod = self.run_synth(args, plat, bootloader, boot_name, cacheable = False) + + build_dir = SQUISHY_BUILD_DIR + if args.build_dir is not None: + build_dir = Path(args.build_dir) if args.whole_device: - log.info('Building whole-device bitstream') - path = self._build_multiboot(args.build_dir, 'squishy-unified.bin', (name, prod), device.flash['geometry']) + log.info('Building full device flash image') + image_name = f'squishy-{plat.revision_str}-monolithic.bin' + image = plat.build_image(image_name, build_dir, boot_name, prod) if args.build_only: - log.info(f'Please flash the file at \'{path}\' on to the hardware to provision the device.') + log.info(f'Provisioning image generated at \'{image}\', Flash to device to provision') + else: + # TODO(aki): Eventually when we have the ability to automatically provision the flash + # This would be done by either making use of something like an attached + # blackmagic probe in SPI, or the "brainslug" passthru of a supervisor. + # + # This kinda depends a lot on the hardware platform so that might need to be + # abstracted out to them, as only the platform really knows how to best provision + # itself. + log.warning('Unable to automatically provision device at this time') + log.warning(f'Provisioning image generated at \'{image}\', Flash to device to provision') + return 0 + + else: + if args.build_only: + log.info(self.dfu_util_msg(args, boot_name, 0, dev)) return 0 - if args.build_only: - log.info(f'Use \'dfu-util\' to flash \'{args.build_dir / name}.bin\' into slot 0 to update the bootloader') - log.info(f'e.g. \'dfu-util -d 1209:ca70,:ca71 -a 0 -R -D {args.build_dir / name}.bin\'') - return 0 + fname = boot_name + if not fname.endswith('.bin'): + fname += '.bin' + + image = plat.pack_artifact(prod.get(fname)) + + if dev is None: + log.error('No device specified, however we were asked to program the device, aborting') + return 1 with Progress( SpinnerColumn(), @@ -141,15 +125,14 @@ def run(self, args: Namespace, dev: SquishyHardwareDevice | None = None) -> int: BarColumn(bar_width = None), transient = True ) as progress: - file_name = name - if not file_name.endswith('.bin'): - file_name += '.bin' - - log.info(f'Programming bootloader with {file_name}') - if dev.upload(prod.get(file_name), 0, progress): - log.info('Resetting Device') - dev.reset() + if args.whole_image: + log.warning('TODO: Whole image flash stuff') else: - log.error('Device upload failed!') - return 1 + log.info('Programming bootloader') + if dev.upload(image, 0, progress): + log.info('Resetting device') + dev.reset() + else: + log.error('Device upload failed') + return 1 return 0 diff --git a/squishy/applets/__init__.py b/squishy/applets/__init__.py index e99e1f58..4d7f7cb2 100644 --- a/squishy/applets/__init__.py +++ b/squishy/applets/__init__.py @@ -1,11 +1,14 @@ # SPDX-License-Identifier: BSD-3-Clause +''' + +''' + from abc import ABCMeta, abstractmethod from argparse import ArgumentParser, Namespace - -from ..gateware import AppletElaboratable -from ..core.device import SquishyHardwareDevice +from ..device import SquishyDevice +from ..gateware import SquishyPlatformType, AppletElaboratable __all__ = ( 'SquishyApplet', @@ -13,164 +16,151 @@ class SquishyApplet(metaclass = ABCMeta): ''' - Squishy applet base class. + Base class for all Squishy applets. - This is the abstract base class that is used - to implement any possible applet for squishy. + This class provides the public facing API for all Squishy applets, both internal + and out-of-tree/third-party applet modules. - It represents a combination of client-side python, - and gateware that will run the the hardware platform. - - Users can then invoke the build and execution of implemented - applets by name. + Squishy applets are made out of a combination of host-site Python logic and hardware-side + gateware. Attributes ---------- - preview : bool - If the applet is a preview/pre-release applet. - - pretty_name : str - A pretty string name of the applet. + name : str + The name used to address this applet and display in the help documentation. - short_help : str - A short section of help for the applet. + description : str + A short description of this applet. - help : str - A longer more detailed help string. + preview : bool + If this applet is preview/pre-release. - description : str - A brief description about the applet. + version : float + The version of the applet. - hardware_rev : str, tuple - A single string, or a tuple of strings for supported hardware revisions + supported_platforms : tuple[tuple[int, int], ...] + The platform revisions this applet supports. ''' + @property @abstractmethod - def preview(self) -> bool: + def name(self) -> str: + ''' The name of the applet. ''' raise NotImplementedError('Applets must implement this property') @property @abstractmethod - def pretty_name(self) -> str: + def description(self) -> str: + ''' Short description of the applet. ''' raise NotImplementedError('Applets must implement this property') @property @abstractmethod - def short_help(self) -> str: + def preview(self) -> bool: + ''' If this applet is a preview or not ''' raise NotImplementedError('Applets must implement this property') @property - def help(self) -> str: - return '' - - @property - def description(self) -> str: - return '' + @abstractmethod + def version(self) -> float: + ''' Applet version ''' + raise NotImplementedError('Applets must implement this property') @property @abstractmethod - def hardware_rev(self) -> str | tuple[str, ...]: + def supported_platforms(self) -> tuple[tuple[int, int], ...]: + ''' The platforms this applet supports. ''' raise NotImplementedError('Applets must implement this property') - def __init__(self): - if not ( - isinstance(self.hardware_rev, str) or - ( - isinstance(self.hardware_rev, tuple) and - all(isinstance(r, str) for r in self.hardware_rev) - ) - ): - raise ValueError(f'Applet `hardware_rev` must be a str or tuple of str not `{type(self.hardware_rev)!r}`') - + def __init__(self) -> None: + pass - def supported_platform(self, platform: str) -> bool: + def is_supported(self, platform: SquishyPlatformType) -> bool: ''' - Check to see if the given platform is supported + Check to see if the given platform is supported. Parameters ---------- - platform : str - The platform to check + platform : squishy.gateware.SquishyPlatformType + The platform to check against. Returns ------- bool - True if the applet supports the platform, otherwise False. - + True if the given platform is supported by this applet, otherwise False. ''' - if isinstance(self.hardware_rev, str): - return platform == self.hardware_rev - else: - return platform in self.hardware_rev - - def show_help(self) -> None: - ''' Shows applets built-in help ''' - pass + return platform.revision in self.supported_platforms @abstractmethod - def init_applet(self, args: Namespace) -> AppletElaboratable: + def register_args(self, parser: ArgumentParser) -> None: ''' - Applet Initialization + Register applet argument parsers. + + Prior to :py:func:`.initialize` and :py:func:`.run` this method will + be called to allow the applet to register any wanted command line options. - Called to initialize the applet prior to - the applet being built and ran + This is also used when displaying help. Parameters ---------- - args : argsparse.Namespace - Any command line arguments passed. - - Returns - ------- - AppletElaboratable - The applet logic/elaboratable + parser : argparse.ArgumentParser + The Squishy CLI argument parser group to register arguments into. Raises ------ NotImplementedError - The abstract method must be implemented by the applet + The abstract method must be implemented by the applet. ''' - - raise NotImplementedError('Applets must implement this method') + raise NotImplementedError('Actions must implement this method') @abstractmethod - def register_args(self, parser: ArgumentParser) -> None: + def initialize(self, args: Namespace) -> AppletElaboratable | None: ''' - Applet argument registration + Initialize applet. - Called to register any applet specific arguments. + This is called prior to the gateware side of the applet being elaborated. It ensures + that any initialization and configuration needed to be done can be done. Parameters ---------- - parser : argparse.ArgumentParser - The root argparse parser. + args : argsparse.Namespace + The parsed arguments from the Squishy CLI + + Returns + ------- + AppletElaboratable | None + An AppletElaboratable if initialization was successful otherwise None Raises ------ NotImplementedError - The abstract method must be implemented by the applet - + The abstract method must be implemented by the applet. ''' - raise NotImplementedError('Applets must implement this method') + @abstractmethod - def run(self, device: SquishyHardwareDevice, args: Namespace) -> int: + def run(self, args: Namespace, dev: SquishyDevice) -> int: ''' - Applet run step + Invoke the applet. - Called to run any specialized machinery for the applet. + This method is run when the Squishy CLI has determined that this applet + was to be ran. + + This is for host-side applet logic only, such as USB communication, if the + applet does not have any host-side logic, this may simple just return ``0`` + as if it ran successfully. Parameters ---------- - device : squishy.core.device.SquishyHardwareDevice - The target squishy device. - args : argsparse.Namespace - Any command line arguments passed. + The parsed arguments from the Squishy CLI + + dev : squishy.device.SquishyDevice + The target device Returns ------- @@ -180,8 +170,7 @@ def run(self, device: SquishyHardwareDevice, args: Namespace) -> int: Raises ------ NotImplementedError - The abstract method must be implemented by the applet + The abstract method must be implemented by the applet. ''' - - raise NotImplementedError('Applets must implement this method') + raise NotImplementedError('Actions must implement this method') diff --git a/squishy/applets/analyzer/__init__.py b/squishy/applets/analyzer/__init__.py index 7aaa9930..f581ff56 100644 --- a/squishy/applets/analyzer/__init__.py +++ b/squishy/applets/analyzer/__init__.py @@ -1,35 +1,46 @@ # SPDX-License-Identifier: BSD-3-Clause +''' + +''' + from torii import Module from argparse import ArgumentParser, Namespace from .. import SquishyApplet -from ...gateware import AppletElaboratable, SquishyPlatform -from ...core.device import SquishyHardwareDevice +from ...gateware import AppletElaboratable, SquishyPlatformType +from ...device import SquishyDevice +__all__ = ( + 'Analyzer', +) class AnalyzerElaboratable(AppletElaboratable): - - def elaborate(self, platform: SquishyPlatform | None) -> Module: + def elaborate(self, platform: SquishyPlatformType | None) -> Module: m = Module() return m class Analyzer(SquishyApplet): - preview = True - pretty_name = 'SCSI Analyzer' - description = 'SCSI Bus analyzer and replay' - short_help = description - hardware_rev = ( - 'rev1', 'rev2' + ''' + + ''' + + name = 'analyzer' + description = 'SCSI Bus analyzer and traffic replay' + version = 0.1 + preview = True + supported_platforms = ( + (1, 0), + (2, 0) ) def register_args(self, parser: ArgumentParser) -> None: pass - def init_applet(self, args: Namespace) -> AppletElaboratable: + def initialize(self, args: Namespace) -> AppletElaboratable: return AnalyzerElaboratable() - def run(self, device: SquishyHardwareDevice, args: Namespace) -> int: + def run(self, args: Namespace, dev: SquishyDevice) -> int: pass diff --git a/squishy/applets/taperipper/__init__.py b/squishy/applets/taperipper/__init__.py deleted file mode 100644 index 224e1f54..00000000 --- a/squishy/applets/taperipper/__init__.py +++ /dev/null @@ -1,190 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from argparse import ArgumentParser, Namespace - -from .. import SquishyApplet -from ...gateware import AppletElaboratable -from ...core.device import SquishyHardwareDevice - - -# def build_bootimage(args): -# log.info('Running taperipper boot image generation') -# if not path.exists(args.efi_fw): -# log.error(f'UEFI firmware {args.efi_fw} does not exist') -# return 1 - -# return 0 - -# def pack_flash(args): -# log.info('Running taperipper flash packing') -# if not path.exists(args.boot_img): -# log.error(f'Boot image {args.boot_img} does not exist') -# return 1 - -# if not path.exists(args.bitstream): -# log.error(f'Bitstream {args.bitstream} does not exist') -# return 1 - -# return 0 - -# def mkboot_tape(args): -# log.info('Running taperipper make boot tape') -# from ..taperipper import tape_image_fmt - -# if not path.exists(args.kernel_image): -# log.error(f'Kernel image {args.kernel_image} does not exist') -# return 1 - -# if not path.exists(args.initramfs_image): -# log.error(f'Kernel initramfs image {args.initramfs_image} does not exist') -# return 1 - -# kernel_size = stat(args.kernel_image).st_size -# initramfs_size = stat(args.initramfs_image).st_size -# tape_img_size = (256 + kernel_size + initramfs_size) - -# tape_img_file = path.join(args.build_dir, args.image_file_name) - -# log.info(f'Kernel is {kernel_size} bytes long') -# log.info(f'initramfs is {initramfs_size} bytes long') - -# if tape_img_size > args.tape_size: -# log.error(f'The total size of the tape image ({tape_img_size} bytes) exceeds' -# 'that of the total size available on the tape ({args.tape_size} bytes)' -# ) - -# log.info(f'Total tape image length will be {tape_img_size} bytes') -# log.info(f'Output file {tape_img_file}') - -# with open(tape_img_file, 'wb') as tape_image: -# kimg_data = None -# iimg_data = None - -# with open(args.kernel_image, 'rb') as kimg: -# kimg_data = kimg.read() - -# with open(args.initramfs_image, 'rb') as iimg: -# iimg_data = iimg.read() - -# tape_img = tape_image_fmt.build(dict( -# header = dict( -# tape_length = tape_img_size, -# kernel_offset = 256, -# kernel_length = kernel_size, -# initram_offset = (256 + kernel_size), -# initram_length = initramfs_size, -# ), -# kernel_img = kimg_data, -# initramfs_img = iimg_data, -# )) - -# tape_image.write(tape_img) - -# return 0 - -# TAPERIPPER_ACTIONS = { -# 'build-bootimage': build_bootimage, -# 'pack-flash': pack_flash, -# 'make-boot-tape': mkboot_tape -# } - -class Taperipper(SquishyApplet): - preview = True - pretty_name = 'Project Taperipper' - description = 'UEFI Boot from 9-track tape' - short_help = description - hardware_rev = ( - 'rev1', 'rev2' - ) - - - def register_args(self, parser: ArgumentParser) -> None: - actions = parser.add_subparsers(dest = 'taperipper_actions') - - bootimage_action = actions.add_parser( - 'build-bootimage', - help = 'build UEFI boot image' - ) - - packflash_action = actions.add_parser( - 'pack-flash', - help = 'pack squishy flash image' - ) - - mkboottap_action = actions.add_parser( - 'make-boot-tape', - help = 'create the boot tape' - ) - - bootimage_action.add_argument( - '--efi-fw', '-f', - dest = 'efi_fw', - type = str, - required = True, - help = 'UEFI firmware blob to pack' - ) - - packflash_action.add_argument( - '--boot-img', '-i', - dest = 'boot_img', - type = str, - required = True, - help = 'Image generated with `squishy taperipper build-bootimage`' - ) - - packflash_action.add_argument( - '--bitstream', '-B', - dest = 'bitstream', - type = str, - required = True, - help = 'Generated squishy bitstream' - ) - - - mkboottap_action.add_argument( - '--kernel-image', '-K', - dest = 'kernel_image', - type = str, - required = True, - help = 'Kernel Image to pack' - ) - - mkboottap_action.add_argument( - '--initramfs-image', '-I', - dest = 'initramfs_image', - type = str, - required = True, - help = 'Kernel initramfs image to pack for loading' - ) - - mkboottap_action.add_argument( - '--image-output', '-i', - dest = 'image_file_name', - type = str, - default = 'squishy-tape.img', - help = 'The output file name for the tape image' - ) - - mkboottap_action.add_argument( - '--tape-size', '-T', - dest = 'tape_size', - type = int, - default = 180e6, - help = 'The size of the tape in bytes' - ) - - mkboottap_action.add_argument( - '--tape-block-size', '-B', - dest = 'tape_block_size', - type = int, - default = 256, - help = 'The size of the native block on the tape' - ) - - - def init_applet(self, args: Namespace) -> AppletElaboratable: - pass - - def run(self, device: SquishyHardwareDevice, args: Namespace) -> int: - # TAPERIPPER_ACTIONS.get(args.taperipper_actions, lambda _: 1)(args) - pass diff --git a/squishy/applets/taperipper/fat32.py b/squishy/applets/taperipper/fat32.py deleted file mode 100644 index b099b33c..00000000 --- a/squishy/applets/taperipper/fat32.py +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from construct import ( - Const, Padding, - Struct, - Int16ul, Int32ul, - Bytes, -) - -__all__ = () - -boot_sector = Struct( - 'jmp' / Const(b'\xEB\x00\x90'), - 'oem_name' / Bytes(8), - 'params' / Struct( - 'bpb' / Struct( - 'sub_bpb' / Struct( - 'log_sec_bytes' / Int16ul, - 'log_sec_clust' / Bytes(1), - 'res_log_sec' / Int16ul, - 'fat_count' / Bytes(1), - 'max_roots' / Int16ul, - 'total_log_sec' / Int16ul, - 'media_desc' / Bytes(1), - 'log_sec_per_fat' / Int16ul, - ), - 'phys_sec' / Int16ul, - 'disk_heads' / Int16ul, - 'hidden_sect' / Int32ul, - 'total_log_sect' / Int32ul, - ), - 'logical_sectors' / Int32ul, - 'drive_desc' / Bytes(2), - 'version' / Bytes(2), - 'root_cluster_id' / Int32ul, - 'fs_logical_sec' / Int16ul, - 'first_log_sec' / Int16ul, - 'reserved' / Padding(12, pattern=b'\xF6'), - 'drive_num' / Bytes(1), - 'dunno_lol' / Bytes(1), - 'ext_boot_sig' / Bytes(1), - 'vol_id' / Bytes(4), - 'vol_label' / Bytes(11), - 'fs_type' / Bytes(8), - ), - 'phys_drive_num' / Bytes(1), - 'boot_sig' / Const(b'\x55\xAA') -) diff --git a/squishy/applets/taperipper/gpt.py b/squishy/applets/taperipper/gpt.py deleted file mode 100644 index 92969ab7..00000000 --- a/squishy/applets/taperipper/gpt.py +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from construct import ( - Padded, Padding, Const, - Struct, Array, - Int16ul, Int32ul, Int32ub, Int64ul, - Bytes, - -) - -__all__ = () - - - -guid = Struct( - 'raw' / Bytes(16) -) - -# Legacy MBR for GPT, prefer protective_mbr over this -legacy_mbr = Padded(int(512), Struct( - 'boot_code' / Bytes(424), # boot code for non-UEFI systems - 'disk_signature' / Int32ub, # unique disk signature - 'reserved' / Int16ul, # Unknown reserved - 'partition_recs' / Array(4, Struct( # Partition Records (*4) - 'boot_indicator' / Bytes(1), # Boot indicator: 0x80 for bootable - 'starting_chs' / Bytes(3), # Start of partition in CHS format - 'os_type' / Bytes(1), # OS Type (0xEF or 0xEE) - 'ending_chs' / Bytes(3), # End of partition in CHS format - 'starting_lba' / Int32ul, # Starting LBA of the partition - 'size_in_lba' / Int32ul, # Size of partition in LBA units - )), - 'signature' / Const(b'\x55\xAA'), # MBR Signature -)) - -protective_mbr = Struct( - 'boot_code' / Bytes(440), - 'disk_signature' / Padding(4), - 'reserved' / Padding(2), - 'part_records' / Array(4, Struct( - 'boot_indicator' / Const(b'\x00'), - 'starting_chs' / Const(b'\x00\x02\x00'), - 'os_type' / Const(b'\xEE'), - 'ending_chs' / Bytes(3), - 'starting_lba' / Const(b'\x01\x00\x00\x00'), - 'size_in_lba' / Int32ul, - )), - 'signature' / Const(b'\x55\xAA'), -) - -gpt_header = Padded(int(512), Struct( - 'signature' / Const(b'\x54\x52\x41\x50\x20\x49\x46\x45'), - 'revision' / Const(b'\x00\x01\x00\x00'), - 'header_size' / Int32ul, - 'crc32' / Int32ul, - Padding(4), - 'my_lba' / Int64ul, - 'alt_lba' / Int64ul, - 'fusable_lba' / Int64ul, - 'luseable_lba' / Int64ul, - 'disk_guid' / guid, - 'part_count' / Int32ul, - 'part_ent_size' / Int32ul, - 'part_ents_crc' / Int32ul, -)) diff --git a/squishy/applets/taperipper/tape.py b/squishy/applets/taperipper/tape.py deleted file mode 100644 index fff626db..00000000 --- a/squishy/applets/taperipper/tape.py +++ /dev/null @@ -1,29 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -# The contents of this module are specific to the -# 'taperipper' project: https://lethalbit.net/projects/taperipper/ - -from construct import ( - this, - Const, - Padded, - Array, AlignedStruct, Struct, - Int32ub, - Byte, -) - -__all__ = () - - -# TODO: header / data checksums? -tape_image_fmt = Padded(int(180e6), AlignedStruct(256, - 'header' / Padded(256, Struct( - 'magic_number' / Const(b'NYA~'), - 'tape_length' / Int32ub, - 'kernel_offset' / Int32ub, - 'kernel_length' / Int32ub, - 'initram_offset' / Int32ub, - 'initram_length' / Int32ub, - )), - 'kernel_img' / Array(this.header.kernel_length, Byte), - 'initramfs_img' / Array(this.header.initram_length, Byte), -)) diff --git a/squishy/cli.py b/squishy/cli.py index 0ea53af5..25653261 100644 --- a/squishy/cli.py +++ b/squishy/cli.py @@ -1,20 +1,30 @@ # SPDX-License-Identifier: BSD-3-Clause import logging as log -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, Namespace +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from rich import traceback from rich.logging import RichHandler -from . import config -from .actions.applet import Applet as ActionApplet -from .actions.cache import Cache as ActionCache -from .actions.provision import Provision as ActionProvision +from .paths import initialize_dirs + +from .actions import SquishyAction +from .actions.applet import AppletAction +from .actions.provision import ProvisionAction + +from .device import SquishyDevice + +from . import __version__ __all__ = ( 'main', ) -def setup_logging(args: Namespace = None) -> None: +AVAILABLE_ACTIONS = ( + (AppletAction.name, AppletAction()), + (ProvisionAction.name, ProvisionAction()), +) + +def setup_logging(verbose: bool = False) -> None: ''' Initialize logging subscriber @@ -23,14 +33,15 @@ def setup_logging(args: Namespace = None) -> None: Parameters ---------- - args : argparse.Namespace - Any command line arguments passed. + verbose : bool + If set, debug logging will be enabled ''' - level = log.INFO - if args is not None and args.verbose: + if verbose: level = log.DEBUG + else: + level = log.INFO log.basicConfig( force = True, @@ -42,100 +53,88 @@ def setup_logging(args: Namespace = None) -> None: ] ) -def init_dirs() -> None: +def main() -> int: ''' - Initialize Squishy application directories. + Squishy CLI Entrypoint. - Creates all of the appropriate directories that Squishy - expects, such as the config, and cache directories. - - This uses the XDG_* environment variables if they exist, - otherwise they assume that all the needed dirs are in the - running users home directory. + Returns + ------- + int + 0 if execution was successful, otherwise any other integer on error ''' - dirs = ( - config.SQUISHY_CACHE, - config.SQUISHY_DATA, - config.SQUISHY_CONFIG, + traceback.install() - config.SQUISHY_APPLETS, - config.SQUISHY_APPLET_CACHE, + initialize_dirs() + setup_logging() - config.SQUISHY_BUILD_DIR, + parser = ArgumentParser( + formatter_class = ArgumentDefaultsHelpFormatter, + description = 'Squishy SCSI Multitool', + prog = 'squishy' ) - for d in dirs: - if not d.exists(): - d.mkdir(parents = True, exist_ok = True) + parser.add_argument( + '--device', '-d', + type = str, + help = 'The serial number of the squishy to use if more than one is attached' + ) + parser.add_argument( + '--verbose', '-v', + action = 'store_true', + help = 'Enable verbose output during synth and pnr' + ) -def main() -> int: - ''' - Squishy CLI/REPL Runner + parser.add_argument( + '--version', '-V', + action = 'version', + version = f'Squishy v{__version__}', + help = 'Print Squishy version and exit' + ) - This is the main invocation point for the Squishy CLI and REPL. + action_parser = parser.add_subparsers( + dest = 'action', + required = True + ) - Returns - ------- - int - 0 if execution was successful, otherwise any other integer on error + # Enumerate available actions and register their arguments + if len(AVAILABLE_ACTIONS) > 0: + for (name, action) in AVAILABLE_ACTIONS: + p = action_parser.add_parser(name, help = action.description) + action.register_args(p) - ''' + # Actually parse the arguments + args = parser.parse_args() + + # Set-up logging *again* but if we want verbose output this time + setup_logging(args.verbose) try: - traceback.install() - - init_dirs() - setup_logging() - - ACTIONS = ( - { 'name': 'applet', 'instance': ActionApplet() }, - { 'name': 'cache', 'instance': ActionCache() }, - { 'name': 'provision', 'instance': ActionProvision() } - ) - - parser = ArgumentParser( - formatter_class = ArgumentDefaultsHelpFormatter, - description = 'Squishy SCSI Multitool', - prog = 'squishy' - ) - - parser.add_argument( - '--device', '-d', - type = str, - help = 'The serial number of the squishy to use if more than one is attached' - ) - - core_options = parser.add_argument_group('Core configuration options') - - core_options.add_argument( - '--verbose', '-v', - action = 'store_true', - help = 'Enable verbose output during synth and pnr' - ) - - action_parser = parser.add_subparsers( - dest = 'action', - required = True - ) - - if len(ACTIONS) > 0: - for act in ACTIONS: - action = act['instance'] - p = action_parser.add_parser( - act['name'], - help = action.short_help, - ) - action.register_args(p) - - args = parser.parse_args() - - setup_logging(args) - - act = list(filter(lambda a: a['name'] == args.action, ACTIONS))[0] - return act['instance'].run(args) + # Get the specified action, and invoke it with the appropriate arguments + act: tuple[str, SquishyAction] = next(filter(lambda a: a[0] == args.action, AVAILABLE_ACTIONS), None) + # Stupidly needed because we can't type an unpacked tuple + (name, instance) = act + + dev: SquishyDevice | None = None + + # Pull in the option, we don't care if it's set right now. + serial: str | None = args.device + + # This is now the specified device, or the first device, or no device + dev = SquishyDevice.get_device(serial = serial) + + # This action requires a device, so we need ensure we have gotten one + if instance.requires_dev: + if dev is None: + log.error('Selected action requires an attached device, but none found, aborting') + return 1 + + log.info(f'Selecting device: {dev}') + + ret = instance.run(args, dev) + return ret except KeyboardInterrupt: log.info('bye!') diff --git a/squishy/config.py b/squishy/config.py deleted file mode 100644 index a287e009..00000000 --- a/squishy/config.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from platformdirs import user_data_path, user_config_path, user_cache_path - -SQUISHY_NAME = 'squishy' - -# Squishy-specific sub dirs -SQUISHY_CACHE = user_cache_path(SQUISHY_NAME, False) -SQUISHY_DATA = user_data_path(SQUISHY_NAME, False) -SQUISHY_CONFIG = user_config_path(SQUISHY_NAME, False) - - -SQUISHY_APPLETS = (SQUISHY_DATA / 'applets') -SQUISHY_APPLET_CACHE = (SQUISHY_CACHE / 'applets') - -SQUISHY_BUILD_DIR = (SQUISHY_CACHE / 'build') - -# File path constants - -# Hardware Metadata, etc -USB_VID = 0x1209 -USB_PID_BOOTLOADER = 0xCA71 -USB_PID_APPLICATION = 0xCA70 -USB_MANUFACTURER = 'Shrine Maiden Heavy Industries' -USB_PRODUCT = { - USB_PID_BOOTLOADER : 'Squishy Bootloader', - USB_PID_APPLICATION: 'Squishy', -} - -SCSI_VID = 'Shrine-0' diff --git a/squishy/core/__init__.py b/squishy/core/__init__.py index 01db44d8..b41daa03 100644 --- a/squishy/core/__init__.py +++ b/squishy/core/__init__.py @@ -1,10 +1,5 @@ # SPDX-License-Identifier: BSD-3-Clause -from .device import ( - SquishyHardwareDevice -) +''' - -__all__ = ( - 'SquishyHardwareDevice', -) +''' diff --git a/squishy/gateware/bootloader/bitstream.py b/squishy/core/bitstream.py similarity index 66% rename from squishy/gateware/bootloader/bitstream.py rename to squishy/core/bitstream.py index 565c60fc..79cab54d 100644 --- a/squishy/gateware/bootloader/bitstream.py +++ b/squishy/core/bitstream.py @@ -1,9 +1,18 @@ # SPDX-License-Identifier: BSD-3-Clause +''' + +This module implements low-level FPGA bitstream munging, mainly used to build/pack +multi-boot bitstream images. + +Currently this is only used by the iCE40 platform (a.k.a Rev1) + +''' + + import logging as log from enum import IntEnum, IntFlag, unique - from construct import ( this, Switch, StopIf, Rebuild, Padded, GreedyRange, @@ -12,20 +21,38 @@ Nibble, Int8ub, Int16ub, Int24ub, Int32ub ) -from ...core.flash import FlashGeometry - -__doc__ = '''\ - -''' +from .flash import Geometry, Partition __all__ = ( 'iCE40BitstreamSlots', ) + class iCE40BitstreamSlots: + ''' + Generate iCE40 multi-boot bitstream slots + + Parameters + ---------- + flash_geometry : Geometry + The target flash geometry. + + Attributes + ---------- + Opcodes + iCE40 bitstream Opcodes. + + SpecialOpcodes + Sub-opcodes for ``Opcodes.SPECIAL``. + + BootMode + iCE40 bitstream boot-mode values. + + ''' @unique class Opcodes(IntEnum): + ''' iCE40 bitstream commands ''' SPECIAL = 0 BANK_NUM = 1 CRC_CHECK = 2 @@ -38,6 +65,7 @@ class Opcodes(IntEnum): @unique class SpecialOpcode(IntEnum): + ''' Sub-opcodes for Opcodes.SPECIAL ''' CRAM_DATA = 1 BRAM_DATA = 3 RESET_CRC = 5 @@ -46,6 +74,7 @@ class SpecialOpcode(IntEnum): @unique class BootModes(IntFlag): + ''' iCE40 Boot mode ''' SIMPLE = 0 COLD = 16 WARM = 32 @@ -94,10 +123,18 @@ class BootModes(IntFlag): ) ) - def __init__(self, flash_geometry: FlashGeometry) -> None: + def __init__(self, flash_geometry: Geometry) -> None: self._geometry = flash_geometry def build(self) -> bytearray: + ''' + Construct the multi-boot bitstream header based on the flash geometry + + Returns + ------- + bytearray + The constructed bitstream slot jump table for the flash image. + ''' data = bytearray(32 * 5) @@ -124,7 +161,20 @@ def build(self) -> bytearray: return data @staticmethod - def _build_slots(flash_geometry: FlashGeometry) -> list[bytes]: + def _build_slots(flash_geometry: Geometry) -> list[bytes]: + ''' + Build the raw slot data for the multi-boot flash. + + Parameters + ---------- + flash_geometry : Geometry + The target flash geometry. + + Returns + ------- + list[bytes] + Collection of serialized multi-boot slot bitstream jumps + ''' partitions = flash_geometry.partitions slots = [] @@ -136,7 +186,21 @@ def _build_slots(flash_geometry: FlashGeometry) -> list[bytes]: @staticmethod - def _build_slot(partition : dict[str, int]) -> bytes: + def _build_slot(partition: Partition) -> bytes: + ''' + Build bitstream stub for given flash slot partition. + + Parameters + ---------- + partition : Partition + Slot partition information + + Returns + ------- + bytes: + Constructed slot jump bitstream + ''' + return iCE40BitstreamSlots._slot.build({ 'bitstream': [ { @@ -145,7 +209,7 @@ def _build_slot(partition : dict[str, int]) -> bytes: }, { 'instruction': iCE40BitstreamSlots.Opcodes.BOOT_ADDR, - 'payload': { 'addr': partition['start_addr'] } + 'payload': { 'addr': partition.start_addr } }, { 'instruction': iCE40BitstreamSlots.Opcodes.BANK_OFFSET, diff --git a/squishy/core/cache.py b/squishy/core/cache.py index fd4d4175..2fe7c59b 100644 --- a/squishy/core/cache.py +++ b/squishy/core/cache.py @@ -1,106 +1,66 @@ # SPDX-License-Identifier: BSD-3-Clause -import logging as log +''' + +''' from pathlib import Path -from lzma import LZMACompressor -from shutil import rmtree +from hashlib import blake2b -from torii.build.run import LocalBuildProducts +from torii.build.run import BuildPlan, BuildProducts -from ..config import SQUISHY_APPLET_CACHE +from ..paths import SQUISHY_APPLET_CACHE __all__ = ( - 'SquishyBitstreamCache', + 'SquishyCache', ) -class SquishyBitstreamCache: - ''' Bitstream Cache system ''' - - # Initialize the cache directory - def _init_cache_dir(self, root: Path, depth: int = 1) -> None: - if depth == 0: - return - - for i in range(256): - cache_stub = root / f'{i:02x}' - if not cache_stub.exists(): - cache_stub.mkdir() - self._init_cache_dir(cache_stub, depth - 1) - - def _decompose_digest(self, digest: str) -> list[str]: - return [ - digest[ - (i*2):((i*2)+2) - ] - for i in range(len(digest) // 2) - ] - - def _get_cache_dir(self, digest: str) -> Path: - return self._cache_root.joinpath( - *self._decompose_digest(digest)[ - :self.tree_depth - ] - ) - - def __init__(self, do_init: bool = True, tree_depth: int = 1, cache_rtl: bool = True) -> None: - self.tree_depth = tree_depth - self.cache_rtl = cache_rtl - self._cache_root = Path(SQUISHY_APPLET_CACHE) - - if do_init: - if not (self._cache_root / 'ca').exists(): - log.debug('Initializing bitstream cache tree') - self._init_cache_dir(self._cache_root, tree_depth) - - def flush(self) -> None: - ''' Flush the cache ''' - log.info('Flushing applet cache') - rmtree(self._cache_root) - self._cache_root.mkdir() - - - def get(self, digest: str) -> dict[str, str | LocalBuildProducts]: - '''Attempt to retrieve a bitstream based on it's elaboration digest''' - bitstream_name = f'{digest}.bin' - cache_dir = self._get_cache_dir(digest) - bitstream = cache_dir / bitstream_name - - log.debug(f'Looking up bitstream \'{bitstream_name}\' in {cache_dir}') - - if not bitstream.exists(): - log.debug('Bitstream not found in cache') - return None - - log.debug('Bitstream found') - - return { - 'name' : bitstream_name, - 'products': LocalBuildProducts(str(cache_dir)) - } - - def store(self, digest: str, products: LocalBuildProducts, name: str) -> None: - ''' Store the synth products in the cache ''' - - bitstream_name = f'{digest}.bin' - cache_dir = self._get_cache_dir(digest) - bitstream = cache_dir / bitstream_name - - log.debug(f'Caching bitstream \'{name}.bin\' in {cache_dir}') - log.debug(f'New bitstream name: \'{bitstream_name}\'') - - with open(bitstream, 'wb') as bit: - bit.write(products.get(f'{name}.bin')) - - if self.cache_rtl: - for rtl_ext in ('debug.v', 'il'): - rtl_name = f'{digest}.{rtl_ext}.xz' - rtl = cache_dir / rtl_name - - log.debug(f'Caching RTL \'{name}.{rtl_ext}\' in {cache_dir}') - log.debug(f'New RTL name: \'{rtl_name}\'') - - cpr = LZMACompressor() - - with open(rtl, 'wb') as r: - r.write(cpr.compress(products.get(f'{name}.{rtl_ext}'))) - r.write(cpr.flush()) +class SquishyCache: + ''' + Squishy on-disk bitstream cache. + + ''' + + + def __init__(self) -> None: + self._tree_depth = 1 + + def get(self, name: str, plan: BuildPlan) -> BuildProducts | None: + ''' + Get the cached version of the built gateware + + Parameters + ---------- + name : str + The name of the gateware. + + plan : BuildPlan + The generated build plan from Torii. + + Returns + ------- + BuildProducts | None + If found in the cache, an instance of LocalBuildProducts, otherwise None + + ''' + return None + + def store(self, name: str, products: BuildProducts) -> BuildProducts: + ''' + Store the gateware, generated HDL, and synthesis/pnr logs in the cache. + + Parameters + ---------- + name : str + The name of the gateware. + + products : BuildProducts + The output BuildProducts from executing the Torii BuildPlan. + + Returns + ------- + BuildProducts + The re-homed BuildProducts from the cache rather than the build directory. + + ''' + + return products diff --git a/squishy/core/collect.py b/squishy/core/collect.py deleted file mode 100644 index fa791235..00000000 --- a/squishy/core/collect.py +++ /dev/null @@ -1,101 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from pkgutil import walk_packages -from importlib import import_module -from inspect import getmembers, isclass -from typing import Callable - -__all__ = ( - 'collect_members', - 'predicate_applet', - 'predicate_action', - 'predicate_class', -) - - -def predicate_applet(member: object) -> bool: - ''' - Applet predicate - - This predicate filters on if the member is a sub class of :py:class:`SquishyApplet` - and not an instance of that class itself. - - Returns - ------- - bool - If the predicate matches. - - ''' - - from ..applets import SquishyApplet - if isclass(member): - return issubclass(member, SquishyApplet) and member is not SquishyApplet - return False - -def predicate_action(member: object) -> bool: - ''' - Action predicate - - This predicate filters on if the member is a sub class of :py:class:`SquishyAction` - and not an instance of that class itself. - - Returns - ------- - bool - If the predicate matches. - - ''' - - from ..actions import SquishyAction - if isclass(member): - return issubclass(member, SquishyAction) and member is not SquishyAction - return False - -def predicate_class(member: object) -> bool: - ''' - Class predicate - - This predicate filters on if the member is a class. - - Returns - ------- - bool - If the predicate matches. - - ''' - - return isclass(member) - -def collect_members( - pkg: str, pred: Callable[[object], bool], prefix: str = '', make_instance: bool = True -) -> list[dict[str, str | object]]: - ''' - Collect members from package - - This method collects list of members from a given package, and optionally creates - and instance of them. - - Returns - ------- - list[dict[str, str | object]] - The list of members, their name and type, or optionally and instance of said type. - - ''' - - members: list[dict[str, str | object]] = list() - - for _, pkg_name, __ in walk_packages( - path = (pkg,), - prefix = prefix - ): - pkg_import = import_module(pkg_name) - found_members = getmembers(pkg_import, pred) - - if len(found_members) > 0: - for name, member in found_members: - members.append({ - 'name' : name.lower(), - 'instance': member() if make_instance else member - }) - - return members diff --git a/squishy/core/config.py b/squishy/core/config.py new file mode 100644 index 00000000..8f0a7786 --- /dev/null +++ b/squishy/core/config.py @@ -0,0 +1,300 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' +This module contains various classes and types +for making dealing with things like configuration +and command line argument options more sane. + +This file also contains the various "set in stone" constants +that are used for default/constant initialization. +''' + +from typing import TypeAlias + +from .flash import Geometry as FlashGeometry + +__all__ = ( + # PLL Configurations for the various hardware platforms + 'ICE40PLLConfig', + 'ECP5PLLOutput', + 'ECP5PLLConfig', + 'PLLConfig', # Type Alias + # Peripherals + 'USBConfig', + 'SCSIConfig', + 'FlashConfig', + + # Fixed/Default configurations + 'USB_DFU_CONFIG', +) + +# Constants + +USB_VID = 0x1209 +USB_DFU_PID = 0xCA71 +USB_APP_PID = 0xCA70 + +USB_MANUFACTURER = 'Shrine Maiden Heavy Industries' + +SCSI_VID = 'Shrine-0' + +# Configuration Wrappers + +class ICE40PLLConfig: + ''' + An iCE40 SB_PLL40_PAD PLL Configuration + + This is only used for the :py:class:`squishy.gateware.rev1` platform. + + Parameters + ---------- + divr : int + PLL reference clock divisor. + + divf : int + PLL feedback divisor. + + divq : int + PLL VCO divisor. + + filter_range : int + PLL filter range. + + ofreq : int + The output frequency of the PLL in MHz + + + Attributes + ---------- + divr : int + PLL reference clock divisor. + + divf : int + PLL feedback divisor. + + divq : int + PLL VCO divisor. + + filter_range : int + PLL filter range. + + ofreq : int + The output frequency of the PLL in MHz + + ''' + + def __init__(self, *, divr: int, divf: int, divq: int, filter_range: int, ofreq: int) -> None: + self.divr = divr + self.divf = divf + self.divq = divq + self.filter_range = filter_range + self.ofreq = ofreq + +class ECP5PLLOutput: + ''' + A ECP5 EHXPLLL output, either the primary output or any of the 3 auxillary outputs. + + Parameters + ---------- + ofreq : int + The frequency of this PLL output in MHz + + clk_div : int + The clock divisor of this PLL output + + cphase : int + The clock phase of this PLL output + + fphase : int + The feedback phase of this PLL output + + Attributes + ---------- + ofreq : int + The frequency of this PLL output in MHz + + clk_div : int + The clock divisor of this PLL output + + cphase : int + The clock phase of this PLL output + + fphase : int + The feedback phase of this PLL output + ''' + + def __init__(self, *, ofreq: int, clk_div: int, cphase: int, fphase: int) -> None: + self.ofreq = ofreq + self.clk_div = clk_div + self.cphase = cphase + self.fphase = fphase + +class ECP5PLLConfig: + ''' + A ECP5 EHXPLLL configuration + + Parameters + ---------- + ifreq : int + The PLLs input clock frequency in MHz + + clki_div : int + The PLL input clock divisor + + clkfb_div : int + The PLL feedback clock divisor + + clkp : ECP5PLLOutput + The Primary PLL output clock configuration + + clks : ECP5PLLOutput + The secondary PLL output clock configuration + + clks2 : ECP5PLLOutput | None + The optional tertiary PLL output clock configuration + + clks3 : ECP5PLLOutput | None + The optional quaternary PLL output clock configuration + + Attributes + ---------- + ifreq : int + The PLLs input clock frequency in MHz + + clki_div : int + The PLL input clock divisor + + clkfb_div : int + The PLL feedback clock divisor + + clkp : ECP5PLLOutput + The Primary PLL output clock configuration + + clks : ECP5PLLOutput | None + The optional secondary PLL output clock configuration + + clks2 : ECP5PLLOutput | None + The optional tertiary PLL output clock configuration + + clks3 : ECP5PLLOutput | None + The optional quaternary PLL output clock configuration + + ''' + + def __init__( + self, *, + ifreq: int, clki_div: int, clkfb_div: int, + clkp: ECP5PLLOutput, + clks: ECP5PLLOutput | None = None, + clks2: ECP5PLLOutput | None = None, + clks3: ECP5PLLOutput | None = None, + ) -> None: + + self.ifreq = ifreq + self.clki_div = clki_div + self.clkfb_div = clkfb_div + self.clkp = clkp + self.clks = clks + self.clks2 = clks2 + self.clks3 = clks3 + +PLLConfig: TypeAlias = ECP5PLLConfig | ICE40PLLConfig + +class USBConfig: + ''' + USB Device Configuration Options + + Parameters + ---------- + vid : int + The USB Vendor ID + + pid : int + The USB Product ID + + mfr : str + The manufacturer field of the USB descriptor + + prod : str + The product field of the USB descriptor + + Attributes + ---------- + vid : int + The USB Vendor ID + + pid : int + The USB Product ID + + manufacturer : str + The manufacturer field of the USB descriptor + + product : str + The product field of the USB descriptor + ''' + + def __init__(self, *, vid: int, pid: int, mfr: str, prod: str) -> None: + self.vid = vid + self.pid = pid + self.manufacturer = mfr + self.product = prod + +# TODO(aki): We should probably support all of the `INQUIRY` fields here, maybe +class SCSIConfig: + ''' + SCSI Configuration + + Parameters + ---------- + vid : str + The SCSI Vendor ID. + + did : str + The SCSI Target/Initiator ID. + + Attributes + ---------- + vid : str + The SCSI Vendor ID. + + did : int + The SCSI Target/Initiator ID. + ''' + + def __init__(self, *, vid: str, did: int) -> None: + self.vid = vid + self.did = did + + +class FlashConfig: + ''' + Configuration options for attached SPI boot flash. + + Attributes + ---------- + geometry : FlashGeometry + The layout of the on-board flash + commands : dict[str, int] | None + The optional mapping of command name to opcode + ''' + + def __init__(self, *, geometry: FlashGeometry, commands: dict[str, int] | None = None) -> None: + self.geometry = geometry + self.commands = commands + + +# Static/Default configurations + +USB_DFU_CONFIG = USBConfig( + vid = USB_VID, + pid = USB_DFU_PID, + mfr = USB_MANUFACTURER, + prod = 'Squishy DFU' +) + +USB_APP_CONFIG = USBConfig( + vid = USB_VID, + pid = USB_APP_PID, + mfr = USB_MANUFACTURER, + prod = 'Squishy' +) diff --git a/squishy/core/device.py b/squishy/core/device.py deleted file mode 100644 index 6f415619..00000000 --- a/squishy/core/device.py +++ /dev/null @@ -1,540 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -import logging as log - -from typing import Iterable, Type, Callable, TypeVar, TYPE_CHECKING -from time import sleep -from datetime import datetime - -from usb1 import USBContext, USBDevice, USBError -from usb1.libusb1 import ( - LIBUSB_REQUEST_TYPE_CLASS, LIBUSB_RECIPIENT_INTERFACE, LIBUSB_ERROR_IO, LIBUSB_ERROR_NO_DEVICE -) - -from usb_construct.types import LanguageIDs -from usb_construct.types.descriptors.dfu import FunctionalDescriptor - -from rich.progress import Progress - -from .dfu_types import DFU_CLASS, DFURequests, DFUState, DFUStatus -from ..config import USB_VID, USB_PID_APPLICATION, USB_PID_BOOTLOADER - -__all__ = ( - 'SquishyHardwareDevice', -) - -# Due to how libusb1 works and how we're using it -# This needs to be global so it can live for the -# life of the runtime -_USB_CTX: USBContext | None = None - -# Type variable to allow generic typing of things -T = TypeVar('T') - -class SquishyHardwareDevice: - ''' - Squishy Hardware Device - - This class represents and abstracted Squishy hardware device, exposing a common - and stable API for applets to interact with the hardware on. - - Parameters - ---------- - dev : usb1.USBDevice - The USB device handle for the hardware platform. - - serial : str - The serial number of the device. - - Attributes - ---------- - serial : str - The serial number of the device. - - rev : int - The revision of the hardware of the device. - - ''' - - def _get_dfu_interface(self, cfg: int | None) -> int | None: - ''' Get the interface ID that matches the ``_DFU_CLASS`` ''' - if self._dfu_iface is None and cfg is not None: - for cfg in self._dev.iterConfigurations(): - for iface in cfg: - for ifset in iface: - if ifset.getClassTuple() == DFU_CLASS: - self._dfu_cfg: int = cfg.getConfigurationValue() - self._dfu_iface: int = ifset.getNumber() - if self._usb_hndl.getConfiguration() != self._dfu_cfg: - self._usb_hndl.setConfiguration(self._dfu_cfg) - return self._dfu_iface - - return self._dfu_iface - - def _get_dfu_status(self) -> tuple[DFUStatus, DFUState]: - ''' Get DFU Status ''' - interface_id = self._get_dfu_interface(self._dfu_cfg) - if interface_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - self._ensure_iface_claimed(interface_id) - - data: bytearray | None = self._usb_hndl.controlRead( - LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, - DFURequests.GetStatus, - 0, - interface_id, - 6, - self._timeout - ) - if data is None: - raise RuntimeError(f'Unable to send control request DFU_GETSTATUS to interface {interface_id}') - - return (DFUStatus(data[0]), DFUState(data[4])) - - def _get_dfu_state(self) -> DFUState: - ''' Get the DFU State ''' - interface_id = self._get_dfu_interface(self._dfu_cfg) - if interface_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - self._ensure_iface_claimed(interface_id) - - data: bytearray | None = self._usb_hndl.controlRead( - LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, - DFURequests.GetState, - 0, - interface_id, - 1, - self._timeout - ) - if data is None: - raise RuntimeError(f'Unable to send control request DFU_GETSTATE to interface {interface_id}') - - return DFUState(data[0]) - - def _send_dfu_detach(self) -> bool: - ''' Invoke a DFU Detach ''' - interface_id = self._get_dfu_interface(self._dfu_cfg) - if interface_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - self._ensure_iface_claimed(interface_id) - - try: - sent: int = self._usb_hndl.controlWrite( - LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, - DFURequests.Detach, - 0, - interface_id, - bytearray(), - self._timeout - ) - except USBError as error: - # If the error is one of the not-actually-an-error errors caused by the device rebooting, palm it off - if error.value in (LIBUSB_ERROR_IO, LIBUSB_ERROR_NO_DEVICE): - self._claimed_interfaces.remove(self._dfu_iface) - sent = 0 - # Otherwise propagate the error properly - else: - raise BufferError( - f'Unable to send control request for DFU_DETACH to interface {interface_id}' - ) from error - - self._ensure_iface_released(interface_id) - return sent == 0 - - def _get_dfu_altmodes(self) -> dict[int, str]: - ''' Get the DFU alt-modes ''' - log.debug('Getting DFU alt-modes') - - interface_id = self._get_dfu_interface(self._dfu_cfg) - config_id = self._dfu_cfg - if interface_id is None or config_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - def find_if(collection: Iterable[T], predicate: Callable[[T], bool]) -> T | None: - for item in collection: - if predicate(item): - return item - return None - - # Find the correct configuration for the DFU interface we're talking to - config = find_if(self._dev.iterConfigurations(), lambda config: config.getConfigurationvalue() == config_id) - if config is None: - raise AssertionError('Failed to re-locate USB configuration for DFU') - # Then also the actual interface descriptors for the interface - iface = find_if(config.iterInterfaces(), lambda iface: next(iter(iface)).getNumber() == interface_id) - if iface is None: - raise AssertionError('Failed to re-locate USB interface for DFU') - - alt_modes: dict[int, str] = dict() - for alt_mode in iface: - alt_mode_id: int = alt_mode.getAlternateSetting() - # Try and get the interface alt-mode's string descriptor - alt_mode_str = self._usb_hndl.getStringDescriptor( - alt_mode.getDescriptor(), - LanguageIDs.ENGLISH_US - ) - # Bake a string if that failed and add it to the dict - alt_modes[alt_mode_id] = alt_mode_str if alt_mode_str is not None else f'Slot {alt_mode_id}' - - log.debug(f'Found {len(alt_modes.keys())} alt-modes') - return alt_modes - - def _get_dfu_tx_size(self) -> int | None: - ''' Get the DFU transaction size ''' - interface_id = self._get_dfu_interface(self._dfu_cfg) - config_id = self._dfu_cfg - if interface_id is None or config_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - def find_if(collection: Iterable[T], predicate: Callable[[T], bool]) -> T | None: - for item in collection: - if predicate(item): - return item - return None - - # Find the correct configuration for the DFU interface we're talking to - config = find_if(self._dev.iterConfigurations(), lambda config: config.getConfigurationvalue() == config_id) - if config is None: - raise AssertionError('Failed to re-locate USB configuration for DFU') - # Then also the actual interface descriptors for the interface - iface = find_if(config.iterInterfaces(), lambda iface: next(iter(iface)).getNumber() == interface_id) - if iface is None: - raise AssertionError('Failed to re-locate USB interface for DFU') - - # Extract the first alt-mode interface descriptor from the interface - settings = next(iter(iface)) - extra = settings.getExtra() - # Check there's one functional descriptor - assert len(extra) == 1, '*sadface' - # Now parse the descriptor as a DFU Functional Descriptor and return the embedded transfer size - func_desc = FunctionalDescriptor.parse(extra[0]) - if TYPE_CHECKING: - assert isinstance(func_desc.wTransferSize, int) - return func_desc.wTransferSize - - def _enter_dfu_mode(self) -> bool: - ''' Enter the DFU bootloader ''' - if self._get_dfu_state() == DFUState.AppIdle: - log.debug('Device is in Application mode, attempting to detach') - self._send_dfu_detach() - self._usb_hndl.close() - self._dev.close() - self._dfu_iface = None - - devices = list() - - log.info(f'Waiting for device \'{self.serial}\' to come back') - sleep(self._timeout / 1000) - while len(devices) == 0: - devices = list(filter( - lambda dev: dev[0] == self.serial, - SquishyHardwareDevice.enumerate() - )) - - log.debug('Device came back, re-caching device handle') - self._dev: USBDevice = devices[0][2] - self._usb_hndl = self._dev.open() - - state = self._get_dfu_state() - - log.debug('Checking DFU state') - if state != DFUState.DFUIdle: - log.error(f'Device was in improper DFU state: {state}') - return False - log.debug('Device is in DFUIdle, ready for operations') - return True - - def _send_dfu_download(self, data: bytearray, chunk_num: int) -> bool: - ''' Send a DFU Download transaction ''' - - interface_id = self._get_dfu_interface(self._dfu_cfg) - if interface_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - self._ensure_iface_claimed(interface_id) - - sent: int = self._usb_hndl.controlWrite( - LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, - DFURequests.Download, - chunk_num, - interface_id, - data, - self._timeout - ) - return sent == len(data) - - def _ensure_iface_claimed(self, id: int) -> None: - if id not in self._claimed_interfaces: - self._usb_hndl.claimInterface(id) - self._claimed_interfaces.append(id) - - def _ensure_iface_released(self, id: int) -> None: - if id in self._claimed_interfaces: - self._claimed_interfaces.remove(id) - # If this throws an exception that matches the no-device condition, turn that into a - # "nothing to see here" as that just means the device is rebooting - try: - self._usb_hndl.releaseInterface(id) - except USBError as error: - if error.value != LIBUSB_ERROR_NO_DEVICE: - raise - - def can_dfu(self) -> bool: - ''' Check to see if the Device can DFU ''' - log.debug('Checking to see if device is DFU capable') - return any( - filter( - lambda t: t == DFU_CLASS, - map( - lambda s: s.getClassTupple(), - self._dev.iterSettings() - ) - ) - ) - - def __init__(self, dev: USBDevice, serial: str, timeout: int = 2500, **kwargs) -> None: - self._dev = dev - self._usb_hndl = self._dev.open() - if not self.can_dfu(): - raise RuntimeError(f'The device {dev.getVendorID():04x}:{dev.getProductID():04x} @ {dev.getBusNumber()} is not DFU capable.') - self._timeout: int = timeout - self._dfu_cfg: int | None = None - self._dfu_iface: int | None = None - self.serial = serial - self.raw_ver = dev.getbcdDevice() - self.dec_ver = self._decode_version(self.raw_ver) - self.rev = int(self.dec_ver) - self.gate_ver = int((self.dec_ver - self.rev) * 100) - self._claimed_interfaces = list() - - def __del__(self) -> None: - self._usb_hndl.close() - self._dev.close() - - @staticmethod - def _decode_version(bcd: int) -> float: - i = bcd >> 8 - i = ((i >> 4) * 10) + (i & 0xf) - d = bcd & 0xff - d = ((d >> 4) * 10) + (d & 0xf) - return i + (d / 100) - - def _update_serial(self) -> None: - ''' Update the serial number from the attached device ''' - hndl = self._dev.open() - - self.serial = hndl.getStringDescriptor( - self._dev.getSerialNumberDescriptor(), - LanguageIDs.ENGLISH_US - ) - - hndl.close() - - @staticmethod - def make_serial() -> str: - ''' - Make a new serial number string. - - The default serial number is the current time and date - in UTC in an ISO 8601-like format. - - Returns - ------- - str - The new serial number - - ''' - return datetime.utcnow().strftime( - '%Y%m%dT%H%M%SZ' - ) - - @classmethod - def get_device(cls: Type['SquishyHardwareDevice'], serial: str = None) -> Type['SquishyHardwareDevice'] | None: - ''' - Get attached Squishy device. - - Get the attached and selected squishy device if possible, or if only - one is attached to the system use that one. - - Parameters - ---------- - serial : str - The serial number if any. - - Returns - ------- - None - If no device is selected - - squishy.core.device.SquishyHardwareDevice - The selected hardware if available. - - ''' - def print_devtree() -> None: - from rich.tree import Tree - from rich import print - - dev_tree = Tree( - '[green]Attached Devices[/]', - guide_style = 'blue' - ) - for idx, tup in enumerate(devices): - node = dev_tree.add(f'[magenta]{idx}[/]') - node.add(f'SN: [bright_green]{tup[0]}[/]') - node.add(f'Rev: [bright_cyan]{int(tup[1])}[/]') - print(dev_tree) - - devices = SquishyHardwareDevice.enumerate() - dev_count = len(devices) - - if dev_count > 1: - if serial is None: - log.error(f'No serial number specified, unable to pick from {dev_count} attached devices.') - print_devtree() - return None - - found = list(filter(lambda sn, _, __: sn == serial, devices)) - - if len(found) > 1: - log.error(f'Multiple devices matching serial number \'{serial}\'') - return None - elif len(found) == 0: - log.error(f'No devices matching serial number \'{serial}\'') - print_devtree() - else: - dev = SquishyHardwareDevice(found[2], found[0]) - log.info(f'Found Squishy rev{dev.rev} matching serial \'{dev.serial}\'') - return dev - elif dev_count == 1: - found = devices[0] - if serial is not None: - if serial != found[0]: - log.error(f'Connected Squishy has serial number \'{found[0]}\' but \'{serial}\' was specified') - return None - else: - log.warn('No serial specified') - log.info('Using only Squishy attached to system') - - dev = SquishyHardwareDevice(found[2], found[0]) - log.info(f'Found Squishy rev{dev.rev} matching serial \'{dev.serial}\'') - return dev - else: - log.error('No Squishy devices found attached to system') - return None - - - @classmethod - def enumerate(cls: Type['SquishyHardwareDevice']) -> list[tuple[str, float, USBDevice]]: - ''' - Enumerate attached devices - - Returns - ------- - List[Tuple[str, float, usb1.USBDevice]] - The collection of :py:class:`SquishyDeviceContainer` objects that match the - enumeration critera. - - ''' - global _USB_CTX - - devices = list() - - if _USB_CTX is None: - _USB_CTX = USBContext() - - for dev in _USB_CTX.getDeviceIterator(): - vid = dev.getVendorID() - pid = dev.getProductID() - - if vid == USB_VID and (pid == USB_PID_APPLICATION or pid == USB_PID_BOOTLOADER): - try: - hndl = dev.open() - - sn = hndl.getStringDescriptor( - dev.getSerialNumberDescriptor(), - LanguageIDs.ENGLISH_US - ) - ver = cls._decode_version(dev.getbcdDevice()) - - devices.append((sn, ver, dev)) - hndl.close() - except USBError as e: - log.error(f'Unable to open suspected squishy device: {e}') - log.error('Maybe check your udev rules?') - return devices - - def get_altmodes(self): - return self._get_dfu_altmodes() - - def reset(self) -> bool: - ''' Reset the device ''' - return self._send_dfu_detach() - - def upload(self, data: bytearray, slot: int, progress: Progress | None = None) -> bool: - ''' Push Firmware/Gateware to device ''' - if not self._enter_dfu_mode(): - return False - - log.info(f'Starting DFU upload of {len(data)} bytes to slot {slot}') - - interface_id = self._get_dfu_interface(self._dfu_cfg) - if interface_id is None: - raise RuntimeError('Unable to get interface ID for DFU Device') - - self._ensure_iface_claimed(interface_id) - - log.debug(f'Setting interface {interface_id} alt to {slot}') - self._usb_hndl.setInterfaceAltSetting(interface_id, slot) - - - def chunker(size: int, data: Iterable): - from itertools import zip_longest - return zip_longest(*[iter(data)]*size) - - tx_size = self._get_dfu_tx_size() - if tx_size is None: - raise RuntimeError('Unable to get DFU transaction size for device') - - - prog_task = progress.add_task('Programming', start = True, total = len(data)) - - log.debug(f'DFU Transfer size is {tx_size}') - - for chunk_num, chunk in enumerate(chunker(tx_size, data)): - chunk_data = bytearray(b for b in chunk if b is not None) - if not self._send_dfu_download(chunk_data, chunk_num): - log.error(f'DFU Transaction failed, did not sent all data for chunk {chunk_num}') - return False - progress.update(prog_task, advance = len(chunk_data)) - while self._get_dfu_state() != DFUState.DlSync: - sleep(0.05) - - status, state = self._get_dfu_status() - if state != DFUState.DlSync: - log.error(f'DFU State is {state} not DlIdle, aborting') - return False - - chunk_num += 1 - - log.debug(f'Wrote {chunk_num} chunks to device') - assert self._send_dfu_download(bytearray(), chunk_num), 'Uoh nowo' - _, state = self._get_dfu_status() - - if state != DFUState.DFUIdle: - log.error('Device did not go idle after upload') - return False - progress.update(prog_task, completed = True) - return True - - - def download(self, slot: int) -> bytearray | None: - ''' Pull Firmware/Gateware from device (if supported) ''' - return None - - def __repr__(self) -> str: - return f'' - - def __str__(self) -> str: - return f'rev{self.rev} SN: {self.serial}' diff --git a/squishy/core/dfu_types.py b/squishy/core/dfu.py similarity index 99% rename from squishy/core/dfu_types.py rename to squishy/core/dfu.py index b1c25476..f259ca1c 100644 --- a/squishy/core/dfu_types.py +++ b/squishy/core/dfu.py @@ -1,13 +1,14 @@ # SPDX-License-Identifier: BSD-3-Clause -from enum import IntEnum, unique - -from usb_construct.types.descriptors import InterfaceClassCodes, ApplicationSubclassCodes +''' -__doc__ = '''\ ''' +from enum import IntEnum, unique + +from usb_construct.types.descriptors import InterfaceClassCodes, ApplicationSubclassCodes + __all__ = ( 'DFUState', 'DFUStatus', diff --git a/squishy/core/exceptions.py b/squishy/core/exceptions.py deleted file mode 100644 index 9b20b762..00000000 --- a/squishy/core/exceptions.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -__all__ = ( - 'SquishyException', - 'SquishyAppletError', - 'SquishyDeviceError', - 'SquishyBuildError', -) - -class SquishyException(Exception): - '''Base class for Squishy related exceptions''' - pass - -class SquishyAppletError(SquishyException): - '''Exceptions related to Squishy applets''' - pass - -class SquishyDeviceError(SquishyException): - '''Exceptions related to Squishy hardware''' - pass - -class SquishyBuildError(SquishyException): - '''Exceptions related to Squishy builds''' - pass diff --git a/squishy/core/flash.py b/squishy/core/flash.py index 6b10d5b1..92f6767c 100644 --- a/squishy/core/flash.py +++ b/squishy/core/flash.py @@ -1,66 +1,165 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = '''\ +''' ''' +from construct import Struct, Int8ul, Int32ul, Int24ul, Array, len_, this + __all__ = ( - 'FlashGeometry', + 'Geometry', + 'Partition', +) + + +# Rev2+ Flash structure +slot_header = Struct( + 'fpga_id' / Int32ul, + 'flags' / Int8ul, + 'bitstream_length' / Int24ul ) -class FlashGeometry: - ''' SPI Flash Geometry ''' +flash_slot = Struct( + 'header' / slot_header, + 'bitstream' / Array(len_(this.header) - 2097152, Int8ul) +) + +flash_layout = Struct( + 'slots' / Array(3, flash_slot), + 'data' / Array(2097152, Int8ul) +) + +class Partition: + ''' + SPI Flash slot partition metadata. + + Parameters + ---------- + start_addr : int + The start address of this partition. + + end_addr : int + The end address of this partition. + + Attributes + ---------- + start_addr : int + The start address of this partition. + + end_addr : int + The end address of this partition. + + size : int + The size in bytes of this partition. + ''' + + def __init__(self, *, start_addr: int, end_addr: int) -> None: + self.start_addr = start_addr + self.end_addr = end_addr + + @property + def size(self) -> int: + return self.end_addr - self.start_addr + +class Geometry: + ''' + SPI Flash Geometry + + This class represents the geometry of the attached SPI flash, such as it's size, + address with. + + It also provides a mechanism to segment the flash into slots for multi-boot/multi-image + situations. + + Parameters + ---------- + size : int + The total size in bytes of the flash. + + page_size : int + The size in bytes of each flash page. - def __init__(self, *, size: int, page_size: int, erase_size: int, addr_width: int = 24) -> None: - ''' ''' + erase_size : int + The size in bytes of the effected area for the `erase` command. + slot_size : int + The size in bytes of any possible slots in this flash. + + slot_size : int + The number of slots to place in flash. (default: 4) + + addr_width : int + The size in bits of addresses for the flash. (default: 24) + + Attributes + ---------- + size : int + The total size in bytes of the flash. + + page_size : int + The size in bytes of each flash page. + + erase_size : int + The size in bytes of the effected area for the `erase` command. + + slot_size : int + The size in bytes of any possible slots in this flash. + + addr_width : int + The size in bits of addresses for the flash. + + max_slots : int + The maximum number of possible slots for this flash. + + slots : int + The number of possible slots for this flash. + + partitions : dict[int, squishy.core.flash.Partition] + The flash partition layout and slot mapping. + + ''' + + def __init__(self, *, size: int, page_size: int, erase_size: int, slot_size: int, slot_count: int = 4, addr_width: int = 24) -> None: self.size = size self.page_size = page_size self.erase_size = erase_size + self.slot_size = slot_size self.addr_width = addr_width + self._slots = slot_count + + @property + def max_slots(self) -> int: + return self.size // self.slot_size @property def slots(self) -> int: - possible_slots = self.size // self.slot_size - slots = min(self._slots, possible_slots) - assert slots > 1, f'{slots}' - return slots + slot_count = min(self._slots, self.max_slots) + return slot_count @slots.setter - def slots(self, slots: int): - assert slots >= 2, f'Must have at least 2 flash slots configured, {slots} specified' + def slots(self, slots: int) -> None: + if slots > 2: + raise ValueError(f'Must have at least 2 slots configured, {slots} specified') + self._slots = slots @property - def partitions(self) -> dict[int, dict[str, int]]: - - partitions = dict() + def partitions(self) -> dict[int, Partition]: + partitions: dict[int, Partition] = {} start_addr = self.erase_size for slot in range(self.slots): end_addr = self.slot_size if slot == 0 else start_addr + self.slot_size - partitions[slot] = { - 'start_addr': start_addr, - 'end_addr': end_addr - } + partitions[slot] = Partition( + start_addr = start_addr, + end_addr = end_addr + ) if slot == 0: start_addr = self.slot_size else: start_addr += self.slot_size - return partitions - def init_slots(self, device: str) -> 'FlashGeometry': - - self.slots = 4 - - self.slot_size = { - 'iCE40HX8K': 2**18, - 'LFE5UM5G-45F': 2**21, - }.get(device, None) - - assert self.slot_size is not None, f'Unsupported platform device {device}' - - return self + return partitions diff --git a/squishy/core/reflection.py b/squishy/core/reflection.py new file mode 100644 index 00000000..ea9dede3 --- /dev/null +++ b/squishy/core/reflection.py @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' +This module provides runtime/dynamic reflection helpers for iterating Python +module/package contents and the like. + +''' + +from pkgutil import walk_packages +from importlib import import_module +from inspect import getmembers, isclass +from typing import Callable + +__all__ = ( + 'collect_members', + # Helper predicates + 'is_applet', +) + +# TODO(aki): This is likely awful +def is_applet(member: object) -> bool: + ''' + Determine if the given object is a :py:class:`SquishyApplet` or not. + + Parameters + ---------- + member : object + The member object to inspect + + Returns + ------- + bool + Returns True if the given ``member`` object is an instance of :py:class:`SquishyApplet`, otherwise False. + ''' + + from ..applets import SquishyApplet + if isclass(member): + return issubclass(member, SquishyApplet) and member is not SquishyApplet + return False + +# TODO(aki): This is slow and bad, needs a re-think +def collect_members(pkg: str, predicate: Callable[[object], bool], prefix: str = '', make_instance: bool = False): + ''' + Collect members from package. + + This method collects a list of members from a given package, and optionally + creates an instance of them. + + Parameters + ---------- + pkg : str + The name of the Python package to iterate over. + + predicate : Callable[[object], bool] + The discriminator predicate used to filter members. + + prefix : str + The prefix to add to the result of the package walk. + + make_instance : bool + If True, instantiate an instance of the found types matching the predicate prior to returning. + + Returns + ------- + + ''' + + members = [] + + for (_, name, _) in walk_packages(path = (pkg, ), prefix = prefix): + pkg_import = import_module(name) + found_members = getmembers(pkg_import, predicate) + + if len(found_members) > 0: + for (_, member) in found_members: + members.append(member if not make_instance else member()) + + return members diff --git a/squishy/device.py b/squishy/device.py new file mode 100644 index 00000000..e1a1dff4 --- /dev/null +++ b/squishy/device.py @@ -0,0 +1,817 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +import logging as log + +from contextlib import contextmanager +from typing import TypeAlias, TypeVar, Iterable, Callable, TYPE_CHECKING, Self +from time import sleep +from datetime import datetime, timezone +from itertools import zip_longest + +from usb1 import USBContext, USBDevice, USBError +from usb1.libusb1 import ( + LIBUSB_REQUEST_TYPE_CLASS, LIBUSB_RECIPIENT_INTERFACE, LIBUSB_ERROR_IO, LIBUSB_ERROR_NO_DEVICE +) + +from usb_construct.types import LanguageIDs +from usb_construct.types.descriptors.dfu import FunctionalDescriptor + +from rich.progress import Progress + +from .core.dfu import DFU_CLASS, DFURequests, DFUState, DFUStatus +from .core.config import USB_VID, USB_APP_PID, USB_DFU_PID + +from .gateware import SquishyPlatformType, AVAILABLE_PLATFORMS + +__all__ = ( + 'SquishyDevice', +) + + +# NOTE(aki): Due to how `libusb1` works and how we're using it, we need to +# hold a global reference to the context, I don't like it any more than the +# next girl, but here we are. +_LIBUSB_CTX: USBContext | None = None + +# Type Alias to simplify life +DeviceContainer: TypeAlias = tuple[str, tuple[int, int], USBDevice] + +T = TypeVar('T') + +# This is here because `next(filter(...), None)` doesn't propagate types properly +# TODO(aki): Maybe move to a helpers/utility module? +def _find_if(collection: Iterable[T], predicate: Callable[[T], bool]) -> T | None: + for item in collection: + if predicate(item): + return item + return None + +# TODO(aki): This kinda sucks, can we directly slice bytearrays? +def _chunker(size: int, data: Iterable[T]): + return zip_longest(*[iter(data)]*size) + +@contextmanager +def usb_device_handle(dev: USBDevice): + ''' Wrap the usb1 dev.open()/hndl.close() in a context manager ''' + handle = dev.open() + try: + yield handle + finally: + handle.close() + + +class SquishyDevice: + ''' + Squishy Hardware Device + + This class represents a Squishy hardware device that is attached to the host, it exposes + a common and stable API for interacting with Squishy devices. + + Parameters + ---------- + dev : usb1.USBDevice + The USB Device handle for the attached Squishy hardware. + + serial : str + The serial number of the device. + + timeout : int + USB Transaction timeout in ms. (default: 2500) + + + Attributes + ---------- + + rev : tuple[int, int] + The revision of the attached Squishy device in the form of (major, minor). + + serial : str + The serial number of this Squishy device + + ''' + + @staticmethod + def _unpack_revision(bcd: int) -> tuple[int, int]: + ''' + Un-pack the Squishy revision from the USB BCD Descriptor. + + Returns + ------- + tuple[int, int] + The revision of the Squishy hardware that was packed into the USB BCD in the form + of (major, minor) + ''' + + major = bcd >> 8 + major = ((major >> 4) * 10) + (major & 0xF) + + minor = bcd & 0xFF + minor = ((minor >> 4) * 10) + (minor & 0xF) + + return (major, minor) + + # def _refresh_serial(self) -> None: + # ''' Update the serial number from the current device ''' + # with usb_device_handle(self._dev) as hndl: + # self.serial = hndl.getStringDescriptor( + # self._dev.getSerialNumberDescriptor(), + # LanguageIDs.ENGLISH_US + # ) + + def _get_dfu_interface(self) -> int | None: + ''' + Get the USB Interface number that matches ``DFU_CLASS`` + + Returns + ------- + int | None + The DFU interface number, or None if not found + ''' + if self._dfu_iface is None and self._dfu_cfg is None: + # Iterate over device configurations + for config in self._dev.iterConfigurations(): + # For each config, iterate over the interfaces + for iface in config: + # For each interface, iterate over the settings + for setting in iface: + # Check to see if it's `DFU_CLASS` + if setting.getClassTupple() == DFU_CLASS: + # If so, then we extract the configuration and interface IDs + self._dfu_cfg: int = config.getConfigurationValue() + self._dfu_iface: int = setting.getNumber() + # Check if the current device configuration is the DFU config + if self._usb_handle.getConfiguration() != self._dfu_cfg: + # If not, we make it the current configuration + self._usb_handle.setConfiguration(self._dfu_cfg) + # And return the DFU interface number + return self._dfu_iface + + return self._dfu_iface + + def _get_dfu_status(self) -> tuple[DFUStatus, DFUState]: + ''' + Get the state and status for the DFU endpoint. + + Returns + ------- + tuple[DFUStatus, DFUState] + The status and state of the DFU endpoint. + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU get status request fails or times out. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + # Ensure we have our grubby little paws on it + self._ensure_iface_claimed(interface_id) + + # Try to request the status + data: bytearray | None = self._usb_handle.controlRead( + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + DFURequests.GetStatus, + 0, + interface_id, + 6, + self._timeout + ) + + # If we didn't get any data back (likely a timeout) then bail + if data is None: + raise RuntimeError(f'Unable to read DFU status from `{self._usb_dev_str}` on interface `{interface_id}`') + + # Otherwise, return the State and Status + return (DFUStatus(data[0]), DFUState(data[4])) + + + def _get_dfu_state(self) -> DFUState: + ''' + Get the state for the DFU endpoint. + + Returns + ------- + DFUState + The state of the DFU endpoint. + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU get state request fails or times out. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + # Ensure we have our grubby little paws on it + self._ensure_iface_claimed(interface_id) + + # Try to request the status + data: bytearray | None = self._usb_handle.controlRead( + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + DFURequests.GetState, + 0, + interface_id, + 1, + self._timeout + ) + + # If we didn't get any data back (likely a timeout) then bail + if data is None: + raise RuntimeError(f'Unable to read DFU state from `{self._usb_dev_str}` on interface `{interface_id}`') + + # Otherwise, return the State and Status + return DFUState(data[0]) + + def _send_dfu_detach(self) -> bool: + ''' + Invoke a DFU detach. + + Returns + ------- + bool + True if the detach was successful, otherwise False + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + # Ensure we have the DFU interface, and clean it up after + with self._ensure_iface(interface_id): + # Try to poke the device to get it to reboot + try: + sent: int = self._usb_handle.controlWrite( + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + DFURequests.Detach, + 0, + interface_id, + bytearray(), + self._timeout + ) + except USBError as e: + # If the error is one of the not-actually-an-error errors caused by the device rebooting, palm it off + if e.value in (LIBUSB_ERROR_IO, LIBUSB_ERROR_NO_DEVICE): + sent = 0 + # Otherwise bubble it up + else: + raise RuntimeError(f'Unable to send DFU detach to `{self._usb_dev_str}` on interface `{interface_id}`') + + return sent == 0 + + def _get_dfu_altmodes(self) -> dict[int, str]: + ''' + Collect and return all of the DFU alt-modes and their name from the device. + + Returns + ------- + dict[int, str] + A mapping of the alt-mode endpoint and it's name. + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + AssertionError + If we lose the DFU configuration or interface somehow. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + cfg_id = self._dfu_cfg + + config = _find_if(self._dev.iterConfigurations(), lambda cfg: cfg.getConfigurationValue() == cfg_id) + if config is None: + raise AssertionError('Failed to re-locate USB DFU configuration') + + interface = _find_if(config.iterInterfaces(), lambda ifc: next(iter(ifc)).getNumber() == interface_id) + if interface is None: + raise AssertionError('Failed to re-locate USB DFU interface') + + alt_modes: dict[int, str] = {} + # Iterate over all of the alt-modes + for alt in interface: + mode_id: int = alt.getAlternateSetting() + # Try to get the alt-mode's string descriptor + mode_name = self._usb_handle.getStringDescriptor( + alt.getDescriptor(), + LanguageIDs.ENGLISH_US + ) + + alt_modes[mode_id] = mode_name if mode_name is not None else f'mode {mode_id}' + + return alt_modes + + def _get_dfu_tx_size(self) -> int | None: + ''' + Get the DFU transaction size in bytes. + + Returns + ------- + int | None + The DFU transaction size in bytes, or if unable to be found None + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + AssertionError + If we lose the DFU configuration or interface somehow. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + cfg_id = self._dfu_cfg + + config = _find_if(self._dev.iterConfigurations(), lambda cfg: cfg.getConfigurationValue() == cfg_id) + if config is None: + raise AssertionError('Failed to re-locate USB DFU configuration') + + interface = _find_if(config.iterInterfaces(), lambda ifc: next(iter(ifc)).getNumber() == interface_id) + if interface is None: + raise AssertionError('Failed to re-locate USB DFU interface') + + # Get the first alt-mode + settings = next(iter(interface)) + extra = settings.getExtra() + + # Check to ensure there is only one functional descriptor + if len(extra) != 1: + raise RuntimeError(f'Expected only one functional descriptor in alt-mode, found {len(extra)}') + + # Pull out the descriptor and get the transfer size + func_desc = FunctionalDescriptor.parse(extra[0]) + if TYPE_CHECKING: + assert isinstance(func_desc.wTransferSize, int) + + return func_desc.wTransferSize + + def _enter_dfu(self) -> bool: + ''' + Instruct the device to enter DFU mode. + + Returns + ------- + bool + True if we managed to enter DFU mode, False otherwise. + ''' + + # Check to see if we're not already in DFU + if self._get_dfu_state() == DFUState.AppIdle: + # We're not, so poke at the device to get use there + self._send_dfu_detach() # BUG(aki): We should do something about this return value, huh? + # Flush the device and handles + self._usb_handle.close() + self._dev.close() + self._dfu_iface = None + self._dfu_cfg = None + + device: DeviceContainer | None = None + + # Re-enumerate the devices after a short timeout + log.debug(f'Waiting for `{self.serial}` to come back') + sleep(self._timeout / 1000) + # BUG(aki): This *might* spin forever in some cases + while device is None: + device = _find_if(self.enumerate(), lambda dev: dev[0] == self.serial) + + # We have the device back, re-attach + log.debug('Device came back, re-attaching') + (_, _, dev) = device + + self._dev = dev + self._usb_handle = self._dev.open() + + # Now that we *should* be in DFU make sure we are actually there + dfu_state = self._get_dfu_state() + log.debug(f'DFU State: {dfu_state}') + + if dfu_state != DFUState.DFUIdle: + log.error(f'Device came back in an improper DFU state: {dfu_state}') + return False + return True + + def _send_dfu_download(self, data: bytearray, chunk_num: int) -> bool: + ''' + Push a chunk of data to the DFU endpoint. In DFU terminology this is a "Download" + + Returns + ------- + bool + True if the DFU transaction was successful, otherwise False. + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + ''' + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + # Ensure we have our grubby little paws on it + self._ensure_iface_claimed(interface_id) + + # Stuff the data in the endpoints face + sent: int = self._usb_handle.controlWrite( + LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, + DFURequests.Download, + chunk_num, + interface_id, + data, + self._timeout + ) + + return sent == len(data) + + @contextmanager + def _ensure_iface(self, iface_id: int): + ''' A context manager helper for wrapping USB interface handling ''' + self._ensure_iface_claimed(iface_id) + try: + yield + finally: + self._ensure_iface_released(iface_id) + + def _ensure_iface_claimed(self, iface_id: int) -> None: + ''' + Ensures the given USB interface is claimed. + + Parameters + ---------- + iface_id : int + The USB interface ID to ensure is claimed. + ''' + + if iface_id not in self._claimed_interfaces: + self._usb_handle.claimInterface(iface_id) + self._claimed_interfaces.append(iface_id) + + def _ensure_iface_released(self, iface_id: int) -> None: + ''' + Ensures the given USB interface is released. + + Parameters + ---------- + iface_id : int + The USB interface ID to ensure is released. + ''' + + if iface_id in self._claimed_interfaces: + self._claimed_interfaces.remove(iface_id) + try: + self._usb_handle.releaseInterface(iface_id) + except USBError as e: + if e.value != LIBUSB_ERROR_NO_DEVICE: + raise + + @property + def _usb_dev_str(self) -> str: + ''' The formatted USB device string in the form of ``VID:PID @ BUSID`` ''' + return f'{self._dev.getVendorID():04x}:{self._dev.getProductID():04x} @ {self._dev.getBusNumber()}' + + def __init__(self, dev: USBDevice, serial: str, timeout: int = 2500) -> None: + # USB Device and handle + self._dev = dev + self._usb_handle = self._dev.open() + if not self.can_dfu(): + raise RuntimeError(f'The device {self._usb_dev_str} is not DFU capable.') + + self._timeout = timeout + self._dfu_cfg: int | None = None + self._dfu_iface: int | None = None + + # Device Metadata + self.serial = serial + self._raw_revision: int = self._dev.getbcdDevice() + self.rev = self._unpack_revision(self._raw_revision) + + # USB Interface accounting + self._claimed_interfaces = list() + + def __del__(self) -> None: + self._usb_handle.close() + self._dev.close() + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + # TODO(aki): The rev bits should be made less, bad, + return f'Squishy rev{self.rev[0]}.{self.rev[1]} SN: {self.serial}' + + @classmethod + def get_device(cls: type[Self], *, serial: str | None = None, first: bool = True) -> Self | None: + ''' + Returns an instance of the first :py:class:`SquishyDevice` attached to the system, + or if ``serial`` is specified the device with that serial number, if possible. + + If there are no Squishy devices attached to the host system, or one with ``serial`` is not found + None is returned instead. + + Parameters + ---------- + serial : str | None + The serial number of the target device wanted. + + first : bool + If there is more than one Squishy attached, and no serial number is specified, + return the first that occurs in the list. + + Returns + ------- + SquishyDevice | None + The requested Squishy device if found, otherwise None + + ''' + + # Get all attached Squishy devices + attached = SquishyDevice.enumerate() + count = len(attached) + + # Bail early if we don't have any devices at all + if count == 0: + log.warning('No Squishy devices found attached to system') + return None + + # There is only one device, or we're just yoinking the first one on the system + if count == 1 or (serial is None and first): + found_device = attached[0] + # We're not yoinking the first, but we don't have a serial number + elif serial is None and not first: + log.error(f'No serial number specified and I\'m not allowed to pick the first of the {count} devices attached.') + log.error('Please specify a serial number') + return None + # There are more the once device, and we have a serial number to look for + else: + # Try to pull out devices that match our serial number (there should only be one) + found = tuple(filter(lambda sn, _, __: sn == serial, attached)) + num_found = len(found) + if num_found > 1: + # ohno + log.error(f'Found {len(found)} Squishy devices matching serial number `{serial}`') + return None + elif num_found == 0: + log.error(f'No Squishy device with serial number `{serial}` found') + else: + found_device = found[0] + + # Now we have a device, time to construct a SquishyDevice around it for use + (serial_number, _, dev) = found_device + # We-forward propagate the serial number incase the input one is None + return cls(dev, serial_number) + + + @classmethod + def enumerate(cls: type[Self]) -> list[DeviceContainer]: + ''' + Collect all of the attached Squishy devices. + + Returns + ------- + list[DeviceContainer] + A collection of Squishy hardware devices attached to the system. + ''' + + # icky icky icky icky + global _LIBUSB_CTX + + # If we don't have a libusb context, make one. + if _LIBUSB_CTX is None: + _LIBUSB_CTX = USBContext() + + devices: list[DeviceContainer] = [] + + # Iterate over all attached USB devices and filter out anything we're interested in + for dev in _LIBUSB_CTX.getDeviceIterator(skip_on_error = True): + dev_vid = dev.getVendorID() + dev_pid = dev.getProductID() + + # Make sure we only try to interact with Squishies + if dev_vid == USB_VID and dev_pid in (USB_APP_PID, USB_DFU_PID): + try: + # Pull out the serial number + with usb_device_handle(dev) as hndl: + serial_number = hndl.getStringDescriptor( + dev.getSerialNumberDescriptor(), + LanguageIDs.ENGLISH_US + ) + + # Un-pack the version from the device BCD + version = cls._unpack_revision(dev.getbcdDevice()) + + # Stick it into the list of known devices + devices.append((serial_number, version, dev)) + + except USBError as e: + log.error(f'Unable to open suspected Squishy device: {e}') + log.error('Maybe check your udev rules?') + + return devices + + @staticmethod + def generate_serial() -> str: + ''' + Generate a new serial number string for a Squishy device. + + The current implementation uses the current datetime in an + ISO 8601-like format. + + Returns + ------- + str + The new serial number. + ''' + + return datetime.now(timezone.utc).strftime( + '%Y%m%dT%H%M%SZ' + ) + + def can_dfu(self) -> bool: + ''' + Determine whether or not this device is DFU capable. + + Returns + ------- + bool + True if the given USB device is DFU capable, otherwise False + ''' + + log.debug(f'Checking if {self._usb_dev_str} is DFU capable') + return any(filter( + lambda cls: cls == DFU_CLASS, + map( + lambda setting: setting.getClassTupple(), + self._dev.iterSettings() + ) + )) + + def get_altmodes(self) -> dict[int, str]: + ''' + Collect and return all of the DFU alt-modes and their name from the device. + + Returns + ------- + dict[int, str] + A mapping of the alt-mode endpoint and it's name. + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + AssertionError + If we lose the DFU configuration or interface somehow. + ''' + + return self._get_dfu_altmodes() + + def reset(self) -> bool: + ''' + Invoke a DFU detach. + + Returns + ------- + bool + True if the detach was successful, otherwise False + + Raises + ------ + RuntimeError + If the DFU interface is unknown, or the DFU control request times out. + ''' + + return self._send_dfu_detach() + + def upload(self, data: bytearray, altmode: int, progress: Progress | None = None) -> bool: + ''' + Push firmware/gateware to device. + + Parameters + ---------- + data : bytearray + The data to upload to the device. + + altmode : int + The alt-mode endpoint to upload to. + + progress : rich.progress.Progress | None + Optional Rich progressbar instance. + + Returns + ------- + bool + Upload was successful, otherwise False + + Raises + ------ + RuntimeError + If the DFU interface is unknown, the DFU control request times out, or we can't determine the transaction size + ''' + + # First try to enter DFU mode + if not self._enter_dfu(): + return False + + # Try to get the DFU interface + interface_id = self._get_dfu_interface() + if interface_id is None: + raise RuntimeError(f'Unable to get DFU interface id for {self._usb_dev_str}') + + # Ensure we have our grubby little paws on it + self._ensure_iface_claimed(interface_id) + + # Set (or at least try to) the alt-mode for the DFU interface + self._usb_handle.setInterfaceAltSetting(interface_id, altmode) + + # Try and get the transaction size so we know how big to make our chunks + trans_size = self._get_dfu_tx_size() + if trans_size is None: + raise RuntimeError(f'Unable to determine DFU transaction size for `{self._usb_dev_str}`') + + log.debug(f'DFU Transfer size: {trans_size}') + + # if there is a progress bar, add task to it + if progress is not None: + prog_task = progress.add_task('Programming', start = True, total = len(data)) + + # Iterate over our chunks + for (chunk_num, chunk) in enumerate(_chunker(trans_size, data)): + # Fold the chunk back into a bytearray + chunk_data = bytearray(b for b in chunk if b is not None) + + # Try to send the data + if not self._send_dfu_download(chunk_data, chunk_num): + log.error(f'DFU transaction failed, was unable to send any/all data for chunk {chunk_num}') + return False + # Update the upload task if we can + if progress is not None: + progress.update(prog_task, advance = len(chunk_data)) + + # Let DFU chew on the chunk and settle a bit + while self._get_dfu_state() != DFUState.DlSync: + sleep(0.05) + + # Get the status of the chunk upload + _, state = self._get_dfu_status() + + if state != DFUState.DlSync: + log.error(f'DFU State is {state} not DlSync, aborting') + return False + + chunk_num += 1 + + # Flush and make sure we go idle + self._send_dfu_download(bytearray(), chunk_num) + _, state = self._get_dfu_status() + + if state != DFUState.DFUIdle: + log.error('Device did not go idle after upload') + return False + + log.debug(f'Wrote {chunk_num} chunks to device') + + # Finally, clean up the progress bar if we were using it + if progress is not None: + progress.update(prog_task, completed = True) + progress.remove_task(prog_task) + + return True + + # TODO(aki): Should this return type be an alias of a union of possible platform? + def get_platform(self) -> type[SquishyPlatformType] | None: + ''' + Get the type Torii platform definition for the currently attached device. + + Returns + ------- + type[SquishyPlatformType] | None + The type Torii platform definition for this device if found, otherwise None + ''' + + hwplat = f'rev{self.rev[0]}' # This is kinda lazy, but for now it works:tm: + + return AVAILABLE_PLATFORMS.get(hwplat, None) diff --git a/squishy/gateware/__init__.py b/squishy/gateware/__init__.py index e590d171..3030b157 100644 --- a/squishy/gateware/__init__.py +++ b/squishy/gateware/__init__.py @@ -1,23 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import Any -import logging as log - -from torii import Elaboratable, Module - -from .applet import AppletElaboratable -from .usb import Rev1USB, Rev2USB -from .scsi import SCSI1, SCSI2, SCSI3 -from .platform.platform import SquishyPlatform - -__all__ = ( - 'AppletElaboratable', - 'SquishyPlatform', - 'Squishy', -) - -__doc__ = '''\ - +''' .. todo: Refine this section The Squishy gateware library is broken into three main parts. The first is the @@ -29,70 +12,54 @@ ''' -''' - Squishy Architecture - - ┌──────────┐ - ┌────────►RAM BUFFER◄───────┐ - │ └────▲─────┘ │ - ┌───▼───┐ ┌────▼─────┐ ┌────▼───┐ -┌─┤USB PHY◄────► APPLET ◄──►SCSI PHY│ -│ └───────┘ └▲───▲───┬─┘ └───┬────┘ -│ ┌───────┘ │ └────────┤ -│ │ │ │ -│ ┌─────▼─┐ ┌───▼──┐ ┌─▼──┐ -├─┤SPI PHY│ │ UART ├───────►LEDS│ -│ └───────┘ └──────┘ └─▲──┘ -│ │ -└────────────────────────────────┘ - -''' # noqa: E101 +from torii import Elaboratable, Module + +from .applet import AppletElaboratable +from .bootloader import SquishyBootloader +from .platform import SquishyPlatform, SquishyPlatformType +from .platform.rev1 import SquishyRev1 +from .platform.rev2 import SquishyRev2 + +__all__ = ( + 'SquishyPlatform', + 'SquishyPlatformType', + 'AppletElaboratable', + + 'Squishy', + 'SquishyBootloader', + # All viable Squishy platforms + 'AVAILABLE_PLATFORMS', +) + +AVAILABLE_PLATFORMS: dict[str, type[SquishyPlatform]] = { + 'rev1': SquishyRev1, + 'rev2': SquishyRev2, +} + class Squishy(Elaboratable): - def _rev1_init(self) -> None: - # USB - # Re-work so the USB device is passed into the applet - # to collect endpoints - self.usb = Rev1USB( - config = self.usb_config, - applet_desc_builder = self.applet.usb_init_descriptors - ) - # SCSI - if self.applet.scsi_version < 1: - raise ValueError('Squishy rev1 can only talk to SCSI-1 buses') - - self.scsi = SCSI1(config = self.scsi_config) - - def _rev2_init(self) -> None: - log.warning('Rev2 Gateware is unimplemented') - - def __init__(self, *, revision: int, - uart_config: dict[str, Any], - usb_config: dict[str, Any], - scsi_config: dict[str, Any], - applet: AppletElaboratable - ) -> None: - # Applet - self.applet = applet - - # PHY Options - self.uart_config = uart_config - self.usb_config = usb_config - self.scsi_config = scsi_config - - { - 1: self._rev1_init, - 2: self._rev2_init - }.get(revision, lambda s: None)() - - - def elaborate(self, platform: SquishyPlatform | None) -> Module: + ''' + Squishy applet gateware superstructure. + + + Parameters + ---------- + revision : tuple[int, int] + The target platforms revision. + + applet : AppletElaboratable + The applet. + + ''' + + def __init__(self, *, revision: tuple[int, int], applet: AppletElaboratable) -> None: + self.applet = applet + self.plat_revision = revision + + def elaborate(self, platform: SquishyPlatformType | None) -> Module: m = Module() - # Setup Submodules - m.submodules.pll = platform.clock_domain_generator() - m.submodules.usb = self.usb - m.submodules.scsi = self.scsi - m.submodules.applet = self.applet + m.submodules.pll = pll = platform.clk_domain_generator() + m.submodules.applet = applet = self.applet return m diff --git a/squishy/gateware/applet/__init__.py b/squishy/gateware/applet/__init__.py index a677f626..f4be384a 100644 --- a/squishy/gateware/applet/__init__.py +++ b/squishy/gateware/applet/__init__.py @@ -1,10 +1,58 @@ # SPDX-License-Identifier: BSD-3-Clause -from .elaboratable import AppletElaboratable +''' + +''' + +from abc import ABCMeta, abstractmethod +from typing import Self + +from torii import Elaboratable, Module + +# TODO(aki): USB3 bits for rev2+ (eventually) +from sol_usb.gateware.usb.usb2.request import USBRequestHandler + +from usb_construct.emitters.descriptors.standard import DeviceDescriptorCollection + +from ..platform import SquishyPlatformType __all__ = ( 'AppletElaboratable', ) -__doc__ = '''\ -''' +class AppletElaboratable(Elaboratable, metaclass = ABCMeta): + ''' + Squishy Applet gateware interface. + + This is the base class for the gateware for Squishy applets. It provides + a common consumable API that allows the Squishy gateware superstructure to + interface with the applet core. + + Attributes + ---------- + + usb_request_handlers : list[USBRequestHandler] | None + Any additional USB request handlers to register. + + ''' + + def __init__(self) -> None: + super().__init__() + + + @property + def usb_request_handlers(self) -> list[USBRequestHandler] | None: + ''' Returns a list of USB request handlers ''' + return None + + + @classmethod + def usb_init_descriptors(cls: Self, desc_collection: DeviceDescriptorCollection) -> int: + ''' Initialize USB descriptors''' + return 0 + + + @abstractmethod + def elaborate(self, platform: SquishyPlatformType) -> Module: + ''' Gateware elaboration ''' + raise NotImplementedError('Applet Elaboratables must implement this method') diff --git a/squishy/gateware/applet/elaboratable.py b/squishy/gateware/applet/elaboratable.py deleted file mode 100644 index 08119ba2..00000000 --- a/squishy/gateware/applet/elaboratable.py +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from abc import ABCMeta, abstractmethod -from typing import Any, Type - -from torii import Elaboratable, Module - -from sol_usb.gateware.usb.usb2.request import USBRequestHandler - -from usb_construct.emitters.descriptors.standard import DeviceDescriptorCollection - -from ..platform.platform import SquishyPlatform - -__all__ = ( - 'AppletElaboratable', -) - -__doc__ = '''\ - -''' - -class AppletElaboratable(Elaboratable, metaclass = ABCMeta): - ''' ''' - - def __init__(self) -> None: - super().__init__() - - @property - def scsi_request_handlers(self) -> list[Any] | None: - ''' Returns a list of SCSI request handlers ''' - return None - - @property - def usb_request_handlers(self) -> list[USBRequestHandler] | None: - ''' Returns a list of USB request handlers ''' - return None - - @property - def scsi_version(self) -> int: - ''' Returns the SCSI Version''' - return 1 - - @classmethod - def usb_init_descriptors(cls: Type['AppletElaboratable'], dev_desc: DeviceDescriptorCollection) -> int: - ''' Initialize USB descriptors''' - return 0 - - - @abstractmethod - def elaborate(self, platform: SquishyPlatform) -> Module: - ''' ''' - raise NotImplementedError('Applet Elaboratables must implement this method') diff --git a/squishy/gateware/bootloader/__init__.py b/squishy/gateware/bootloader/__init__.py index bbc315fc..36a1aa46 100644 --- a/squishy/gateware/bootloader/__init__.py +++ b/squishy/gateware/bootloader/__init__.py @@ -1,9 +1,241 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = '''\ +''' ''' -__all__ = ( +from torii import Elaboratable, Module, ResetSignal, Signal +from torii.hdl.ast import Operator +from torii.lib.fifo import AsyncFIFO + +from sol_usb.usb2 import USBDevice +from sol_usb.gateware.usb.request import SetupPacket +from sol_usb.gateware.usb.usb2.request import StallOnlyRequestHandler +from usb_construct.types import USBRequestType +from usb_construct.emitters.descriptors.standard import ( + DeviceDescriptorCollection, LanguageIDs, DeviceClassCodes, + InterfaceClassCodes, ApplicationSubclassCodes, DFUProtocolCodes ) +from usb_construct.types.descriptors.dfu import * +from usb_construct.contextmgrs.descriptors.dfu import * +from usb_construct.types.descriptors.microsoft import * +from usb_construct.contextmgrs.descriptors.microsoft import * + +from .rev1 import Rev1 +from .rev2 import Rev2 +from ..platform import SquishyPlatformType +from ..peripherals.usb.dfu import DFURequestHandler +from ..peripherals.usb.quirks.windows import WindowsRequestHandler +from ...core.config import USB_DFU_CONFIG + + +__all__ = ( + 'SquishyBootloader', +) + +class SquishyBootloader(Elaboratable): + ''' + Squishy DFU Bootloader + + This is the "top" module representing a Squishy DFU capable bootloader. + + It provides DFU alt-modes for each flash slot, including the bootloader, as well + as dispatch to the appropriate programming interface for the given platform. + + + For :py:class:`SquishyRev1` platforms, the method of programming is direct SPI flash, followed + by an `SB_WARMBOOT` trigger. + + For :py:class:`SquishyRev2` this is more complicated, as we have the supervisor MCU in the mix. + First the image is written to the SPI PSRAM, a signal is then sent to the supervisor to reboot + us and re-program us with the new bitstream. + + + Note + ---- + There needs to be some consideration for hardware platforms that support ephemeral programming, + any transfers to that slot must be distinguished from a normal slot transfer, for Rev1 platforms + this is not an issue, as there is no way of doing an ephemeral applet, however for Rev2, in order + to try to tide wearing out flash with write cycles (even though they're good for like, 100k cycles) + we have an (optional?) onboard PSRAM that acts as both a cache for doing flash updates as well as + doing hot-loading without actually touching flash. + + This can be done mostly opaquely from the root of the bootloader module itself, other than having + to properly name the ephemeral DFU slot, as all the machinery for updating the platform is within + the target module for that anyway. + + Warning + ------- + Currently there is no flash protection for the bootloader slot (slot 0), it is exposed by default, + and treated like any other applet slot. + + We also don't have any checksums, which might be a bit problematic, but due to some platform limitations + specifically due to Rev1 where we write directly into flash and don't have a buffer that can be used + and discarded, we write-over the slot as we update. This is particularly dangerous for the bootloader. + + Parameters + ---------- + serial_number : str + The device serial number to use. + + revision: tuple[int, int] + The device revision. + + Attributes + ---------- + serial_number : str + The device serial number assigned. + + ''' + + def __init__(self, *, serial_number: str, revision: tuple[int, int]) -> None: + self.serial_number = serial_number + self._rev_raw = revision + # This is so stupid but it works for now:tm: + self._rev_bcd = (self._rev_raw[0] + 0.00) + round(self._rev_raw[1] * 0.1, 3) + + def elaborate(self, platform: SquishyPlatformType | None) -> Module: + m = Module() + + # Set up our PLL and clock domains + m.submodules.pll = pll = platform.clk_domain_generator() + + # Set up the USB2 ULPI-based device + ulpi_bus = platform.request('ulpi', 0) + m.submodules.usb_dev = dev = USBDevice(bus = ulpi_bus) + + # Set up USB Descriptors + descriptors = DeviceDescriptorCollection() + + # Setup the Device + with descriptors.DeviceDescriptor() as dev_desc: + dev_desc.bcdUSB = 2.01 + dev_desc.bDeviceClass = DeviceClassCodes.INTERFACE + # NOTE(aki): When the class is INTERFACE and `bDeviceSubclass` and `bDeviceProtocol` are both `0` + # then the host is to use the class information from the interface descriptors instead. + dev_desc.bDeviceSubclass = 0 + dev_desc.bDeviceProtocol = 0 + dev_desc.idVendor = USB_DFU_CONFIG.vid + dev_desc.idProduct = USB_DFU_CONFIG.pid + dev_desc.bcdDevice = self._rev_bcd + dev_desc.iManufacturer = USB_DFU_CONFIG.manufacturer + dev_desc.iProduct = USB_DFU_CONFIG.product + dev_desc.iSerialNumber = self.serial_number + dev_desc.bNumConfigurations = 1 # Just the DFU configuration + + # Now set up our 1 configuration + with descriptors.ConfigurationDescriptor() as cfg_desc: + cfg_desc.bConfigurationValue = 1 + cfg_desc.iConfiguration = 'Squishy DFU' + cfg_desc.bmAttributes = 0x80 # Default: 0b100'000 + cfg_desc.bMaxPower = 250 # 2mA * 250 + + # Populate our valid DFU slots + for slot, _ in platform.flash.geometry.partitions.items(): + with cfg_desc.InterfaceDescriptor() as int_desc: + int_desc.bInterfaceNumber = 0 + int_desc.bAlternateSetting = slot + int_desc.bInterfaceClass = InterfaceClassCodes.APPLICATION + int_desc.bInterfaceSubclass = ApplicationSubclassCodes.DFU + int_desc.bInterfaceProtocol = DFUProtocolCodes.DFU + # TODO(aki): We should have a way have the bootloader slot be hidden but to + # allow for it to be "unlocked" for updating. + # + # On rev2 hardware we can just have the user hold down the DFU button + # for 5sec or so then the supervisor MCU can bap us to have the slot. + # However, on rev1 we have no way to do anything like that, maybe if we + # have a special USB endpoint that you need to send the unlock code to? + if slot == 0: + int_desc.iInterface = r'Bootloader ( /!\ Danger /!\ )' + elif platform.ephemeral_slot is not None and slot == platform.ephemeral_slot: + int_desc.iInterface = 'Ephemeral Slot' + else: + int_desc.iInterface = f'Applet Slot {slot}' + + with FunctionalDescriptor(int_desc) as func_desc: + func_desc.bmAttributes = ( + DFUWillDetach.YES | DFUManifestationTolerant.NO | DFUCanUpload.NO | DFUCanDownload.YES + ) + func_desc.wDetachTimeOut = 1000 + func_desc.wTransferSize = platform.flash.geometry.erase_size + + # Windows needs this extra stuff for it to not be stupid + plat_descs = PlatformDescriptorCollection() + with descriptors.BOSDescriptor() as bos_desc: + with PlatformDescriptor(bos_desc, platform_collection = plat_descs) as plat_desc: + with plat_desc.DescriptorSetInformation() as dset_info: + dset_info.bMS_VendorCode = 1 + with dset_info.SetHeaderDescriptor() as set_header: + with set_header.SubsetHeaderConfiguration() as sset_cfg: + sset_cfg.bConfigurationValue = 1 + with sset_cfg.SubsetHeaderFunction() as sset_func: + sset_func.bFirstInterface = 0 + with sset_func.FeatureCompatibleID() as compat_id: + compat_id.CompatibleID = 'WINUSB' + compat_id.SubCompatibleID = '' + + # Setup the language for the descriptor strings + descriptors.add_language_descriptor((LanguageIDs.ENGLISH_US, )) + + # Bundle our mess of descriptors into a control endpoint + ep0 = dev.add_standard_control_endpoint(descriptors) + + # NOTE(aki): We might need to domain rename the SPI stuff into USB or have a SPI domain + # Set up the bitstream/firmware FIFO + m.submodules.bit_fifo = bit_fifo = AsyncFIFO( + width = 8, depth = platform.flash.geometry.erase_size, r_domain = 'sync', w_domain = 'usb' + ) + + # Set up the DFU and the special Windows compat request handlers + dfu_handler = DFURequestHandler(configuration = 1, interface = 1, boot_stub = False, fifo = bit_fifo) + win_handler = WindowsRequestHandler(plat_descs) + + # Add our handlers to the endpoint + ep0.add_request_handler(dfu_handler) + ep0.add_request_handler(win_handler) + + # We need to add a new stall condition to ensure we stall properly + def _stall_condition(setup: SetupPacket) -> Operator: + return ~( + (setup.type == USBRequestType.STANDARD) | + dfu_handler.handler_condition(setup) | + win_handler.handler_condition(setup) + ) + ep0.add_request_handler(StallOnlyRequestHandler(stall_condition = _stall_condition)) + + # TODO(aki): Hook up the internal DFU transfer signals to our platform-specific programming stuff + + # Instantiate the correct platform interface + match self._rev_raw[0]: + case 1: + platform_interface = Rev1(bit_fifo) + case 2: + platform_interface = Rev2(bit_fifo) + + m.submodules.platform_interface = platform_interface + + m.d.comb += [ + # ensure we connect the USB device + dev.connect.eq(1), + # Make sure we can do all the speeds + dev.low_speed_only.eq(0), + dev.full_speed_only.eq(0), + # TODO(aki): Should this be tied to the PLL lock like we do with 'sync'? + # Release the reset on the USB clock domain + ResetSignal('usb').eq(pll.pll_locked), + # Hook together the platform interface and the DFU handler + # TODO(aki): These really *really* should be pulled into an interface + platform_interface.trigger_reboot.eq(dfu_handler.trigger_reboot), + platform_interface.slot_selection.eq(dfu_handler.slot_selection), + platform_interface.slot_changed.eq(dfu_handler.slot_changed), + platform_interface.dl_start.eq(dfu_handler.dl_start), + platform_interface.dl_finish.eq(dfu_handler.dl_finish), + platform_interface.dl_reset_slot.eq(dfu_handler.dl_reset_slot), + platform_interface.dl_size.eq(dfu_handler.dl_size), + dfu_handler.slot_ack.eq(platform_interface.slot_ack), + dfu_handler.dl_ready.eq(platform_interface.dl_ready), + dfu_handler.dl_done.eq(platform_interface.dl_done), + ] + + return m diff --git a/squishy/gateware/bootloader/dfu.py b/squishy/gateware/bootloader/dfu.py deleted file mode 100644 index 6b42afc2..00000000 --- a/squishy/gateware/bootloader/dfu.py +++ /dev/null @@ -1,436 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from enum import IntEnum, unique -from struct import pack, unpack - -from torii import ( - Module, Signal, DomainRenamer, Cat, Memory, Const -) -from torii.hdl.ast import Operator -from torii.lib.fifo import AsyncFIFO - -from usb_construct.types import ( - USBRequestType, USBRequestRecipient, USBStandardRequests -) -from usb_construct.types.descriptors.dfu import DFURequests - -from sol_usb.gateware.usb.usb2.request import ( - USBRequestHandler, SetupPacket -) -from sol_usb.gateware.usb.stream import ( - USBInStreamInterface, USBOutStreamInterface -) -from sol_usb.gateware.stream.generator import ( - StreamSerializer -) - -from ...core.flash import FlashGeometry - -from ..core.flash import SPIFlash -from ..platform.platform import SquishyPlatform - -__all__ = ( - 'DFURequestHandler', -) - - -@unique -class DFUState(IntEnum): - Idle = 2 - DlSync = 3 - DlBusy = 4 - DlIdle = 5 - UpIdle = 9 - Error = 10 - -@unique -class DFUStatus(IntEnum): - Okay = 0 - - -class DFUConfig: - def __init__(self) -> None: - self.status = Signal(4, decoder = DFUStatus) - self.state = Signal(4, decoder = DFUState) - - -class DFURequestHandler(USBRequestHandler): - def __init__(self, *, configuration: int, interface: int, resource_name: tuple[str, int]): - super().__init__() - - self._configuration = configuration - self._interface = interface - self._flash = resource_name - - self.triggerReboot = Signal() - - - def elaborate(self, platform: SquishyPlatform | None) -> Module: - m = Module() - - interface = self.interface - setup: SetupPacket = interface.setup - - rxTrig = Signal() - rxStream = USBOutStreamInterface(payload_width = 8) - - recvStart = Signal() - recvCount = Signal.like(setup.length) - recvConsumed = Signal.like(setup.length) - - slot = Signal(8) - - - _flash: dict[str, dict[str, int] | FlashGeometry] = platform.flash - cfg = DFUConfig() - - m.submodules.bitstream_fifo = bitstream_fifo = AsyncFIFO( - width = 8, depth = _flash['geometry'].erase_size, r_domain = 'usb', w_domain = 'usb' - ) - - flash: SPIFlash = DomainRenamer({'sync': 'usb'})( - SPIFlash(flash_resource = self._flash, flash_geometry = platform.flash['geometry'], fifo = bitstream_fifo) - ) - m.submodules.flash = flash - - m.submodules.transmitter = transmitter = StreamSerializer( - data_length = 6, domain = 'usb', stream_type = USBInStreamInterface, max_length_width = 3 - ) - - slot_rom = self._make_rom(_flash) - - m.submodules.slots = slots = slot_rom.read_port(domain = 'usb', transparent = False) - - m.d.comb += [ - flash.start.eq(0), - flash.finish.eq(0), - flash.resetAddrs.eq(0), - ] - - with m.FSM(domain = 'usb', name = 'dfu'): - with m.State('RESET'): - m.d.usb += [ - cfg.status.eq(DFUStatus.Okay), - cfg.state.eq(DFUState.Idle), - slot.eq(0) - ] - with m.If(flash.ready): - m.next = 'READ_SLOT_DATA' - - with m.State('IDLE'): - with m.If(setup.received & self.handler_condition(setup)): - with m.If(setup.type == USBRequestType.CLASS): - with m.Switch(setup.request): - with m.Case(DFURequests.DETACH): - m.next = 'HANDLE_DETACH' - with m.Case(DFURequests.DOWNLOAD): - m.next = 'HANDLE_DOWNLOAD' - with m.Case(DFURequests.GET_STATUS): - m.next = 'HANDLE_GET_STATUS' - with m.Case(DFURequests.CLR_STATUS): - m.next = 'HANDLE_CLR_STATUS' - with m.Case(DFURequests.GET_STATE): - m.next = 'HANDLE_GET_STATE' - with m.Default(): - m.next = 'UNHANDLED' - with m.Elif(setup.type == USBRequestType.STANDARD): - with m.Switch(setup.request): - with m.Case(USBStandardRequests.GET_INTERFACE): - m.next = 'GET_INTERFACE' - with m.Case(USBStandardRequests.SET_INTERFACE): - m.next = 'SET_INTERFACE' - with m.Default(): - m.next = 'UNHANDLED' - - with m.If(flash.done): - m.d.comb += [ - flash.finish.eq(1), - ] - - m.d.usb += [ - cfg.state.eq(DFUState.DlSync) - ] - - with m.State('HANDLE_DETACH'): - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp(), - ] - with m.Elif(interface.handshakes_in.ack): - m.d.usb += [ - self.triggerReboot.eq(1), - ] - - with m.State('HANDLE_DOWNLOAD'): - with m.If(setup.is_in_request | (setup.length > _flash['geometry'].erase_size)): - m.next = 'UNHANDLED' - with m.Elif(setup.length): - m.d.comb += [ - flash.start.eq(1), - flash.byteCount.eq(setup.length), - ] - m.d.usb += [ - cfg.state.eq(DFUState.DlBusy), - ] - m.next = 'HANDLE_DOWNLOAD_DATA' - with m.Else(): - m.next = 'HANDLE_DOWNLOAD_COMPLETE' - - with m.State('HANDLE_DOWNLOAD_DATA'): - m.d.comb += [ - interface.rx.connect(rxStream) - ] - with m.If(~rxTrig): - m.d.comb += [ - recvStart.eq(1), - ] - m.d.usb += [ - rxTrig.eq(1), - ] - - with m.If(interface.rx_ready_for_response): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp(), - ] - with m.If(self.interface.handshakes_in.ack): - m.d.usb += [ - rxTrig.eq(0), - ] - m.next = 'IDLE' - - with m.State('HANDLE_DOWNLOAD_COMPLETE'): - with m.If(interface.status_requested): - m.d.usb += [ - cfg.state.eq(DFUState.Idle), - ] - m.d.comb += [ - self.send_zlp(), - ] - - with m.If(interface.handshakes_in.ack): - m.next = 'IDLE' - - with m.State('HANDLE_GET_STATUS'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(6), - transmitter.data[0].eq(cfg.status), - Cat(transmitter.data[1:4]).eq(0), - transmitter.data[4].eq(Cat(cfg.state, 0)), - transmitter.data[5].eq(0), - ] - - with m.If(self.interface.data_requested): - with m.If(setup.length == 6): - m.d.comb += [ - transmitter.start.eq(1), - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - with m.If(cfg.state == DFUState.DlSync): - m.d.usb += [ - cfg.state.eq(DFUState.DlIdle) - ] - m.next = 'IDLE' - - with m.State('HANDLE_CLR_STATUS'): - with m.If(setup.length == 0): - with m.If(cfg.state == DFUState.Error): - m.d.usb += [ - cfg.status.eq(DFUStatus.Okay), - cfg.state.eq(DFUState.Idle), - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp(), - ] - with m.If(interface.handshakes_in.ack): - m.next = 'IDLE' - - with m.State('HANDLE_GET_STATE'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(1), - ] - - m.d.comb += [ - transmitter.data[0].eq(Cat(cfg.state, 0)) - ] - - with m.If(self.interface.data_requested): - with m.If(setup.length == 1): - m.d.comb += [ - transmitter.start.eq(1), - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - m.next = 'IDLE' - - with m.State('GET_INTERFACE'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(1), - transmitter.data[0].eq(slot), - ] - - with m.If(self.interface.data_requested): - with m.If(setup.length == 1): - m.d.comb += [ - transmitter.start.eq(1) - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1) - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1) - ] - m.next = 'IDLE' - - with m.State('SET_INTERFACE'): - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp() - ] - - with m.If(interface.handshakes_in.ack): - m.d.usb += [ - slot.eq(setup.value[0:8]), - ] - m.next = 'READ_SLOT_DATA' - - with m.State('UNHANDLED'): - with m.If(interface.data_requested | interface.status_requested): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - with m.State('READ_SLOT_DATA'): - m.d.comb += [ - slots.addr.eq(Cat(Const(0, 1), slot)), - ] - m.next = 'READ_SLOT_BEGIN' - - with m.State('READ_SLOT_BEGIN'): - m.d.comb += [ - slots.addr.eq(Cat(Const(1, 1), slot)), - ] - m.d.usb += [ - flash.startAddr.eq(slots.data), - ] - m.next = 'READ_SLOT_END' - - with m.State('READ_SLOT_END'): - m.d.usb += [ - flash.endAddr.eq(slots.data), - ] - m.d.comb += [ - flash.resetAddrs.eq(1), - ] - m.next = 'IDLE' - - m.d.comb += [ - bitstream_fifo.w_en.eq(0), - bitstream_fifo.w_data.eq(rxStream.payload) - ] - recvCont = (recvConsumed < recvCount) - - with m.FSM(domain = 'usb', name = 'download'): - with m.State('IDLE'): - m.d.usb += [ - recvConsumed.eq(0), - ] - - with m.If(recvStart): - m.d.usb += [ - recvCount.eq(setup.length - 1), - ] - m.next = 'STREAMING' - with m.State('STREAMING'): - with m.If(rxStream.valid & rxStream.next): - m.d.comb += [ - bitstream_fifo.w_en.eq(1), - ] - - with m.If(recvCont): - m.d.usb += [ - recvConsumed.eq(recvConsumed + 1), - ] - with m.Else(): - m.next = 'IDLE' - - return m - - def handler_condition(self, setup: SetupPacket) -> Operator: - return ( - (self.interface.active_config == self._configuration) & - ((setup.type == USBRequestType.CLASS) | (setup.type == USBRequestType.STANDARD)) & - (setup.recipient == USBRequestRecipient.INTERFACE) & - (setup.index == self._interface) - ) - - - def _make_rom(self, flash: dict[str, dict[str, int] | FlashGeometry]) -> Memory: - ''' - Generate ROM layout of the flash. - - The layout is as follows: - - +---------+--------------+ - | Address | Data | - +=========+==============+ - | 0 | Slot 0 Begin | - +---------+--------------+ - | 1 | Slot 0 End | - +---------+--------------+ - | 2 | Slot 1 Begin | - +---------+--------------+ - | 3 | Slot 1 End | - +---------+--------------+ - | ... | | - +---------+--------------+ - - ''' - - total_size = flash['geometry'].slots * 8 - - rom = bytearray(total_size) - rom_addr = 0 - for partition in range(flash['geometry'].slots): - slot = flash['geometry'].partitions[partition] - addr_range = pack('>II', slot['start_addr'], slot['end_addr']) - rom[rom_addr:rom_addr + 8] = addr_range - rom_addr += 8 - - rom_entries = (rom[i:i + 4] for i in range(0, total_size, 4)) - initializer = [unpack('>I', rom_entry)[0] for rom_entry in rom_entries] - return Memory(width = 24, depth = flash['geometry'].slots * 2, init = initializer) diff --git a/squishy/gateware/bootloader/rev1.py b/squishy/gateware/bootloader/rev1.py index 58c2529b..f58ad5e1 100644 --- a/squishy/gateware/bootloader/rev1.py +++ b/squishy/gateware/bootloader/rev1.py @@ -1,156 +1,146 @@ # SPDX-License-Identifier: BSD-3-Clause -from torii import ( - Elaboratable, Module, ClockDomain, - ResetSignal, Instance, Signal -) -from torii.hdl.ast import ( - Operator -) +''' -from sol_usb.usb2 import ( - USBDevice -) -from sol_usb.gateware.usb.request import ( - SetupPacket -) -from sol_usb.gateware.usb.usb2.request import ( - StallOnlyRequestHandler -) +''' -from usb_construct.types import ( - USBRequestType -) -from usb_construct.emitters.descriptors.standard import ( - DeviceDescriptorCollection, LanguageIDs, DeviceClassCodes, - InterfaceClassCodes, ApplicationSubclassCodes, DFUProtocolCodes +from struct import pack, unpack + +from torii import Elaboratable, Module, Instance, Signal, DomainRenamer, Const, Memory +from torii.lib.fifo import AsyncFIFO + +from ..platform import SquishyPlatformType +from ..peripherals.flash import SPIFlash +from ...core.flash import Geometry + + +__all__ = ( + 'Rev1' ) -from usb_construct.types.descriptors.dfu import * -from usb_construct.contextmgrs.descriptors.dfu import * -from usb_construct.types.descriptors.microsoft import * -from usb_construct.contextmgrs.descriptors.microsoft import * -from .dfu import DFURequestHandler -from ..platform.platform import SquishyPlatform -from ..quirks.usb.windows import WindowsRequestHandler +class Rev1(Elaboratable): + ''' + Parameters + ---------- + fifo : AsyncFIFO | None + The storage FIFO. -__doc__ = '''\ + Attributes + ---------- + trigger_reboot : Signal + FPGA reboot trigger from DFU. -POR -> Slot1 (Squishy Applet) - Slot0 (DFU Bootloader) + slot_selection : Signal(2) + Flash slot destination from DFU alt-mode. -Applet DFU Stub (reboot into Slot 0, w/ warmboot) -DFU Bootloader (DFU Alt-mods are slots) + dl_start : Signal + Input: Start of a DFU transfer. -dfu-util -a 0 -dfu-util -a 1 + dl_finish : Signal + Input: An acknowledgement of the `dl_done` signal -''' + dl_ready : Signal + Output: If the backing storage is ready for data. -__all__ = ( - 'rev1Bootloader', -) + dl_done : Signal + Output: When the backing storage is done storing the data. + + dl_reset_slot : Signal + Input: Signals to the storage to reset the active slot. + dl_size : Signal(16) + Input: The size of the DFU transfer into the the FIFO -class Bootloader(Elaboratable): - ''' ''' - def __init__(self, *, serial_number: str) -> None: - self._serial_number = serial_number + slot_changed : Signal + Input: Raised when the DFU alt-mode is changed. - def elaborate(self, platform: SquishyPlatform | None) -> Module: + slot_ack : Signal + Output: When the `slot_changed` signal was acted on. + + ''' + + def __init__(self, fifo: AsyncFIFO) -> None: + self.trigger_reboot = Signal() + self.slot_selection = Signal(2) + + self._bit_fifo = fifo + + self.dl_start = Signal() + self.dl_finish = Signal() + self.dl_ready = Signal() + self.dl_done = Signal() + self.dl_reset_slot = Signal() + self.dl_size = Signal(16) + + self.slot_changed = Signal() + self.slot_ack = Signal() + + def elaborate(self, platform: SquishyPlatformType | None) -> Module: m = Module() - trigger_reboot = Signal() - slot_select = Signal(2) + slot = Signal(8) + slot_rom = self._mk_rom(platform.flash.geometry) + + m.submodules.slots = slots = slot_rom.read_port(domain = 'usb', transparent = False) + + flash = SPIFlash(flash_resource = ('spi_flash_1x', 0), flash_geometry = platform.flash.geometry, fifo = self._bit_fifo) - m.domains.usb = ClockDomain() - ulpi = platform.request('ulpi', 0) - m.submodules.dev = dev = USBDevice(bus = ulpi, handle_clocking = True) - m.submodules += Instance( + m.submodules.flash = flash + + # Set up the iCE40 warmboot + m.submodules.warmboot = Instance( 'SB_WARMBOOT', - i_BOOT = trigger_reboot, - i_S0 = slot_select[0], - i_S1 = slot_select[1], + i_BOOT = self.trigger_reboot, + i_S0 = self.slot_selection[0], + i_S1 = self.slot_selection[1], ) - descriptors = DeviceDescriptorCollection() - with descriptors.DeviceDescriptor() as dev_desc: - dev_desc.bcdUSB = 2.01 - dev_desc.bDeviceClass = DeviceClassCodes.INTERFACE - dev_desc.bDeviceSubclass = 0 - dev_desc.bDeviceProtocol = 0 - dev_desc.idVendor = platform.usb_vid - dev_desc.idProduct = platform.usb_pid_boot - dev_desc.bcdDevice = (0.00 + platform.revision) - dev_desc.iManufacturer = platform.usb_mfr - dev_desc.iProduct = platform.usb_prod[platform.usb_pid_boot] - dev_desc.iSerialNumber = self._serial_number - dev_desc.bNumConfigurations = 1 - - with descriptors.ConfigurationDescriptor() as cfg_desc: - cfg_desc.bConfigurationValue = 1 - cfg_desc.iConfiguration = 'Squishy Bootloader' - cfg_desc.bmAttributes = 0x80 - cfg_desc.bMaxPower = 250 - - for slot in platform.flash['geometry'].partitions: - with cfg_desc.InterfaceDescriptor() as int_desc: - int_desc.bInterfaceNumber = 0 - int_desc.bAlternateSetting = slot - int_desc.bInterfaceClass = InterfaceClassCodes.APPLICATION - int_desc.bInterfaceSubclass = ApplicationSubclassCodes.DFU - int_desc.bInterfaceProtocol = DFUProtocolCodes.DFU - int_desc.iInterface = f'Slot {slot}' - - with FunctionalDescriptor(int_desc) as func_desc: - func_desc.bmAttributes = ( - DFUWillDetach.YES | DFUManifestationTolerant.NO | DFUCanUpload.NO | DFUCanDownload.YES - ) - func_desc.wDetachTimeOut = 1000 - func_desc.wTransferSize = platform.flash['geometry'].erase_size - - platform_desc = PlatformDescriptorCollection() - with descriptors.BOSDescriptor() as bos_desc: - with PlatformDescriptor(bos_desc, platform_collection = platform_desc) as plat_desc: - with plat_desc.DescriptorSetInformation() as desc_set_info: - desc_set_info.bMS_VendorCode = 1 - - with desc_set_info.SetHeaderDescriptor() as set_header: - with set_header.SubsetHeaderConfiguration() as subset_cfg: - subset_cfg.bConfigurationValue = 1 - - with subset_cfg.SubsetHeaderFunction() as subset_func: - subset_func.bFirstInterface = 0 - - with subset_func.FeatureCompatibleID() as compat_id: - compat_id.CompatibleID = 'WINUSB' - compat_id.SubCompatibleID = '' - - descriptors.add_language_descriptor((LanguageIDs.ENGLISH_US, )) - ep0 = dev.add_standard_control_endpoint(descriptors) - dfu_handler = DFURequestHandler(configuration = 1, interface = 0, resource_name = ('spi_flash_1x', 0)) - win_handler = WindowsRequestHandler(platform_desc) - - def stall_condition(setup: SetupPacket) -> Operator: - return ~( - (setup.type == USBRequestType.STANDARD) | - dfu_handler.handler_condition(setup) | - win_handler.handler_condition(setup) - ) - - ep0.add_request_handler(dfu_handler) - ep0.add_request_handler(win_handler) - ep0.add_request_handler(StallOnlyRequestHandler(stall_condition = stall_condition)) - - m.d.comb += [ - dev.connect.eq(1), - dev.low_speed_only.eq(0), - dev.full_speed_only.eq(0), - ResetSignal('usb').eq(0), - trigger_reboot.eq(dfu_handler.triggerReboot), - slot_select.eq(0b01) - ] +# with m.State('READ_SLOT_DATA'): +# m.d.comb += [ slots.addr.eq(Cat(Const(0, 1), slot)), ] +# +# m.next = 'READ_SLOT_BEGIN' +# +# with m.State('READ_SLOT_BEGIN'): +# m.d.comb += [ slots.addr.eq(Cat(Const(1, 1), slot)), ] +# m.d.usb += [ self._spi_flash.startAddr.eq(slots.data), ] +# +# m.next = 'READ_SLOT_END' +# +# with m.State('READ_SLOT_END'): +# m.d.usb += [ self._spi_flash.endAddr.eq(slots.data), ] +# m.d.comb += [ self._spi_flash.resetAddrs.eq(1), ] +# +# m.next = 'IDLE' return m + + def _mk_rom(self, flash_geometry: Geometry) -> Memory: + ''' + Generate the ROM layout for the flash addresses. + + Parameters + ---------- + flash_geometry : Geometry + The platform flash geometry. + + Returns + ------- + Memory + The ROM containing the flash address maps. + ''' + + total_size = flash_geometry.slots * 8 + + rom = bytearray(total_size) + rom_addr = 0 + + for idx, partition in flash_geometry.partitions.items(): + addr_range = pack('>II', partition.start_addr, partition.end_addr) + rom[rom_addr:rom_addr + 8] = addr_range + rom_addr += 8 + + rom_entries = ( rom[i:i + 4] for i in range(0, total_size, 4) ) + initializer = [ unpack('>I', rom_entry)[0] for rom_entry in rom_entries ] + return Memory(width = 24, depth = flash_geometry.slots * 2, init = initializer) diff --git a/squishy/gateware/bootloader/rev2.py b/squishy/gateware/bootloader/rev2.py new file mode 100644 index 00000000..bfd3aba2 --- /dev/null +++ b/squishy/gateware/bootloader/rev2.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +from torii import Elaboratable, Module, Signal +from torii.lib.fifo import AsyncFIFO + +from ..platform import SquishyPlatformType + + +__all__ = ( + 'Rev2' +) + +class Rev2(Elaboratable): + ''' + + Parameters + ---------- + fifo : AsyncFIFO | None + The storage FIFO. + + Attributes + ---------- + trigger_reboot : Signal + FPGA reboot trigger from DFU. + + slot_selection : Signal(2) + Flash slot destination from DFU alt-mode. + + dl_start : Signal + Input: Start of a DFU transfer. + + dl_finish : Signal + Input: An acknowledgement of the `dl_done` signal + + dl_ready : Signal + Output: If the backing storage is ready for data. + + dl_done : Signal + Output: When the backing storage is done storing the data. + + dl_reset_slot : Signal + Input: Signals to the storage to reset the active slot. + + dl_size : Signal(16) + Input: The size of the DFU transfer into the the FIFO + + slot_changed : Signal + Input: Raised when the DFU alt-mode is changed. + + slot_ack : Signal + Output: When the `slot_changed` signal was acted on. + + ''' + + def __init__(self, fifo: AsyncFIFO) -> None: + self.trigger_reboot = Signal() + self.slot_selection = Signal(2) + + self._bit_fifo = fifo + + self.dl_start = Signal() + self.dl_finish = Signal() + self.dl_ready = Signal() + self.dl_done = Signal() + self.dl_reset_slot = Signal() + self.dl_size = Signal(16) + + self.slot_changed = Signal() + self.slot_ack = Signal() + + def elaborate(self, platform: SquishyPlatformType | None) -> Module: + m = Module() + + + + return m diff --git a/squishy/gateware/core/__init__.py b/squishy/gateware/core/__init__.py index ac8b97fd..b41daa03 100644 --- a/squishy/gateware/core/__init__.py +++ b/squishy/gateware/core/__init__.py @@ -1,36 +1,5 @@ # SPDX-License-Identifier: BSD-3-Clause -from .scsi import SCSIInterface -from .spi import SPIInterface -from .uart import UARTInterface +''' -from .pll import ICE40ClockDomainGenerator -from .pll import ECP5ClockDomainGenerator - -__all__ = ( - 'SCSIInterface', - 'SPIInterface', - 'UARTInterface', - - 'ICE40ClockDomainGenerator', - 'ECP5ClockDomainGenerator', -) - - -__doc__ = '''\ - -This module contains the internal elaboratables that are used to construct the -gateware wrapper for Squishy applets. They are not intended to be manual instantiated -outside of the Squishy gateware wrapper, but they are available to do so if writing -custom gateware for Squishy hardware outside of the applet ecosystem. - -As such, they are documented to allow for consumption, but do not hold any API stability -promises as they are still considered to be internal to the applet system and not -for general consumption. - -It is roughly broken up into 3 submodules: - * :py:mod:`squishy.gateware.core.pll` - PLL helpers for various FPGAs. - * :py:mod:`squishy.gateware.core.spi` - Generic SPI interface. - * :py:mod:`squishy.gateware.core.uart` - Debug UART. - -''' # noqa: E101 +''' diff --git a/squishy/gateware/core/pll.py b/squishy/gateware/core/pll.py deleted file mode 100644 index 1cb4b0c7..00000000 --- a/squishy/gateware/core/pll.py +++ /dev/null @@ -1,148 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from torii import ( - Elaboratable, Module, Instance, ClockDomain, - Signal, Const, ClockSignal -) - -__all__ = ( - 'ICE40ClockDomainGenerator', - 'ECP5ClockDomainGenerator', -) - -class ICE40ClockDomainGenerator(Elaboratable): - ''' - PLL Wrapper for iCE40 based Squishy platforms. - - This elaboratable declares two clock domains, `usb` and `sync`. The `usb` domain is - a 60MHz clock coming from a ULPI phy, and the `sync` domain is a PLL'd up value from - the system clock. - - In Squishy rev1 the PLL for the `sync` domain is set for 100MHz. - - ''' - def elaborate(self, platform) -> Module: - m = Module() - - m.domains.usb = ClockDomain() - m.domains.sync = ClockDomain() - - - platform.lookup(platform.default_clk).attrs['GLOBAL'] = False - - # :nya_a: - pll_clk = Signal(attrs = {'keep': 'true'}) - m.submodules.pll = Instance( - 'SB_PLL40_PAD', - i_PACKAGEPIN = platform.request(platform.default_clk, dir = 'i'), - i_RESETB = Const(1), - i_BYPASS = Const(0), - - o_PLLOUTGLOBAL = pll_clk, - - p_FEEDBACK_PATH = 'SIMPLE', - p_PLLOUT_SELECT = 'GENCLK', - - # 200MHz - p_DIVR = platform.pll_config['divr'], - p_DIVF = platform.pll_config['divf'], - p_DIVQ = platform.pll_config['divq'], - p_FILTER_RANGE = platform.pll_config['frange'], - ) - - - platform.add_clock_constraint(pll_clk, platform.pll_config['freq']) - - m.d.comb += [ - ClockSignal('sync').eq(pll_clk), - ] - - return m - -class ECP5ClockDomainGenerator(Elaboratable): - ''' - PLL Wrapper for ECP5 based Squishy platforms. - - This elaboratable declares two clock domains, `usb` and `sync`. The `usb` domain is - a 60MHz clock coming from a ULPI phy, and the `sync` domain is a PLL'd up value from - the system clock. - - In Squishy rev2 the PLL for the `sync` domain is set for 400MHz. - - Attributes - ---------- - pll_locked : Signal - An active high signal indicating if the ECP5 PLL is locked and stable. - - ''' - - - def __init__(self): - self.pll_locked = Signal() - - def elaborate(self, platform) -> Module: - m = Module() - - m.domain.usb = ClockDomain() - m.domains.sync = ClockDomain() - - platform.lookup(platform.default_clk).attrs['GLOBAL'] = False - - # :nya_a: - pll_clk = Signal(attrs = {'keep': 'true'}) - - # TODO: Verify PLL settings - m.submodules.pll = Instance( - 'EHXPLLL', - - i_CLKI = platform.request(platform.default_clk, dir = 'i'), - - o_CLKOP = pll_clk, - i_CLKFB = pll_clk, - i_ENCLKOP = Const(0), - o_LOCK = self.pll_locked, - - i_RST = Const(0), - i_STDBY = Const(0), - - i_PHASESEL0 = Const(0), - i_PHASESEL1 = Const(0), - i_PHASEDIR = Const(1), - i_PHASESTEP = Const(1), - i_PHASELOADREG = Const(1), - i_PLLWAKESYNC = Const(0), - - # Params - p_PLLRST_ENA = 'DISABLED', - p_INTFB_WAKE = 'DISABLED', - p_STDBY_ENABLE = 'DISABLED', - p_DPHASE_SOURCE = 'DISABLED', - p_OUTDIVIDER_MUXA = 'DIVA', - p_OUTDIVIDER_MUXB = 'DIVB', - p_OUTDIVIDER_MUXC = 'DIVC', - p_OUTDIVIDER_MUXD = 'DIVD', - p_CLKOP_ENABLE = 'ENABLED', - p_CLKOP_CPHASE = Const(0), - p_CLKOP_FPHASE = Const(0), - p_FEEDBK_PATH = 'CLKOP', - - p_CLKI_DIV = platform.pll_config['clki_div'], - p_CLKOP_DIV = platform.pll_config['clkop_div'], - p_CLKFB_DIV = platform.pll_config['clkfb_div'], - - - # Attributes for synth - a_FREQUENCY_PIN_CLKI = str(platform.pll_config['ifreq']), - a_FREQUENCY_PIN_CLKOP = str(platform.pll_config['ofreq']), - a_ICP_CURRENT = '12', - a_LPF_RESISTOR = '8', - a_MFG_ENABLE_FILTEROPAMP = '1', - a_MFG_GMCREF_SEL = '2', - ) - - platform.add_clock_constraint(pll_clk, platform.pll_config['freq']) - - m.d.comb += [ - ClockSignal('sync').eq(pll_clk) - ] - - return m diff --git a/squishy/gateware/core/scsi.py b/squishy/gateware/core/scsi.py deleted file mode 100644 index 92dd561a..00000000 --- a/squishy/gateware/core/scsi.py +++ /dev/null @@ -1,233 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from math import ceil - -from torii import * -from torii.util.units import ns_to_sec -from torii.lib.soc.wishbone import Interface -from torii.lib.soc.csr.bus import Element, Multiplexer -from torii.lib.soc.csr.wishbone import WishboneCSRBridge - - -__all__ = ( - 'SCSIInterface', -) - -# This is the SCSI 1,2,3 HVD,LVD,SE 50,68,80 PHY Block -class SCSIInterface(Elaboratable): - ''' - SCSI Interface - - - Danger - ------ - This interface is depreciated and will be replaced with the system under :py:mod:`squishy.gateware.scsi`. - - ''' - def __init__(self, *, config, wb_config): - self.config = config - self._scsi_id = Signal(8) - - self._wb_cfg = wb_config - - self.ctl_bus = Interface( - addr_width = self._wb_cfg['addr'], - data_width = self._wb_cfg['data'], - granularity = self._wb_cfg['gran'], - features = self._wb_cfg['feat'] - ) - - self._csr = { - 'mux' : None, - 'elements': {} - } - self._init_csrs() - self._csr_bridge = WishboneCSRBridge(self._csr['mux'].bus) - self.bus = self._csr_bridge.wb_bus - - self.scsi_phy = None - - self._scsi_in_fifo = None - self._usb_out_fifo = None - - self._status_led = None - - def connect_fifo(self, *, scsi_in, usb_out): - self._scsi_in_fifo = scsi_in - self._usb_out_fifo = usb_out - - def _init_csrs(self): - self._csr['regs'] = { - 'status': Element(8, 'r', name = 'scsi_status') - } - - self._csr['mux'] = Multiplexer( - addr_width = 1, - data_width = self._wb_cfg['data'] - ) - - self._csr['mux'].add(self._csr['regs']['status'], addr = 0) - - def _csr_elab(self, m): - m.d.comb += [ - self._csr['regs']['status'].r_data.eq(self._interface_status) - ] - - def _elab_rev1(self, platform): - self.scsi_phy = platform.request('scsi_phy') - self._status_led = platform.request('led', 1) - - self._interface_status = Signal(8) - - # SCSI Bus timings: - # min arbitration delay - 2.2us - # min assertion period - 90ns - # min bus clear delay - 800ns - # max bus clear delay - 1.2us - # min bus free delay - 800ns - # max bus set delay - 1.8us - # min bus settle delay - 400ns - # max cable skew delay - 10ns - # max data release delay - 400ns - # min deskew delay - 45ns - # min hold time - 45ns - # min negation period - 90ns - # min reset hold time - 25us - # max sel abort time - 200us - # min sel timeout delay - 250ms (recommended) - - bus_settle_cnt = int(ceil(ns_to_sec(400) * platform.pll_config['freq']) + 2) - bus_settle_tmr = Signal(range(bus_settle_cnt)) - bus_settled = Signal() - - hold_time_cnt = int(ceil(ns_to_sec(45) * platform.pll_config['freq']) + 2) - hold_time_tmr = Signal(range(hold_time_cnt)) - - m = Module() - m.submodules += self._csr_bridge - m.submodules.csr_mux = self._csr['mux'] - - self._csr_elab(m) - - m.d.comb += [ - self._interface_status[0:7].eq(Cat( - self.scsi_phy.tp_en, - self.scsi_phy.tx_en, - self.scsi_phy.aa_en, - self.scsi_phy.bsy_en, - self.scsi_phy.sel_en, - self.scsi_phy.mr_en, - self.scsi_phy.diff_sense - )), - bus_settled.eq(0) - ] - - with m.If((~self.scsi_phy.sel.rx) & (~self.scsi_phy.bsy.rx)): - with m.If(bus_settle_tmr == (bus_settle_cnt - 1)): - m.d.comb += bus_settled.eq(1) - with m.Else(): - m.d.sync += bus_settle_tmr.eq(bus_settle_tmr + 1) - with m.Else(): - m.d.sync += bus_settle_tmr.eq(0) - - with m.FSM(reset = 'rst'): - with m.State('rst'): - m.d.sync += [ - self.scsi_phy.tp_en.eq(0), - self.scsi_phy.tx_en.eq(0), - self.scsi_phy.aa_en.eq(0), - self.scsi_phy.bsy_en.eq(0), - self.scsi_phy.sel_en.eq(0), - self.scsi_phy.mr_en.eq(0), - - self.scsi_phy.d0.tx.eq(0), - self.scsi_phy.dp0.tx.eq(0), - ] - - with m.If(bus_settled): - m.next = 'bus_free' - - # bus_free - no scsi device is using the bus - # - with m.State('bus_free'): - # All signals are left high-z due to no target/initiator - m.d.sync += [ - self.scsi_phy.tp_en.eq(0), - self.scsi_phy.tx_en.eq(0), - self.scsi_phy.aa_en.eq(0), - self.scsi_phy.bsy_en.eq(0), - self.scsi_phy.sel_en.eq(0), - self.scsi_phy.mr_en.eq(0), - - self.scsi_phy.d0.tx.eq(0), - self.scsi_phy.dp0.tx.eq(0), - ] - - with m.If(self._scsi_in_fifo.r_rdy): - m.next = 'selection' - - - m.next = 'bus_free' - - with m.State('selection'): - m.d.sync += [ - self.scsi_phy.mr_en.eq(1), - self.scsi_phy.io.tx.eq(~self.scsi_phy.io.tx) - ] - - - - m.next = 'bus_free' - - with m.State('command'): - - - m.next = 'bus_free' - - with m.State('data_in'): - - - - - m.next = 'bus_free' - - with m.State('data_out'): - - - - m.next = 'bus_free' - - with m.State('message_in'): - - - - m.next = 'bus_free' - - with m.State('message_out'): - - - - m.next = 'bus_free' - - with m.State('status'): - - - m.next = 'bus_free' - - return m - - def _elab_rev2(self, platform): - m = Module() - - return m - - def elaborate(self, platform): - if platform is None: - m = Module() - return m - else: - if platform.revision == 1: - return self._elab_rev1(platform) - elif platform.revision == 2: - return self._elab_rev2(platform) - else: - raise ValueError(f'Unknown platform revision {platform.revision}') diff --git a/squishy/gateware/core/uart.py b/squishy/gateware/core/uart.py deleted file mode 100644 index 85542a16..00000000 --- a/squishy/gateware/core/uart.py +++ /dev/null @@ -1,108 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from torii import * -from torii.lib.fifo import AsyncFIFO -from torii.lib.stdio.serial import AsyncSerial -from torii.lib.soc.wishbone import Interface - -from ..platform.platform import SquishyPlatform - -__all__ = ( - 'UARTInterface', -) - -class UARTInterface(Elaboratable): - ''' - Trivial UART debug interface. - - Warning - ------- - This interface is only provided for debugging, not sideband communication. - - This elaboratable wraps the :py:class:`torii.lib.stdio.serial.AsyncSerial` UART - elaboratable and attaches it to an internal wishbone bus. - - ''' - def __init__(self, *, config, wb_config): - self.config = config - self._wb_cfg = wb_config - - self.ctl_bus = Interface( - addr_width = self._wb_cfg['addr'], - data_width = self._wb_cfg['data'], - granularity = self._wb_cfg['gran'], - features = self._wb_cfg['feat'] - ) - - self._status_led = None - - self._output_fifo = AsyncFIFO( - width = 8, - depth = 128, - r_domain = 'sync', - w_domain = 'sync', - ) - - self._uart = None - - def elaborate(self, platform: SquishyPlatform | None) -> Module: - self._status_led = platform.request('led', 0) - - self._uart = AsyncSerial( - # TODO: Figure out how to extract the global clock freq and stuff it into the divisor calc - divisor = int(platform.pll_config['freq'] // self.config['baud']), - divisor_bits = None, # Will force use of `bits_for(divisor)`, - data_bits = self.config['data_bits'], - parity = self.config['parity'], - pins = platform.request('uart') - ) - - m = Module() - - uart_in = Signal(self.config['data_bits']) - uart_out = Signal(self.config['data_bits']) - - m.submodules += self._uart, self._output_fifo - - m.d.comb += [ - self._uart.rx.ack.eq(0) - ] - - # TODO: Handle commands w/ more than one byte - - with m.FSM(reset = 'idle'): - with m.State('idle'): - m.d.sync += self._status_led.eq(0) - - with m.If(self._uart.rx.rdy): - m.d.sync += [ - uart_in.eq(self._uart.rx.data), - self._status_led.eq(1) - ] - - m.next = 'uart_ack' - - with m.State('uart_ack'): - m.d.comb += self._uart.rx.ack.eq(1) - m.next = 'cmd_proc' - - with m.State('cmd_proc'): - with m.Switch(uart_in): - with m.Case(0x00): - pass - with m.Default(): - m.next = 'idle' - - - - with m.State('data_write'): - - m.next = 'idle' - - - m.d.sync += [ - self._output_fifo.r_en.eq(self._uart.tx.rdy), - self._uart.tx.data.eq(self._output_fifo.r_data), - self._uart.tx.ack.eq(self._output_fifo.r_rdy), - ] - - return m diff --git a/tests/gateware/usb/__init__.py b/squishy/gateware/peripherals/__init__.py similarity index 60% rename from tests/gateware/usb/__init__.py rename to squishy/gateware/peripherals/__init__.py index 310f6dd9..40c032df 100644 --- a/tests/gateware/usb/__init__.py +++ b/squishy/gateware/peripherals/__init__.py @@ -1,2 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause -__all__ = () + +''' + +''' + +__all__ = ( + +) diff --git a/squishy/gateware/core/flash.py b/squishy/gateware/peripherals/flash.py similarity index 93% rename from squishy/gateware/core/flash.py rename to squishy/gateware/peripherals/flash.py index 332283a8..eef7814c 100644 --- a/squishy/gateware/core/flash.py +++ b/squishy/gateware/peripherals/flash.py @@ -1,21 +1,20 @@ # SPDX-License-Identifier: BSD-3-Clause -from enum import IntEnum, auto, unique -from torii import Elaboratable, Module, Signal +''' -from torii.lib.fifo import AsyncFIFO +''' -from ...core.flash import FlashGeometry -from ..platform.platform import SquishyPlatform -from .spi import SPIInterface +from enum import IntEnum, auto, unique -__doc__ = '''\ - -''' +from torii import Elaboratable, Module, Signal +from torii.lib.fifo import AsyncFIFO +from .spi import SPIInterface +from ..platform import SquishyPlatformType +from ...core.flash import Geometry __all__ = ( 'SPIFlash', - 'SPIFlashOp' + 'SPIFlashOp', ) @unique @@ -43,14 +42,13 @@ class SPIFlashCmd(IntEnum): RELEASE_PWRDWN = 0xAB class SPIFlash(Elaboratable): - def __init__(self, *, flash_resource: str, flash_geometry: FlashGeometry, fifo: AsyncFIFO, erase_cmd: int = None): + def __init__(self, *, flash_resource: tuple[str, int], flash_geometry: Geometry, fifo: AsyncFIFO, erase_cmd: int = None): self._flash_resource = flash_resource self.geometry = flash_geometry self._fifo = fifo self._spi = SPIInterface(resource_name = self._flash_resource) self._erase_cmd = erase_cmd - self.ready = Signal() self.start = Signal() self.done = Signal() @@ -63,11 +61,11 @@ def __init__(self, *, flash_resource: str, flash_geometry: FlashGeometry, fifo: self.writeAddr = Signal(self.geometry.addr_width) self.byteCount = Signal(self.geometry.addr_width) - def elaborate(self, platform: SquishyPlatform | None) -> Module: + def elaborate(self, platform: SquishyPlatformType | None) -> Module: m = Module() if hasattr(platform, 'flash'): - erase_cmd = platform.flash['commands']['erase'] + erase_cmd = platform.flash.commands['erase'] else: erase_cmd = self._erase_cmd diff --git a/squishy/gateware/scsi/__init__.py b/squishy/gateware/peripherals/scsi/__init__.py similarity index 58% rename from squishy/gateware/scsi/__init__.py rename to squishy/gateware/peripherals/scsi/__init__.py index 7170cecb..9747096b 100644 --- a/squishy/gateware/scsi/__init__.py +++ b/squishy/gateware/peripherals/scsi/__init__.py @@ -1,35 +1,13 @@ # SPDX-License-Identifier: BSD-3-Clause -from .scsi1 import SCSI1 -from .scsi2 import SCSI2 -from .scsi3 import SCSI3 - -from .device import SCSI1Device, SCSI2Device, SCSI3Device -from .initiator import SCSI1Initiator, SCSI2Initiator, SCSI3Initiator - -__all__ = ( - 'SCSI1', - 'SCSI2', - 'SCSI3', - - 'SCSI1Device', - 'SCSI1Initiator', - - 'SCSI2Device', - 'SCSI2Initiator', - - 'SCSI3Device', - 'SCSI3Initiator', -) - -__doc__ = '''\ +''' Anatomy of a SCSI Bus --------------------- SCSI Is a bus based system, all devices on the bus have a unique ID and are split into two categories, Initiator, and Target. In general Initiators are show as an adapter connected to a host, and Targets are -shown as controllers attatches to a target device. This abstraction serves to represent that there can be +shown as controllers attaches to a target device. This abstraction serves to represent that there can be multiple possible targets behind a single controller, which share a single bus connection. As SCSI is not a purely point-to-point bus, and allows for multiple bus initiators, there are three possible @@ -252,4 +230,60 @@ M -> BF [label = ""]; } + +The following table lists the timing requirements for each SCSI version. + ++-----------------------+--------------+--------------+--------------+ +| Name | SCSI1 | SCSI2 | SCSI3 | ++=======================+==============+==============+==============+ +| Arbitration | 2.2us | 2.4us | 2.4us | ++-----------------------+--------------+--------------+--------------+ +| Assertion | 90ns | 90ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Bus Clear | 800ns | 800ns | 800ns | ++-----------------------+--------------+--------------+--------------+ +| Bus Free | 800ns | 800ns | 800ns | ++-----------------------+--------------+--------------+--------------+ +| Bus Set | 1.8us | 1.8us | 1.6us | ++-----------------------+--------------+--------------+--------------+ +| Bus Settle | 400ns | 400ns | 400ns | ++-----------------------+--------------+--------------+--------------+ +| Cable Skew | 10ns | 10ns | 4ns | ++-----------------------+--------------+--------------+--------------+ +| Data Release | 400ns | 400ns | 400ns | ++-----------------------+--------------+--------------+--------------+ +| Deskew | 45ns | 45ns | 45ns | ++-----------------------+--------------+--------------+--------------+ +| Hold Time | 45ns | 45ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Negation | 90ns | 90ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Reset Hold | 25us | 25us | 25us | ++-----------------------+--------------+--------------+--------------+ +| Selection Abort | 200us | 200us | 200us | ++-----------------------+--------------+--------------+--------------+ +| Selection Timeout | 250ms | 250ms | 250ms | ++-----------------------+--------------+--------------+--------------+ +| Disconnect | Unspecified | 200us | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Power to Selection | Unspecified | 10s | 10s | ++-----------------------+--------------+--------------+--------------+ +| Reset to Selection | Unspecified | 250ms | 250ms | ++-----------------------+--------------+--------------+--------------+ +| Fast Assert | Unspecified | 30ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Fast Cable Skew | Unspecified | 5ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Fast Deskew | Unspecified | 20ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Fast Hold | Unspecified | 10ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ +| Fast Negation | Unspecified | 30ns | Unspecified | ++-----------------------+--------------+--------------+--------------+ + + ''' # noqa: E101 + +__all__ = ( + +) diff --git a/squishy/gateware/peripherals/scsi/quirks/__init__.py b/squishy/gateware/peripherals/scsi/quirks/__init__.py new file mode 100644 index 00000000..40c032df --- /dev/null +++ b/squishy/gateware/peripherals/scsi/quirks/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +__all__ = ( + +) diff --git a/squishy/gateware/core/spi.py b/squishy/gateware/peripherals/spi.py similarity index 90% rename from squishy/gateware/core/spi.py rename to squishy/gateware/peripherals/spi.py index 5545a805..69f97f81 100644 --- a/squishy/gateware/core/spi.py +++ b/squishy/gateware/peripherals/spi.py @@ -1,8 +1,11 @@ # SPDX-License-Identifier: BSD-3-Clause -from torii import Elaboratable, Signal, Module, Cat +''' -from ..platform.platform import SquishyPlatform +''' + +from torii import Elaboratable, Signal, Module, Cat +from ..platform import SquishyPlatformType __all__ = ( 'SPIInterface', @@ -42,7 +45,7 @@ def __init__(self, *, resource_name: tuple[str, int]) -> None: self.wdat = Signal(8) self.rdat = Signal(8) - def elaborate(self, platform: SquishyPlatform | None) -> Module: + def elaborate(self, platform: SquishyPlatformType | None) -> Module: self._spi = platform.request(*self._spi_resource) m = Module() diff --git a/squishy/gateware/peripherals/usb/__init__.py b/squishy/gateware/peripherals/usb/__init__.py new file mode 100644 index 00000000..40c032df --- /dev/null +++ b/squishy/gateware/peripherals/usb/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +__all__ = ( + +) diff --git a/squishy/gateware/peripherals/usb/dfu.py b/squishy/gateware/peripherals/usb/dfu.py new file mode 100644 index 00000000..e38a15c2 --- /dev/null +++ b/squishy/gateware/peripherals/usb/dfu.py @@ -0,0 +1,392 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +from torii import Module, Signal, Cat +from torii.hdl.ast import Operator +from torii.lib.fifo import AsyncFIFO + +from usb_construct.types import USBRequestType, USBRequestRecipient, USBStandardRequests +from usb_construct.types.descriptors.dfu import DFURequests + +from sol_usb.gateware.usb.usb2.request import USBRequestHandler, SetupPacket +from sol_usb.gateware.usb.stream import USBInStreamInterface, USBOutStreamInterface +from sol_usb.gateware.stream.generator import StreamSerializer + +from ....core.dfu import DFUState, DFUStatus +from ...platform import SquishyPlatformType + +__all__ = ( + 'DFURequestHandler', +) + +class DFUConfig: + ''' + + Attributes + ---------- + + status : Signal(4) + DFU Status + + state : Signal(4) + DFU State + + ''' + + def __init__(self) -> None: + self.status = Signal(4, decoder = DFUStatus) + self.state = Signal(4, decoder = DFUState) + +class DFURequestHandler(USBRequestHandler): + ''' + USB DFU Request handler. + + This implements both a fully DFU capable endpoint for firmware flashing as well + as a simple DFU stub that is used to reboot the device into bootloader mode. + + Parameters + ---------- + configuration : int + The configuration ID for this DFU endpoint + + interface : int + The interface ID for this DFU endpoint + + boot_stub : bool + If True, only the bare minimum for triggering a DFU reboot will be + generated, otherwise if False a full DFU implementation will be + generated. + + fifo : AsyncFIFO | None + The storage FIFO. + + Attributes + ---------- + trigger_reboot : Signal + Output: driven high when the DFU handler wants to reboot the device + + slot_selection : Signal(2) + Output: the flash slot address + + dl_start : Signal + Output: Start of a DFU transfer. + + dl_finish : Signal + Output: An acknowledgement of the `dl_done` signal + + dl_ready : Signal + Input: If the backing storage is ready for data. + + dl_done : Signal + Input: When the backing storage is done storing the data. + + dl_reset_slot : Signal + Output: Signals to the storage to reset the active slot. + + dl_size : Signal(16) + Output: The size of the DFU transfer into the the FIFO + + slot_changed : Signal + Output: Raised when the DFU alt-mode is changed. + + slot_ack : Signal + Input: When the `slot_changed` signal was acted on. + + + Raises + ------ + ValueError + If fifo is `None` when `boot_stub` is False. + + ''' + + def __init__(self, configuration: int, interface: int, boot_stub: bool, *, fifo: AsyncFIFO | None = None) -> None: + super().__init__() + + # DFU interface + self._interface_id = interface + self._config_id = configuration + + # Used to alter gateware synth if we're just a DFU reboot stub or a full impl + self._is_stub = boot_stub + + if not self._is_stub: + if fifo is None: + raise ValueError('fifo parameter must not be None for non-stub DFU implementations') + + self._bit_fifo = fifo + + self.dl_start = Signal() + self.dl_finish = Signal() + self.dl_ready = Signal() + self.dl_done = Signal() + self.dl_reset_slot = Signal() + self.dl_size = Signal(16) + + self.slot_changed = Signal() + self.slot_ack = Signal() + + self.trigger_reboot = Signal() + self.slot_selection = Signal(2) + + + def elaborate(self, platform: SquishyPlatformType) -> Module: + m = Module() + + # DFU Stub + + interface = self.interface + setup_pkt = interface.setup + + if not self._is_stub: + rx_trig = Signal() + rx_stream = USBOutStreamInterface(payload_width = 8) + + recv_start = Signal() + recv_count = Signal.like(setup_pkt.length) + recv_consumed = Signal.like(setup_pkt.length) + + dfu_cfg = DFUConfig() + + m.d.comb += [ + self.dl_start.eq(0), + self.dl_finish.eq(0), + self.dl_reset_slot.eq(0), + self.slot_changed.eq(0), + ] + + + m.submodules.transmitter = transmitter = StreamSerializer( + data_length = 6, domain = 'usb', stream_type = USBInStreamInterface, max_length_width = 3 + ) + + with m.FSM(domain = 'usb', name = 'dfu'): + if not self._is_stub: + with m.State('RESET'): + m.d.usb += [ + dfu_cfg.status.eq(DFUStatus.Okay), + dfu_cfg.state.eq(DFUState.DFUIdle), + self.slot_selection.eq(0), + ] + # TODO(aki): This might be okay to be yeeted, as the slot data stuff moved + # with m.If(self.dl_ready): + # m.next = 'READ_SLOT_DATA' + + with m.State('IDLE'): + with m.If(setup_pkt.received & self.handler_condition(setup_pkt)): + with m.If(setup_pkt.type == USBRequestType.CLASS): + with m.Switch(setup_pkt.request): + with m.Case(DFURequests.DETACH): + m.next = 'HANDLE_DETACH' + with m.Case(DFURequests.GET_STATUS): + m.next = 'HANDLE_GET_STATUS' + with m.Case(DFURequests.GET_STATE): + m.next = 'HANDLE_GET_STATE' + if not self._is_stub: + with m.Case(DFURequests.DOWNLOAD): + m.next = 'HANDLE_DOWNLOAD' + with m.Case(DFURequests.CLR_STATUS): + m.next = 'HANDLE_CLR_STATUS' + with m.Default(): + m.next = 'UNHANDLED' + with m.Elif(setup_pkt.type == USBRequestType.STANDARD): + with m.Switch(setup_pkt.request): + with m.Case(USBStandardRequests.GET_INTERFACE): + m.next = 'GET_INTERFACE' + with m.Case(USBStandardRequests.SET_INTERFACE): + m.next = 'SET_INTERFACE' + with m.Default(): + m.next = 'UNHANDLED' + if not self._is_stub: + with m.If(self.dl_done): + m.d.comb += [ self.dl_finish.eq(1), ] + m.d.usb += [ dfu_cfg.state.eq(DFUState.DlSync), ] + + with m.State('HANDLE_DETACH'): + with m.If(interface.status_requested): + m.d.comb += [ self.send_zlp(), ] + with m.If(interface.handshakes_in.ack): + m.d.usb += [ self.trigger_reboot.eq(1), ] + + with m.State('HANDLE_GET_STATUS'): + m.d.comb += [ + transmitter.stream.connect(interface.tx), + transmitter.max_length.eq(6), + transmitter.data[0].eq(DFUStatus.Okay if self._is_stub else dfu_cfg.status), + Cat(transmitter.data[1:4]).eq(0), + transmitter.data[4].eq(DFUState.AppIdle if self._is_stub else Cat(dfu_cfg.state, 0)), + transmitter.data[5].eq(0), + ] + + with m.If(interface.data_requested): + with m.If(setup_pkt.length == 6): + m.d.comb += [ transmitter.start.eq(1), ] + with m.Else(): + m.d.comb += [ interface.handshakes_out.stall.eq(1), ] + m.next = 'IDLE' + + with m.If(interface.status_requested): + m.d.comb += [ interface.handshakes_out.ack.eq(1), ] + + if not self._is_stub: + with m.If(dfu_cfg.state == DFUState.DlSync): + m.d.usb += [ dfu_cfg.state.eq(DFUState.DlIdle), ] + + m.next = 'IDLE' + + + with m.State('HANDLE_GET_STATE'): + m.d.comb += [ + transmitter.stream.connect(interface.tx), + transmitter.max_length.eq(1), + ] + + if self._is_stub: + m.d.comb += [ transmitter.data[0].eq(DFUState.AppIdle), ] + else: + m.d.comb += [ transmitter.data[0].eq(Cat(dfu_cfg.state, 0)), ] + + with m.If(interface.data_requested): + with m.If(setup_pkt.length == 1): + m.d.comb += [ transmitter.start.eq(1), ] + with m.Else(): + m.d.comb += [ interface.handshakes_out.stall.eq(1), ] + m.next = 'IDLE' + + with m.If(interface.status_requested): + m.d.comb += [ interface.handshakes_out.ack.eq(1), ] + m.next = 'IDLE' + + if not self._is_stub: + with m.State('HANDLE_DOWNLOAD'): + with m.If(setup_pkt.is_in_request | (setup_pkt.length > platform.flash.geometry.erase_size)): + m.next = 'UNHANDLED' + with m.Elif(setup_pkt.length): + m.d.comb += [ + self.dl_start.eq(1), + self.dl_size.eq(setup_pkt.length), + ] + m.d.usb += [ dfu_cfg.state.eq(DFUState.DlBusy) ] + + m.next = 'HANDLE_DOWNLOAD_DATA' + with m.Else(): + m.next = 'HANDLE_DOWNLOAD_COMPLETE' + + with m.State('HANDLE_DOWNLOAD_DATA'): + m.d.comb += [ interface.rx.connect(rx_stream), ] + + with m.If(~rx_trig): + m.d.comb += [ recv_start.eq(1), ] + m.d.usb += [ rx_trig.eq(1), ] + + with m.If(interface.rx_ready_for_response): + m.d.comb += [ interface.handshakes_out.ack.eq(1), ] + + with m.If(interface.status_requested): + m.d.comb += [ self.send_zlp(), ] + + with m.If(interface.handshakes_in.ack): + m.d.usb += [ rx_trig.eq(0), ] + m.next = 'IDLE' + + with m.State('HANDLE_DOWNLOAD_COMPLETE'): + with m.If(interface.status_requested): + m.d.usb += [ dfu_cfg.state.eq(DFUState.AppIdle), ] + m.d.comb += [ self.send_zlp(), ] + + with m.If(interface.handshakes_in.ack): + m.next = 'IDLE' + + with m.State('HANDLE_CLR_STATUS'): + with m.If(setup_pkt.length == 0): + with m.If(dfu_cfg.state == DFUState.Error): + m.d.usb += [ + dfu_cfg.status.eq(DFUStatus.Okay), + dfu_cfg.state.eq(DFUState.AppIdle), + ] + with m.Else(): + m.d.comb += [ interface.handshakes_out.stall.eq(1), ] + m.next = 'IDLE' + + with m.If(interface.status_requested): + m.d.comb += [ self.send_zlp(), ] + with m.If(interface.handshakes_in.ack): + m.next = 'IDLE' + + with m.State('SLOT_WAIT'): + with m.If(self.slot_ack): + m.next = 'IDLE' + + with m.State('GET_INTERFACE'): + m.d.comb += [ + transmitter.stream.connect(interface.tx), + transmitter.max_length.eq(1), + # TODO(aki): This inline if might blow up + transmitter.data[0].eq(0 if self._is_stub else self.slot_selection), + ] + + with m.If(self.interface.data_requested): + with m.If(setup_pkt.length == 1): + m.d.comb += [ transmitter.start.eq(1), ] + with m.Else(): + m.d.comb += [ interface.handshakes_out.stall.eq(1), ] + m.next = 'IDLE' + + with m.If(interface.status_requested): + m.d.comb += [ interface.handshakes_out.ack.eq(1), ] + m.next = 'IDLE' + + with m.State('SET_INTERFACE'): + with m.If(interface.status_requested): + m.d.comb += [ self.send_zlp(), ] + with m.If(interface.handshakes_in.ack): + if self._is_stub: + m.next = 'IDLE' + else: + m.d.usb += [ self.slot_selection.eq(setup_pkt.value[0:2]), ] + m.d.comb += [ self.slot_changed.eq(1), ] + m.next = 'SLOT_WAIT' + + with m.State('UNHANDLED'): + with m.If(interface.data_requested | interface.status_requested): + m.d.comb += [ interface.handshakes_out.stall.eq(1), ] + m.next = 'IDLE' + + + if not self._is_stub: + m.d.comb += [ + self._bit_fifo.w_en.eq(0), + self._bit_fifo.w_data.eq(rx_stream.payload), + ] + + recv_cont = (recv_consumed < recv_count) + + with m.FSM(domain = 'usb', name = 'download'): + with m.State('IDLE'): + m.d.usb += [ recv_consumed.eq(0), ] + + with m.If(recv_start): + m.d.usb += [ recv_count.eq(setup_pkt.length - 1), ] + m.next = 'STREAMING' + + with m.State('STREAMING'): + with m.If(rx_stream.valid & rx_stream.next): + m.d.comb += [ self._bit_fifo.w_en.eq(1), ] + + with m.If(recv_cont): + m.d.usb += [ recv_consumed.eq(recv_consumed + 1), ] + with m.Else(): + m.next = 'IDLE' + + return m + + def handler_condition(self, setup: SetupPacket) -> Operator: + return ( + (self.interface.active_config == self._config_id) & + ((setup.type == USBRequestType.CLASS) | (setup.type == USBRequestType.STANDARD)) & + (setup.recipient == USBRequestRecipient.INTERFACE) & + (setup.index == self._interface_id) + ) diff --git a/squishy/gateware/peripherals/usb/quirks/__init__.py b/squishy/gateware/peripherals/usb/quirks/__init__.py new file mode 100644 index 00000000..68c38318 --- /dev/null +++ b/squishy/gateware/peripherals/usb/quirks/__init__.py @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' +This module contains USB quirks for various platforms. + +The lame duck is currently only Windows which needs special USB descriptors, +and are in the :py:mod:`.windows` +module. +''' + +__all__ = ( + +) diff --git a/squishy/gateware/quirks/usb/windows.py b/squishy/gateware/peripherals/usb/quirks/windows.py similarity index 94% rename from squishy/gateware/quirks/usb/windows.py rename to squishy/gateware/peripherals/usb/quirks/windows.py index 3c151886..c0d1de0b 100644 --- a/squishy/gateware/quirks/usb/windows.py +++ b/squishy/gateware/peripherals/usb/quirks/windows.py @@ -1,41 +1,22 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import ( - Type -) +''' -from struct import ( - pack, unpack -) +''' -from torii import ( - Module, Signal, Elaboratable, Memory, DomainRenamer -) -from torii.hdl.ast import ( - Operator -) +from typing import Type -from usb_construct.types import ( - USBRequestType, USBRequestRecipient -) -from usb_construct.types.descriptors.microsoft import ( - MicrosoftRequests -) -from usb_construct.emitters.descriptors.microsoft import ( - PlatformDescriptorCollection -) - -from sol_usb.gateware.usb.stream import ( - USBInStreamInterface -) +from struct import pack, unpack -from sol_usb.gateware.usb.usb2.request import ( - USBRequestHandler, SetupPacket -) +from torii import Module, Signal, Elaboratable, Memory, DomainRenamer +from torii.hdl.ast import Operator -__doc__ = '''\ +from usb_construct.types import USBRequestType, USBRequestRecipient +from usb_construct.types.descriptors.microsoft import MicrosoftRequests +from usb_construct.emitters.descriptors.microsoft import PlatformDescriptorCollection -''' +from sol_usb.gateware.usb.stream import USBInStreamInterface +from sol_usb.gateware.usb.usb2.request import USBRequestHandler, SetupPacket __all__ = ( 'WindowsRequestHandler', diff --git a/squishy/gateware/platform/__init__.py b/squishy/gateware/platform/__init__.py index 9adbe9dd..09cb59b9 100644 --- a/squishy/gateware/platform/__init__.py +++ b/squishy/gateware/platform/__init__.py @@ -1,21 +1,172 @@ # SPDX-License-Identifier: BSD-3-Clause -from .rev1 import SquishyRev1 -from .rev2 import SquishyRev2 -__all__ = ( - 'SquishyRev1', - 'SquishyRev2', +''' + +''' + +from abc import ABCMeta, abstractmethod +from pathlib import Path +from typing import TypeAlias +from itertools import count + +from torii import Elaboratable +from torii.build import Resource, ResourceError +from torii.build.plat import Platform +from torii.build.run import BuildProducts + +from ...core.config import PLLConfig, FlashConfig - 'AVAILABLE_PLATFORMS', +__all__ = ( + 'SquishyPlatform', + 'SquishyPlatformType', ) -__doc__ = '''\ +class SquishyPlatform(metaclass = ABCMeta): + ''' + Base Squishy Hardware platform + + This represents all the common properties and methods + that all Squishy hardware platforms are required to have. + + This also implements the applet bitstream cache mechanisms. + + Attributes + ---------- + + revision : tuple[int, int] + The revision of the hardware this platform supports, in the form + of (major, minor). + + revision_str : string + The canonicalize revision as a string in the form of 'major.minor' + + flash : FlashConfig + The configuration of the attached SPI boot flash. + + pll_cfg : PLLConfig + The PLL configuration that is passed to the ``clk_domain_generator`` of this platform + when instantiated. + + clk_domain_generator : type[torii.Elaboratable] + The type of clock domain generator for this platform. It is instantiated and hooked up + to the gateware on elaboration. + + ephemeral_slot : int | None + If this platform supports ephemeral applet flashing, then this is the DFU alt-mode to use, otherwise None + + Important + --------- + Platforms are also still required to inherit from the appropriate :py:mod:`torii.vendor.platform` + in order to properly be used. + + ''' + + @property + @abstractmethod + def revision(self) -> tuple[int, int]: + ''' The hardware revision of this platform in the form of (major, minor)''' + raise NotImplementedError('SquishyPlatform requires a revision to be set') + + @property + def revision_str(self) -> str: + ''' The canonicalize revision as a string in the form of 'major.minor' ''' + return '.'.join(map(lambda p: str(p), self.revision)) + + @property + @abstractmethod + def flash(self) -> FlashConfig: + ''' The attached SPI boot flash configuration ''' + raise NotImplementedError('SquishyPlatform requires a flash config to be set') + + @property + @abstractmethod + def pll_cfg(self) -> PLLConfig: + ''' PLL Configuration for the platforms clock domain generator ''' + raise NotImplementedError('SquishyPlatform requires a PLL config to be set') + + @property + @abstractmethod + def clk_domain_generator(self) -> type[Elaboratable]: + ''' The Torii Elaboratable used to setup the PLL and clock domains for the platform ''' + raise NotImplementedError('SquishyPlatform requires a PLL config to be set') + + @property + def ephemeral_slot(self) -> int | None: + ''' If this platform supports ephemeral applet flashing, then this is the DFU alt-mode to use ''' + return None + + # TODO(aki): single bitstream/artifact packing + whole image packing + @abstractmethod + def pack_artifact(self, artifact: bytes) -> bytes: + ''' + Pack a signal bitstream image into a device appropriate artifact. + + Parameters + ---------- + artifact : bytes + The input data of the result of gateware elaboration, typically + the raw FPGA bitstream file. + + Returns + ------- + bytes + The result of the artifact packing process + + ''' + raise NotImplementedError('SquishyPlatform requires pack_artifact to be implemented') + + @abstractmethod + def build_image(self, name: str, build_dir: Path, boot_name: str, products: BuildProducts) -> Path: + ''' + Build a platform compatible flash image for provisioning. + + Parameters + ---------- + name : str + The name of the flash image to produce. + + build_dir : Path + Output directory for the finalized flash image. + + boot_name: str + The name of the bootloader in the build products + + products : BuildProducts + The resulting build products from the bootloader build. + + Returns + ------- + Path + The path to the resulting image file. + ''' + raise NotImplementedError('SquishyPlatform requires build_image to be implemented') + + def all_resources_by_name(self, name: str) -> list[Resource]: + ''' + Get all resources sharing a common root name, e.g. all LEDs + + Parameters + ---------- + name : str + The base name of the resources to collect. -.. todo:: Flesh this section out + Returns + ------- + list[Resources] + A list of all of the found Torii resources matching the given base name. + ''' -''' # noqa: E101 + res = [] + for num in count(): + try: + res.append(self.request(name, num)) + except ResourceError: + break + return res -AVAILABLE_PLATFORMS = { - 'rev1': SquishyRev1, -} +# XXX(aki): This is a stupid hack so we get proper typing on the platform +# without the nightmare that is recursive imports and all that jazz. +# I would really *really* love to do a proper composite type class but +# a union works for now. +SquishyPlatformType: TypeAlias = SquishyPlatform | Platform diff --git a/squishy/gateware/platform/mixins.py b/squishy/gateware/platform/mixins.py deleted file mode 100644 index 278e6467..00000000 --- a/squishy/gateware/platform/mixins.py +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -import logging as log - -from rich.progress import Progress - -from ...core.cache import SquishyBitstreamCache - -__all__ = ( - 'SquishyCacheMixin', -) - -__doc__ = '''\ - -The following are mixins that are used to add additional features to torii platforms -without any extra setup work for the platform itself. - -''' - -class SquishyCacheMixin: - ''' - Squishy Platform Cache mixin. - - This mixin overrides the :py:class:`torii.build.plat.Platform`. `build` method - to inject FPGA bitstream caching via the :py:class:`squishy.core.cache.SquishyBitstreamCache`. - which handles all bitstream and build caching based on the elaborated designs digest. - - This shortens build times, and removes the need to re-build unchanged applets. - - ''' - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - self._cache = SquishyBitstreamCache() - - def _build_elaboratable(self, elaboratable, progress: Progress, name: str = 'top', - build_dir: str = 'build', do_build: bool = False, - program_opts: str = None, **kwargs): - - skip_cache = kwargs.get('skip_cache', False) - - if skip_cache: - log.warning('Skipping cache lookup, this might take a [yellow][i]while[/][/]', extra = { 'markup': True }) - - task = progress.add_task('Elaborating Bitstream', start=False) - - plan = super().build(elaboratable, name, - build_dir, do_build = False, - program_opts = program_opts, do_program = False, **kwargs) - - - if not do_build: - return (name, plan) - - digest = plan.digest(size = 32).hex() - cache_obj = self._cache.get(digest) - - progress.update(task, description = 'Building Bitstream') - - if cache_obj is None or skip_cache: - if not skip_cache: - log.debug('Bitstream is not cached, building. This might take a [yellow][i]while[/][/]', extra = { 'markup': True }) - - prod = plan.execute_local(build_dir) - log.debug('Bitstream built') - - if not skip_cache: - self._cache.store(digest, prod, name) - else: - name = cache_obj['name'] - prod = cache_obj['products'] - - log.info(f'Using cached bitstream \'{name}\'') - - progress.remove_task(task) - return (name, prod) - - - def build(self, elaboratable, name: str = 'top', - build_dir: str = 'build', do_build: bool = False, - program_opts: str = None, do_program: bool = False, - progress: Progress = None, **kwargs - ): - - return self._build_elaboratable(elaboratable, progress, name, build_dir, do_build, program_opts, **kwargs) diff --git a/squishy/gateware/platform/platform.py b/squishy/gateware/platform/platform.py deleted file mode 100644 index ed0a1feb..00000000 --- a/squishy/gateware/platform/platform.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from abc import ABCMeta, abstractmethod -from torii import Elaboratable - -from .mixins import SquishyCacheMixin - -from ...config import USB_VID, USB_PID_APPLICATION, USB_PID_BOOTLOADER -from ...config import USB_MANUFACTURER, USB_PRODUCT -from ...config import SCSI_VID - -__all__ = ( - 'SquishyPlatform' -) - -class SquishyPlatform(SquishyCacheMixin, metaclass = ABCMeta): - ''' - Squishy Base Platform - - This is a base platform for Squishy hardware designs. It is built to abstract away a chunk of - the things that would be constantly repeated for new Squishy platforms and variants. - - The primary things that are here are as follows: - * ``usb_vid`` - The USB Vendor ID - * ``usb_pid_app`` - The USB PID for the main gateware - * ``usb_pid_boot`` - This USB PID for the bootloader - * ``usb_mfr`` - The USB Manufacturer string - * ``usb_prod`` - The USB PID to string mapping - * ``scsi_vid`` - The default SCSI Vendor ID - - The things that the platforms are expected to provide are as follows: - * ``revision`` - The platform revision - * ``clock_domain_generator`` - The Torii Elaboratable PLL/Clock Domain generator for this Squishy platform - * ``pll_config`` - The PLL configuration for the ``clock_domain_generator`` - - Platforms are also still required to inherit from the appropriate :py:mod:`torii.vendor.platform` - in order to properly be used. - - ''' - - @property - def usb_vid(self) -> int: - ''' The USB Vendor ID used for Squishy endpoints ''' - return USB_VID - - @property - def usb_pid_app(self) -> int: - ''' The USB PID for the main Squishy gateware ''' - return USB_PID_APPLICATION - - @property - def usb_pid_boot(self) -> int: - ''' The USB VID for the Squishy bootloader ''' - return USB_PID_BOOTLOADER - - @property - def usb_mfr(self) -> str: - ''' The USB Manufacturer string ''' - return USB_MANUFACTURER - - @property - def usb_prod(self) -> dict[int, str]: - ''' The USB VID to USB Product string mapping ''' - return USB_PRODUCT - - @property - def scsi_vid(self) -> str: - ''' The SCSI Vendor ID ''' - return SCSI_VID - - @property - @abstractmethod - def revision(self) -> float: - ''' The hardware platform revision ''' - raise NotImplementedError('SquishyPlatform requires a revision to be set') - - @property - @abstractmethod - def clock_domain_generator(self) -> Elaboratable: - ''' The Torii Elaboratable that is the PLL/Clock Domain generator for this Squishy platform ''' - raise NotImplementedError('SquishyPlatform requires a clock domain generator to be set') - - @property - @abstractmethod - def pll_config(self) -> dict[str, int]: - ''' The PLL configuration for the given clock_domain_generator ''' - raise NotImplementedError('SquishyPlatform requires a pll config to be set') diff --git a/squishy/gateware/platform/resources/__init__.py b/squishy/gateware/platform/resources/__init__.py index d44d7ae2..b41daa03 100644 --- a/squishy/gateware/platform/resources/__init__.py +++ b/squishy/gateware/platform/resources/__init__.py @@ -1,15 +1,5 @@ # SPDX-License-Identifier: BSD-3-Clause -from .scsi import SCSIConnectorResource, SCSIDifferentialResource -from .scsi import SCSISingleEndedResource, SCSIPhyResource - -__all__ = ( - 'SCSIConnectorResource', - 'SCSIDifferentialResource', - 'SCSISingleEndedResource', - 'SCSIPhyResource', -) - -__doc__ = '''\ +''' ''' diff --git a/squishy/gateware/platform/resources/scsi.py b/squishy/gateware/platform/resources/scsi.py deleted file mode 100644 index b8a6d65d..00000000 --- a/squishy/gateware/platform/resources/scsi.py +++ /dev/null @@ -1,324 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from typing import ( - Literal, Union, Optional -) - -from torii.build import ( - Attrs, Pins, PinsN, Subsignal, DiffPairs, Resource -) - -__all__ = ( - 'SCSIConnectorResource', - 'SCSIDifferentialResource', - 'SCSISingleEndedResource', - 'SCSIPhyResource', -) - -__doc__ = '''\ - -''' - -# Type Aliases -PinDiff = tuple[str, str] -PinDef = Union[str, PinDiff] -PinDir = Literal['i', 'o', 'io'] - -def TransceiverPairs( - tx: str, rx: str, *, - invert: bool = False, conn: str = None, - assert_width: Optional[int] = None -) -> tuple[Subsignal]: - ''' - Returns a tuple of subsignals for RX and TX pairs - - Parameters - ---------- - tx : str - The PHY TX pins. - - rx : str - The PHY RX pins. - - invert : bool - If the signals are inverted or not, - - conn : str - The connector, if any. - - assert_width : int - The width of the pin pairs. - - Returns - ------- - tuple[Subsignal, Subsignal] - The RX/TX pair with correct directions and inversion set up. - - ''' - return ( - Subsignal('tx', Pins(tx, dir = 'o', invert = invert, conn = conn, assert_width = assert_width)), - Subsignal('rx', Pins(rx, dir = 'i', invert = invert, conn = conn, assert_width = assert_width)), - ) - -def SCSIConnectorResource(*args, diff: bool, - ack: PinDef, atn: PinDef, bsy: PinDef, cd: PinDef, io: PinDef, msg: PinDef, - sel: PinDef, req: PinDef, rst: PinDef, diff_sense: str, d0: PinDef, dp0: PinDef, - d1: Optional[PinDef] = None, dp1: Optional[PinDef] = None, scsi_id: Optional[str] = None, - led: Optional[str] = None, spindle: Optional[str] = None, rmt: Optional[str] = None, - dlyd: Optional[str] = None, dir: PinDir = 'io', attrs: Optional[Attrs] = None) -> Resource: - ''' - Represents a raw SCSI connector - - Parameters - ---------- - diff : bool - If the SCSI connector is Differential. - - ack : str, tuple[str, str] - The pin or pins for the SCSI ACK signal. - - atn : str, tuple[str, str] - The pin or pins for the SCSI ATN signal. - - bsy : str, tuple[str, str] - The pin or pins for the SCSI BSY signal. - - cd : str, tuple[str, str] - The pin or pins for the SCSI CD signal. - - io : str, tuple[str, str] - The pin or pins for the SCSI IO signal. - - msg : str, tuple[str, str] - The pin or pins for the SCSI MSG signal. - - req : str, tuple[str, str] - The pin or pins for the SCSI REQ signal. - - rst : str, tuple[str, str] - The pin or pins for the SCSI RST signal. - - diff_sense : str - The SCSI differential sense pin. - - d0 : str, tuple[str, str] - The pin set or set of pin sets for the first SCSI data byte lines. - - dp0 : str, tuple[str, str] - The pin or pins for the first SCSI data byte parity bit. - - Other Parameters - ---------------- - d1 : str, tuple[str, str] - The pin set or set of pin sets for the second SCSI data byte lines. - - dp1 : str, tuple[str, str] - The pin or pins for the second SCSI data byte parity bit. - - scsi_id : str - The pin set of set of pin sets for the dedicated SCSI_ID pins. - - led : str - The SCSI bus LED signal pin. - - spindle : str - The SCSI spindle signal pin. - - rmt : str - The SCSI RMT signal pin. - - dlyd : str - The SCSI dlyd signal pin. - - dir : str - The direction of the SCSI connector pins, defaults to 'io' - - Returns - ------- - :py:class:`torii.build.dsl.Resource` - The SCSI Connector Resource - - ''' - - if diff: - io = [ - Subsignal('ack', DiffPairs(*ack, dir = dir, assert_width = 1)), - Subsignal('atn', DiffPairs(*atn, dir = dir, assert_width = 1)), - Subsignal('bsy', DiffPairs(*bsy, dir = dir, assert_width = 1)), - Subsignal('cd', DiffPairs(*cd, dir = dir, assert_width = 1)), - Subsignal('io', DiffPairs(*io, dir = dir, assert_width = 1)), - Subsignal('msg', DiffPairs(*msg, dir = dir, assert_width = 1)), - Subsignal('sel', DiffPairs(*sel, dir = dir, assert_width = 1)), - Subsignal('req', DiffPairs(*req, dir = dir, assert_width = 1)), - Subsignal('rst', DiffPairs(*rst, dir = dir, assert_width = 1)), - Subsignal('d0', DiffPairs(*d0, dir = dir, assert_width = 8)), - Subsignal('dp0', DiffPairs(*dp0, dir = dir, assert_width = 1)), - Subsignal('diff_sense', Pins(diff_sense, dir = dir, assert_width = 1)), - ] - else: - io = [ - Subsignal('ack', Pins(ack, dir = dir, assert_width = 1)), - Subsignal('atn', Pins(atn, dir = dir, assert_width = 1)), - Subsignal('bsy', Pins(bsy, dir = dir, assert_width = 1)), - Subsignal('cd', Pins(cd, dir = dir, assert_width = 1)), - Subsignal('io', Pins(io, dir = dir, assert_width = 1)), - Subsignal('msg', Pins(msg, dir = dir, assert_width = 1)), - Subsignal('sel', Pins(sel, dir = dir, assert_width = 1)), - Subsignal('req', Pins(req, dir = dir, assert_width = 1)), - Subsignal('rst', Pins(rst, dir = dir, assert_width = 1)), - Subsignal('d0', Pins(d0, dir = dir, assert_width = 8)), - Subsignal('dp0', Pins(dp0, dir = dir, assert_width = 1)), - Subsignal('diff_sense', Pins(diff_sense, dir = dir, assert_width = 1)), - ] - - if d1 is not None: - assert dp1 is not None, 'Parity bit for d1 must be present' - if diff: - io.append(Subsignal('d1', DiffPairs(*d1, dir = dir, assert_width = 8))), - io.append(Subsignal('dp1', DiffPairs(*dp1, dir = dir, assert_width = 1))), - else: - io.append(Subsignal('d1', Pins(d1, dir = dir, assert_width = 8))), - io.append(Subsignal('dp1', Pins(dp1, dir = dir, assert_width = 1))), - - if scsi_id is not None: - assert led is not None - assert spindle is not None - assert rmt is not None - assert dlyd is not None - - io.append(Subsignal('id', Pins(scsi_id, dir = dir, assert_width = 4))), - io.append(Subsignal('led', Pins(led, dir = dir, assert_width = 1))), - io.append(Subsignal('spindle', Pins(spindle, dir = dir, assert_width = 1))), - io.append(Subsignal('rmt', Pins(rmt, dir = dir, assert_width = 1))), - io.append(Subsignal('dlyd', Pins(dlyd, dir = dir, assert_width = 1))), - - if attrs is not None: - io.append(attrs) - - return Resource.family(*args, default_name = 'scsi_conn', ios = io) - - -def SCSIPhyResource(*args, - ack: PinDiff, atn: PinDiff, bsy: PinDiff, cd: PinDiff, io: PinDiff, msg: PinDiff, - sel: PinDiff, req: PinDiff, rst: PinDiff, d0: PinDiff, dp0: PinDiff, - tp_en: str, tx_en: str, aa_en: str, bsy_en: str, sel_en: str, mr_en: str, - diff_sense: str, d1: Optional[PinDiff] = None, dp1: Optional[PinDiff] = None, - scsi_id: Optional[PinDiff] = None, led: Optional[PinDiff] = None, - spindle: Optional[PinDiff] = None, rmt: Optional[PinDiff] = None, - dlyd: Optional[PinDiff] = None, attrs: Optional[Attrs] = None) -> Resource: - ''' - Represents a Squishy SCSI PHY Resource - - Parameters - ---------- - ack : tuple[str, str] - The pins for the SCSI ACK tx and rx signals. - - atn : tuple[str, str] - The pins for the SCSI ATN tx and rx signals. - - bsy : tuple[str, str] - The pins for the SCSI BSY tx and rx signals. - - cd : tuple[str, str] - The pins for the SCSI CD tx and rx signals. - - io : tuple[str, str] - The pins for the SCSI IO tx and rx signals. - - msg : tuple[str, str] - The pins for the SCSI MSG tx and rx signals. - - sel : tuple[str, str] - The pins for the SCSI SEL tx and rx signals. - - req : tuple[str, str] - The pins for the SCSI REQ tx and rx signals. - - rst : tuple[str, str] - The pins for the SCSI RST tx and rx signals. - - d0 : tuple[str, str] - The pins for the SCSI data byte one tx and rx signals. - - dp0 : tuple[str, str] - The pins for the SCSI data byte one parity tx and rx signals. - - tp_en : str - The enable pin for the TP portion of the PHY. - - tx_en : str - The enable pin for the TX portion of the PHY. - - aa_en : str - The enable pin for the AA portion of the PHY. - - bsy_en : str - The enable pin for the BSY portion of the PHY. - - sel_en : str - The enable pin for the SEL portion of the PHY. - - mr_en : str - The enable pin for the MSG/REQ portion of the PHY. - - diff_sense : str - The SCSI bus DIFF_SENSE pin. - - Returns - ------- - :py:class:`torii.build.dsl.Resource` - The SCSI Connector Resource - - ''' - - io = [ - Subsignal('ack', *TransceiverPairs(*ack, assert_width = 1)), - Subsignal('atn', *TransceiverPairs(*atn, assert_width = 1)), - Subsignal('bsy', *TransceiverPairs(*bsy, assert_width = 1)), - Subsignal('cd', *TransceiverPairs(*cd, assert_width = 1)), - Subsignal('io', *TransceiverPairs(*io, assert_width = 1)), - Subsignal('msg', *TransceiverPairs(*msg, assert_width = 1)), - Subsignal('sel', *TransceiverPairs(*sel, assert_width = 1)), - Subsignal('req', *TransceiverPairs(*req, assert_width = 1)), - Subsignal('rst', *TransceiverPairs(*rst, assert_width = 1)), - Subsignal('d0', *TransceiverPairs(*d0, assert_width = 8)), - Subsignal('dp0', *TransceiverPairs(*dp0, assert_width = 1)), - Subsignal('tp_en', PinsN(tp_en, dir = 'o', assert_width = 1)), - Subsignal('tx_en', PinsN(tx_en, dir = 'o', assert_width = 1)), - Subsignal('aa_en', PinsN(aa_en, dir = 'o', assert_width = 1)), - Subsignal('bsy_en', PinsN(bsy_en, dir = 'o', assert_width = 1)), - Subsignal('sel_en', PinsN(sel_en, dir = 'o', assert_width = 1)), - Subsignal('mr_en', PinsN(mr_en, dir = 'o', assert_width = 1)), - Subsignal('diff_sense', Pins(diff_sense, dir = 'i', assert_width = 1)), - ] - - if d1 is not None: - assert dp1 is not None, 'Parity bit for d1 must be present' - io.append(Subsignal('d1', *TransceiverPairs(*d1, assert_width = 8))), - io.append(Subsignal('dp1', *TransceiverPairs(*dp1, assert_width = 1))), - - if scsi_id is not None: - assert led is not None - assert spindle is not None - assert rmt is not None - assert dlyd is not None - - io.append(Subsignal('id', *TransceiverPairs(*scsi_id, assert_width = 4))), - io.append(Subsignal('led', *TransceiverPairs(*led, assert_width = 1))), - io.append(Subsignal('spindle', *TransceiverPairs(*spindle, assert_width = 1))), - io.append(Subsignal('rmt', *TransceiverPairs(*rmt, assert_width = 1))), - io.append(Subsignal('dlyd', *TransceiverPairs(*dlyd, assert_width = 1))), - - if attrs is not None: - io.append(attrs) - - return Resource.family(*args, default_name = 'scsi_phy', ios = io) - - -def SCSIDifferentialResource(*args, **kwargs) -> SCSIConnectorResource: - ''' Constructs an explicitly differential :py:func:`SCSIConnectorResource` ''' - return SCSIConnectorResource(*args, diff = True, **kwargs) - -def SCSISingleEndedResource(*args, **kwargs) -> SCSIConnectorResource: - ''' Constructs an explicitly single-ended :py:func:`SCSIConnectorResource` ''' - return SCSIConnectorResource(*args, diff = False, **kwargs) diff --git a/squishy/gateware/platform/rev1.py b/squishy/gateware/platform/rev1.py index 9d181dfd..162c8b53 100644 --- a/squishy/gateware/platform/rev1.py +++ b/squishy/gateware/platform/rev1.py @@ -1,47 +1,124 @@ # SPDX-License-Identifier: BSD-3-Clause + +''' +This is the `Torii`_. platform definition for Squishy rev1 hardware. If you are for +some reason using Squishy rev1 as a general-purpose FPGA development board with Torii, +this is the platform you need to invoke. + +Important +------- +This platform is for specialized hardware, as such it can not be used with any other hardware +than it was designed for. This includes any popular FPGA development or evaluation boards. + +Note +---- +There are no official releases of the Squishy rev1 hardware for purchase, and building one +is not recommended due to the current hardware errata for the platform. + + +.. _Torii: https://torii.shmdn.link/ + +''' + +import logging as log +from pathlib import Path + from torii import * from torii.build import * +from torii.build.run import BuildProducts from torii.platform.vendor.lattice.ice40 import ICE40Platform from torii.platform.resources.memory import SPIFlashResources from torii.platform.resources.user import LEDResources from torii.platform.resources.interface import UARTResource -from ...core.flash import FlashGeometry -from ..core import ICE40ClockDomainGenerator -from ..bootloader.rev1 import Bootloader as iCE40Bootloader -from .resources import SCSIPhyResource -from .platform import SquishyPlatform +from . import SquishyPlatform +from ...core.flash import Geometry as FlashGeometry +from ...core.config import FlashConfig, ICE40PLLConfig -__doc__ = '''\ +__all__ = ( + 'SquishyRev1' +) -This is the torii platform definition for Squishy rev1 hardware, if you are using -Squishy rev1 as a generic FPGA development board, this is the platform you need to invoke. -Warning -------- -This platform is for specialized hardware and **must not** be used with any other -hardware other than the hardware it was designed for. This include any popular -development or eval boards. +class Rev1ClockDomainGenerator(Elaboratable): + ''' + Clock domain and PLL generator for Squishy rev1. -Note ----- -There are no official released of the Squishy rev1 hardware for purchase, you can build your -own, however it is recommended to start with the :py:class:`squishy.gateware.platform.rev2.SquishyRev2` hardware. + This module sets up two clock domains, ``usb`` and ``sync``. The ``usb`` + domain a 60MHz clock domain, and is fed from an external ULPI phy, where + as the ``sync`` domain is the primary core clock domain and set for 100MHz + and is fed from the global system input clock. + Attributes + ---------- + pll_locked : Signal + An active high signal indicating if the PLL is locked and stable. + + ''' + def __init__(self): + self.pll_locked = Signal() + + def elaborate(self, platform: 'SquishyRev1') -> Module: + m = Module() + + # The clock domains we want to have + m.domains.usb = ClockDomain() + m.domains.sync = ClockDomain() + + # Pull out the PLL config and define the new PLL clock signal + pll_cfg: ICE40PLLConfig = platform.pll_cfg + pll_sync_clk = Signal() + + # Set up the PLL + m.submodules.pll = Instance( + 'SB_PLL40_PAD', + i_PACKAGEPIN = platform.request(platform.default_clk, dir = 'i'), + i_RESETB = Const(1), + i_BYPASS = Const(0), + + o_PLLOUTGLOBAL = pll_sync_clk, + o_LOCK = self.pll_locked, + + p_FEEDBACK_PATH = 'SIMPLE', + p_PLLOUT_SELECT = 'GENCLK', + + + p_DIVR = pll_cfg.divr, + p_DIVF = pll_cfg.divf, + p_DIVQ = pll_cfg.divq, + p_FILTER_RANGE = pll_cfg.filter_range, + ) + + # Add a clocking constraint for the new PLL core clock + platform.add_clock_constraint(pll_sync_clk, pll_cfg.ofreq * 1e6) + + # Make sure we wiggle the domain on the clock + m.d.comb += [ + # Hold domain in reset until the PLL stabilizes + ResetSignal('sync').eq(~self.pll_locked), + + # Wiggle the clock + ClockSignal('sync').eq(pll_sync_clk), + ] + + return m -''' class SquishyRev1(SquishyPlatform, ICE40Platform): ''' - Squishy hardware Revision 1 + Squishy hardware, Revision 1. + + This `Torii`_. platform is for the first revision of the Squishy SCSI hardware. It + is based on the `Lattice iCE40-HX8K`_. and was primarily built to target SCSI-1 HVD + only. - This is the torii platform for the first revision of the Squishy hardware. - It is based around the `Lattice iCE40-HX8K `_ - in the BG121 footprint. + The hardware `design files`_. can be found in the hardware repository on `GitHub`_. under + the ``release/rev1`` tree. - The design files for this version of the hardware can be found - `in the git repo `_ under - the `rev1` tree. + .. _Torii: https://torii.shmdn.link/ + .. _Lattice iCE40-HX8K: https://www.latticesemi.com/iCE40 + .. _design files: https://github.com/squishy-scsi/hardware/tree/main/release/rev1 + .. _GitHub: https://github.com/squishy-scsi/hardware ''' @@ -50,31 +127,30 @@ class SquishyRev1(SquishyPlatform, ICE40Platform): default_clk = 'clk' toolchain = 'IceStorm' - revision = 1.0 + revision = (1, 0) - clock_domain_generator = ICE40ClockDomainGenerator - - pll_config = { - 'freq' : 1e8, - 'divr' : 2, - 'divf' : 49, - 'divq' : 3, - 'frange': 1, - } - - flash = { - 'geometry': FlashGeometry( + flash = FlashConfig( + geometry = FlashGeometry( size = 8388608, # 8MiB page_size = 256, erase_size = 4096, # 4KiB + slot_size = 262144, # 256KiB addr_width = 24 - ).init_slots(device = device), - 'commands': { + ), + commands = { 'erase': 0x20, } - } + ) + + pll_cfg = ICE40PLLConfig( + divr = 2, + divf = 49, + divq = 3, + filter_range = 1, + ofreq = 100 + ) - bootloader_module = iCE40Bootloader + clk_domain_generator = Rev1ClockDomainGenerator resources = [ Resource('clk', 0, @@ -108,20 +184,21 @@ class SquishyRev1(SquishyPlatform, ICE40Platform): Attrs(IO_STANDARD = 'SB_LVCMOS') ), - SCSIPhyResource(0, - ack = ('C11', 'B11'), atn = ('H11', 'H10'), bsy = ('E11', 'E10'), - cd = ('B5', 'A4' ), io = ('B3', 'A2' ), msg = ('A8', 'B9' ), - sel = ('B7', 'A6' ), req = ('B4', 'A3' ), rst = ('E9', 'D9' ), - d0 = ('J11 G11 F11 D11 A10 C8 C9 B8', 'J10 G10 F10 D10 A11 C7 A9 A7'), - dp0 = ('B6', 'A5' ), - - tp_en = 'A1', tx_en = 'K11', aa_en = 'G8', - bsy_en = 'G9', sel_en = 'F9', mr_en = 'E8', - - diff_sense = 'D7', - - attrs = Attrs(IO_STANDARD = 'SB_LVCMOS') - ), + # TODO(aki): This needs to be re-thought out + # SCSIPhyResource(0, + # ack = ('C11', 'B11'), atn = ('H11', 'H10'), bsy = ('E11', 'E10'), + # cd = ('B5', 'A4' ), io = ('B3', 'A2' ), msg = ('A8', 'B9' ), + # sel = ('B7', 'A6' ), req = ('B4', 'A3' ), rst = ('E9', 'D9' ), + # d0 = ('J11 G11 F11 D11 A10 C8 C9 B8', 'J10 G10 F10 D10 A11 C7 A9 A7'), + # dp0 = ('B6', 'A5' ), + # + # tp_en = 'A1', tx_en = 'K11', aa_en = 'G8', + # bsy_en = 'G9', sel_en = 'F9', mr_en = 'E8', + # + # diff_sense = 'D7', + # + # attrs = Attrs(IO_STANDARD = 'SB_LVCMOS') + # ), *LEDResources( pins = [ @@ -147,3 +224,111 @@ class SquishyRev1(SquishyPlatform, ICE40Platform): ] connectors = [] + + + def pack_artifact(self, artifact: bytes) -> bytes: + ''' + Pack bitstream/gateware into device artifact. + + On Squishy rev1 platforms, there is no additional processing needed + so this is effectively a no-op. + + Parameters + ---------- + artifact : bytes + The input data of the result of gateware elaboration, typically + the raw FPGA bitstream file. + + Returns + ------- + bytes + The input bytes from ``artifact`` + + ''' + + return artifact + + + def _build_slots(self, geometry: FlashGeometry) -> bytes: + ''' + Construct an iCE40 multi-boot viable flash image based on the platform flash topology. + + Parameters + ---------- + geometry : FlashGeometry + The target device flash geometry + + Returns + ------- + bytes + The resulting slot data. + ''' + + from ...core.bitstream import iCE40BitstreamSlots + + slot_data = bytearray(geometry.erase_size) + slots = iCE40BitstreamSlots(geometry).build() + + # Replace the slot data as appropriate + slot_data[0:len(slots)] = slots + # Pad the remaining + slot_data[len(slots):] = (0xFF for _ in range(geometry.erase_size - len(slots))) + + return bytes(slot_data) + + def build_image(self, name: str, build_dir: Path, boot_name: str, products: BuildProducts) -> Path: + ''' + Build multi-boot compatible flash image to provision onto the device. + + Parameters + ---------- + name : str + The name of the flash image to produce. + + build_dir : Path + Output directory for the finalized flash image. + + boot_name: str + The name of the bootloader in the build products + + products : BuildProducts + The resulting build products from the bootloader build. + + Returns + ------- + Path + The path to the resulting image file. + ''' + + build_path = (build_dir / name) + + log.debug(f'Building multi-boot flash image in \'{build_path}\'') + + # Construct the bootloader asset name + asset_name = boot_name + if not asset_name.endswith('.bin'): + asset_name += '.bin' + + with build_path.open('wb') as image: + slot_data = self._build_slots(self.flash.geometry) + + log.debug(f'Writing {len(slot_data)} bytes of slot data') + image.write(slot_data) + + log.debug(f'Writing bootloader \'{boot_name}\'') + image.write(products.get(asset_name, 'b')) + + # Pad the result so we hit full slot density + start = image.tell() + end = self.flash.geometry.partitions[1].start_addr + + pad_size = end - start + + log.debug(f'Padding bitstream with \'{pad_size}\' bytes') + for _ in range(pad_size): + image.write(b'\xFF') + + # Copy the bootloader entry pointer to the active slot + image.write(slot_data[32:64]) + + return build_path diff --git a/squishy/gateware/platform/rev2.py b/squishy/gateware/platform/rev2.py index d7035497..25bf6a54 100644 --- a/squishy/gateware/platform/rev2.py +++ b/squishy/gateware/platform/rev2.py @@ -1,45 +1,164 @@ # SPDX-License-Identifier: BSD-3-Clause + +''' +This is the `Torii`_. platform definition for Squishy rev2 hardware. If you are for +some reason using Squishy rev2 as a general-purpose FPGA development board with Torii, +this is the platform you need to invoke. + +Important +------- +This platform is for specialized hardware, as such it can not be used with any other hardware +than it was designed for. This includes any popular FPGA development or evaluation boards. + +Note +---- +There are no official releases of the Squishy rev2 hardware for purchase as it is currently +in the early engineering-validation-test phases, and will likely change drastically before +any are offered. + + +.. _Torii: https://torii.shmdn.link/ + +''' +import logging as log +from pathlib import Path + from torii import * from torii.build import * +from torii.build.run import BuildProducts from torii.platform.vendor.lattice.ecp5 import ECP5Platform from torii.platform.resources.memory import SDCardResources from torii.platform.resources.user import LEDResources from torii.platform.resources.interface import ULPIResource -from ...core.flash import FlashGeometry -from ..core import ECP5ClockDomainGenerator -from .platform import SquishyPlatform +from . import SquishyPlatform +from ...core.flash import Geometry as FlashGeometry +from ...core.config import ECP5PLLConfig, ECP5PLLOutput, FlashConfig -__doc__ = '''\ +__all__ = ( + 'SquishyRev2', +) -This is the torii platform definition for Squishy rev2 hardware, if you are using -Squishy rev2 as a generic FPGA development board, this is the platform you need to invoke. -Warning -------- -This platform is for specialized hardware and **must not** be used with any other -hardware other than the hardware it was designed for. This include any popular -development or eval boards. +class Rev2ClockDomainGenerator(Elaboratable): + ''' + Clock domain and PLL generator for Squishy rev2. -Note ----- -There are no official released of the Squishy rev2 hardware for purchase at the moment. You can -build your own, or keep an eye out for when the campaign goes live. + This module sets up 3 primary clock domains, ``sync``, ``usb``, and ``scsi``. The first + domain ``sync`` is the global core clock, the ``usb`` domain is a 60MHz domain originating + from the ULPI PHY. The final domain ``scsi`` is the SCSI PHY domain. -''' + + Attributes + ---------- + pll_locked : Signal + An active high signal indicating if the PLL is locked and stable. + ''' + + def __init__(self): + self.pll_locked = Signal() + + def elaborate(self, platform: 'SquishyRev2') -> Module: + m = Module() + + # Set up our domains + m.domains.usb = ClockDomain() + m.domains.sync = ClockDomain() + m.domains.scsi = ClockDomain() + + # The ECP5 PLL and clock output configs + # TODO(aki): We don't need them at the moment but cascaded PLLs might come in handy + pll_cfg: ECP5PLLConfig = platform.pll_cfg + + pll_sync_cfg = pll_cfg.clkp + # TODO(aki): Handle secondary, tertiary, and quaternary PLL outputs + + # The PLL output clocks + pll_sync_clk = Signal() + + m.submodules.pll = Instance( + 'EHXPLLL', + + i_CLKI = platform.request(platform.default_clk, dir = 'i'), + + o_CLKOP = pll_sync_clk, + i_CLKFB = pll_sync_clk, + i_ENCLKOP = Const(0), + o_LOCK = self.pll_locked, + + i_RST = Const(0), + i_STDBY = Const(0), + + i_PHASESEL0 = Const(0), + i_PHASESEL1 = Const(0), + i_PHASEDIR = Const(1), + i_PHASESTEP = Const(1), + i_PHASELOADREG = Const(1), + i_PLLWAKESYNC = Const(0), + + # Params + p_PLLRST_ENA = 'DISABLED', + p_INTFB_WAKE = 'DISABLED', + p_STDBY_ENABLE = 'DISABLED', + p_DPHASE_SOURCE = 'DISABLED', + p_OUTDIVIDER_MUXA = 'DIVA', + p_OUTDIVIDER_MUXB = 'DIVB', + p_OUTDIVIDER_MUXC = 'DIVC', + p_OUTDIVIDER_MUXD = 'DIVD', + p_FEEDBK_PATH = 'CLKOP', + + p_CLKI_DIV = pll_cfg.clki_div, + p_CLKFB_DIV = pll_cfg.clkfb_div, + + p_CLKOP_DIV = pll_sync_cfg.clk_div, + p_CLKOP_ENABLE = 'ENABLED', + p_CLKOP_CPHASE = Const(pll_sync_cfg.cphase), + p_CLKOP_FPHASE = Const(pll_sync_cfg.fphase), + + # Attributes for synth + a_FREQUENCY_PIN_CLKI = str(pll_cfg.ifreq), + a_FREQUENCY_PIN_CLKOP = str(pll_sync_cfg.ofreq), + a_ICP_CURRENT = '12', + a_LPF_RESISTOR = '8', + a_MFG_ENABLE_FILTEROPAMP = '1', + a_MFG_GMCREF_SEL = '2', + ) + + # Set up clock constraints + platform.add_clock_constraint(pll_sync_clk, pll_sync_cfg.ofreq * 1e6) + + # Hook up needed PLL outputs + m.d.comb += [ + # Hold domain in reset until the PLL stabilizes + ResetSignal('sync').eq(~self.pll_locked), + + # Wiggle the clock + ClockSignal('sync').eq(pll_sync_clk) + ] + + return m class SquishyRev2(SquishyPlatform, ECP5Platform): ''' - Squishy hardware Revision 2 + Squishy hardware, Revision 2. + + This `Torii`_. platform is for the first revision of the Squishy SCSI hardware. It + is based on the `Lattice ECP5-5G`_. Specifically the ``LFE5UM5G-45F`` and is built + to be as flexible as possible, as such it is split between the main board, the + SCSI PHY, and the various connectors boards. - This is the torii platform for the first revision of the Squishy hardware. - It is based around the `Lattice ECP5-5G LFE5UM5G-45F `_ - in the BG381 footprint. + The hardware `design files`_. can be found in the hardware repository on `GitHub`_. under + the ``release/rev2-evt`` tree. - The design files for this version of the hardware can be found - `in the git repo `_ under - the `boards/squishy` tree. + Warning + ------- + Squishy rev2 is currently in engineering-validation-test, and is unstable, the hardware + may change and new, possibly fatal errata may be found at any time. Use with caution. + .. _Lattice ECP5-5G: https://www.latticesemi.com/Products/FPGAandCPLD/ECP5 + .. _Torii: https://torii.shmdn.link/ + .. _design files: https://github.com/squishy-scsi/hardware/tree/main/release/rev2-evt + .. _GitHub: https://github.com/squishy-scsi/hardware ''' @@ -49,33 +168,39 @@ class SquishyRev2(SquishyPlatform, ECP5Platform): default_clk = 'clk' toolchain = 'Trellis' - revision = 2.0 - - clock_domain_generator = ECP5ClockDomainGenerator + revision = (2, 0) - # generated with `ecppll -i 100 -o 400 -f /dev/stdout` - pll_config = { - 'freq' : 4e8, - 'ifreq' : 100, - 'ofreq' : 400, - 'clki_div' : 1, - 'clkop_div': 1, - 'clkfb_div': 4, - } - - flash = { - 'geometry': FlashGeometry( + flash = FlashConfig( + geometry = FlashGeometry( size = 8388608, # 8MiB page_size = 256, erase_size = 4096, # 4KiB + slot_size = 2097152, # 2MiB addr_width = 24 - ).init_slots(device = device), - 'commands': { + ), + commands = { 'erase': 0x20, } - } + ) - bootloader_module = None + # generated with `ecppll -i 100 -o 400 -f /dev/stdout` + pll_cfg = ECP5PLLConfig( + ifreq = 100, + clki_div = 1, + clkfb_div = 4, + # Primary `sync` clock, 400 is too high but as a placeholder it works + clkp = ECP5PLLOutput( + ofreq = 400, + clk_div = 1, + cphase = 0, + fphase = 0, + ) + ) + + clk_domain_generator = Rev2ClockDomainGenerator + + # Set DFU alt-mode slot for the ephemeral endpoint + ephemeral_slot = 3 resources = [ Resource('clk', 0, @@ -137,10 +262,10 @@ class SquishyRev2(SquishyPlatform, ECP5Platform): Attrs(IO_TYPE = 'LVCMOS18', SLEWRATE = 'FAST') ), - ULPIResource('usb2', 0, + ULPIResource('ulpi', 0, # D0 D1 D2 D3 D4 D5 D6 D7 data = 'R18 R20 P19 P20 N20 N19 M20 M19', - clk = 'P18', clk_dir = 'i', + clk = 'P18', clk_dir = 'i', # NOTE(aki): This /not technically/ a clock input pin, oops dir = 'T19', nxt = 'T20', stp = 'U20', @@ -204,7 +329,52 @@ class SquishyRev2(SquishyPlatform, ECP5Platform): ), ] + connectors = [] - connectors = [ + def pack_artifact(self, artifact: bytes) -> bytes: + ''' + Pack bitstream/gateware into device artifact. - ] + Parameters + ---------- + artifact : bytes + The input data of the result of gateware elaboration, typically + the raw FPGA bitstream file. + + Returns + ------- + bytes + The resulting packed artifact for DFU upload. + + ''' + + log.warning('TODO: pack_artifact() for rev2') + return artifact + + + def build_image(self, name: str, build_dir: Path, boot_name: str, products: BuildProducts) -> Path: + ''' + Build multi-boot compatible flash image to provision onto the device. + + Parameters + ---------- + name : str + The name of the flash image to produce. + + build_dir : Path + Output directory for the finalized flash image. + + boot_name: str + The name of the bootloader in the build products + + products : BuildProducts + The resulting build products from the bootloader build. + + Returns + ------- + Path + The path to the resulting image file. + ''' + + log.warning('TODO: build_image() for rev2') + return build_dir diff --git a/squishy/gateware/quirks/__init__.py b/squishy/gateware/quirks/__init__.py deleted file mode 100644 index b9c7be03..00000000 --- a/squishy/gateware/quirks/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -__doc__ = '''\ -This module contains implementations of various non-standard or broken implementations -of mechanisms or protocols that Squishy uses. - -Currently the only quirks are for USB in the :py:mod:`squishy.quirks.usb` module, which -contains USB descriptors specific to Windows to allow for full DFU compatibility. - -''' - -__all__ = ( - - -) diff --git a/squishy/gateware/quirks/usb/__init__.py b/squishy/gateware/quirks/usb/__init__.py deleted file mode 100644 index 8bfd7dcb..00000000 --- a/squishy/gateware/quirks/usb/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -__doc__ = '''\ -This module contains USB quirks for various platforms. The lame duck is currently only -Windows which needs special USB descriptors, and are in the :py:mod:`squishy.quirks.usb.windows` -module. - -''' - -__all__ = ( - - -) diff --git a/squishy/gateware/scsi/common/__init__.py b/squishy/gateware/scsi/common/__init__.py deleted file mode 100644 index 99f4c527..00000000 --- a/squishy/gateware/scsi/common/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -__all__ = ( -) - -__doc__ = '''\ - -The following table lists the timing requirements for each SCSI version. - -+-----------------------+--------------+--------------+--------------+ -| Name | SCSI1 | SCSI2 | SCSI3 | -+=======================+==============+==============+==============+ -| Arbitration | 2.2us | 2.4us | 2.4us | -+-----------------------+--------------+--------------+--------------+ -| Assertion | 90ns | 90ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Bus Clear | 800ns | 800ns | 800ns | -+-----------------------+--------------+--------------+--------------+ -| Bus Free | 800ns | 800ns | 800ns | -+-----------------------+--------------+--------------+--------------+ -| Bus Set | 1.8us | 1.8us | 1.6us | -+-----------------------+--------------+--------------+--------------+ -| Bus Settle | 400ns | 400ns | 400ns | -+-----------------------+--------------+--------------+--------------+ -| Cable Skew | 10ns | 10ns | 4ns | -+-----------------------+--------------+--------------+--------------+ -| Data Release | 400ns | 400ns | 400ns | -+-----------------------+--------------+--------------+--------------+ -| Deskew | 45ns | 45ns | 45ns | -+-----------------------+--------------+--------------+--------------+ -| Hold Time | 45ns | 45ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Negation | 90ns | 90ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Reset Hold | 25us | 25us | 25us | -+-----------------------+--------------+--------------+--------------+ -| Selection Abort | 200us | 200us | 200us | -+-----------------------+--------------+--------------+--------------+ -| Selection Timeout | 250ms | 250ms | 250ms | -+-----------------------+--------------+--------------+--------------+ -| Disconnect | Unspecified | 200us | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Power to Selection | Unspecified | 10s | 10s | -+-----------------------+--------------+--------------+--------------+ -| Reset to Selection | Unspecified | 250ms | 250ms | -+-----------------------+--------------+--------------+--------------+ -| Fast Assert | Unspecified | 30ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Fast Cable Skew | Unspecified | 5ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Fast Deskew | Unspecified | 20ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Fast Hold | Unspecified | 10ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ -| Fast Negation | Unspecified | 30ns | Unspecified | -+-----------------------+--------------+--------------+--------------+ - - -''' diff --git a/squishy/gateware/scsi/device.py b/squishy/gateware/scsi/device.py deleted file mode 100644 index caa6b761..00000000 --- a/squishy/gateware/scsi/device.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from .scsi1 import Device as SCSI1Device -from .scsi2 import Device as SCSI2Device -from .scsi3 import Device as SCSI3Device - -__all__ = ( - 'SCSI1Device', - 'SCSI2Device', - 'SCSI3Device', -) - -__doc__ = '''\ - -This submodule provides wrapper methods to instantiate SCSI Device elaboratables -for :py:mod:`.scsi1`, :py:mod:`.scsi2`, and :py:mod:`.scsi3`. For more details -on the differences between them and the inner workings, see the documentation for -each particular SCSI version in its module. - -''' diff --git a/squishy/gateware/scsi/initiator.py b/squishy/gateware/scsi/initiator.py deleted file mode 100644 index 4d78e587..00000000 --- a/squishy/gateware/scsi/initiator.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from .scsi1 import Initiator as SCSI1Initiator -from .scsi2 import Initiator as SCSI2Initiator -from .scsi3 import Initiator as SCSI3Initiator - -__all__ = ( - 'SCSI1Initiator', - 'SCSI2Initiator', - 'SCSI3Initiator', -) - -__doc__ = '''\ - -This submodule provides wrapper methods to instantiate SCSI Initiator elaboratables -for :py:mod:`.scsi1`, :py:mod:`.scsi2`, and :py:mod:`.scsi3`. For more details -on the differences between them and the inner workings, see the documentation for -each particular SCSI version in its module. - -''' diff --git a/squishy/gateware/scsi/scsi1/__init__.py b/squishy/gateware/scsi/scsi1/__init__.py deleted file mode 100644 index 2bd72344..00000000 --- a/squishy/gateware/scsi/scsi1/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from torii import Elaboratable, Module - -__all__ = ( - 'SCSI1', - - 'Device', - 'Initiator', -) - -__doc__ = '''\ - -''' - -class SCSI1(Elaboratable): - ''' - SCSI 1 Elaboratable - - This elaboratable represents an interface for interacting with SCSI-1 compliant buses. - - Parameters - ---------- - config : dict - The configuration for this Elaboratable, including SCSI VID and DID. - - ''' - - def __init__(self, *, config: dict) -> None: - self.config = config - - def elaborate(self, platform) -> Module: - m = Module() - - # TODO: timers et. al. :nya_flop: - - - # SCSI-1 State Machine Overview - # ┌────────────────────────┐ - # │ Reset │ - # └────────────────────────┘ - # │ - # │ - # ▼ - # ┌──────────────────────────────────┐ - # ┌▶ │ Bus Free │ - # │ └──────────────────────────────────┘ - # │ │ ▲ ▲ - # │ │ │ │ - # │ ▼ │ │ - # │ ┌────────────────────────┐ │ │ - # │ │ Arbitration │ ─┘ │ - # │ └────────────────────────┘ │ - # │ │ │ - # │ │ │ - # │ ▼ │ - # │ ┌────────────────────────┐ │ - # │ │ Selection │ ──────┘ - # │ └────────────────────────┘ - # │ │ - # │ │ - # │ ▼ - # │ ┌────────────────────────┐ - # └─ │ Command, Message, Data │ - # └────────────────────────┘ - with m.FSM(reset = 'rst'): - with m.State('rst'): - - m.next = 'bus-free' - - with m.State('bus-free'): - - m.next = 'bus-free' - - with m.State('arbitration'): - - m.next = 'bus-free' - - with m.State('selection'): - - m.next = 'bus-free' - - with m.State('re-selection'): - - m.next = 'bus-free' - - with m.State('data-in'): - - m.next = 'bus-free' - - with m.State('data_out'): - - m.next = 'bus-free' - - with m.State('command'): - - m.next = 'bus-free' - - with m.State('status'): - - m.next = 'bus-free' - - with m.State('message-in'): - - m.next = 'bus-free' - - with m.State('message-out'): - - m.next = 'bus-free' - - return m - -def Device(*, config: dict) -> SCSI1: - ''' Create a SCSI-1 Device Elaboratable ''' - return SCSI1({'is_device': True, **config}) - -def Initiator(*, config: dict) -> SCSI1: - ''' Create a SCSI-1 Initiator Elaboratable ''' - return SCSI1({'is_device': False, **config}) - -# -------------- # - -# from ....core.test import SquishyGatewareTestCase, sim_test -# -# class SCSI1Tests(SquishyGatewareTestCase): -# dut = SCSI1 -# dut_args = { -# 'is_device': True, -# 'arbitrating': True, -# 'config': None -# } -# -# @sim_test -# def test_uwu(self): -# yield from self.step(30) diff --git a/squishy/gateware/scsi/scsi2/__init__.py b/squishy/gateware/scsi/scsi2/__init__.py deleted file mode 100644 index 72ed42f2..00000000 --- a/squishy/gateware/scsi/scsi2/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from torii import Elaboratable, Module - -__all__ = ( - 'SCSI2', - - 'Device', - 'Initiator', -) - -__doc__ = '''\ - -''' - -class SCSI2(Elaboratable): - ''' - SCSI 2 Elaboratable - - This elaboratable represents an interface for interacting with SCSI-2 compliant buses. - - Parameters - ---------- - config : dict - The configuration for this Elaboratable, including SCSI VID and DID. - - ''' - - def __init__(self, *, config: dict) -> None: - self.config = config - - def elaborate(self, platform) -> Module: - m = Module() - - - return m - -def Device(*, config: dict) -> SCSI2: - ''' Create a SCSI-2 Device Elaboratable ''' - return SCSI2({'is_device': True, **config}) - -def Initiator(*, config: dict) -> SCSI2: - ''' Create a SCSI-2 Initiator Elaboratable ''' - return SCSI2({'is_device': False, **config}) diff --git a/squishy/gateware/scsi/scsi3/__init__.py b/squishy/gateware/scsi/scsi3/__init__.py deleted file mode 100644 index f666a9ee..00000000 --- a/squishy/gateware/scsi/scsi3/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from torii import Elaboratable, Module - -__all__ = ( - 'SCSI3', - - 'Device', - 'Initiator', -) - -__doc__ = '''\ - -''' - -class SCSI3(Elaboratable): - ''' - SCSI 3 Elaboratable - - This elaboratable represents an interface for interacting with SCSI-3 compliant buses. - - Parameters - ---------- - config : dict - The configuration for this Elaboratable, including SCSI VID and DID. - - ''' - - def __init__(self, *, config: dict) -> None: - self.config = config - - def elaborate(self, platform) -> Module: - m = Module() - - - return m - -def Device(*, config: dict) -> SCSI3: - ''' Create a SCSI-3 Device Elaboratable ''' - return SCSI3({'is_device': True, **config}) - -def Initiator(*, config: dict) -> SCSI3: - ''' Create a SCSI-3 Initiator Elaboratable ''' - return SCSI3({'is_device': False, **config}) diff --git a/squishy/gateware/usb/__init__.py b/squishy/gateware/usb/__init__.py deleted file mode 100644 index 77261c82..00000000 --- a/squishy/gateware/usb/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from .rev1 import Rev1USB -from .rev2 import Rev2USB - -__doc__ = '''\ - -''' - -__all__ = ( - 'Rev1USB', - 'Rev2USB' -) diff --git a/squishy/gateware/usb/dfu.py b/squishy/gateware/usb/dfu.py deleted file mode 100644 index fe8a84d5..00000000 --- a/squishy/gateware/usb/dfu.py +++ /dev/null @@ -1,213 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from enum import ( - IntEnum, unique -) - -from torii import ( - Module, Signal, Cat, Instance -) -from torii.hdl.ast import ( - Operator -) - -from usb_construct.types import ( - USBRequestType, USBRequestRecipient, - USBStandardRequests -) -from usb_construct.types.descriptors.dfu import ( - DFURequests -) - -from sol_usb.gateware.usb.usb2.request import ( - USBRequestHandler, SetupPacket -) -from sol_usb.gateware.usb.stream import ( - USBInStreamInterface -) -from sol_usb.gateware.stream.generator import ( - StreamSerializer -) - - -__doc__ = '''\ - DFU Stub -''' - -__all__ = ( - 'DFURequestHandler', -) - - -@unique -class DFUState(IntEnum): - APP_IDLE = 0 - -@unique -class DFUStatus(IntEnum): - OKAY = 0 - -class DFURequestHandler(USBRequestHandler): - ''' ''' - - def __init__(self, configuration_num: int, interface_num: int): - super().__init__() - self._configuration = configuration_num - self._interface_num = interface_num - self._trigger_reboot = Signal(name = 'trigger_reboot') - self._slot_select = Signal(2, name = 'slot_select') - - def elaborate(self, platform) -> Module: - m = Module() - - interface = self.interface - setup: SetupPacket = interface.setup - - m.submodules.transmitter = transmitter = StreamSerializer( - data_length = 6, domain = 'usb', stream_type = USBInStreamInterface, max_length_width = 3 - ) - - trigger_reboot = self._trigger_reboot - slot_select = self._slot_select - - with m.FSM(domain = 'usb', name = 'dfu'): - with m.State('IDLE'): - with m.If(setup.received & self.handler_condition(setup)): - with m.If(setup.type == USBRequestType.CLASS): - with m.Switch(setup.request): - with m.Case(DFURequests.DETACH): - m.next = 'HANDLE_DETACH' - with m.Case(DFURequests.GET_STATUS): - m.next = 'HANDLE_GET_STATUS' - with m.Case(DFURequests.GET_STATE): - m.next = 'HANDLE_GET_STATE' - with m.Default(): - m.next = 'UNHANDLED' - with m.Elif(setup.type == USBRequestType.STANDARD): - with m.Switch(setup.request): - with m.Case(USBStandardRequests.GET_INTERFACE): - m.next = 'GET_INTERFACE' - with m.Case(USBStandardRequests.SET_INTERFACE): - m.next = 'SET_INTERFACE' - with m.Default(): - m.next = 'UNHANDLED' - - with m.State('HANDLE_DETACH'): - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp(), - ] - with m.If(interface.handshakes_in.ack): - m.d.usb += [ - trigger_reboot.eq(1), - ] - - with m.State('HANDLE_GET_STATUS'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(6), - transmitter.data[0].eq(DFUStatus.OKAY), - Cat(transmitter.data[1:4]).eq(0), - transmitter.data[4].eq(DFUState.APP_IDLE), - transmitter.data[5].eq(0), - ] - - with m.If(interface.data_requested): - with m.If(setup.length == 6): - m.d.comb += [ - transmitter.start.eq(1) - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - m.next = 'IDLE' - - with m.State('HANDLE_GET_STATE'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(1), - transmitter.data[0].eq(DFUState.APP_IDLE), - ] - - with m.If(interface.data_requested): - with m.If(setup.length == 1): - m.d.comb += [ - transmitter.start.eq(1), - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - m.next = 'IDLE' - - with m.State('GET_INTERFACE'): - m.d.comb += [ - transmitter.stream.connect(interface.tx), - transmitter.max_length.eq(1), - transmitter.data[0].eq(0), - ] - with m.If(self.interface.data_requested): - with m.If(setup.length == 1): - m.d.comb += [ - transmitter.start.eq(1), - ] - with m.Else(): - m.d.comb += [ - interface.handshakes_out.stall.eq(1) - ] - m.next = 'IDLE' - - with m.If(interface.status_requested): - m.d.comb += [ - interface.handshakes_out.ack.eq(1), - ] - m.next = 'IDLE' - - with m.State('SET_INTERFACE'): - with m.If(interface.status_requested): - m.d.comb += [ - self.send_zlp(), - ] - with m.If(interface.handshakes_in.ack): - m.next = 'IDLE' - - with m.State('UNHANDLED'): - with m.If(interface.data_requested | interface.status_requested): - m.d.comb += [ - interface.handshakes_out.stall.eq(1), - ] - m.next = 'IDLE' - - m.submodules += Instance( - 'SB_WARMBOOT', - i_BOOT = trigger_reboot, - i_S0 = slot_select[0], - i_S1 = slot_select[1], - ) - - m.d.comb += [ - slot_select.eq(0b00), - ] - - return m - - def handler_condition(self, setup: SetupPacket) -> Operator: - return ( - (self.interface.active_config == self._configuration) & - ((setup.type == USBRequestType.CLASS) | (setup.type == USBRequestType.STANDARD)) & - (setup.recipient == USBRequestRecipient.INTERFACE) & - (setup.index == self._interface_num) - ) diff --git a/squishy/gateware/usb/rev1.py b/squishy/gateware/usb/rev1.py deleted file mode 100644 index 0bcac2f6..00000000 --- a/squishy/gateware/usb/rev1.py +++ /dev/null @@ -1,195 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause -from typing import ( - Any, Iterable, Optional, Callable, Union -) - -from torii import ( - Elaboratable, Module, - ResetSignal, Cat -) -from torii.hdl.ast import ( - Operator -) - -from sol_usb.usb2 import ( - USBDevice -) - - -from sol_usb.gateware.usb.usb2.request import ( - StallOnlyRequestHandler, USBRequestHandler, - SetupPacket -) - -from usb_construct.types import ( - LanguageIDs, USBRequestType -) - -from usb_construct.types.descriptors.dfu import ( - DFUWillDetach, DFUManifestationTolerant, - DFUCanUpload, DFUCanDownload -) -from usb_construct.contextmgrs.descriptors.dfu import ( - FunctionalDescriptor -) - -from usb_construct.emitters.descriptors.standard import ( - DeviceDescriptorCollection, DeviceClassCodes, InterfaceClassCodes, - ApplicationSubclassCodes, DFUProtocolCodes -) - -from usb_construct.emitters.descriptors.microsoft import ( - PlatformDescriptorCollection -) -from usb_construct.contextmgrs.descriptors.microsoft import ( - PlatformDescriptor -) - -from .dfu import ( - DFURequestHandler -) -from ..quirks.usb.windows import ( - WindowsRequestHandler -) - -__doc__ = '''\ - -''' - -__all__ = ( - 'Rev1USB', -) - - -class Rev1USB(Elaboratable): - ''' - SOL based USB ULPI Interface - - Warning - ------- - Currently this is a USB 2.0 only interface, and is not able to interact with - the hardware on Squishy rev2, this is to be fixed in the future. - - - ''' - - def __init__(self, *, - config: dict[str, Any], - applet_desc_builder: Callable[..., None] - ) -> None: - self.config = config - self.applet_desc_builder = applet_desc_builder - self.request_handlers: list[USBRequestHandler] = list() - self.endpoints = list() - - self.dev: Optional[USBDevice] = None - - - def init_descriptors(self) -> DeviceDescriptorCollection: - descriptors = DeviceDescriptorCollection() - - with descriptors.DeviceDescriptor() as dev_desc: - dev_desc.bcdDevice = 1.01 - dev_desc.bcdUSB = 2.01 - dev_desc.bDeviceClass = DeviceClassCodes.INTERFACE - dev_desc.bDeviceSubclass = 0x00 - dev_desc.bDeviceProtocol = 0x00 - dev_desc.idVendor = self.config['vid'] - dev_desc.idProduct = self.config['pid'] - dev_desc.iManufacturer = self.config['manufacturer'] - dev_desc.iProduct = self.config['product'] - dev_desc.iSerialNumber = self.config['serial_number'] - dev_desc.bNumConfigurations = self.applet_desc_builder(descriptors) + 1 - - with descriptors.ConfigurationDescriptor() as cfg_desc: - cfg_desc.bConfigurationValue = 1 - cfg_desc.iConfiguration = 'Squishy' - cfg_desc.bmAttributes = 0x80 - cfg_desc.bMaxPower = 250 - - with cfg_desc.InterfaceDescriptor() as iface_desc: - iface_desc.bInterfaceNumber = 0 - iface_desc.bAlternateSetting = 0 - iface_desc.bInterfaceClass = InterfaceClassCodes.APPLICATION - iface_desc.bInterfaceSubclass = ApplicationSubclassCodes.DFU - iface_desc.bInterfaceProtocol = DFUProtocolCodes.APPLICATION - iface_desc.iInterface = 'Squishy DFU' - - with FunctionalDescriptor(iface_desc) as func_desc: - func_desc.bmAttributes = ( - DFUWillDetach.YES | DFUManifestationTolerant.NO | - DFUCanUpload.NO | DFUCanDownload.YES - ) - func_desc.wDetachTimeOut = 1000 - func_desc.wTransferSize = 4096 - - # Thanks Microsoft:tm: /s - platform_desc = PlatformDescriptorCollection() - with descriptors.BOSDescriptor() as bos_desc: - with PlatformDescriptor(bos_desc, platform_collection = platform_desc) as plat_desc: - with plat_desc.DescriptorSetInformation() as desc_set_info: - desc_set_info.bMS_VendorCode = 1 - - with desc_set_info.SetHeaderDescriptor() as set_header: - with set_header.SubsetHeaderConfiguration() as subset_cfg: - subset_cfg.bConfigurationValue = 1 - - with subset_cfg.SubsetHeaderFunction() as subset_func: - subset_func.bFirstInterface = 0 - - with subset_func.FeatureCompatibleID() as compat_id: - compat_id.CompatibleID = 'WINUSB' - compat_id.SubCompatibleID = '' - - descriptors.add_language_descriptor((LanguageIDs.ENGLISH_US, )) - - return descriptors, platform_desc - - def add_request_handlers(self, request_handlers: Union[USBRequestHandler, Iterable[USBRequestHandler]]) -> None: - if isinstance(request_handlers, USBRequestHandler): - self.request_handlers.append(request_handlers) - else: - self.request_handlers.extend(request_handlers) - - def elaborate(self, platform) -> Module: - m = Module() - - ulpi = platform.request('ulpi', 0) - - m.submodules.dev = self.dev = USBDevice(bus = ulpi, handle_clocking = True) - - descriptors, platform_desc = self.init_descriptors() - - ep0 = self.dev.add_standard_control_endpoint( - descriptors - ) - - dfu_handler = DFURequestHandler(configuration_num = 1, interface_num = 0) - win_handler = WindowsRequestHandler(platform_desc) - - self.add_request_handlers((dfu_handler, win_handler)) - - def stall_condition(setup: SetupPacket) -> Operator: - return ~( - (setup.type == USBRequestType.STANDARD) | - Cat( - handler.handler_condition(setup) for handler in self.request_handlers - ).any() - ) - - - for hndlr in self.request_handlers: - ep0.add_request_handler(hndlr) - - ep0.add_request_handler( - StallOnlyRequestHandler(stall_condition = stall_condition) - ) - - m.d.comb += [ - self.dev.connect.eq(1), - self.dev.low_speed_only.eq(0), - self.dev.full_speed_only.eq(0), - ResetSignal('usb').eq(0), - ] - - return m diff --git a/squishy/gateware/usb/rev2.py b/squishy/gateware/usb/rev2.py deleted file mode 100644 index c8aa88b7..00000000 --- a/squishy/gateware/usb/rev2.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from typing import Any - -from torii import Elaboratable, Module - - -__doc__ = '''\ - -''' - -__all__ = ( - 'Rev2USB', -) - - -class Rev2USB(Elaboratable): - def __init__(self, *, config: dict[str, Any]) -> None: - self.config = config - - def elaborate(self, platform) -> Module: - m = Module() - - - return m diff --git a/squishy/paths.py b/squishy/paths.py new file mode 100644 index 00000000..0d257711 --- /dev/null +++ b/squishy/paths.py @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' +This module is just to declare the path constants for Squishy. + +These generally follows the XDG Base Directory Specification or any +other appropriate places depending on the OS thanks to platformdirs. + + +The following base directories are defined: + +* ``SQUISHY_CACHE`` - Used for bitstream builds and any needed cached info that can be ignored in backcups +* ``SQUISHY_DATA`` - Used for user-defined or third-party external applets and any other runtime deps +* ``SQUISHY_CONFIG`` - Used for any host-side configuration and/or settings for Squishy and related + +Within the ``SQUISHY_CACHE`` directory there are two sub-directories: + +* ``SQUISHY_APPLET_CACHE`` - The built-gateware cache directory, see the cache mechanism for more details +* ``SQUISHY_BUILD_DIR`` - The last-built/in-progress builds for Squishy gateware/bootloader bitstreams. + +Both of these can be safely deleted with no side-effects other than every applet/bootloader build hitting a +cache miss after when first ran/built. + +Within ``SQUISHY_DATA`` there is one directory, that being `applets`, it is used for out-of-tree and user +defined applets. + +''' + +__all__ = ( + # Root directories + 'SQUISHY_CACHE', + 'SQUISHY_DATA', + 'SQUISHY_CONFIG', + # Cache Subdirs/Files + 'SQUISHY_APPLET_CACHE', + 'SQUISHY_BUILD_DIR', + # Data Subdirs/Files + 'SQUISHY_APPLETS', + # Config Subdirs/Files + 'SQUISHY_SETTINGS', + # Helpers + 'initialize_dirs', +) + +from platformdirs import user_data_path, user_config_path, user_cache_path + +# Squishy-specific base directories +SQUISHY_CACHE = user_cache_path('squishy', False) +SQUISHY_DATA = user_data_path('squishy', False) +SQUISHY_CONFIG = user_config_path('squishy', False) + +# SQUISHY_CACHE subdirectories/files +SQUISHY_APPLET_CACHE = (SQUISHY_CACHE / 'applets') +''' Squishy built applet gateware cache (``$SQUISHY_CACHE/applets``) ''' +SQUISHY_BUILD_DIR = (SQUISHY_CACHE / 'build' ) +''' Squishy gateware/bootloader build directory (``$SQUISHY_CACHE/build``) ''' +# SQUISHY_DATA subdirectories/files +SQUISHY_APPLETS = (SQUISHY_DATA / 'applets') +''' Squishy out-of-tree/third-party applets (``$SQUISHY_DATA/applets``) ''' + +# SQUISHY_CONFIG subdirectories/files +SQUISHY_SETTINGS = (SQUISHY_CONFIG / 'config.json') +''' Squishy settings file (``$SQUISHY_CONFIG/config.json``) ''' + +def initialize_dirs() -> None: + ''' + Initialize Squishy application directories. + ''' + _dirs = ( + # Root directories + SQUISHY_CACHE, + SQUISHY_DATA, + SQUISHY_CONFIG, + # Cache Subdirs + SQUISHY_APPLET_CACHE, + SQUISHY_BUILD_DIR, + # Data Subdirs + SQUISHY_APPLETS, + ) + + # TODO(aki): This is likely not very performant, oops + for directory in _dirs: + directory.mkdir(parents = True, exist_ok = True) diff --git a/squishy/scsi/__init__.py b/squishy/scsi/__init__.py index 56295175..fa67755b 100644 --- a/squishy/scsi/__init__.py +++ b/squishy/scsi/__init__.py @@ -1,5 +1,14 @@ # SPDX-License-Identifier: BSD-3-Clause +''' +.. todo:: Refine this section + +The Squishy Python library defines all the machinery needed to consume and emit +SCSI messages, as well as helpers for dealing with SCSI devices and SCSI traffic. + +''' + + from . import messages from . import commands @@ -7,12 +16,3 @@ 'messages', 'commands', ) - -__doc__ = '''\ - -.. todo:: Refine this section - -The Squishy Python library defines all the machinery needed to consume and emit -SCSI messages, as well as helpers for dealing with SCSI devices and SCSI traffic. - -''' diff --git a/squishy/scsi/command.py b/squishy/scsi/command.py index 2a5edf19..ac383e7a 100644 --- a/squishy/scsi/command.py +++ b/squishy/scsi/command.py @@ -1,5 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause +''' + + +''' + from enum import IntEnum, unique from itertools import takewhile from typing import Any @@ -25,9 +30,6 @@ 'CommandEmitter', ) -__doc__ = '''\ - -''' @unique class GroupCode(IntEnum): @@ -828,6 +830,7 @@ class SCSICommand12(SCSICommand): def __init__(self, opcode: int, *subcons, **subconmskw) -> None: super().__init__(opcode, GroupCode.GROUP5, *subcons, **subconmskw) +# TODO(aki): We should probably see if we can try to type this class CommandEmitter: ''' Creates an emitter based on the specified SCSI command. diff --git a/squishy/scsi/commands/__init__.py b/squishy/scsi/commands/__init__.py index 1a112744..1dea246e 100644 --- a/squishy/scsi/commands/__init__.py +++ b/squishy/scsi/commands/__init__.py @@ -1,5 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause +''' + +''' + from . import common from . import direct from . import sequential @@ -17,6 +21,3 @@ 'worm', 'ro_direct', ) - -__doc__ = '''\ -''' diff --git a/squishy/scsi/commands/common.py b/squishy/scsi/commands/common.py index 3bc634ab..04b3c1de 100644 --- a/squishy/scsi/commands/common.py +++ b/squishy/scsi/commands/common.py @@ -1,15 +1,12 @@ # SPDX-License-Identifier: BSD-3-Clause -from ..command import ( - SCSICommand6, SCSICommand10, - SCSICommandField -) - -__doc__ = ''' +''' This module contains common commands, that other device classes can support. ''' +from ..command import SCSICommand6, SCSICommand10, SCSICommandField + __all__ = ( 'TestUnitReady', 'RequestSense', diff --git a/squishy/scsi/commands/direct.py b/squishy/scsi/commands/direct.py index 3313f2b3..75e3cbc9 100644 --- a/squishy/scsi/commands/direct.py +++ b/squishy/scsi/commands/direct.py @@ -1,11 +1,13 @@ # SPDX-License-Identifier: BSD-3-Clause -from construct import * -__doc__ = ''' + +''' This module defines the commands that are specific to direct access devices. ''' +from construct import * + __all__ = ( 'rezero_unit', 'format_unit', diff --git a/squishy/scsi/commands/printer.py b/squishy/scsi/commands/printer.py index 64e8a5c2..ef4342ba 100644 --- a/squishy/scsi/commands/printer.py +++ b/squishy/scsi/commands/printer.py @@ -1,10 +1,11 @@ # SPDX-License-Identifier: BSD-3-Clause -from construct import * -__doc__ = ''' +''' This module defines the commands that are specific to printers ''' +from construct import * + __all__ = ( 'format_printer', 'print_cmd', diff --git a/squishy/scsi/commands/processor.py b/squishy/scsi/commands/processor.py index d531eb51..f74f2749 100644 --- a/squishy/scsi/commands/processor.py +++ b/squishy/scsi/commands/processor.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = ''' +''' This module defines the commands that are specific to processors. ''' diff --git a/squishy/scsi/commands/ro_direct.py b/squishy/scsi/commands/ro_direct.py index 90c228fd..16029d5c 100644 --- a/squishy/scsi/commands/ro_direct.py +++ b/squishy/scsi/commands/ro_direct.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = ''' +''' This module defines the commands that are specific to read-only direct access devices. ''' diff --git a/squishy/scsi/commands/sequential.py b/squishy/scsi/commands/sequential.py index 3ce5a74f..f87a7fd5 100644 --- a/squishy/scsi/commands/sequential.py +++ b/squishy/scsi/commands/sequential.py @@ -1,11 +1,12 @@ # SPDX-License-Identifier: BSD-3-Clause -from construct import * -__doc__ = ''' +''' This module defines the commands that are specific to sequential access devices. ''' +from construct import * + __all__ = ( 'rewind', 'read_block_limits', diff --git a/squishy/scsi/commands/worm.py b/squishy/scsi/commands/worm.py index e6495ca3..dab0c493 100644 --- a/squishy/scsi/commands/worm.py +++ b/squishy/scsi/commands/worm.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause - -__doc__ = ''' +''' This module defines the commands that are specific to WORM devices. ''' diff --git a/squishy/scsi/common.py b/squishy/scsi/common.py index fbdadee0..d3d11076 100644 --- a/squishy/scsi/common.py +++ b/squishy/scsi/common.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause -from enum import ( - auto, unique, IntEnum -) +''' + +''' -from typing import Union +from enum import auto, unique, IntEnum __all__ = ( 'SCSIInterface', @@ -13,10 +13,6 @@ 'SCSI_BUSSES', ) -__doc__ = '''\ -''' - - @unique class SCSIStandard(IntEnum): ''' The SCSI Standard ''' @@ -73,11 +69,13 @@ class SCSIClockMode(IntEnum): DDR = auto() ''' Double Data Rate clock ''' +# TODO(aki): This should be cleaned up into more sane types/objects + SCSIBusSpeed = tuple[float, SCSIClockMode] ''' The tuple of speed in MHz and clock mode (SDR vs DDR) ''' SCSIBusElectrical = tuple[SCSIElectrical, ...] ''' The rough electrical characteristics of the SCSI Bus ''' -SCSIBusDefinition = dict[str, Union[SCSIBusElectrical, int, SCSIBusSpeed]] +SCSIBusDefinition = dict[str, SCSIBusElectrical | int | SCSIBusSpeed] ''' A definition of the type of bus the given SCSI version supports ''' SCSI_BUSSES: dict[SCSIInterface, SCSIBusDefinition] = { diff --git a/squishy/scsi/device.py b/squishy/scsi/device.py index 9dcdab81..7f0c7eeb 100644 --- a/squishy/scsi/device.py +++ b/squishy/scsi/device.py @@ -1,11 +1,11 @@ # SPDX-License-Identifier: BSD-3-Clause -from enum import IntEnum, unique - -__doc__ = '''\ +''' ''' +from enum import IntEnum, unique + __all__ = ( 'PeripheralDeviceType', ) diff --git a/squishy/scsi/messages.py b/squishy/scsi/messages.py index 50ac35f9..256c1d8c 100644 --- a/squishy/scsi/messages.py +++ b/squishy/scsi/messages.py @@ -1,4 +1,9 @@ # SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + from enum import IntEnum, unique __all__ = ( @@ -6,9 +11,6 @@ 'ExtendedMessageCodes', ) -__doc__ = '''\ - -''' @unique class MessageCodes(IntEnum): diff --git a/squishy/scsi/vid.py b/squishy/scsi/vid.py index 7e767b67..7e5938bc 100644 --- a/squishy/scsi/vid.py +++ b/squishy/scsi/vid.py @@ -1,6 +1,6 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = '''\ +''' This files contains the full map of SCSI VID to Vendor Name. It is up-to-date as of 2023-02-14 diff --git a/squishy/support/__init__.py b/squishy/support/__init__.py new file mode 100644 index 00000000..5c1c39aa --- /dev/null +++ b/squishy/support/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' +This module contains miscellaneous support infrastructure for Squishy. +''' + +__all__ = ( + +) diff --git a/tests/gateware_test.py b/squishy/support/test.py similarity index 73% rename from tests/gateware_test.py rename to squishy/support/test.py index c9cd08a3..e79f96b0 100644 --- a/tests/gateware_test.py +++ b/squishy/support/test.py @@ -1,19 +1,33 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import Tuple, Union +''' + +This module contains support infrastructure for gateware testing. + +There are two test harnesses, the first being :py:class:`SquishyUSBGatewareTest` which is +specialized for testing gateware that runs in the USB clock domain and/or has USB specific +functionality. The second is :py:class:`SquishySCSIGatewareTest` which is like the harness +for USB, but directed at SCSI instead. + +''' from torii.sim import Settle from torii.test import ToriiTestCase from torii.test.mock import MockPlatform + from usb_construct.types import USBRequestRecipient, USBRequestType, USBStandardRequests __all__ = ( - 'SquishyUSBGatewareTestCase', - 'SquishySCSIGatewareTestCase', + 'SquishyUSBGatewareTest', + 'SquishySCSIGatewareTest', ) -class SquishyUSBGatewareTestCase(ToriiTestCase): - domains = (('usb', 60e6), ) +class SquishyUSBGatewareTest(ToriiTestCase): + ''' + Squishy test harness for gateware in the USB clock domain. + ''' + + domains = (('usb', 60e6), ) # USB Domain @ 60MHz platform = MockPlatform() def setupReceived(self): @@ -27,14 +41,16 @@ def setupReceived(self): def sendSetupSetInterface(self): yield from self.sendSetup( - type = USBRequestType.STANDARD, retrieve = False, - req = USBStandardRequests.SET_INTERFACE, value = (0, 1), - index = (0, 0), length = 0 + type = USBRequestType.STANDARD, + retrieve = False, + req = USBStandardRequests.SET_INTERFACE, + value = (0, 1), + index = (0, 0), + length = 0 ) - def sendSetup(self, *, - type: USBRequestType, retrieve: bool, req, - value: Union[Tuple[int, int], int], index: Union[Tuple[int, int], int], + def sendSetup( + self, *, type: USBRequestType, retrieve: bool, req, value: tuple[int, int] | int, index: tuple[int, int] | int, length: int, recipient: USBRequestRecipient = USBRequestRecipient.INTERFACE ): yield self.dut.interface.setup.recipient.eq(recipient) @@ -54,10 +70,7 @@ def sendSetup(self, *, yield self.dut.interface.setup.length.eq(length) yield from self.setupReceived() - def receiveData(self, *, - data: Union[Tuple[int], bytes], - check: bool = True - ): + def receiveData(self, *, data: tuple[int, ...] | bytes, check: bool = True): result = True yield self.dut.interface.tx.ready.eq(1) yield self.dut.interface.data_requested.eq(1) @@ -70,26 +83,13 @@ def receiveData(self, *, yield Settle() yield for idx, val in enumerate(data): - self.assertEqual( - (yield self.dut.interface.tx.first), - (1 if idx == 0 else 0) - ) - self.assertEqual( - (yield self.dut.interface.tx.last), - (1 if idx == len(data) - 1 else 0) - ) - self.assertEqual( - (yield self.dut.interface.tx.valid), - 1 - ) + self.assertEqual((yield self.dut.interface.tx.first), (1 if idx == 0 else 0)) + self.assertEqual((yield self.dut.interface.tx.last), (1 if idx == len(data) - 1 else 0)) + self.assertEqual((yield self.dut.interface.tx.valid), 1) if check: - self.assertEqual( - (yield self.dut.interface.tx.payload), - val - ) + self.assertEqual((yield self.dut.interface.tx.payload), val) if (yield self.dut.interface.tx.payload) != val: result = False - self.assertEqual((yield self.dut.interface.handshakes_out.ack), 0) if idx == len(data) - 1: yield self.dut.interface.tx.ready.eq(0) @@ -123,9 +123,7 @@ def receiveZLP(self): yield Settle() yield - def sendData(self, *, - data: Union[Tuple[int], bytes] - ): + def sendData(self, *, data: tuple[int, ...] | bytes): yield self.dut.interface.rx.valid.eq(1) for val in data: yield Settle() @@ -151,7 +149,6 @@ def sendData(self, *, yield Settle() yield - def ensure_stall(self): yield self.dut.interface.tx.ready.eq(1) yield self.dut.interface.data_requested.eq(1) @@ -170,6 +167,10 @@ def ensure_stall(self): yield Settle() yield -class SquishySCSIGatewareTestCase(ToriiTestCase): - domains = (('scsi', 100e6), ) +class SquishySCSIGatewareTest(ToriiTestCase): + ''' + Squishy test harness for gateware in the SCSI clock domain. + ''' + + domains = (('scsi', 100e6), ) # SCSI Domain @ 100MHz platform = MockPlatform() diff --git a/tests/gateware/quirks/__init__.py b/tests/gateware/applet/__init__.py similarity index 100% rename from tests/gateware/quirks/__init__.py rename to tests/gateware/applet/__init__.py diff --git a/tests/gateware/quirks/usb/__init__.py b/tests/gateware/peripherals/__init__.py similarity index 100% rename from tests/gateware/quirks/usb/__init__.py rename to tests/gateware/peripherals/__init__.py diff --git a/tests/gateware/scsi/__init__.py b/tests/gateware/peripherals/scsi/__init__.py similarity index 100% rename from tests/gateware/scsi/__init__.py rename to tests/gateware/peripherals/scsi/__init__.py diff --git a/tests/gateware/scsi/common/__init__.py b/tests/gateware/peripherals/scsi/quirks/__init__.py similarity index 100% rename from tests/gateware/scsi/common/__init__.py rename to tests/gateware/peripherals/scsi/quirks/__init__.py diff --git a/tests/gateware/core/test_flash.py b/tests/gateware/peripherals/test_flash.py similarity index 85% rename from tests/gateware/core/test_flash.py rename to tests/gateware/peripherals/test_flash.py index 02eb325f..cf01d68a 100644 --- a/tests/gateware/core/test_flash.py +++ b/tests/gateware/peripherals/test_flash.py @@ -1,18 +1,17 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import Optional, Tuple +from torii import Elaboratable, Module, Record +from torii.hdl.rec import DIR_FANIN, DIR_FANOUT +from torii.lib.fifo import AsyncFIFO +from torii.sim import Settle +from torii.test import ToriiTestCase +from torii.test.mock import MockPlatform -from torii import Elaboratable, Module, Record -from torii.hdl.rec import DIR_FANIN, DIR_FANOUT -from torii.lib.fifo import AsyncFIFO -from torii.sim import Settle -from torii.test import ToriiTestCase -from torii.test.mock import MockPlatform +from squishy.core.flash import Geometry +from squishy.core.config import FlashConfig +from squishy.gateware.peripherals.flash import SPIFlash -from squishy.core.flash import FlashGeometry -from squishy.gateware.core.flash import SPIFlash - -_DFU_DATA = ( +_FLASH_DATA = ( 0xff, 0x00, 0x00, 0xff, 0x7e, 0xaa, 0x99, 0x7e, 0x51, 0x00, 0x01, 0x05, 0x92, 0x00, 0x20, 0x62, 0x03, 0x67, 0x72, 0x01, 0x10, 0x82, 0x00, 0x00, 0x11, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -31,6 +30,7 @@ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ) + _SPI_RECORD = Record(( ('clk', [ ('o', 1, DIR_FANOUT), @@ -46,18 +46,19 @@ ]), )) -class DFUPlatform: - flash = { - 'geometry': FlashGeometry( +class DUTPlatform: + flash = FlashConfig( + geometry = Geometry( size = 512*1024, page_size = 64, erase_size = 256, + slot_size = 262144, addr_width = 24 - ).init_slots(device = 'iCE40HX8K'), - 'commands': { + ), + commands = { 'erase': 0x20, } - } + ) def request(self, name, number): return _SPI_RECORD @@ -65,15 +66,13 @@ def request(self, name, number): class DUTWrapper(Elaboratable): def __init__(self, *, resource) -> None: + self._fifo = AsyncFIFO( - width = 8, depth = DFUPlatform.flash['geometry'].erase_size, - r_domain = 'sync', w_domain = 'usb' + width = 8, depth = DUTPlatform.flash.geometry.erase_size, r_domain = 'sync', w_domain = 'usb' ) + self._flash = SPIFlash( - flash_resource = resource, - flash_geometry = DFUPlatform.flash['geometry'], - fifo = self._fifo, - erase_cmd = 0x20 + flash_resource = resource, flash_geometry = DUTPlatform.flash.geometry, fifo = self._fifo, erase_cmd = 0x20 ) # Pull out the raw SPI bus for testing @@ -92,7 +91,7 @@ def __init__(self, *, resource) -> None: self.writeAddr = self._flash.writeAddr self.byteCount = self._flash.byteCount - def elaborate(self, platform) -> Module: + def elaborate(self, _) -> Module: m = Module() m.submodules.flash = self._flash @@ -110,8 +109,7 @@ class SPIFlashTests(ToriiTestCase): platform = MockPlatform() def spi_trans(self, *, - copi: Optional[Tuple[int]] = None, cipo: Optional[Tuple[int]] = None, - partial: bool = False, continuation: bool = False + copi: tuple[int, ...] | None = None, cipo: tuple[int, ...] | None = None, partial: bool = False, continuation: bool = False ): if cipo is not None and copi is not None: self.assertEqual(len(cipo), len(copi)) @@ -165,7 +163,7 @@ def fifo(self): while not self.dut.fill_fifo: yield yield self.dut._fifo.w_en.eq(1) - for byte in _DFU_DATA: + for byte in _FLASH_DATA: yield self.dut._fifo.w_data.eq(byte) yield yield self.dut._fifo.w_en.eq(0) @@ -188,7 +186,7 @@ def flash(self): yield Settle() yield yield self.dut.start.eq(1) - yield self.dut.byteCount.eq(len(_DFU_DATA)) + yield self.dut.byteCount.eq(len(_FLASH_DATA)) yield Settle() yield yield self.dut.start.eq(0) @@ -226,26 +224,26 @@ def flash(self): self.assertEqual((yield self.dut._spi_bus.cs.o), 1) self.assertEqual((yield self.dut._spi_bus.clk.o), 1) # :< - yield from self.spi_trans(copi = _DFU_DATA[0:64], continuation = True) + yield from self.spi_trans(copi = _FLASH_DATA[0:64], continuation = True) self.assertEqual((yield self.dut.writeAddr), 64) yield from self.spi_trans(copi = (0x05, None), cipo = (None, 0x03)) yield from self.spi_trans(copi = (0x05, None), cipo = (None, 0x00)) yield from self.spi_trans(copi = (0x06,)) yield from self.spi_trans(copi = (0x02, 0x00, 0x00, 0x40), partial = True) - yield from self.spi_trans(copi = _DFU_DATA[64:128], continuation = True) + yield from self.spi_trans(copi = _FLASH_DATA[64:128], continuation = True) self.assertEqual((yield self.dut.writeAddr), 128) yield from self.spi_trans(copi = (0x05, None), cipo = (None, 0x00)) yield from self.spi_trans(copi = (0x06,)) yield from self.spi_trans(copi = (0x02, 0x00, 0x00, 0x80), partial = True) - yield from self.spi_trans(copi = _DFU_DATA[128:192], continuation = True) + yield from self.spi_trans(copi = _FLASH_DATA[128:192], continuation = True) self.assertEqual((yield self.dut.writeAddr), 192) yield from self.spi_trans(copi = (0x05, None), cipo = (None, 0x00)) yield from self.spi_trans(copi = (0x06,)) yield from self.spi_trans(copi = (0x02, 0x00, 0x00, 0xC0), partial = True) - yield from self.spi_trans(copi = _DFU_DATA[192:256], continuation = True) + yield from self.spi_trans(copi = _FLASH_DATA[192:256], continuation = True) self.assertEqual((yield self.dut.writeAddr), 256) yield from self.spi_trans(copi = (0x05, None), cipo = (None, 0x00)) diff --git a/tests/gateware/core/test_spi.py b/tests/gateware/peripherals/test_spi.py similarity index 88% rename from tests/gateware/core/test_spi.py rename to tests/gateware/peripherals/test_spi.py index dd869d34..edea99f3 100644 --- a/tests/gateware/core/test_spi.py +++ b/tests/gateware/peripherals/test_spi.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: BSD-3-Clause -from torii.sim import Settle -from torii.test import ToriiTestCase -from torii.test.mock import MockPlatform +from torii.sim import Settle +from torii.test import ToriiTestCase +from torii.test.mock import MockPlatform -from squishy.gateware.core.spi import SPIInterface +from squishy.gateware.peripherals.spi import SPIInterface class SPIInterfaceTests(ToriiTestCase): dut: SPIInterface = SPIInterface diff --git a/tests/gateware/scsi/scsi1/__init__.py b/tests/gateware/peripherals/usb/__init__.py similarity index 100% rename from tests/gateware/scsi/scsi1/__init__.py rename to tests/gateware/peripherals/usb/__init__.py diff --git a/tests/gateware/scsi/scsi2/__init__.py b/tests/gateware/peripherals/usb/quirks/__init__.py similarity index 100% rename from tests/gateware/scsi/scsi2/__init__.py rename to tests/gateware/peripherals/usb/quirks/__init__.py diff --git a/tests/gateware/quirks/usb/test_windows.py b/tests/gateware/peripherals/usb/quirks/test_windows.py similarity index 83% rename from tests/gateware/quirks/usb/test_windows.py rename to tests/gateware/peripherals/usb/quirks/test_windows.py index 035a2516..db4adeaa 100644 --- a/tests/gateware/quirks/usb/test_windows.py +++ b/tests/gateware/peripherals/usb/quirks/test_windows.py @@ -1,24 +1,20 @@ # SPDX-License-Identifier: BSD-3-Clause -from ....gateware_test import SquishyUSBGatewareTestCase -from torii.sim import Settle -from torii.test import ToriiTestCase -from usb_construct.emitters.descriptors.microsoft import ( - PlatformDescriptorCollection, SetHeaderDescriptorEmitter -) -from usb_construct.types import USBRequestRecipient, USBRequestType -from usb_construct.types.descriptors.microsoft import MicrosoftRequests +from torii.sim import Settle +from torii.test import ToriiTestCase -from squishy.gateware.quirks.usb.windows import ( - GetDescriptorSetHandler, WindowsRequestHandler -) +from usb_construct.emitters.descriptors.microsoft import PlatformDescriptorCollection, SetHeaderDescriptorEmitter +from usb_construct.types import USBRequestRecipient, USBRequestType +from usb_construct.types.descriptors.microsoft import MicrosoftRequests +from squishy.support.test import SquishyUSBGatewareTest +from squishy.gateware.peripherals.usb.quirks.windows import GetDescriptorSetHandler, WindowsRequestHandler def _make_platform_descriptors(): desc_collection = PlatformDescriptorCollection() set_header = SetHeaderDescriptorEmitter() - with set_header.SubsetHeaderConfiguration() as sub_cfg: # 👉👈🥺 + with set_header.SubsetHeaderConfiguration() as sub_cfg: sub_cfg.bConfigurationValue = 1 with sub_cfg.SubsetHeaderFunction() as sub_func: @@ -33,14 +29,13 @@ def _make_platform_descriptors(): return (desc_collection, desc_collection.descriptors) -class GetDescriptorSetHandlerTests(SquishyUSBGatewareTestCase): +class GetDescriptorSetHandlerTests(SquishyUSBGatewareTest): _desc_collection, _descriptors = _make_platform_descriptors() dut: GetDescriptorSetHandler = GetDescriptorSetHandler dut_args = { 'desc_collection': _desc_collection } - @ToriiTestCase.simulation @ToriiTestCase.sync_domain(domain = 'usb') def test_get_desc_set(self): @@ -131,8 +126,7 @@ def test_get_desc_set(self): yield Settle() yield - -class WindowsRequestHandlerTests(SquishyUSBGatewareTestCase): +class WindowsRequestHandlerTests(SquishyUSBGatewareTest): _desc_collection, _descriptors = _make_platform_descriptors() dut: WindowsRequestHandler = WindowsRequestHandler dut_args = { diff --git a/tests/gateware/bootloader/test_dfu.py b/tests/gateware/peripherals/usb/test_dfu.py similarity index 59% rename from tests/gateware/bootloader/test_dfu.py rename to tests/gateware/peripherals/usb/test_dfu.py index 9a34fb29..f70787b4 100644 --- a/tests/gateware/bootloader/test_dfu.py +++ b/tests/gateware/peripherals/usb/test_dfu.py @@ -1,15 +1,16 @@ # SPDX-License-Identifier: BSD-3-Clause -from ...gateware_test import SquishyUSBGatewareTestCase -from torii import Record -from torii.hdl.rec import DIR_FANIN, DIR_FANOUT -from torii.sim import Settle -from torii.test import ToriiTestCase -from usb_construct.types import USBRequestType -from usb_construct.types.descriptors.dfu import DFURequests +from torii import Record +from torii.hdl.rec import DIR_FANIN, DIR_FANOUT +from torii.sim import Settle +from torii.test import ToriiTestCase +from usb_construct.types import USBRequestType +from usb_construct.types.descriptors.dfu import DFURequests -from squishy.core.flash import FlashGeometry -from squishy.gateware.bootloader.dfu import DFURequestHandler, DFUState +from squishy.support.test import SquishyUSBGatewareTest +from squishy.gateware.peripherals.usb.dfu import DFURequestHandler, DFUState +from squishy.core.config import FlashConfig +from squishy.core.flash import Geometry _DFU_DATA = ( 0xff, 0x00, 0x00, 0xff, 0x7e, 0xaa, 0x99, 0x7e, 0x51, 0x00, 0x01, 0x05, 0x92, 0x00, 0x20, 0x62, @@ -46,47 +47,90 @@ )) class DFUPlatform: - flash = { - 'geometry': FlashGeometry( + flash = FlashConfig( + geometry = Geometry( size = 8388608, # 8MiB page_size = 256, erase_size = 4096, # 4KiB + slot_size = 262144, addr_width = 24 - ).init_slots(device = 'iCE40HX8K'), - 'commands': { + ), + commands = { 'erase': 0x20, } - } + ) def request(self, name, number): return _SPI_RECORD +class DFURequestHandlerStubTests(SquishyUSBGatewareTest): + dut: DFURequestHandler = DFURequestHandler + dut_args = { + 'configuration_num': 1, + 'interface_num': 0, + 'boot_stub': True + } + + def sendDFUDetach(self): + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = False, req = DFURequests.DETACH, value = 1000, index = 0, length = 0 + ) + + def sendDFUGetStatus(self): + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = True, req = DFURequests.GET_STATUS, value = 0, index = 0, length = 6 + ) + + @ToriiTestCase.simulation + @ToriiTestCase.sync_domain(domain = 'usb') + def test_dfu_stub(self): + self.assertEqual((yield self.dut._slot_select), 0) + yield self.dut.interface.active_config.eq(1) + yield Settle() + yield + yield from self.sendSetupSetInterface() + yield from self.receiveZLP() + yield + yield + yield + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.APP_IDLE, 0)) + yield from self.sendDFUDetach() + yield from self.receiveZLP() + self.assertEqual((yield self.dut._trigger_reboot), 1) + self.assertEqual((yield self.dut._slot_select), 0) + -class DFURequestHandlerTests(SquishyUSBGatewareTestCase): +# TODO(aki): We need to build a DUTWrapper for this test now +class DFURequestHandlerTests(SquishyUSBGatewareTest): dut: DFURequestHandler = DFURequestHandler dut_args = { 'configuration': 1, 'interface': 0, - 'resource_name': ('spi_flash_x1', 0) + 'boot_stub': False, } domains = (('usb', 60e6),) platform = DFUPlatform() def sendDFUDetach(self): - yield from self.sendSetup(type = USBRequestType.CLASS, retrieve = False, - req = DFURequests.DETACH, value = 1000, index = 0, length = 0) + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = False, req = DFURequests.DETACH, value = 1000, index = 0, length = 0 + ) def sendDFUDownload(self): - yield from self.sendSetup(type = USBRequestType.CLASS, retrieve = False, - req = DFURequests.DOWNLOAD, value = 0, index = 0, length = 256) + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = False, req = DFURequests.DOWNLOAD, value = 0, index = 0, length = 256 + ) def sendDFUGetStatus(self): - yield from self.sendSetup(type = USBRequestType.CLASS, retrieve = True, - req = DFURequests.GET_STATUS, value = 0, index = 0, length = 6) + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = True, req = DFURequests.GET_STATUS, value = 0, index = 0, length = 6 + ) def sendDFUGetState(self): - yield from self.sendSetup(type = USBRequestType.CLASS, retrieve = True, - req = DFURequests.GET_STATE, value = 0, index = 0, length = 1) + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = True, req = DFURequests.GET_STATE, value = 0, index = 0, length = 1 + ) @ToriiTestCase.simulation diff --git a/tests/gateware/usb/test_dfu.py b/tests/gateware/usb/test_dfu.py deleted file mode 100644 index 175444a6..00000000 --- a/tests/gateware/usb/test_dfu.py +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-License-Identifier: BSD-3-Clause - -from ...gateware_test import SquishyUSBGatewareTestCase -from torii.sim import Settle -from torii.test import ToriiTestCase -from usb_construct.types import USBRequestType -from usb_construct.types.descriptors.dfu import DFURequests - -from squishy.gateware.usb.dfu import DFURequestHandler, DFUState - - -class DFURequestHandlerStubTests(SquishyUSBGatewareTestCase): - dut: DFURequestHandler = DFURequestHandler - dut_args = { - 'configuration_num': 1, - 'interface_num': 0 - } - - - def sendDFUDetach(self): - yield from self.sendSetup( - type = USBRequestType.CLASS, retrieve = False, - req = DFURequests.DETACH, value = 1000, - index = 0, length = 0 - ) - - def sendDFUGetStatus(self): - yield from self.sendSetup( - type = USBRequestType.CLASS, retrieve = True, - req = DFURequests.GET_STATUS, value = 0, - index = 0, length = 6 - ) - - @ToriiTestCase.simulation - @ToriiTestCase.sync_domain(domain = 'usb') - def test_dfu_stub(self): - self.assertEqual((yield self.dut._slot_select), 0) - yield self.dut.interface.active_config.eq(1) - yield Settle() - yield - yield from self.sendSetupSetInterface() - yield from self.receiveZLP() - yield - yield - yield - yield from self.sendDFUGetStatus() - yield from self.receiveData(data = (0, 0, 0, 0, DFUState.APP_IDLE, 0)) - yield from self.sendDFUDetach() - yield from self.receiveZLP() - self.assertEqual((yield self.dut._trigger_reboot), 1) - self.assertEqual((yield self.dut._slot_select), 0) diff --git a/tests/gateware/scsi/scsi3/__init__.py b/tests/scsi/__init__.py similarity index 100% rename from tests/gateware/scsi/scsi3/__init__.py rename to tests/scsi/__init__.py