diff --git a/.github/ISSUE_TEMPLATE/applet_request.yml b/.github/ISSUE_TEMPLATE/applet_request.yml index 901722a5..67838996 100644 --- a/.github/ISSUE_TEMPLATE/applet_request.yml +++ b/.github/ISSUE_TEMPLATE/applet_request.yml @@ -1,6 +1,6 @@ name: Applet Request description: Do you think we're missing a built in Applet? -title: "[APPLET REQ]: " +title: "" labels: ["Applets"] body: - type: markdown diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4a30c7c9..4f9d3334 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Report an issue with Squishy -title: "[BUG]: " +title: "" labels: ["Bug"] body: - type: markdown @@ -15,9 +15,7 @@ body: attributes: label: Which part of Squishy shows this problem options: - - GUI - CLI - - REPL - Gateware Library - Python Library - Applet diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index d83f25ff..07d55f6c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature Request description: Have a feature you'd like? Let us know. -title: "[FEATURE]: " +title: "" labels: ["Feature"] body: - type: markdown @@ -16,9 +16,7 @@ body: attributes: label: Which part of Squishy does this feature generally fit into? options: - - GUI - CLI - - REPL - Gateware Library - Python Library - Applet diff --git a/.github/ISSUE_TEMPLATE/rfc.yml b/.github/ISSUE_TEMPLATE/rfc.yml deleted file mode 100644 index bf53e244..00000000 --- a/.github/ISSUE_TEMPLATE/rfc.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: RFC -description: Submit a Request for Comments (RFC) -title: "[RFC]: " -labels: ["RFC"] -body: - - type: textarea - id: rfc-summary - attributes: - label: Summary - description: One paragraph explanation of the feature. - validations: - required: true - - - type: textarea - id: rfc-motivation - attributes: - label: Motivation - description: Why are we doing this? What use cases does it support? What is the expected outcome? - validations: - required: true - - - type: textarea - id: rfc-guide-explanation - attributes: - label: Guide-level explanation - description: Explain the proposal as if it was already included in the language and you were teaching it to another Squishy user. - validations: - required: true - - - type: textarea - id: rfc-reference-explination - attributes: - label: Reference-level explanation - description: This is the technical portion of the RFC. Explain the design in sufficient detail. - validations: - required: true - - - type: textarea - id: rfc-drawbacks - attributes: - label: Drawbacks - description: Why should we not do this? - validations: - required: true - - - type: textarea - id: rfc-rationale - attributes: - label: Rationale and alternatives - description: Why is this design the best in the space of possible designs? What other designs have been considered and what is the rationale for not choosing them? - validations: - required: true - - - type: textarea - id: rfc-prior-art - attributes: - label: Prior art - description: Discuss prior art, both the good and the bad, in relation to this proposal. - validations: - required: true - - - type: textarea - id: rfc-questions - attributes: - label: Unresolved questions - description: What parts of the design do you expect to resolve through the RFC process before this gets merged? What parts of the design do you expect to resolve through the implementation of this feature before stabilization? - validations: - required: true - - - type: textarea - id: rfc-future - attributes: - label: Future possibilities - description: Think about what the natural extension and evolution of your proposal would be and how it would affect Squishy as a whole in a holistic way. - validations: - required: true - - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/squishy-scsi/squishy/blob/main/CODE_OF_CONDUCT.md) - options: - - label: I agree to follow this project's Code of Conduct - required: true diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 33497a20..20abe49e 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -39,7 +39,7 @@ jobs: name: 'Test Squishy (Python ${{ matrix.python-version }})' strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13.0-rc.2', 'pypy3.10-v7.3.16'] + python-version: ['3.11', '3.12', '3.13', '3.14.0-alpha.1'] fail-fast: true steps: @@ -71,7 +71,7 @@ jobs: - name: 'Run Tests' timeout-minutes: 15 # Python 3.12 and 3.13 seem to just hang on testing ~sometimes~ - continue-on-error: ${{ matrix.python-version == '3.12' || matrix.python-version == '3.13.0-rc.2' }} + continue-on-error: ${{ matrix.python-version == '3.12' || matrix.python-version == '3.13' }} shell: bash run: | nox -s test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6b802e94..e33e5627 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,12 +13,13 @@ jobs: name: 'Test Squishy (Python ${{ matrix.python-version }})' strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13.0-rc.2', 'pypy3.10-v7.3.16'] + python-version: ['3.11', '3.12', '3.13', '3.14.0-alpha.1'] steps: - name: 'Setup Python' uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: 'Initialize Environment' shell: bash env: @@ -47,7 +48,7 @@ jobs: - name: 'Run Tests' shell: bash timeout-minutes: 15 # Python 3.12 and 3.13 seem to just hang on testing ~sometimes~ - continue-on-error: ${{ matrix.python-version == '3.12' || matrix.python-version == '3.13.0-rc.2' }} + continue-on-error: ${{ matrix.python-version == '3.12' || matrix.python-version == '3.13' }} run: | nox -s test -- --coverage diff --git a/contrib/.mypy.ini b/contrib/.mypy.ini index 5da2d853..4404531b 100644 --- a/contrib/.mypy.ini +++ b/contrib/.mypy.ini @@ -4,15 +4,6 @@ warn_return_any = True [mypy-construct.*] ignore_missing_imports = True -[mypy-PySide2.*] -ignore_missing_imports = True - -[mypy-PySide6.*] -ignore_missing_imports = True - -[mypy-luna.*] -ignore_missing_imports = True - [mypy-rich.*] ignore_missing_imports = True diff --git a/contrib/scsidump b/contrib/scsidump index bf5dba59..e4163b67 100755 --- a/contrib/scsidump +++ b/contrib/scsidump @@ -7,15 +7,18 @@ import sys -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, Namespace -from pathlib import Path +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from pathlib import Path try: from squishy import __version__ except ImportError: sys.path.insert(0, '/pool/abyss/Projects/squishy/squishy') -from squishy import __version__ + from squishy import __version__ + +from squishy.gateware import AVAILABLE_PLATFORMS +from squishy.device import SquishyDevice def _setup_extcap_parser(parser: ArgumentParser): parser.add_argument( @@ -67,70 +70,16 @@ def _setup_extcap_parser(parser: ArgumentParser): APPLET_OPTIONS = { - 'platform': { - 'type': str, - 'required': True, - 'default': 'rev2', - 'help': 'Squishy Hardware Platform', - 'extcap_type': 'selector', - 'extcap_name': 'Hardware Platform', - 'values': ('rev1', 'rev2'), - 'group': 'Hardware Options', - }, - 'scsi-did': { - 'type': int, - 'required': True, - 'default': 7, - 'range': (0, 7), - 'help': 'SCSI ID', - 'extcap_type': 'integer', - 'extcap_name': 'SCSI ID', - 'group': 'SCSI Options' - }, - 'scsi-arbitrating': { - 'type': bool, - 'help': 'Enable SCSI Bus arbitration', - 'extcap_type': 'boolean', - 'extcap_name': 'Bus Arbitration', - 'group': 'SCSI Options' - }, - - 'skip-cache': { - 'type': bool, - 'help': 'Skip cache lookup for built gateware', - 'extcap_type': 'boolean', - 'extcap_name': 'Skip Cache', - 'group': 'Gateware Options' - }, - 'use-router2': { - 'type': bool, - 'help': 'Use nextpnr\'s router2', - 'extcap_type': 'boolean', - 'extcap_name': 'Use router2', - 'group': 'Gateware Options' - }, - 'tmg-ripup': { - 'type': bool, - 'help': 'Use the timing-driven ripup router', - 'extcap_type': 'boolean', - 'extcap_name': 'Use the timing-driven ripup router', - 'group': 'Gateware Options' - }, - 'pnr-seed': { - 'type': int, - 'help': 'nextpnr seed', - 'defualt': 0, - 'extcap_type': 'integer', - 'extcap_name': 'PNR seed', - 'group': 'Gateware Options' - }, - 'no-abc9': { - 'type': bool, - 'help': 'Disable ABC9', - 'extcap_type': 'boolean', - 'extcap_name': 'Disable ABC9', - 'group': 'Gateware Options' - } + # 'platform': { + # 'type': str, + # 'required': True, + # 'default': list(AVAILABLE_PLATFORMS.keys())[-1], + # 'help': 'Squishy Hardware Platform', + # 'extcap_type': 'selector', + # 'extcap_name': 'Hardware Platform', + # 'values': (rev for rev in list(AVAILABLE_PLATFORMS.keys())), + # 'group': 'Hardware Options', + # } } @@ -150,7 +99,9 @@ def _setup_parser(parser: ArgumentParser): def extcap_list_interfaces(): print(f'extcap {{version={__version__}}}{{help=https://docs.scsi.moe/extra.html#scsidump}}') - print('interface {value=scsidump}{display=Squishy SCSI Bus capture}') + + for (sn, rev, _) in SquishyDevice.enumerate(): + print(f'interface {{value={sn}}}{{display=Squishy rev{rev[0]}: SCSI Bus capture}}') def extcap_list_dlts(): print('dlt {number=147}{name=squishy}{display=SCSI}') @@ -181,7 +132,7 @@ def extcap_list_config(): extcap_arg_value += '{default=true}' print(extcap_arg_value) - # print('arg {number=0}{call=--meow}{display=Meow}{type=boolean}{tooltip=Meow Meow Meow}') + def main() -> int: parser = ArgumentParser( @@ -209,7 +160,6 @@ def main() -> int: extcap_list_config() - return 0 diff --git a/contrib/squishy-completion.zsh b/contrib/squishy-completion.zsh index 3d3bbaf2..01ed6fa2 100644 --- a/contrib/squishy-completion.zsh +++ b/contrib/squishy-completion.zsh @@ -6,7 +6,6 @@ _squishy_command() { local -a commands=( 'applet[Squishy applet subsystem]' - 'cache[Squishy applet cache managment]' 'provision[Squishy hardware provisioning]' ) _values 'squishy commands' : $commands @@ -15,7 +14,6 @@ _squishy_command() { _squishy_applet_command() { local -a commands=( 'analyzer[Squishy SCSI analyzer]' - 'taperipper[UEFI Boot from 9-track tape]' ) _values 'squishy applets' : $commands } @@ -31,50 +29,30 @@ _squishy_applet_analyzer() { _arguments -s : $arguments } -_squishy_applet_taperipper() { - local arguments - - arguments=( - '(-h --help)'{-h,--help}'[Show help message and exit]' - - ) - - _arguments -s : $arguments -} - _squishy_applet() { local arguments - local platforms=`python -m squishy applet -h | tail -n +4 | grep -m1 -e '\-\-platform\s{' | sed 's/,\s-p\s.*$//' | sed 's/--platform\s{\([^}]*\)}/\1/'` + local platforms=`python -m squishy applet -h | tail -n +4 | grep -m1 -e '--platform\s{' | sed 's/,\s-p\s.*$//' | sed 's/--platform\s{\([^}]*\)}/\1/'` arguments=( '(-h --help)'{-h,--help}'[Show help message and exit]' '(-p --platforms)'{-p=,--platforms=}"[Target hardware platform]:platform:(${(s/,/)platforms})" - '--skip-cache[Skip bitstream cache lookup]' - '(-b --build-dir)'{-b,--build-dir}"[Output directory for build products]:dir:_directories" - '--loud[Enables output from PnR and synthesis]' - '--build-only[Only build the applet]' - - '--use-router2[Use nextpnrs router2 rather than router1]' - '--tmp-ripup[Use timing driven ripup]' - '--detailed-timing-report[Output a detailed timing report]' - '--routed-svg[Save an svg of the routed output]:svg:_files -g "*.svg"' - '--routed-json[Save routed json netlist]:json:_files -g "*.json"' - '--pnr-seed[Specify PNR seed]:seed:_numbers -l 0 "PNR_SEED"' - + '(-Y --noconfirm)'{-Y,--noconfirm}'[Do not ask for confirmation if the target applet is in preview]' + '(-F --flash)'{-f,--flash}'[Flash the applet into persistent storage raather then doing an ephemeral load if supported]' - '--no-abc9[Disable ABC9 durring synthesis]' + '(-B --build-only)'{-B,--build-only}'[Only build and pack the applet, skip device programming]' + '(-b --build-dir)'{-b,--build-dir}"[Output directory for build products]:dir:_directories" + '(-C --skip-cache)'{-C,--skip-cache}'[Skip artifact cache lookup and squesequent insertion when build is completed]' + '--build-verbose[Enable verbose tool output during build]' - '--enable-webusb[Enable the experimental WebUSB support]' - '--webusb-url=[WebUSB URL to encode]:url:_urls' + '--no-abc9[Disable use of abc9, will likely result in worse applet performance]' + '--no-aggressive-mapping[Disable multiple abc9 passes, speeds up build time in exchange for worse performance]' - '(-U --enable-uart)'{-U,--enable-uart}'[Enable debug UART]' - '(-B --baud)'{-B,--baud}'=[Baud rate to run the debug UART at]:baud:_numbers -d 9600' - '(-D --data-bits)'{-D,--data-bits}'=[Data bits to use for the debug UART]:data:_numbers -d 8' - '(-c --parity)'{-c,--parity}'=[Parity mode to use for the debug UART]:parity:(none mark spaceeven odd)' + '--detailed-report[Output a detailed timing report]' + '--no-routed-netlist[Do not save routed json netlist]' + '--pnr-seed[Specify PNR seed]:seed:_numbers -l 0 "PNR_SEED"' - '--scsi-did=[The SCSI ID to use]:scsi_id:_numbers -l 0 -m 7 "SCSI ID"' - '--scsi-arbitrating[Enable SCSI bus arbitration]' + '--dont-compress[Disable bitstream compression if supported on the platform]' '(-): :->applet' '(-)*:: :->applet_args' @@ -92,9 +70,6 @@ _squishy_applet() { (analyzer) _squishy_applet_analyzer && ret=0 ;; - (taperipper) - _squishy_applet_taperipper && ret=0 - ;; esac ;; esac @@ -103,28 +78,30 @@ _squishy_applet() { _squishy_provision() { local arguments - local platforms=`python -m squishy provision -h | tail -n +4 | grep -m1 -e '\-\-platform\s{' | sed 's/,\s-p\s.*$//' | sed 's/--platform\s{\([^}]*\)}/\1/'` + local platforms=`python -m squishy provision -h | tail -n +4 | grep -m1 -e '--platform\s{' | sed 's/,\s-p\s.*$//' | sed 's/--platform\s{\([^}]*\)}/\1/'` arguments=( '(-h --help)'{-h,--help}'[Show help message and exit]' '(-p --platforms)'{-p=,--platforms=}"[Target hardware platform]:platform:(${(s/,/)platforms})" - '--skip-cache[Skip bitstream cache lookup]' + '(-Y --noconfirm)'{-Y,--noconfirm}'[Do not ask for confirmation if the target applet is in preview]' + '(-F --flash)'{-f,--flash}'[Flash the applet into persistent storage raather then doing an ephemeral load if supported]' + + '(-B --build-only)'{-B,--build-only}'[Only build and pack the applet, skip device programming]' '(-b --build-dir)'{-b,--build-dir}"[Output directory for build products]:dir:_directories" - '--loud[Enables output from PnR and synthesis]' - '--build-only[Only build the applet]' - - '--use-router2[Use nextpnrs router2 rather than router1]' - '--tmp-ripup[Use timing driven ripup]' - '--detailed-timing-report[Output a detailed timing report]' - '--routed-svg[Save an svg of the routed output]:svg:_files -g "*.svg"' - '--routed-json[Save routed json netlist]:json:_files -g "*.json"' + '--build-verbose[Enable verbose tool output during build]' + + '--no-abc9[Disable use of abc9, will likely result in worse applet performance]' + '--no-aggressive-mapping[Disable multiple abc9 passes, speeds up build time in exchange for worse performance]' + + '--detailed-report[Output a detailed timing report]' + '--no-routed-netlist[Do not save routed json netlist]' '--pnr-seed[Specify PNR seed]:seed:_numbers -l 0 "PNR_SEED"' - '--no-abc9[Disable ABC9 durring synthesis]' + '--dont-compress[Disable bitstream compression if supported on the platform]' - '(-S --serial-number)'{-S,--serial-number}'[Specify Serial Number to use]' - '(-W --whole-device)'{-W,--whole-device}'=[Generate a whole device provisioning image for factory progrssming]' + '(-S --serial-number)'{-S,--serial-number}'[Specify serial number to use]' + '(-W --whole-device)'{-W,--whole-device}'=[Generate a whole device provisioning image for factory programming]' ) _arguments -s : $arguments && return @@ -133,65 +110,6 @@ _squishy_provision() { return 0 } -_squishy_cache_command() { - local -a commands=( - 'list[Show cache status]' - 'clear[Clear cache]' - ) - _values 'squishy cache' : $commands -} - -_squishy_cache_list() { - local arguments - arguments=( - '(-h --help)'{-h,--help}'[Show help message and exit]' - '--list-cache-items[List each item in the cache (WARNING: CAN BE LARGE)]' - ) - - _arguments -s : $arguments -} - -_squishy_cache_clear() { - local arguments - arguments=( - '(-h --help)'{-h,--help}'[Show help message and exit]' - - ) - - _arguments -s : $arguments -} - -_squishy_cache() { - local arguments - integer ret=1 - - arguments=( - '(-h --help)'{-h,--help}'[Show help and exit]' - - '(-): :->action' - '(-)*:: :->action_args' - ) - - _arguments -s : $arguments && return - - case $state in - (action) - _squishy_cache_command && ret=0 - ;; - (action_args) - curcontext=${curcontext%:*:*}:squishy-$words[1]: - case $words[1] in - (list) - _squishy_cache_list && ret=0 - ;; - (clear) - _squishy_cache_clear && ret=0 - ;; - esac - ;; - esac - return $ret -} _squishy() { local arguments context curcontext=$curcontext state state_descr line @@ -201,6 +119,7 @@ _squishy() { '(-h --help)'{-h,--help}'[show version and help then exit]' '(-d --device)'{-d,--device}'=[specify device serial number]' '(-v --verbose)'{-v,--verbose}'[verbose logging]' + '(-V --version)'{-V,--version}'[show version and exit]' '(-): :->command' '(-)*:: :->arguments' ) @@ -217,9 +136,6 @@ _squishy() { (applet) _squishy_applet && ret=0 ;; - (cache) - _squishy_cache && ret=0 - ;; (provision) _squishy_provision && ret=0 esac diff --git a/docs/_static/css/styles.css b/docs/_static/css/styles.css index 77331410..ae9e319f 100644 --- a/docs/_static/css/styles.css +++ b/docs/_static/css/styles.css @@ -10,6 +10,14 @@ code.literal { border-radius: 5px; } +div.sidebar-container { + width: 25em; +} + +div.toc-drawer { + width: 20em; +} + svg.WaveDrom { padding: 10px; border-radius: 5px; diff --git a/docs/applets/api/cli.md b/docs/applets/api/cli.md deleted file mode 100644 index f3652786..00000000 --- a/docs/applets/api/cli.md +++ /dev/null @@ -1,8 +0,0 @@ -# Squishy Applet: CLI API - -```{toctree} -:hidden: -``` -```{todo} -Write this section. -``` diff --git a/docs/applets/api/device.md b/docs/applets/api/device.md deleted file mode 100644 index 6ad0f2d9..00000000 --- a/docs/applets/api/device.md +++ /dev/null @@ -1,16 +0,0 @@ -# `squishy.core.device.SquishyHardwareDevice` - -```{toctree} -:hidden: -``` - -```{todo} -Flesh this out -``` - -```{eval-rst} - -.. autoclass:: squishy.core.device.SquishyHardwareDevice - :members: - -``` diff --git a/docs/applets/index.md b/docs/applets/index.md index 19d52c4f..c57f4743 100644 --- a/docs/applets/index.md +++ b/docs/applets/index.md @@ -1,11 +1,9 @@ -# Squishy Applets +# Applets ```{toctree} :hidden: analyzer -taperipper -api/index ``` ```{todo} @@ -16,7 +14,7 @@ Flesh this section out Squishy allows for the development of modular pieces of combined code and [Torii HDL] gateware called an applet. It gives Squishy it's functionality and allows for the extension of said functionality and/or entirely new custom functionality. -There are currently two built-in applets, the [analyzer], and [taperipper]applets. With more built-in applets are planned for the future. +There is currently one built-in applet, the [analyzer], with more built-in applets are planned for the future. Squishy allows you to run your own custom applets, any python packages in the `SQUISHY_APPLETS` directory are attempted to be loaded as an applet, and then exposed to the user to allow them to invoke. @@ -24,5 +22,4 @@ For more details on custom applets, see the [Custom Applet] tutorial for a walkt [Custom Applet]: ../tutorials/applets/index.md [analyzer]: ./analyzer.md -[taperipper]: ./taperipper.md [Torii HDL]: https://github.com/shrine-maiden-heavy-industries/torii-hdl diff --git a/docs/applets/taperipper.md b/docs/applets/taperipper.md deleted file mode 100644 index c3eaa193..00000000 --- a/docs/applets/taperipper.md +++ /dev/null @@ -1,7 +0,0 @@ -# Taperipper Applet - -```{todo} -Flesh this out -``` - -The taperipper applet turns squishy into a bootable interface for SCSI based 9-track tape drives, to allow for booting modern machines off of tape. diff --git a/docs/conf.py b/docs/conf.py index 9b871e62..dccda034 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,10 +31,11 @@ '.md': 'markdown', } -pygments_style = 'monokai' -autodoc_member_order = 'bysource' -graphviz_output_format = 'svg' -todo_include_todos = True +pygments_style = 'monokai' +autodoc_member_order = 'bysource' +autodoc_docstring_signature = False +graphviz_output_format = 'svg' +todo_include_todos = True intersphinx_mapping = { 'python' : ('https://docs.python.org/3', None), @@ -43,12 +44,13 @@ 'construct': ('https://construct.readthedocs.io/en/latest', None), } -napoleon_google_docstring = False -napoleon_numpy_docstring = True -napoleon_use_ivar = True -napoleon_custom_sections = [ - 'Platform overrides' -] +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_use_ivar = True +napoleon_use_admonition_for_notes = True +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_references = True + myst_heading_anchors = 3 @@ -79,8 +81,7 @@ ogp_image = f'{html_baseurl}/_images/og-image.png' autosectionlabel_prefix_document = True -# Disable CDN so we use the local copy -mermaid_version = '' + offline_skin_js_path = '_static/js/wavedrom.skin.js' offline_wavedrom_js_path = '_static/js/wavedrom.min.js' diff --git a/docs/hardware/index.md b/docs/hardware/index.md index f3b5e4fa..3c9db7c6 100644 --- a/docs/hardware/index.md +++ b/docs/hardware/index.md @@ -32,7 +32,7 @@ There are two main ways to get squishy hardware, buying it, or building it. ### Buying Hardware ```{note} -As of 2024-03-10, Squishy hardware is not available for sale, however, once engineering and validation of Revision 2 is completed, rev2 units are expected to be available to purchase. +As of 2024-11-07, Squishy hardware is not available for sale, however, once engineering and validation of Revision 2 is completed, rev2 units are expected to be available to purchase. ``` ### Building Hardware @@ -48,11 +48,11 @@ If you buy a pre-built Squishy, it will already be provisioned, but in case some ### Work-In-Progress Hardware -The hardware is always a work in progress, you can view a live rendering of the WIP hardware in your web browser [here](https://kicanvas.org/?github=https%3A%2F%2Fgithub.com%2Fsquishy-scsi%2Fhardware%2Ftree%2Fmain%2Fboards%2Fsquishy), thanks to [KiCanvas]. +The hardware is always a work in progress, you can view a live rendering of the WIP hardware in your web browser [here](https://kicanvas.org/?github=https://github.com/squishy-scsi/hardware/blob/main/boards/squishy/main-unit/squishy-main.kicad_pro), thanks to [KiCanvas]. [provisioning]: ../tutorials/provisioning.md [`rev1`]: ./rev1.md [`rev2`]: ./rev2.md -[here]: https://kicanvas.org/?github=https%3A%2F%2Fgithub.com%2Fsquishy-scsi%2Fhardware%2Ftree%2Fmain%2Fboards%2Fsquishy +[here]: https://kicanvas.org/?github=https://github.com/squishy-scsi/hardware/blob/main/boards/squishy/main-unit/squishy-main.kicad_pro [KiCanvas]: https://kicanvas.org diff --git a/docs/index.md b/docs/index.md index 613aede0..702483be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,7 +40,7 @@ Squishy is not a *specialized* device targeting only a single aspect of the SCSI ## What Squishy Is -Squishy is a platform, it allows you to accomplish almost any goal you wish to that involves a SCSI bus. It can do things as mundane as emulating a SCSI hard drive, but also you can use it to [sniff, analyze, and replay SCSI bus traffic], or even [boot a modern system from 9-track tape]. +Squishy is a platform, it allows you to accomplish almost any goal you wish to that involves a SCSI bus. It can do things as mundane as emulating a SCSI hard drive, but also you can use it to [sniff, analyze, and replay SCSI bus traffic], or even boot a modern system from 9-track tape. You can think of Squishy as being "Software Defined SCSI", much like how a Software Defined Radio works with a hardware transceiver and a software ecosystem, Squishy provides the same, but for SCSI. @@ -99,7 +99,6 @@ There are also [GitHub Discussions] enabled on the repository if you have any qu Squishy does not have an official discord, nor any endorsed discord servers, for an explanation as to why, see the [F.A.Q.] ``` [sniff, analyze, and replay SCSI bus traffic]: ./applets/analyzer.md -[boot a modern system from 9-track tape]: ./applets/taperipper.md [gateware]: ./library/gateware/index.md [python]: ./library/python/index.md [hardware]: ./hardware/index.md diff --git a/docs/applets/api/applet.md b/docs/library/applet/applet.md similarity index 80% rename from docs/applets/api/applet.md rename to docs/library/applet/applet.md index 4f934956..aa751534 100644 --- a/docs/applets/api/applet.md +++ b/docs/library/applet/applet.md @@ -1,4 +1,4 @@ -# `squishy.applets.SquishyApplet` +# Squishy Applet ```{toctree} :hidden: diff --git a/docs/library/applet/device.md b/docs/library/applet/device.md new file mode 100644 index 00000000..0b7ff4b1 --- /dev/null +++ b/docs/library/applet/device.md @@ -0,0 +1,16 @@ +# Squishy Device + +```{toctree} +:hidden: +``` + +```{todo} +Flesh this out +``` + +```{eval-rst} + +.. autoclass:: squishy.device.SquishyDevice + :members: + +``` diff --git a/docs/applets/api/index.md b/docs/library/applet/index.md similarity index 87% rename from docs/applets/api/index.md rename to docs/library/applet/index.md index d5ed020d..f20105a9 100644 --- a/docs/applets/api/index.md +++ b/docs/library/applet/index.md @@ -3,7 +3,6 @@ ```{toctree} :hidden: -cli applet device ``` @@ -13,8 +12,7 @@ Flesh this out ``` The following APIs are available for use within Squishy applets. They allow the applet -to register various components with the Squishy framework, such as the -[CLI]. Out of the three subsystems, only the CLI is mandatory, the GUI and REPL are optional. +to register various components with the Squishy framework. For details on how to write Squishy applets, see the [Applets Tutorial] for a walk-through. diff --git a/docs/library/gateware/applet/index.md b/docs/library/gateware/applet/index.md new file mode 100644 index 00000000..4987ae4b --- /dev/null +++ b/docs/library/gateware/applet/index.md @@ -0,0 +1,12 @@ +# Applets + +```{toctree} +:hidden: + + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.applet + :members: +``` diff --git a/docs/library/gateware/bootloader/index.md b/docs/library/gateware/bootloader/index.md index e4bcfd2b..cbbf8d85 100644 --- a/docs/library/gateware/bootloader/index.md +++ b/docs/library/gateware/bootloader/index.md @@ -1,4 +1,4 @@ -# `squishy.gateware.bootloader` +# Bootloader ```{toctree} :hidden: @@ -8,23 +8,16 @@ ```{eval-rst} .. automodule:: squishy.gateware.bootloader - -``` - - -```{eval-rst} -.. automodule:: squishy.gateware.bootloader.bitstream :members: ``` ```{eval-rst} -.. automodule:: squishy.gateware.bootloader.dfu +.. automodule:: squishy.gateware.bootloader.rev1 :members: ``` - ```{eval-rst} -.. automodule:: squishy.gateware.bootloader.rev1 +.. automodule:: squishy.gateware.bootloader.rev2 :members: ``` diff --git a/docs/library/gateware/core/index.md b/docs/library/gateware/core/index.md index 5477f1e9..62c901ce 100644 --- a/docs/library/gateware/core/index.md +++ b/docs/library/gateware/core/index.md @@ -3,10 +3,6 @@ ```{toctree} :hidden: -pll -scsi -spi -uart ``` ```{eval-rst} diff --git a/docs/library/gateware/core/pll.md b/docs/library/gateware/core/pll.md deleted file mode 100644 index 548b82eb..00000000 --- a/docs/library/gateware/core/pll.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.core.pll` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.core.pll - :members: - -``` diff --git a/docs/library/gateware/core/scsi.md b/docs/library/gateware/core/scsi.md deleted file mode 100644 index 4a65b4e8..00000000 --- a/docs/library/gateware/core/scsi.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.core.scsi` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.core.scsi - :members: - -``` diff --git a/docs/library/gateware/core/spi.md b/docs/library/gateware/core/spi.md deleted file mode 100644 index 1b7c06b4..00000000 --- a/docs/library/gateware/core/spi.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.core.spi` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.core.spi - :members: - -``` diff --git a/docs/library/gateware/core/uart.md b/docs/library/gateware/core/uart.md deleted file mode 100644 index 4a76e060..00000000 --- a/docs/library/gateware/core/uart.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.core.uart` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.core.uart - :members: - -``` diff --git a/docs/library/gateware/index.md b/docs/library/gateware/index.md index a55bfc48..3eed18dc 100644 --- a/docs/library/gateware/index.md +++ b/docs/library/gateware/index.md @@ -3,12 +3,14 @@ ```{toctree} :hidden: +applet/index +bootloader/index core/index +peripherals/index platform/index scsi/index usb/index -quirks/index -bootloader/index +test/ ``` diff --git a/docs/library/gateware/peripherals/index.md b/docs/library/gateware/peripherals/index.md new file mode 100644 index 00000000..f989ecd1 --- /dev/null +++ b/docs/library/gateware/peripherals/index.md @@ -0,0 +1,21 @@ +# Peripherals + +```{toctree} +:hidden: + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.peripherals + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.peripherals.spi + :members: +``` + +```{eval-rst} +.. automodule:: squishy.gateware.peripherals.flash + :members: +``` diff --git a/docs/library/gateware/platform/index.md b/docs/library/gateware/platform/index.md index 8d35975a..bd66306a 100644 --- a/docs/library/gateware/platform/index.md +++ b/docs/library/gateware/platform/index.md @@ -1,15 +1,14 @@ -# `squishy.gateware.platform` +# Hardware Platforms ```{toctree} :hidden: rev1 rev2 -mixins resources ``` ```{eval-rst} .. automodule:: squishy.gateware.platform - + :members: ``` diff --git a/docs/library/gateware/platform/mixins.md b/docs/library/gateware/platform/mixins.md deleted file mode 100644 index c39e7bdd..00000000 --- a/docs/library/gateware/platform/mixins.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.platform.mixins` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.platform.mixins - :members: - -``` diff --git a/docs/library/gateware/platform/resources.md b/docs/library/gateware/platform/resources.md index 436f9c8e..5b10ca19 100644 --- a/docs/library/gateware/platform/resources.md +++ b/docs/library/gateware/platform/resources.md @@ -1,4 +1,4 @@ -# `squishy.gateware.platform.resources` +# Platform Resources ```{toctree} :hidden: @@ -8,9 +8,3 @@ .. automodule:: squishy.gateware.platform.resources ``` - -```{eval-rst} -.. automodule:: squishy.gateware.platform.resources.scsi - :members: - -``` diff --git a/docs/library/gateware/platform/rev1.md b/docs/library/gateware/platform/rev1.md index bd734750..95b037da 100644 --- a/docs/library/gateware/platform/rev1.md +++ b/docs/library/gateware/platform/rev1.md @@ -1,4 +1,4 @@ -# `squishy.gateware.platform.rev1` +# Rev1 ```{toctree} :hidden: @@ -8,4 +8,6 @@ .. automodule:: squishy.gateware.platform.rev1 :members: +.. autoclass:: squishy.gateware.platform.rev1.Rev1ClockDomainGenerator + ``` diff --git a/docs/library/gateware/platform/rev2.md b/docs/library/gateware/platform/rev2.md index 85411d54..21749937 100644 --- a/docs/library/gateware/platform/rev2.md +++ b/docs/library/gateware/platform/rev2.md @@ -1,4 +1,4 @@ -# `squishy.gateware.platform.rev2` +# Rev2 ```{toctree} :hidden: @@ -8,4 +8,7 @@ .. automodule:: squishy.gateware.platform.rev2 :members: +.. autoclass:: squishy.gateware.platform.rev2.Rev2ClockDomainGenerator + + ``` diff --git a/docs/library/gateware/quirks/index.md b/docs/library/gateware/quirks/index.md deleted file mode 100644 index d40568f3..00000000 --- a/docs/library/gateware/quirks/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# `squishy.gateware.quirks` - -```{toctree} -:hidden: - - -``` - -```{eval-rst} -.. automodule:: squishy.gateware.quirks - -``` - -```{eval-rst} -.. automodule:: squishy.gateware.quirks.usb - :members: - -``` - -```{eval-rst} -.. automodule:: squishy.gateware.quirks.usb.windows - :members: - -``` diff --git a/docs/library/gateware/scsi/common.md b/docs/library/gateware/scsi/common.md deleted file mode 100644 index bd37e974..00000000 --- a/docs/library/gateware/scsi/common.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.common` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.common - :members: - -``` diff --git a/docs/library/gateware/scsi/device.md b/docs/library/gateware/scsi/device.md deleted file mode 100644 index b6875062..00000000 --- a/docs/library/gateware/scsi/device.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.device` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.device - :members: - -``` diff --git a/docs/library/gateware/scsi/index.md b/docs/library/gateware/scsi/index.md index 94d015dc..9df94f58 100644 --- a/docs/library/gateware/scsi/index.md +++ b/docs/library/gateware/scsi/index.md @@ -1,14 +1,9 @@ -# `squishy.gateware.scsi` +# SCSI ```{toctree} :hidden: -common -device -initiator -scsi1 -scsi2 -scsi3 +quirks/index ``` ```{eval-rst} diff --git a/docs/library/gateware/scsi/initiator.md b/docs/library/gateware/scsi/initiator.md deleted file mode 100644 index 5e3cc373..00000000 --- a/docs/library/gateware/scsi/initiator.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.initiator` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.initiator - :members: - -``` diff --git a/docs/library/gateware/scsi/quirks/index.md b/docs/library/gateware/scsi/quirks/index.md new file mode 100644 index 00000000..fb39dc3b --- /dev/null +++ b/docs/library/gateware/scsi/quirks/index.md @@ -0,0 +1,11 @@ +# SCSI Quirks + +```{toctree} +:hidden: + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.scsi.quirks + +``` diff --git a/docs/library/gateware/scsi/scsi1.md b/docs/library/gateware/scsi/scsi1.md deleted file mode 100644 index fad69ebb..00000000 --- a/docs/library/gateware/scsi/scsi1.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.scsi1` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.scsi1 - :members: - -``` diff --git a/docs/library/gateware/scsi/scsi2.md b/docs/library/gateware/scsi/scsi2.md deleted file mode 100644 index 50756e23..00000000 --- a/docs/library/gateware/scsi/scsi2.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.scsi2` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.scsi2 - :members: - -``` diff --git a/docs/library/gateware/scsi/scsi3.md b/docs/library/gateware/scsi/scsi3.md deleted file mode 100644 index c2754f88..00000000 --- a/docs/library/gateware/scsi/scsi3.md +++ /dev/null @@ -1,11 +0,0 @@ -# `squishy.gateware.scsi.scsi3` - -```{toctree} -:hidden: -``` - -```{eval-rst} -.. automodule:: squishy.gateware.scsi.scsi3 - :members: - -``` diff --git a/docs/library/gateware/test.md b/docs/library/gateware/test.md new file mode 100644 index 00000000..d91ace14 --- /dev/null +++ b/docs/library/gateware/test.md @@ -0,0 +1,8 @@ +# Test Harness + + +```{eval-rst} +.. automodule:: squishy.support.test + :members: + +``` diff --git a/docs/library/gateware/usb/index.md b/docs/library/gateware/usb/index.md index 665013d9..3d5cd8e7 100644 --- a/docs/library/gateware/usb/index.md +++ b/docs/library/gateware/usb/index.md @@ -1,9 +1,9 @@ -# `squishy.gateware.usb` +# USB ```{toctree} :hidden: - +quirks/index ``` ```{eval-rst} @@ -13,18 +13,5 @@ ```{eval-rst} .. automodule:: squishy.gateware.usb.dfu - :members: - -``` - -```{eval-rst} -.. automodule:: squishy.gateware.usb.rev1 - :members: - -``` - -```{eval-rst} -.. automodule:: squishy.gateware.usb.rev2 - :members: - + :members: ``` diff --git a/docs/library/gateware/usb/quirks/index.md b/docs/library/gateware/usb/quirks/index.md new file mode 100644 index 00000000..2311d365 --- /dev/null +++ b/docs/library/gateware/usb/quirks/index.md @@ -0,0 +1,16 @@ +# USB Quirks + +```{toctree} +:hidden: + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.usb.quirks + +``` + +```{eval-rst} +.. automodule:: squishy.gateware.usb.quirks.windows + :members: +``` diff --git a/docs/library/index.md b/docs/library/index.md index 44c9e75b..122b524d 100644 --- a/docs/library/index.md +++ b/docs/library/index.md @@ -2,6 +2,8 @@ ```{toctree} :hidden: + +applet/index gateware/index python/index diff --git a/docs/library/python/index.md b/docs/library/python/index.md index ca12c68e..23c35c44 100644 --- a/docs/library/python/index.md +++ b/docs/library/python/index.md @@ -3,12 +3,6 @@ ```{toctree} :hidden: -commands/index -device -messages -``` - -```{eval-rst} -.. automodule:: squishy.scsi +scsi/index ``` diff --git a/docs/library/python/commands/common.md b/docs/library/python/scsi/commands/common.md similarity index 75% rename from docs/library/python/commands/common.md rename to docs/library/python/scsi/commands/common.md index 686e913a..0abdc087 100644 --- a/docs/library/python/commands/common.md +++ b/docs/library/python/scsi/commands/common.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.common` +# Common ```{toctree} :hidden: diff --git a/docs/library/python/commands/direct.md b/docs/library/python/scsi/commands/direct.md similarity index 75% rename from docs/library/python/commands/direct.md rename to docs/library/python/scsi/commands/direct.md index e96d1539..1a580caf 100644 --- a/docs/library/python/commands/direct.md +++ b/docs/library/python/scsi/commands/direct.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.direct` +# Direct-Access ```{toctree} :hidden: diff --git a/docs/library/python/commands/index.md b/docs/library/python/scsi/commands/index.md similarity index 88% rename from docs/library/python/commands/index.md rename to docs/library/python/scsi/commands/index.md index 18f05f26..295b55c0 100644 --- a/docs/library/python/commands/index.md +++ b/docs/library/python/scsi/commands/index.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands` +# Commands ```{toctree} :hidden: diff --git a/docs/library/python/commands/printer.md b/docs/library/python/scsi/commands/printer.md similarity index 75% rename from docs/library/python/commands/printer.md rename to docs/library/python/scsi/commands/printer.md index 9c364398..b8b8fa00 100644 --- a/docs/library/python/commands/printer.md +++ b/docs/library/python/scsi/commands/printer.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.printer` +# Printer ```{toctree} :hidden: diff --git a/docs/library/python/commands/processor.md b/docs/library/python/scsi/commands/processor.md similarity index 74% rename from docs/library/python/commands/processor.md rename to docs/library/python/scsi/commands/processor.md index 5dd4515a..63c994c3 100644 --- a/docs/library/python/commands/processor.md +++ b/docs/library/python/scsi/commands/processor.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.processor` +# Processor ```{toctree} :hidden: diff --git a/docs/library/python/commands/ro_direct.md b/docs/library/python/scsi/commands/ro_direct.md similarity index 74% rename from docs/library/python/commands/ro_direct.md rename to docs/library/python/scsi/commands/ro_direct.md index 1d898994..a3679bb6 100644 --- a/docs/library/python/commands/ro_direct.md +++ b/docs/library/python/scsi/commands/ro_direct.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.ro_direct` +# Read-Only Direct-Access ```{toctree} :hidden: diff --git a/docs/library/python/commands/sequential.md b/docs/library/python/scsi/commands/sequential.md similarity index 74% rename from docs/library/python/commands/sequential.md rename to docs/library/python/scsi/commands/sequential.md index 57b869b7..236b520b 100644 --- a/docs/library/python/commands/sequential.md +++ b/docs/library/python/scsi/commands/sequential.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.sequential` +# Sequential ```{toctree} :hidden: diff --git a/docs/library/python/commands/worm.md b/docs/library/python/scsi/commands/worm.md similarity index 76% rename from docs/library/python/commands/worm.md rename to docs/library/python/scsi/commands/worm.md index f437cfd2..43d30796 100644 --- a/docs/library/python/commands/worm.md +++ b/docs/library/python/scsi/commands/worm.md @@ -1,4 +1,4 @@ -# `squishy.scsi.commands.worm` +# WORM ```{toctree} :hidden: diff --git a/docs/library/python/device.md b/docs/library/python/scsi/device.md similarity index 79% rename from docs/library/python/device.md rename to docs/library/python/scsi/device.md index 011f2183..0afe2637 100644 --- a/docs/library/python/device.md +++ b/docs/library/python/scsi/device.md @@ -1,4 +1,4 @@ -# `squishy.scsi.device` +# Device ```{toctree} :hidden: diff --git a/docs/library/python/scsi/index.md b/docs/library/python/scsi/index.md new file mode 100644 index 00000000..d6e67938 --- /dev/null +++ b/docs/library/python/scsi/index.md @@ -0,0 +1,14 @@ +# SCSI + +```{toctree} +:hidden: + +commands/index +device +messages +``` + +```{eval-rst} +.. automodule:: squishy.scsi + +``` diff --git a/docs/library/python/messages.md b/docs/library/python/scsi/messages.md similarity index 78% rename from docs/library/python/messages.md rename to docs/library/python/scsi/messages.md index 43210efe..61778281 100644 --- a/docs/library/python/messages.md +++ b/docs/library/python/scsi/messages.md @@ -1,4 +1,4 @@ -# `squishy.scsi.messages` +# Messages ```{toctree} :hidden: diff --git a/noxfile.py b/noxfile.py index d80598a3..851c42ce 100644 --- a/noxfile.py +++ b/noxfile.py @@ -91,6 +91,7 @@ def typecheck(session: Session) -> None: session.install('mypy') session.install('lxml') + session.install('construct-typing') session.install('.') session.run( 'mypy', '--non-interactive', '--install-types', '--pretty', diff --git a/setup.py b/setup.py index 2258cfd3..e0882b89 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: BSD-3-Clause -from typing import Callable, TypeAlias, Type -from setuptools import setup, find_packages -from pathlib import Path +from typing import Callable, TypeAlias +from setuptools import setup, find_packages +from pathlib import Path -ScmVersion: TypeAlias = Type['setuptools_scm.version.ScmVersion'] +ScmVersion: TypeAlias = 'setuptools_scm.version.ScmVersion' REPO_ROOT = Path(__file__).parent README_FILE = (REPO_ROOT / 'README.md') @@ -22,20 +22,6 @@ def scheme(version: ScmVersion) -> str: 'local_scheme': scheme } -def doc_ver() -> str: - try: - from setuptools_scm.git import parse as parse_git - except ImportError: - return '' - - git = parse_git('.') - if not git: - return '' - elif git.exact: - return git.format_with('{tag}') - else: - return 'latest' - setup( name = 'Squishy', use_scm_version = vcs_ver(), @@ -43,7 +29,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..c15e06c7 100644 --- a/squishy/actions/__init__.py +++ b/squishy/actions/__init__.py @@ -1,20 +1,25 @@ # 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 ..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 +28,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 +95,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 +124,349 @@ 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, build_dir: Path, cacheable: bool = True + ) -> 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. + + build_dir : Path + The requested build directory, is overridden by the `--build-dir` cli option + + cacheable : bool + Whether or not to process cache-related options. Default: True + + 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) 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}.tim.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_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 not args.no_routed_netlist: + pnr_opts.append(f'--write {name}.pnr.json') - 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') + + log.info(f'Using platform version: {platform.revision_str}') + log.info(f' Device: {platform.device}-{platform.package}') - # Actually do the build + # 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(plan) + + # Run the build + if prod is None: + log.info('Bitstream is not cached, this might take [yellow][i]a while[/][/]', extra = { 'markup': True }) + progress.update(task, description = 'Building bitstream') + prod = plan.execute_local(build_dir) + # If we're allowed to, cache the products and then return that cached version + if not skip_cache: + progress.update(task, description = 'Caching build') + prod = self._cache.store(name, prod, plan, platform) + + progress.remove_task(task) + # 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) - def register_synth_args(self, parser: ArgumentParser, cacheable: bool = False) -> None: - ''' Register the common gateware options ''' + return prod + + + def register_synth_args(self, parser: ArgumentParser, cacheable: bool = True) -> None: + ''' + 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. Default: True + ''' 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' + help = 'Have nextpnr output a detailed timing report' ) pnr_options.add_argument( - '--routed-svg', - type = Path, - default = None, - help = 'Write a render of the routing to an SVG' - ) - - pnr_options.add_argument( - '--routed-json', - type = Path, - default = None, - help = 'Write the PnR output json for viewing in nextpnr after PnR' + '--no-routed-netlist', + action = 'store_true', + help = 'Don\'t 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' - ) - - pnr_options.add_argument( - '--hunt-n-peck', - action = 'store_true', - help = 'If PnR fails with given seed, try to find one that passes timing' + help = 'The place and route RNG seed to use.' ) - # 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, name: str, slot: int, build_dir: Path, dev: SquishyDevice | None = None) -> str: + ''' + Build up a message that accuratly displays how to flash a built artifact to the given Squishy device. + + Parameters + ---------- + name : str + The name of the artifact that was generated. + + slot : int + The DFU slot/alt-mode to specify. + + build_dir : Path + The gateware build directory that was used + + 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_file = build_dir / name + + 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}.tim.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..3dc06a2c 100644 --- a/squishy/actions/applet.py +++ b/squishy/actions/applet.py @@ -1,213 +1,174 @@ # 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, SQUISHY_BUILD_APPLET +from ..device import SquishyDevice +from ..core.reflection import collect_members, is_applet +from ..gateware import Squishy as SquishyGateware + +__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') + self.register_synth_args(parser) - # 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 + + # Setup our requested build dir + build_dir = SQUISHY_BUILD_APPLET + if args.build_dir is not None: + build_dir = Path(args.build_dir) + + # 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 = f'squishy_applet_{applet.name}_v{plat.revision_str}' + + # 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, build_dir) + + f_name = f'{applet_name}.{plat.bitstream_suffix}' + # 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(f_name, slot, build_dir, dev)) return 0 + + # If we *are* programming the device, then with Progress( SpinnerColumn(), TextColumn('[progress.description]{task.description}'), @@ -215,17 +176,23 @@ 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): 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(f_name, 'b')) + + # 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 \'{f_name}\'') + 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..50cbdffd 100644 --- a/squishy/actions/provision.py +++ b/squishy/actions/provision.py @@ -1,139 +1,121 @@ # 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_BOOT +from ..device import SquishyDevice +from ..gateware import 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) + build_dir = SQUISHY_BUILD_BOOT + if args.build_dir is not None: + build_dir = Path(args.build_dir) + + # TODO(aki): Booloader opts etc + bootloader = SquishyBootloader( + serial_number = serial, revision = plat.revision + ) + + boot_name = f'squishy_boot_v{plat.revision_str}' 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, build_dir, cacheable = False) 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: + f_name = f'{boot_name}.{plat.bitstream_suffix}' + if args.build_only: + log.info(self.dfu_util_msg(f_name, 0, build_dir, 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 + image = plat.pack_artifact(prod.get(f_name)) + + 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 +123,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..5252b0d3 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,89 @@ 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!') + return 0 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..d9fbeb81 100644 --- a/squishy/core/cache.py +++ b/squishy/core/cache.py @@ -1,106 +1,135 @@ # SPDX-License-Identifier: BSD-3-Clause -import logging as log -from pathlib import Path -from lzma import LZMACompressor -from shutil import rmtree +''' -from torii.build.run import LocalBuildProducts +''' -from ..config import SQUISHY_APPLET_CACHE +import logging as log +from io import BytesIO +from tarfile import open as tf_open +from tarfile import TarInfo + +from torii.build.run import BuildPlan, BuildProducts, LocalBuildProducts + +from ..paths import SQUISHY_ASSET_CACHE +from ..gateware.platform import SquishyPlatformType __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') +class SquishyCache: + ''' + Squishy on-disk bitstream cache. + + ''' + + ARCHIVE_ASSETS = ( + # Synthesis Input + 'debug.v', 'il', 'ys', + # PnR Output + 'tim', 'tim.json', 'pnr.json', + ) + + def __init__(self) -> None: + self._cache_root = SQUISHY_ASSET_CACHE + + def get(self, plan: BuildPlan) -> BuildProducts | None: + ''' + Get the cached version of the built gateware + + Parameters + ---------- + plan : BuildPlan + The generated build plan from Torii. + + Returns + ------- + BuildProducts | None + If found in the cache, an instance of LocalBuildProducts, otherwise None + + ''' + + plan_digest = plan.digest(size = 32).hex() + cache_dir = self._cache_root / plan_digest[0:2] / plan_digest + + if not cache_dir.exists(): return None - log.debug('Bitstream found') + log.debug(f'Found cache entry \'{plan_digest}\'') + + return LocalBuildProducts(cache_dir) + + def store(self, name: str, products: BuildProducts, plan: BuildPlan, plat: SquishyPlatformType) -> 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. + + products : BuildPlan + The plan that was used to produce `products` + + plat : SquishyPlatformType + The platform that we built against + + Returns + ------- + BuildProducts + The re-homed BuildProducts from the cache rather than the build directory. + + ''' + + plan_digest = plan.digest(size = 32).hex() + cache_dir = self._cache_root / plan_digest[0:2] / plan_digest + + log.debug(f'Caching build assets for \'{name}\'') + log.debug(f'Cache path: \'{cache_dir}\'') + + if cache_dir.exists(): + log.warning(f'Cache collision on asset entry \'{plan_digest}\'') + for item in cache_dir.iterdir(): + item.unlink() + else: + log.debug('No cache entry found, creating') + cache_dir.mkdir(parents = True) + - return { - 'name' : bitstream_name, - 'products': LocalBuildProducts(str(cache_dir)) - } + # Archive build assets + arc_name = f'{name}.src.tar.xz' + arc_path = cache_dir / arc_name - def store(self, digest: str, products: LocalBuildProducts, name: str) -> None: - ''' Store the synth products in the cache ''' + log.debug(f'Archiving build assets to cache in \'{arc_name}\'') - bitstream_name = f'{digest}.bin' - cache_dir = self._get_cache_dir(digest) - bitstream = cache_dir / bitstream_name + with tf_open(arc_path, 'w:xz') as arc: + for asset in self.ARCHIVE_ASSETS: + f_name = f'{name}.{asset}' + try: + data = products.get(f_name, 'b') - log.debug(f'Caching bitstream \'{name}.bin\' in {cache_dir}') - log.debug(f'New bitstream name: \'{bitstream_name}\'') + log.debug(f' => \'{f_name}\'') - with open(bitstream, 'wb') as bit: - bit.write(products.get(f'{name}.bin')) + info = TarInfo(f_name) + info.size = len(data) - if self.cache_rtl: - for rtl_ext in ('debug.v', 'il'): - rtl_name = f'{digest}.{rtl_ext}.xz' - rtl = cache_dir / rtl_name + arc.addfile(info, BytesIO(data)) + except OSError: + continue - log.debug(f'Caching RTL \'{name}.{rtl_ext}\' in {cache_dir}') - log.debug(f'New RTL name: \'{rtl_name}\'') + # Copy the timing/utilization report out before we re-home + log.debug('Caching PnR Utilization report outside of asset archive') + with (cache_dir / f'{name}.tim.json').open('wb') as bitstream: + bitstream.write(products.get(f'{name}.tim.json', 'b')) - cpr = LZMACompressor() + # Cache the bitstream + log.debug('Caching bitstream') + f_name = f'{name}.{plat.bitstream_suffix}' + with (cache_dir / f_name).open('wb') as bitstream: + bitstream.write(products.get(f_name, 'b')) - with open(rtl, 'wb') as r: - r.write(cpr.compress(products.get(f'{name}.{rtl_ext}'))) - r.write(cpr.flush()) + return LocalBuildProducts(cache_dir) 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..9bb16102 --- /dev/null +++ b/squishy/device.py @@ -0,0 +1,809 @@ +# 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 _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: bytes, altmode: int, progress: Progress | None = None) -> bool: + ''' + Push firmware/gateware to device. + + Parameters + ---------- + data : bytes + 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..46e4188b 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,53 @@ ''' -''' - 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', + + '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.pll = platform.clk_domain_generator() m.submodules.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..6a95f73b 100644 --- a/squishy/gateware/bootloader/__init__.py +++ b/squishy/gateware/bootloader/__init__.py @@ -1,9 +1,240 @@ # SPDX-License-Identifier: BSD-3-Clause -__doc__ = '''\ +''' ''' -__all__ = ( +from torii import Elaboratable, Module, ResetSignal +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 ..usb.dfu import DFURequestHandler +from ..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_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..c100a87b 100644 --- a/squishy/gateware/bootloader/rev1.py +++ b/squishy/gateware/bootloader/rev1.py @@ -1,156 +1,198 @@ # 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, Const, Memory, Cat +from torii.lib.cdc import FFSynchronizer, PulseSynchronizer +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 + Input: FPGA reboot trigger from DFU. -POR -> Slot1 (Squishy Applet) - Slot0 (DFU Bootloader) + slot_selection : Signal(2) + Input: 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_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._bit_fifo = fifo + self.trigger_reboot = Signal() -class Bootloader(Elaboratable): - ''' ''' - def __init__(self, *, serial_number: str) -> None: - self._serial_number = serial_number + self.slot_selection = Signal(2) + self.slot_changed = Signal() + self.slot_ack = Signal() - def elaborate(self, platform: SquishyPlatform | None) -> Module: + self.dl_start = Signal() + self.dl_finish = Signal() + self.dl_ready = Signal() + self.dl_done = Signal() + self.dl_size = Signal(16) + + def elaborate(self, platform: SquishyPlatformType | None) -> Module: m = Module() - trigger_reboot = Signal() - slot_select = Signal(2) - - m.domains.usb = ClockDomain() - ulpi = platform.request('ulpi', 0) - m.submodules.dev = dev = USBDevice(bus = ulpi, handle_clocking = True) - m.submodules += Instance( - 'SB_WARMBOOT', - i_BOOT = trigger_reboot, - i_S0 = slot_select[0], - i_S1 = slot_select[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) + trigger_reboot = Signal.like(self.trigger_reboot) + + slot_selection = Signal.like(self.slot_selection) + slot_changed = Signal.like(self.slot_changed) + slot_ack = Signal.like(self.slot_ack) + active_slot = Signal.like(slot_selection) + + dl_ready = Signal.like(self.dl_ready) + dl_ready_delay = Signal.like(dl_ready) + + slot_rom = self._mk_rom(platform.flash.geometry) + m.submodules.slots = slots = slot_rom.read_port(transparent = False) + + flash = SPIFlash(flash_resource = ('spi_flash_1x', 0), flash_geometry = platform.flash.geometry, fifo = self._bit_fifo) + + m.submodules.flash = flash + + # Set up the iCE40 warmboot if we're not in Sim + if not hasattr(platform, 'SIM_PLATFORM'): + m.submodules.warmboot = Instance( + 'SB_WARMBOOT', + i_BOOT = trigger_reboot, + i_S0 = slot_selection[0], + i_S1 = slot_selection[1], ) - ep0.add_request_handler(dfu_handler) - ep0.add_request_handler(win_handler) - ep0.add_request_handler(StallOnlyRequestHandler(stall_condition = stall_condition)) + m.d.comb += [ + flash.resetAddrs.eq(0), + dl_ready.eq(0), + slot_ack.eq(0), + ] + + m.d.sync += [ + dl_ready_delay.eq(dl_ready), + ] + + with m.FSM(name = 'storage'): + with m.State('RESET'): + m.d.sync += [ active_slot.eq(0), ] + with m.If(flash.ready): + m.next = 'READ_SLOT_DATA' + + with m.State('READ_SLOT_DATA'): + m.d.comb += [ slots.addr.eq(Cat(Const(0, 1), active_slot)), ] + m.next = 'READ_SLOT_BEGIN' + + with m.State('READ_SLOT_BEGIN'): + m.d.comb += [ slots.addr.eq(Cat(Const(1, 1), active_slot)), ] + m.d.sync += [ flash.startAddr.eq(slots.data), ] + m.next = 'READ_SLOT_END' + + with m.State('READ_SLOT_END'): + m.d.sync += [ flash.endAddr.eq(slots.data), ] + m.d.comb += [ + flash.resetAddrs.eq(1), + slot_ack.eq(1), + ] + m.next = 'IDLE' + + with m.State('IDLE'): + m.d.comb += [ dl_ready.eq(1), ] + with m.If(slot_changed): + m.d.sync += [ active_slot.eq(slot_selection), ] + m.next = 'READ_SLOT_DATA' + + # We don't need to sync reboot if we are in sim + if not hasattr(platform, 'SIM_PLATFORM'): + m.submodules.ffs_reboot = FFSynchronizer(self.trigger_reboot, trigger_reboot) + + m.submodules.ffs_dl_finish = FFSynchronizer(self.dl_finish, flash.finish) + m.submodules.ffs_dl_size = FFSynchronizer(self.dl_size, flash.byteCount) + m.submodules.ffs_dl_start = FFSynchronizer(self.dl_start, flash.start) + m.submodules.ffs_slot_chg = FFSynchronizer(self.slot_changed, slot_changed) + m.submodules.ffs_slot_sel = FFSynchronizer(self.slot_selection, slot_selection) + m.submodules.ffs_dl_done = FFSynchronizer(flash.done, self.dl_done, o_domain = 'usb') + + m.submodules.ps_dl_ready = ps_dl_ready = PulseSynchronizer(i_domain = 'sync', o_domain = 'usb') + m.submodules.ps_slot_ack = ps_slot_ack = PulseSynchronizer(i_domain = 'sync', o_domain = 'usb') + 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) + ps_dl_ready.i.eq(dl_ready & ~dl_ready_delay), + self.dl_ready.eq(ps_dl_ready.o), + + ps_slot_ack.i.eq(slot_ack), + self.slot_ack.eq(ps_slot_ack.o), ] 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..dfcd6eb2 --- /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/scsi/scsi3/__init__.py b/squishy/gateware/peripherals/__init__.py similarity index 60% rename from tests/gateware/scsi/scsi3/__init__.py rename to squishy/gateware/peripherals/__init__.py index 310f6dd9..40c032df 100644 --- a/tests/gateware/scsi/scsi3/__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/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/platform/__init__.py b/squishy/gateware/platform/__init__.py index 9adbe9dd..18eb2230 100644 --- a/squishy/gateware/platform/__init__.py +++ b/squishy/gateware/platform/__init__.py @@ -1,21 +1,181 @@ # 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 : str + The canonicalize revision as a string in the form of 'major.minor'. + + bitstream_suffix : str + The suffix of the FPGA bitstream that is generated when gateware is built for this platform. + + 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 bitstream_suffix(self) -> str: + ''' The suffix of the FPGA bitstream that is generated when gateware is built for this platform ''' + raise NotImplementedError('SquishyPlatform requires a revision to be set') + + @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..e2db4ce8 100644 --- a/squishy/gateware/platform/rev1.py +++ b/squishy/gateware/platform/rev1.py @@ -1,80 +1,151 @@ # 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. + +''' + +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. - -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. +class Rev1ClockDomainGenerator(Elaboratable): + ''' + Clock domain and PLL generator for Squishy rev1. + 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. -class SquishyRev1(SquishyPlatform, ICE40Platform): ''' - Squishy hardware Revision 1 - 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. + def __init__(self) -> None: + self.pll_locked = Signal() - The design files for this version of the hardware can be found - `in the git repo `_ under - the `rev1` tree. + def elaborate(self, platform: 'SquishyRev1') -> Module: + m = Module() + # The clock domains we want to have + m.domains.usb = ClockDomain() + m.domains.sync = ClockDomain() + + # Set the clock to no-longer be a global so we can latch onto it. + platform.lookup(platform.default_clk).attrs['GLOBAL'] = False + + # 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_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. - device = 'iCE40HX8K' - package = 'BG121' - default_clk = 'clk' - toolchain = 'IceStorm' + 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. - revision = 1.0 + The hardware `design files `_ can be found + in the hardware repository on `GitHub `_ under the ``release/rev1`` tree. - clock_domain_generator = ICE40ClockDomainGenerator + ''' - pll_config = { - 'freq' : 1e8, - 'divr' : 2, - 'divf' : 49, - 'divq' : 3, - 'frange': 1, - } + device = 'iCE40HX8K' + package = 'BG121' + default_clk = 'clk' + toolchain = 'IceStorm' + bitstream_suffix = 'bin' - flash = { - 'geometry': FlashGeometry( + revision = (1, 0) + + 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, } - } + ) - bootloader_module = iCE40Bootloader + pll_cfg = ICE40PLLConfig( + divr = 2, + divf = 34, + divq = 3, + filter_range = 1, + ofreq = 70 + ) + + clk_domain_generator = Rev1ClockDomainGenerator resources = [ Resource('clk', 0, @@ -108,20 +179,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 +219,114 @@ class SquishyRev1(SquishyPlatform, ICE40Platform): ] connectors = [] + + def __init__(self) -> None: + # Force us to always use the FOSS toolchain + super().__init__(toolchain = 'IceStorm') + + 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..a0f70d0f 100644 --- a/squishy/gateware/platform/rev2.py +++ b/squishy/gateware/platform/rev2.py @@ -1,81 +1,201 @@ # 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. + +''' +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) -> None: + 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 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. + 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. - The design files for this version of the hardware can be found - `in the git repo `_ under - the `boards/squishy` tree. + The hardware `design files `_ can be + found in the hardware repository on `GitHub `_ under + the ``release/rev2-evt`` 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. - device = 'LFE5UM5G-45F' - speed = '8' - package = 'BG381' - default_clk = 'clk' - toolchain = 'Trellis' + ''' - revision = 2.0 + device = 'LFE5UM5G-45F' + speed = '8' + package = 'BG381' + default_clk = 'clk' + toolchain = 'Trellis' + bitstream_suffix = 'bit' - 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 +257,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 +324,56 @@ class SquishyRev2(SquishyPlatform, ECP5Platform): ), ] + connectors = [] - connectors = [ + def __init__(self) -> None: + # Force us to always use the FOSS + super().__init__(toolchain = 'Trellis') - ] + 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/__init__.py b/squishy/gateware/scsi/__init__.py index 7170cecb..9747096b 100644 --- a/squishy/gateware/scsi/__init__.py +++ b/squishy/gateware/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/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/quirks/__init__.py b/squishy/gateware/scsi/quirks/__init__.py new file mode 100644 index 00000000..40c032df --- /dev/null +++ b/squishy/gateware/scsi/quirks/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: BSD-3-Clause + +''' + +''' + +__all__ = ( + +) 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 index 77261c82..40c032df 100644 --- a/squishy/gateware/usb/__init__.py +++ b/squishy/gateware/usb/__init__.py @@ -1,13 +1,9 @@ # 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 index fe8a84d5..d58808b2 100644 --- a/squishy/gateware/usb/dfu.py +++ b/squishy/gateware/usb/dfu.py @@ -1,213 +1,391 @@ # 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 torii import Module, Signal, Cat +from torii.hdl.ast import Operator +from torii.lib.fifo import AsyncFIFO -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 -) +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 -__doc__ = '''\ - DFU Stub -''' +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 -@unique -class DFUState(IntEnum): - APP_IDLE = 0 + ''' -@unique -class DFUStatus(IntEnum): - OKAY = 0 + 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_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. - def __init__(self, configuration_num: int, interface_num: int): + slot_ack : Signal + Input: When the `slot_changed` signal was acted on. + + Note + ---- + All of the signals for this module are expected to be on the 'USB' clock domain, + meaning you need an FFSynchronizer for any other domain. + + 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__() - 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: + # 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_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() - interface = self.interface - setup: SetupPacket = interface.setup + # 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.slot_changed.eq(0), + ] + 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'): + 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), + ] + + with m.If(self.dl_ready): + m.next = 'IDLE' + 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.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.type == USBRequestType.STANDARD): - with m.Switch(setup.request): + 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(), - ] + m.d.comb += [ self.send_zlp(), ] with m.If(interface.handshakes_in.ack): - m.d.usb += [ - trigger_reboot.eq(1), - ] + 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), + 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.APP_IDLE), + 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.length == 6): - m.d.comb += [ - transmitter.start.eq(1) - ] + 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.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.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), - transmitter.data[0].eq(DFUState.APP_IDLE), ] + 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.length == 1): - m.d.comb += [ - transmitter.start.eq(1), - ] + 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.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.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), - transmitter.data[0].eq(0), + # 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.length == 1): - m.d.comb += [ - transmitter.start.eq(1), - ] + 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.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.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(), - ] + m.d.comb += [ self.send_zlp(), ] with m.If(interface.handshakes_in.ack): - m.next = 'IDLE' + if self._is_stub: + m.next = 'IDLE' + else: + m.d.usb += [ self.slot_selection.eq(setup_pkt.value[0:8]), ] + 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.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), - ] + 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._configuration) & - ((setup.type == USBRequestType.CLASS) | (setup.type == USBRequestType.STANDARD)) & + (self.interface.active_config == self._config_id) & + ((setup.type == USBRequestType.CLASS) | (setup.type == USBRequestType.STANDARD)) & (setup.recipient == USBRequestRecipient.INTERFACE) & - (setup.index == self._interface_num) + (setup.index == self._interface_id) ) diff --git a/squishy/gateware/usb/quirks/__init__.py b/squishy/gateware/usb/quirks/__init__.py new file mode 100644 index 00000000..68c38318 --- /dev/null +++ b/squishy/gateware/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/usb/quirks/windows.py similarity index 94% rename from squishy/gateware/quirks/usb/windows.py rename to squishy/gateware/usb/quirks/windows.py index 3c151886..c0d1de0b 100644 --- a/squishy/gateware/quirks/usb/windows.py +++ b/squishy/gateware/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/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..6aa258a6 --- /dev/null +++ b/squishy/paths.py @@ -0,0 +1,94 @@ +# 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_ASSET_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. +* ``SQUISHY_BUILD_BOOT`` - The last-built/in-progress builds for Squishy the bootloader. +* ``SQUISHY_BUILD_APPLET`` - The last-built/in-progress builds for Squishy applet 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_ASSET_CACHE', + 'SQUISHY_BUILD_DIR', + 'SQUISHY_BUILD_BOOT', + 'SQUISHY_BUILD_APPLET', + # 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_ASSET_CACHE = (SQUISHY_CACHE / 'assets') +''' Squishy built applet gateware cache (``$SQUISHY_CACHE/assets``) ''' +SQUISHY_BUILD_DIR = (SQUISHY_CACHE / 'build') +''' Squishy build directory (``$SQUISHY_CACHE/build``) ''' +SQUISHY_BUILD_BOOT = (SQUISHY_BUILD_DIR / 'boot') +''' Squishy bootloader build directory (``$SQUISHY_BUILD_DIR/boot``) ''' +SQUISHY_BUILD_APPLET = (SQUISHY_BUILD_DIR / 'applet') +''' Squishy applet build directory (``$SQUISHY_BUILD_DIR/applet``) ''' + +# 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_ASSET_CACHE, + SQUISHY_BUILD_DIR, + SQUISHY_BUILD_BOOT, + SQUISHY_BUILD_APPLET, + # 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..3c613f81 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): @@ -410,7 +412,7 @@ def _type_from_size(cls, size: int): return BitsInteger(size) - def __init__(self, description: str = '', default: Any = None, *, length: int = None) -> None: + def __init__(self, description: str = '', default: Any = None, *, length: int | None = None) -> None: self.description = description self.default = default self.len = length @@ -605,7 +607,7 @@ class SCSICommand(Struct): ) # TODO(aki): Allow for custom control block layouts - def __init__(self, opcode: int, group_code: GroupCode, *subcons, size: int = None, **subconskw) -> None: + def __init__(self, opcode: int, group_code: GroupCode, *subcons, size: int | None = None, **subconskw) -> None: self.opcode = opcode self.group_code = group_code if group_code not in _KNOWN_SIZED_GROUPS: @@ -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..5e53733d 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', @@ -47,7 +44,7 @@ ''' Request Sense -This command requests that the target transfers :ref:`sense data` to the initiator. +This command requests that the target transfers Sense Data to the initiator. The sense data shall be valid for a ``CHECK CONDITION`` status returned on the prior command. It will also be preserved by the target for the initiator until it is retrieved @@ -61,7 +58,7 @@ The ``AllocLen`` field specifies the number of bytes that the initiator has allocated for the returned sense data. An allocation length of zero indicates that four bytes of sense data will be transferred. Any -other value indicates the maximum number of bytes to be transferred. The target must terminate the :ref:`DATA IN` +other value indicates the maximum number of bytes to be transferred. The target must terminate the ``DATA IN`` phase when ``AllocLen`` bytes have been sent to the initiator or when all sense data has been exhausted, whichever is less. @@ -93,7 +90,7 @@ The non-extended sense data is depicted below: -.. sense data: +.. sense-data: +---------+-----------+----+----+---+---+---+---+---+ | .. centered:: Non-extended Sense Data | @@ -418,55 +415,55 @@ +----------+----------+ + + + | Source | Dest | | | | +==========+==========+===============+====================+==============================+ -| ``0x00`` | ``0x01`` | ``0x00`` | :ref:`segment 1` | **Direct to Sequential** | -+----------+----------+-------+-------+--------------------+ + -| ``0x00`` | ``0x02`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x00`` | ``0x03`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x04`` | ``0x01`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x04`` | ``0x02`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x04`` | ``0x03`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x05`` | ``0x01`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x05`` | ``0x02`` | ``0x00`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x05`` | ``0x03`` | ``0x00`` | :ref:`segment 1` | | +| ``0x00`` | ``0x01`` | ``0x00`` | Segment 1 | **Direct to Sequential** | ++----------+----------+-------+-------+ + + +| ``0x00`` | ``0x02`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x00`` | ``0x03`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x04`` | ``0x01`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x04`` | ``0x02`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x04`` | ``0x03`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x05`` | ``0x01`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x05`` | ``0x02`` | ``0x00`` | | | ++----------+----------+-------+-------+ + + +| ``0x05`` | ``0x03`` | ``0x00`` | | | +----------+----------+-------+-------+--------------------+------------------------------+ -| ``0x01`` | ``0x00`` | ``0x01`` | :ref:`segment 1` | **Sequential to Direct** | -+----------+----------+-------+-------+--------------------+ + -| ``0x01`` | ``0x04`` | ``0x01`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x03`` | ``0x00`` | ``0x01`` | :ref:`segment 1` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x03`` | ``0x04`` | ``0x01`` | :ref:`segment 1` | | +| ``0x01`` | ``0x00`` | ``0x01`` | Segment 1 | **Sequential to Direct** | ++----------+----------+-------+-------+ + + +| ``0x01`` | ``0x04`` | ``0x01`` | | | ++----------+----------+-------+-------+ + + +| ``0x03`` | ``0x00`` | ``0x01`` | | | ++----------+----------+-------+-------+ + + +| ``0x03`` | ``0x04`` | ``0x01`` | | | +----------+----------+-------+-------+--------------------+------------------------------+ -| ``0x00`` | ``0x00`` | ``0x02`` | :ref:`segment 2` | **Direct to Direct** | -+----------+----------+-------+-------+--------------------+ + -| ``0x00`` | ``0x04`` | ``0x02`` | :ref:`segment 2` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x04`` | ``0x00`` | ``0x02`` | :ref:`segment 2` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x04`` | ``0x04`` | ``0x02`` | :ref:`segment 2` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x05`` | ``0x00`` | ``0x02`` | :ref:`segment 2` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x05`` | ``0x04`` | ``0x02`` | :ref:`segment 2` | | +| ``0x00`` | ``0x00`` | ``0x02`` | Segment 2 | **Direct to Direct** | ++----------+----------+-------+-------+ + + +| ``0x00`` | ``0x04`` | ``0x02`` | | | ++----------+----------+-------+-------+ + + +| ``0x04`` | ``0x00`` | ``0x02`` | | | ++----------+----------+-------+-------+ + + +| ``0x04`` | ``0x04`` | ``0x02`` | | | ++----------+----------+-------+-------+ + + +| ``0x05`` | ``0x00`` | ``0x02`` | | | ++----------+----------+-------+-------+ + + +| ``0x05`` | ``0x04`` | ``0x02`` | | | +----------+----------+-------+-------+--------------------+------------------------------+ -| ``0x01`` | ``0x01`` | ``0x03`` | :ref:`segment 3` | **Sequential to Sequential** | -+----------+----------+-------+-------+--------------------+ + -| ``0x01`` | ``0x02`` | ``0x03`` | :ref:`segment 3` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x01`` | ``0x03`` | ``0x03`` | :ref:`segment 3` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x03`` | ``0x01`` | ``0x03`` | :ref:`segment 3` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x03`` | ``0x02`` | ``0x03`` | :ref:`segment 3` | | -+----------+----------+-------+-------+--------------------+ + -| ``0x03`` | ``0x03`` | ``0x03`` | :ref:`segment 3` | | +| ``0x01`` | ``0x01`` | ``0x03`` | Segment 3 | **Sequential to Sequential** | ++----------+----------+-------+-------+ + + +| ``0x01`` | ``0x02`` | ``0x03`` | | | ++----------+----------+-------+-------+ + + +| ``0x01`` | ``0x03`` | ``0x03`` | | | ++----------+----------+-------+-------+ + + +| ``0x03`` | ``0x01`` | ``0x03`` | | | ++----------+----------+-------+-------+ + + +| ``0x03`` | ``0x02`` | ``0x03`` | | | ++----------+----------+-------+-------+ + + +| ``0x03`` | ``0x03`` | ``0x03`` | | | +----------+----------+-------+-------+--------------------+------------------------------+ The ``Device Type`` is defined by the values in the :py:class:`squishy.scsi.device.PeripheralDeviceType` enum. @@ -504,8 +501,6 @@ number of blocks that were successfully copied. -.. _segment 1: - The segment descriptor format for copies between direct and sequential access devices are specified below. The descriptor may be repeated up to 256 times within the parameter list as long as it falls within the parameter @@ -558,7 +553,6 @@ The ``Sequential-Access Device Block-Length`` field specifies the block length to be used on the sequential-access logical unit during this segment of the ``COPY`` command. -.. _segment 2: +---------+-----------+----+----+-----+----+-----+-----+-----+ | Segment Descriptor for functions ``0x02`` | @@ -599,8 +593,6 @@ +---------+-----------+----+----+-----+----+-----+-----+-----+ -.. _segment 3: - +---------+-----------+----+----+-----+----+-----+-----+-----+ | Segment Descriptor for functions ``0x03`` | +---------+-----------+----+----+-----+----+-----+-----+-----+ 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..f08fd17b 100644 --- a/squishy/scsi/vid.py +++ b/squishy/scsi/vid.py @@ -1,9 +1,11 @@ # 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 +Source: https://www.t10.org/ftp/t10/vendorid.txt + +It is up-to-date as of 2024-11-10 ''' @@ -33,6 +35,7 @@ 'AERODISK': 'AERO DISK LLC', 'AERONICS': 'Aeronics, Inc.', 'AGFA': 'AGFA', + 'AGILACK': 'Agilack', 'Agilent': 'Agilent Technologies', 'AIC': 'Advanced Industrial Computer, Inc.', 'AIPTEK': 'AIPTEK International Inc.', @@ -148,6 +151,7 @@ 'CDC': 'Control Data or MPI', 'CDP': 'Columbia Data Products', 'CDS': 'Cirrus Data Solutions, Inc.', + 'ceacent': 'Shenzhen Jiahua Zhongli Technology Co.', 'Celsia': 'A M Bromley Limited', 'CenData': 'Central Data Corporation', 'Cereva': 'Cereva Networks Inc.', @@ -355,7 +359,7 @@ 'HYUNWON': 'HYUNWON inc', 'i-cubed': 'i-cubed ltd.', 'I-O DATA': 'I-O DATA DEVICE', - 'IBM': 'International Business Machines', + 'IBM': 'International Business Machines Corporation', 'Icefield': 'Icefield Tools Corporation', 'Iceweb': 'Iceweb Storage Corp', 'ICL': 'ICL', @@ -431,6 +435,7 @@ 'LASERDRV': 'LaserDrive Limited', 'LASERGR': 'Lasergraphics, Inc.', 'LeapFrog': 'LeapFrog Enterprises, Inc.', + 'LeapIO': 'LeapIO', 'LEFTHAND': 'LeftHand Networks (now HPE)', 'Leica': 'Leica Camera AG', 'LENOVO': 'Lenovo', @@ -626,6 +631,7 @@ 'Qi-Hardw': 'Qi Hardware', 'QIC': 'Quarter-Inch Cartridge Drive Standards, Inc.', 'QLogic': 'QLogic Corporation', + 'QLS': 'QLS Consulting', 'QNAP': 'QNAP Systems', 'Qsan': 'QSAN Technology, Inc.', 'QStar': 'QStar Technologies, Inc', @@ -859,6 +865,7 @@ 'YINHE': 'NUDT Computer Co.', 'YIXUN': 'Yixun Electronic Co.,Ltd.', 'YOTTA': 'YottaYotta, Inc.', + 'zadara': 'Zadara Inc.', 'Zarva': 'Zarva Digital Technology Co., Ltd.', 'ZBS': 'SMARTX Corporation', 'ZETTA': 'Zetta Systems, Inc.', @@ -866,3 +873,4 @@ 'ZVAULT': 'Zetavault', 'Zvezda': 'Zvezda LLC', } +''' A VID to Company name mapping for SCSI Vendor IDs ''' 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 72% rename from tests/gateware_test.py rename to squishy/support/test.py index c9cd08a3..021f61c9 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): @@ -25,16 +39,18 @@ def setupReceived(self): yield yield - def sendSetupSetInterface(self): + def sendSetupSetInterface(self, *, interface: int = 0, alt_mode: int = 1): 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 = alt_mode, + index = interface, + 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/bootloader/test_dfu.py b/tests/gateware/bootloader/test_dfu.py deleted file mode 100644 index 9a34fb29..00000000 --- a/tests/gateware/bootloader/test_dfu.py +++ /dev/null @@ -1,129 +0,0 @@ -# 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 squishy.core.flash import FlashGeometry -from squishy.gateware.bootloader.dfu import DFURequestHandler, DFUState - -_DFU_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, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -) - -_SPI_RECORD = Record(( - ('clk', [ - ('o', 1, DIR_FANOUT), - ]), - ('cs', [ - ('o', 1, DIR_FANOUT), - ]), - ('copi', [ - ('o', 1, DIR_FANOUT), - ]), - ('cipo', [ - ('i', 1, DIR_FANIN), - ]), -)) - -class DFUPlatform: - flash = { - 'geometry': FlashGeometry( - size = 8388608, # 8MiB - page_size = 256, - erase_size = 4096, # 4KiB - addr_width = 24 - ).init_slots(device = 'iCE40HX8K'), - 'commands': { - 'erase': 0x20, - } - } - - def request(self, name, number): - return _SPI_RECORD - - -class DFURequestHandlerTests(SquishyUSBGatewareTestCase): - dut: DFURequestHandler = DFURequestHandler - dut_args = { - 'configuration': 1, - 'interface': 0, - 'resource_name': ('spi_flash_x1', 0) - } - 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) - - def sendDFUDownload(self): - 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) - - def sendDFUGetState(self): - yield from self.sendSetup(type = USBRequestType.CLASS, retrieve = True, - req = DFURequests.GET_STATE, value = 0, index = 0, length = 1) - - - @ToriiTestCase.simulation - @ToriiTestCase.sync_domain(domain = 'usb') - def test_dfu_handler(self): - yield self.dut.interface.active_config.eq(1) - yield Settle() - yield from self.step(2) - yield from self.wait_until_low(_SPI_RECORD.cs.o) - yield from self.step(2) - yield from self.sendDFUGetStatus() - yield from self.receiveData(data = (0, 0, 0, 0, DFUState.Idle, 0)) - yield from self.sendSetupSetInterface() - yield from self.receiveZLP() - yield from self.step(3) - yield from self.sendDFUDownload() - yield from self.sendData(data = _DFU_DATA) - yield from self.sendDFUGetStatus() - yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlBusy, 0)) - yield from self.sendDFUGetState() - yield from self.receiveData(data = (DFUState.DlBusy, )) - yield from self.step(6) - yield from self.sendDFUGetState() - # Keep checking for Download Busy - while (yield from self.receiveData(data = (DFUState.DlBusy, ), check = False)): - yield from self.sendDFUGetState() - yield from self.sendDFUGetState() - yield from self.receiveData(data = (DFUState.DlSync,)) - yield from self.sendDFUGetStatus() - yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlSync, 0)) - yield from self.sendDFUGetState() - yield from self.receiveData(data = (DFUState.DlIdle,)) - yield - yield from self.sendDFUDetach() - yield from self.receiveZLP() - self.assertEqual((yield self.dut.triggerReboot), 1) - yield Settle() - yield - self.assertEqual((yield self.dut.triggerReboot), 1) - yield diff --git a/tests/gateware/bootloader/test_rev1.py b/tests/gateware/bootloader/test_rev1.py new file mode 100644 index 00000000..1181093a --- /dev/null +++ b/tests/gateware/bootloader/test_rev1.py @@ -0,0 +1,227 @@ +# SPDX-License-Identifier: BSD-3-Clause + +from random import randbytes + +from torii import Record, Elaboratable, Module +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 usb_construct.types import USBRequestType +from usb_construct.types.descriptors.dfu import DFURequests + +from squishy.support.test import SquishyUSBGatewareTest +from squishy.core.config import FlashConfig +from squishy.core.flash import Geometry + +from squishy.gateware.usb.dfu import DFURequestHandler, DFUState +from squishy.gateware.bootloader.rev1 import Rev1 + +_DFU_DATA = randbytes(256) + +_SPI_RECORD = Record(( + ('clk', [ + ('o', 1, DIR_FANOUT), + ]), + ('cs', [ + ('o', 1, DIR_FANOUT), + ]), + ('copi', [ + ('o', 1, DIR_FANOUT), + ]), + ('cipo', [ + ('i', 1, DIR_FANIN), + ]), +)) + +class DFUPlatform: + flash = FlashConfig( + geometry = Geometry( + size = 8388608, # 8MiB + page_size = 256, + erase_size = 4096, # 4KiB + slot_size = 262144, + addr_width = 24 + ), + commands = { + 'erase': 0x20, + } + ) + + SIM_PLATFORM = True + + def request(self, *_): + return _SPI_RECORD + +class DUTWrapper(Elaboratable): + def __init__(self) -> None: + + self.fifo = AsyncFIFO( + width = 8, depth = DFUPlatform.flash.geometry.erase_size, r_domain = 'sync', w_domain = 'usb' + ) + + self.dfu = DFURequestHandler(1, 0, False, fifo = self.fifo) + self.rev1 = Rev1(self.fifo) + + self.interface = self.dfu.interface + + def elaborate(self, _) -> Module: + m = Module() + + m.submodules.fifo = self.fifo + m.submodules.dfu = self.dfu + m.submodules.rev1 = self.rev1 + + m.d.comb += [ + self.rev1.trigger_reboot.eq(self.dfu.trigger_reboot), + self.rev1.slot_selection.eq(self.dfu.slot_selection), + self.rev1.slot_changed.eq(self.dfu.slot_changed), + self.rev1.dl_start.eq(self.dfu.dl_start), + self.rev1.dl_finish.eq(self.dfu.dl_finish), + self.rev1.dl_size.eq(self.dfu.dl_size), + self.dfu.slot_ack.eq(self.rev1.slot_ack), + self.dfu.dl_ready.eq(self.rev1.dl_ready), + self.dfu.dl_done.eq(self.rev1.dl_done), + ] + + return m + +class DFURequestHandlerTests(SquishyUSBGatewareTest): + dut: DUTWrapper = DUTWrapper + dut_args = {} + domains = (('usb', 60e6), ('sync', 80e6), ) + platform = DFUPlatform() + + def sendDFUDetach(self): + 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 + ) + + def sendDFUGetStatus(self): + 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 + ) + + def spi_trans(self, *, + 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)) + + bytes = max(0 if copi is None else len(copi), 0 if cipo is None else len(cipo)) + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + if continuation: + yield Settle() + self.assertEqual((yield _SPI_RECORD.cs.o), 1) + else: + self.assertEqual((yield _SPI_RECORD.cs.o), 0) + yield Settle() + yield + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + self.assertEqual((yield _SPI_RECORD.cs.o), 1) + yield Settle() + yield + for byte in range(bytes): + for bit in range(8): + self.assertEqual((yield _SPI_RECORD.clk.o), 0) + if copi is not None and copi[byte] is not None: + self.assertEqual((yield _SPI_RECORD.copi.o), ((copi[byte] << bit) & 0x80) >> 7) + self.assertEqual((yield _SPI_RECORD.cs.o), 1) + yield Settle() + if cipo is not None and cipo[byte] is not None: + yield _SPI_RECORD.cipo.i.eq(((cipo[byte] << bit) & 0x80) >> 7) + yield + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + self.assertEqual((yield _SPI_RECORD.cs.o), 1) + yield Settle() + yield + if byte < bytes - 1: + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + self.assertEqual((yield _SPI_RECORD.cs.o), 1) + yield Settle() + if cipo is not None and cipo[byte] is not None: + yield _SPI_RECORD.cipo.i.eq(0) + if byte < bytes - 1: + yield + if not partial: + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + self.assertEqual((yield _SPI_RECORD.cs.o), 0) + yield Settle() + yield + + @ToriiTestCase.simulation + def test_integration(self): + @ToriiTestCase.sync_domain(domain = 'usb') + def dfu(self: DFURequestHandlerTests): + # Setup the active interface + yield self.dut.dfu.interface.active_config.eq(1) + yield Settle() + yield from self.wait_until_high(self.dut.dfu.dl_ready) + # Make sure we're in Idle + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DFUIdle, 0)) + # Set the interface up + yield from self.sendSetupSetInterface(interface = 0, alt_mode = 1) + yield from self.receiveZLP() + self.assertEqual((yield self.dut.dfu.slot_selection), 1) + yield from self.wait_until_high(self.dut.dfu.slot_ack) + yield from self.step(3) + # Yeet the data + yield from self.sendDFUDownload() + yield from self.sendData(data = _DFU_DATA) + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlBusy, 0)) + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlBusy, )) + yield from self.step(6) + yield from self.sendDFUGetState() + # The backing storage is chewing on the data, just spin for a bit + while (yield from self.receiveData(data = (DFUState.DlBusy,), check = False)): + yield from self.sendDFUGetState() + yield from self.step(3) + # Make sure we're in sync + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlSync,)) + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlSync, 0)) + # And back to Idle + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlIdle,)) + yield + # And trigger a reboot + self.assertEqual((yield self.dut.dfu.trigger_reboot), 0) + yield from self.sendDFUDetach() + yield from self.receiveZLP() + self.assertEqual((yield self.dut.dfu.trigger_reboot), 1) + yield Settle() + yield + self.assertEqual((yield self.dut.dfu.trigger_reboot), 1) + yield + yield from self.step(10) + + @ToriiTestCase.sync_domain(domain = 'sync') + def flash(self: DFURequestHandlerTests): + yield from self.spi_trans(copi = (0xAB,)) + yield from self.step(45) + yield from self.spi_trans(copi = (0x06,)) + yield from self.spi_trans(copi = (self.platform.flash.commands['erase'], 0x00, 0x10, 0x00)) + yield from self.wait_until_high(self.dut.rev1.dl_finish) + self.assertEqual((yield _SPI_RECORD.cs.o), 0) + self.assertEqual((yield _SPI_RECORD.clk.o), 1) + self.assertEqual((yield _SPI_RECORD.copi.o), 0) + self.assertEqual((yield _SPI_RECORD.cipo.i), 0) + + + dfu(self) + flash(self) 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/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/common/__init__.py b/tests/gateware/scsi/quirks/__init__.py similarity index 100% rename from tests/gateware/scsi/common/__init__.py rename to tests/gateware/scsi/quirks/__init__.py diff --git a/tests/gateware/scsi/scsi1/__init__.py b/tests/gateware/usb/quirks/__init__.py similarity index 100% rename from tests/gateware/scsi/scsi1/__init__.py rename to tests/gateware/usb/quirks/__init__.py diff --git a/tests/gateware/quirks/usb/test_windows.py b/tests/gateware/usb/quirks/test_windows.py similarity index 89% rename from tests/gateware/quirks/usb/test_windows.py rename to tests/gateware/usb/quirks/test_windows.py index 035a2516..b63df1c9 100644 --- a/tests/gateware/quirks/usb/test_windows.py +++ b/tests/gateware/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.emitters.descriptors.microsoft import PlatformDescriptorCollection, SetHeaderDescriptorEmitter from usb_construct.types import USBRequestRecipient, USBRequestType from usb_construct.types.descriptors.microsoft import MicrosoftRequests -from squishy.gateware.quirks.usb.windows import ( - GetDescriptorSetHandler, WindowsRequestHandler -) - +from squishy.support.test import SquishyUSBGatewareTest +from squishy.gateware.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/usb/test_dfu.py b/tests/gateware/usb/test_dfu.py index 175444a6..9aa92e28 100644 --- a/tests/gateware/usb/test_dfu.py +++ b/tests/gateware/usb/test_dfu.py @@ -1,40 +1,91 @@ # SPDX-License-Identifier: BSD-3-Clause -from ...gateware_test import SquishyUSBGatewareTestCase +from torii import Record, Elaboratable, Module +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 usb_construct.types import USBRequestType from usb_construct.types.descriptors.dfu import DFURequests +from squishy.support.test import SquishyUSBGatewareTest from squishy.gateware.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, + 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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +) -class DFURequestHandlerStubTests(SquishyUSBGatewareTestCase): +_SPI_RECORD = Record(( + ('clk', [ + ('o', 1, DIR_FANOUT), + ]), + ('cs', [ + ('o', 1, DIR_FANOUT), + ]), + ('copi', [ + ('o', 1, DIR_FANOUT), + ]), + ('cipo', [ + ('i', 1, DIR_FANIN), + ]), +)) + +class DFUPlatform: + flash = FlashConfig( + geometry = Geometry( + size = 8388608, # 8MiB + page_size = 256, + erase_size = 4096, # 4KiB + slot_size = 262144, + addr_width = 24 + ), + 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 + 'configuration': 1, + 'interface': 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 + 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 + 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) + self.assertEqual((yield self.dut.slot_selection), 0) yield self.dut.interface.active_config.eq(1) yield Settle() yield @@ -44,8 +95,133 @@ def test_dfu_stub(self): yield yield yield from self.sendDFUGetStatus() - yield from self.receiveData(data = (0, 0, 0, 0, DFUState.APP_IDLE, 0)) + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.AppIdle, 0)) + yield from self.sendDFUDetach() + yield from self.receiveZLP() + self.assertEqual((yield self.dut.trigger_reboot), 1) + self.assertEqual((yield self.dut.slot_selection), 0) + + +class DUTWrapper(Elaboratable): + def __init__(self) -> None: + self.fifo = AsyncFIFO( + width = 8, depth = DFUPlatform.flash.geometry.erase_size, r_domain = 'usb', w_domain = 'usb' + ) + + self.dfu = DFURequestHandler(1, 0, False, fifo = self.fifo) + + self.interface = self.dfu.interface + + def elaborate(self, platform) -> Module: + m = Module() + + m.submodules.fifo = self.fifo + m.submodules.dfu = self.dfu + + return m + +# TODO(aki): We need to build a DUTWrapper for this test now +class DFURequestHandlerTests(SquishyUSBGatewareTest): + dut: DUTWrapper = DUTWrapper + dut_args = { + + } + 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 + ) + + def sendDFUDownload(self): + 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 + ) + + def sendDFUGetState(self): + yield from self.sendSetup( + type = USBRequestType.CLASS, retrieve = True, req = DFURequests.GET_STATE, value = 0, index = 0, length = 1 + ) + + @ToriiTestCase.simulation + @ToriiTestCase.sync_domain(domain = 'usb') + def test_dfu_handler(self): + # Setup the active interface + yield self.dut.dfu.interface.active_config.eq(1) + yield Settle() + yield from self.step(2) + yield from self.wait_until_low(_SPI_RECORD.cs.o) + yield from self.step(2) + # Have the backing storage/programming interface tell the DFU state machine it's happy + yield Settle() + yield + yield self.dut.dfu.dl_ready.eq(1) + yield Settle() + yield + yield self.dut.dfu.dl_ready.eq(0) + yield Settle() + yield + # Make sure we're in Idle + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DFUIdle, 0)) + # Set the interface up + yield from self.sendSetupSetInterface(interface = 0, alt_mode = 1) + yield from self.receiveZLP() + yield from self.step(3) + self.assertEqual((yield self.dut.dfu.slot_selection), 1) + # Have the backing programmer tell the DFU state machine we've updated the slot + yield Settle() + yield + yield self.dut.dfu.slot_ack.eq(1) + yield Settle() + yield + yield self.dut.dfu.slot_ack.eq(0) + yield Settle() + yield + # Yeet the data + yield from self.sendDFUDownload() + yield from self.sendData(data = _DFU_DATA) + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlBusy, 0)) + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlBusy, )) + yield from self.step(6) + # The backing storage is chewing on the data, just spin for a bit + for _ in range(5): + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlBusy, ), check = False) + yield from self.step(3) + # Have the backing storage tell the DFU FSM that we have ingested the data + yield Settle() + yield + yield self.dut.dfu.dl_done.eq(1) + yield Settle() + yield + yield self.dut.dfu.dl_done.eq(0) + yield Settle() + yield + # Make sure we're in sync + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlSync,)) + yield from self.sendDFUGetStatus() + yield from self.receiveData(data = (0, 0, 0, 0, DFUState.DlSync, 0)) + # And back to Idle + yield from self.sendDFUGetState() + yield from self.receiveData(data = (DFUState.DlIdle,)) + yield + # And trigger a reboot + self.assertEqual((yield self.dut.dfu.trigger_reboot), 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) + self.assertEqual((yield self.dut.dfu.trigger_reboot), 1) + yield Settle() + yield + self.assertEqual((yield self.dut.dfu.trigger_reboot), 1) + yield + yield from self.step(10) diff --git a/tests/gateware/scsi/scsi2/__init__.py b/tests/scsi/__init__.py similarity index 100% rename from tests/gateware/scsi/scsi2/__init__.py rename to tests/scsi/__init__.py