├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ └── .keepme │ ├── _templates │ └── .keepme │ ├── conf.py │ ├── index.rst │ ├── modules.rst │ ├── usbcore.cpu.rst │ ├── usbcore.rst │ ├── usbcore.rx.rst │ ├── usbcore.sm.rst │ ├── usbcore.test.rst │ ├── usbcore.tx.rst │ └── usbcore.utils.rst ├── pytest.ini ├── requirements.txt ├── setup.py ├── sim ├── Makefile ├── README.md ├── cocotb-eptri.py ├── dec-usb.sh ├── generate_verilog.py ├── gtkwave-sigrok-filter.py ├── gtkwave.init ├── tb.v ├── test-common.py ├── test-dummyusb.py ├── test-eptri.py └── wishbone.py ├── src └── usb-epfifo.c ├── test-suite └── conf │ ├── environment.yml │ └── requirements.txt ├── valentyusb ├── usbcore │ ├── __init__.py │ ├── cpu │ │ ├── __init__.py │ │ ├── dummyusb.py │ │ ├── epfifo.py │ │ ├── epfifo_test.py │ │ ├── epmem.py │ │ ├── epmem_test.py │ │ ├── eptri.py │ │ ├── eptri_test.py │ │ ├── simplehostusb.py │ │ ├── unififo.py │ │ ├── unififo_test.py │ │ ├── usbwishbonebridge.py │ │ └── usbwishboneburstbridge.py │ ├── endpoint.py │ ├── io.py │ ├── io_test.py │ ├── pid.py │ ├── rx │ │ ├── __init__.py │ │ ├── bitstuff.py │ │ ├── bitstuff_test.py │ │ ├── clock.py │ │ ├── clock_test.py │ │ ├── crc.py │ │ ├── crc_test.py │ │ ├── detect.py │ │ ├── detect_test.py │ │ ├── nrzi.py │ │ ├── nrzi_test.py │ │ ├── pipeline.py │ │ ├── pipeline_test.py │ │ ├── pullup_detect.py │ │ ├── shifter.py │ │ └── shifter_test.py │ ├── sm │ │ ├── __init__.py │ │ ├── header.py │ │ ├── header_test.py │ │ ├── hosttransfer.py │ │ ├── send.py │ │ ├── send_test.py │ │ ├── transfer.py │ │ └── transfer_test.py │ ├── test │ │ ├── __init__.py │ │ ├── clock.py │ │ └── common.py │ ├── tx │ │ ├── __init__.py │ │ ├── bitstuff.py │ │ ├── bitstuff_test.py │ │ ├── crc.py │ │ ├── crc_test.py │ │ ├── nrzi.py │ │ ├── nrzi_test.py │ │ ├── pipeline.py │ │ ├── pipeline_test.py │ │ ├── shifter.py │ │ ├── shifter_test.py │ │ └── tester.py │ └── utils │ │ ├── CrcMoose3.py │ │ ├── __init__.py │ │ ├── asserts.py │ │ ├── bits.py │ │ ├── packet.py │ │ ├── pprint.py │ │ ├── sdiff.py │ │ └── vcd.py └── utils │ ├── dec-usb.sh │ └── gtkwave-sigrok-filter.py └── vcd └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vcd/ 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ 118 | 119 | # Vim 120 | *~ 121 | *.swp 122 | 123 | # vscode files 124 | .vscode 125 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | jobs: 5 | include: 6 | # Travis does not support matrix expansion in stages, 7 | # so we use YAML anchors to get parallel builds 8 | - &test_body 9 | env: 10 | - T=valentyusb S=test-eptri OP=sim CDC=1 11 | install: 12 | # Clone these here rather than as part of the main repo, 13 | # in order to avoid cloning a second copy of litex when 14 | # this is used as part of another repo. 15 | - git -C test-suite clone https://github.com/antmicro/usb-test-suite-testbenches.git 16 | - git -C test-suite clone https://github.com/antmicro/usb-test-suite-cocotb-usb.git 17 | - git -C test-suite clone https://github.com/enjoy-digital/litex.git 18 | # We need to install those in virtualenv, as cocotb will use this instead of conda 19 | - pip install -r test-suite/conf/requirements.txt 20 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 21 | - bash miniconda.sh -b -p $HOME/miniconda 22 | - source "$HOME/miniconda/etc/profile.d/conda.sh" 23 | - hash -r 24 | - conda config --set always_yes yes --set changeps1 no 25 | - conda update -q conda 26 | - conda info -a 27 | - conda env create --file test-suite/conf/environment.yml 28 | - conda activate usb-test-suite-env 29 | # conda complains if it does not have the libs as well 30 | - pip install -r test-suite/conf/requirements.txt 31 | - pip install -e test-suite/usb-test-suite-cocotb-usb/ 32 | - cp test-suite/litex/litex_setup.py test-suite 33 | - ./test-suite/litex_setup.py init install --user 34 | script: 35 | - cd test-suite/usb-test-suite-testbenches 36 | - make PYTHONPATH=../litex:../.. TARGET=$T TEST_SCRIPT=$S $OP CDC=$CDC && ! grep -q failure results.xml 37 | 38 | - <<: *test_body 39 | env: 40 | - T=valentyusb S=test-clocks OP=sim CDC=1 41 | - <<: *test_body 42 | env: 43 | - T=valentyusb S=test-clocks OP=sim CDC=0 44 | - <<: *test_body 45 | env: 46 | - T=valentyusb S=test-enum OP=sim CDC=1 47 | - <<: *test_body 48 | env: 49 | - T=valentyusb S=test-w10enum OP=sim CDC=1 50 | - <<: *test_body 51 | env: 52 | - T=valentyusb S=test-macOSenum OP=sim CDC=1 53 | - <<: *test_body 54 | env: 55 | - T=valentyusb S=test-valenty-cdc OP=sim CDC=1 56 | - <<: *test_body 57 | env: 58 | - T=valentyusb S=test-eptri OP=sim CDC=0 59 | - <<: *test_body 60 | env: 61 | - T=valentyusb S=test-enum OP=sim CDC=0 62 | - <<: *test_body 63 | env: 64 | - T=valentyusb S=test-w10enum OP=sim CDC=0 65 | - <<: *test_body 66 | env: 67 | - T=valentyusb S=test-macOSenum OP=sim CDC=0 68 | - <<: *test_body 69 | env: 70 | - T=valentyusb S=test-valenty-cdc OP=sim CDC=0 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Luke Valenty 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ValentyUSB 2 | 3 | USB Full-Speed core written in Migen/LiteX. This core has been tested and is known to work on various incarnations of Fomu. It requires you to have a 48 MHz clock and a 12 MHz clock. It optionally comes with a debug bridge for debugging Wishbone. 4 | 5 | ## Usage 6 | 7 | To use this in your project, instantiate one of the CPU instances. The `epfifo` instances is the most widely-tested API, however `dummyusb` can be used for designs without a CPU. 8 | 9 | * **DummyUsb**: A wishbone device with no CPU interface that simply enumerates and can act as a Wishbone bridge 10 | * **PerEndpointFifoInterface**: Requires a CPU to configure and manage the device. 11 | 12 | ```python 13 | _io = [ 14 | ("usb", 0, 15 | Subsignal("d_p", Pins("34")), 16 | Subsignal("d_n", Pins("37")), 17 | Subsignal("pullup", Pins("35")), 18 | IOStandard("LVCMOS33") 19 | ), 20 | ] 21 | 22 | class BaseSoC(SoCCore): 23 | def __init__(self, platform, **kwargs): 24 | clk_freq = int(12e6) 25 | SoCCore.__init__(self, platform, clk_freq, with_uart=False, **kwargs) 26 | 27 | from valentyusb.usbcore.cpu import epfifo, dummyusb 28 | usb_pads = platform.request("usb") 29 | usb_iobuf = usbio.IoBuf(usb_pads.d_p, usb_pads.d_n, usb_pads.pullup) 30 | 31 | # If a CPU is present, add a per-endpoint interface. Otherwise, add a dummy 32 | # interface that simply acts as a Wishbone bridge. 33 | # Note that the dummy interface only really makes sense when doing a debug build. 34 | # Also note that you can add a dummyusb interface to a CPU if you only care 35 | # about the wishbone bridge. 36 | if hasattr(self, "cpu"): 37 | self.submodules.usb = epfifo.PerEndpointFifoInterface(usb_iobuf, debug=usb_debug) 38 | else: 39 | self.submodules.usb = dummyusb.DummyUsb(usb_iobuf, debug=usb_debug) 40 | if usb_debug: 41 | self.add_wb_master(self.usb.debug_bridge.wishbone) 42 | self.register_mem("vexriscv_debug", 0xf00f0000, self.cpu.debug_bus, 0x100) 43 | 44 | platform = LatticePlatform("ice40-up5k-sg48", _io, [], toolchain="icestorm") 45 | soc = BaseSoC(platform, cpu_type="vexriscv", cpu_variant="min+debug") # set cpu_type=None to build without a CPU 46 | builder = Builder(soc) 47 | soc.do_exit(builder.build()) 48 | ``` 49 | 50 | ## Debug tools 51 | 52 | You can use the `litex_server` built into the litex distribution to communicate with the device: 53 | 54 | ```sh 55 | $ litex_server --usb --usb-pid 0x70bl 56 | ``` 57 | 58 | Alternately, you can use [wishbone-tool](https://github.com/xobs/wishbone-utils/releases): 59 | 60 | ```sh 61 | $ wishbone-tool 0 62 | INFO [wishbone_tool::usb_bridge] opened USB device device 017 on bus 001 63 | Value at 00000000: 6f80106f 64 | $ 65 | ``` 66 | 67 | ## GDB server 68 | 69 | You can use `wishbone-tool` to run a GDB server: 70 | 71 | ```sh 72 | $ wishbone-tool -s gdb 73 | INFO [wishbone_tool::usb_bridge] opened USB device device 017 on bus 001 74 | INFO [wishbone_tool] accepting connections on 0.0.0.0:1234 75 | ``` -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = ValentyUSB 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | venv: 16 | virtualenv --python=python3 venv 17 | source venv/bin/activate; pip install -r requirements.txt 18 | 19 | apidoc: 20 | source venv/bin/activate; cd ..; sphinx-apidoc -f -o docs/source valentyusb 21 | 22 | .PHONY: venv help Makefile 23 | 24 | # Catch-all target: route all unknown targets to Sphinx using the new 25 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 26 | %: Makefile 27 | source venv/bin/activate; $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=ValentyUSB 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | recommonmark 2 | sphinx 3 | sphinx_rtd_theme 4 | -e git+https://github.com/bavovanachte/sphinx-wavedrom.git#egg=sphinxcontrib-wavedrom 5 | sphinx-autodoc-typehints 6 | -r ../requirements.txt 7 | -------------------------------------------------------------------------------- /docs/source/_static/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/docs/source/_static/.keepme -------------------------------------------------------------------------------- /docs/source/_templates/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/docs/source/_templates/.keepme -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath(os.path.join('..', '..', 'valentyusb'))) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'ValentyUSB' 23 | copyright = '2019, ValentyUSB Authors' 24 | author = 'ValentyUSB Authors' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'recommonmark', 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.todo', 46 | 'sphinx.ext.mathjax', 47 | 'sphinx.ext.viewcode', 48 | 'sphinx.ext.githubpages', 49 | 'sphinx.ext.autosectionlabel', 50 | 'sphinx.ext.napoleon', 51 | 'sphinx_rtd_theme', 52 | 'sphinx_autodoc_typehints', 53 | 'sphinxcontrib.wavedrom', 54 | ] 55 | 56 | # Prevent sphinx from reordering our classes 57 | autodoc_member_order = 'bysource' 58 | 59 | # Add any paths that contain templates here, relative to this directory. 60 | templates_path = ['_templates'] 61 | 62 | # The suffix(es) of source filenames. 63 | # You can specify multiple suffix as a list of string: 64 | # 65 | source_suffix = ['.rst', '.md'] 66 | # source_suffix = '.rst' 67 | 68 | # The master toctree document. 69 | master_doc = 'index' 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This pattern also affects html_static_path and html_extra_path . 81 | exclude_patterns = [] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = 'sphinx_rtd_theme' 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['_static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # The default sidebars (for documents that don't match any pattern) are 109 | # defined by theme itself. Builtin themes are using these templates by 110 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 111 | # 'searchbox.html']``. 112 | # 113 | # html_sidebars = {} 114 | 115 | # -- Options for HTMLHelp output --------------------------------------------- 116 | 117 | # Output file base name for HTML help builder. 118 | htmlhelp_basename = 'ValentyUSBdoc' 119 | 120 | 121 | # -- Options for LaTeX output ------------------------------------------------ 122 | 123 | latex_elements = { 124 | # The paper size ('letterpaper' or 'a4paper'). 125 | # 126 | # 'papersize': 'letterpaper', 127 | 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | 132 | # Additional stuff for the LaTeX preamble. 133 | # 134 | # 'preamble': '', 135 | 136 | # Latex figure (float) alignment 137 | # 138 | # 'figure_align': 'htbp', 139 | } 140 | 141 | # Grouping the document tree into LaTeX files. List of tuples 142 | # (source start file, target name, title, 143 | # author, documentclass [howto, manual, or own class]). 144 | latex_documents = [ 145 | (master_doc, 'ValentyUSB.tex', 'ValentyUSB Documentation', 146 | 'ValentyUSB Authors', 'manual'), 147 | ] 148 | 149 | 150 | # -- Options for manual page output ------------------------------------------ 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [ 155 | (master_doc, 'valentyusb', 'ValentyUSB Documentation', 156 | [author], 1) 157 | ] 158 | 159 | 160 | # -- Options for Texinfo output ---------------------------------------------- 161 | 162 | # Grouping the document tree into Texinfo files. List of tuples 163 | # (source start file, target name, title, author, 164 | # dir menu entry, description, category) 165 | texinfo_documents = [ 166 | (master_doc, 'ValentyUSB', 'ValentyUSB Documentation', 167 | author, 'ValentyUSB', 'One line description of project.', 168 | 'Miscellaneous'), 169 | ] 170 | 171 | 172 | # -- Extension configuration ------------------------------------------------- 173 | 174 | # -- Options for intersphinx extension --------------------------------------- 175 | 176 | # Example configuration for intersphinx: refer to the Python standard library. 177 | intersphinx_mapping = {'https://docs.python.org/': None} 178 | 179 | # -- Options for todo extension ---------------------------------------------- 180 | 181 | # If true, `todo` and `todoList` produce output, else they produce nothing. 182 | todo_include_todos = True 183 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ValentyUSB documentation master file, created by 2 | sphinx-quickstart on Mon Sep 2 09:57:51 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ValentyUSB's documentation! 7 | ====================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | valentyusb 2 | ========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | usbcore 8 | -------------------------------------------------------------------------------- /docs/source/usbcore.cpu.rst: -------------------------------------------------------------------------------- 1 | usbcore.cpu package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.cpu.dummyusb module 8 | --------------------------- 9 | 10 | .. automodule:: usbcore.cpu.dummyusb 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.cpu.epfifo module 16 | ------------------------- 17 | 18 | .. automodule:: usbcore.cpu.epfifo 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | usbcore.cpu.epmem module 24 | ------------------------ 25 | 26 | .. automodule:: usbcore.cpu.epmem 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | usbcore.cpu.eptri module 32 | ------------------------ 33 | 34 | .. automodule:: usbcore.cpu.eptri 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | usbcore.cpu.unififo module 40 | -------------------------- 41 | 42 | .. automodule:: usbcore.cpu.unififo 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | usbcore.cpu.usbwishbonebridge module 48 | ------------------------------------ 49 | 50 | .. automodule:: usbcore.cpu.usbwishbonebridge 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/source/usbcore.rst: -------------------------------------------------------------------------------- 1 | usbcore package 2 | =============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | usbcore.cpu 10 | usbcore.rx 11 | usbcore.sm 12 | usbcore.tx 13 | usbcore.utils 14 | 15 | Submodules 16 | ---------- 17 | 18 | usbcore.endpoint module 19 | ----------------------- 20 | 21 | .. automodule:: usbcore.endpoint 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | 26 | usbcore.io module 27 | ----------------- 28 | 29 | .. automodule:: usbcore.io 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | usbcore.pid module 35 | ------------------ 36 | 37 | .. automodule:: usbcore.pid 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | 43 | Module contents 44 | --------------- 45 | 46 | .. automodule:: usbcore 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | -------------------------------------------------------------------------------- /docs/source/usbcore.rx.rst: -------------------------------------------------------------------------------- 1 | usbcore.rx package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.rx.bitstuff module 8 | -------------------------- 9 | 10 | .. automodule:: usbcore.rx.bitstuff 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.rx.bitstuff\_test module 16 | -------------------------------- 17 | 18 | .. automodule:: usbcore.rx.bitstuff_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | usbcore.rx.clock module 24 | ----------------------- 25 | 26 | .. automodule:: usbcore.rx.clock 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | usbcore.rx.clock\_test module 32 | ----------------------------- 33 | 34 | .. automodule:: usbcore.rx.clock_test 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | usbcore.rx.crc module 40 | --------------------- 41 | 42 | .. automodule:: usbcore.rx.crc 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | usbcore.rx.crc\_test module 48 | --------------------------- 49 | 50 | .. automodule:: usbcore.rx.crc_test 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | usbcore.rx.detect module 56 | ------------------------ 57 | 58 | .. automodule:: usbcore.rx.detect 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | usbcore.rx.detect\_test module 64 | ------------------------------ 65 | 66 | .. automodule:: usbcore.rx.detect_test 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | usbcore.rx.nrzi module 72 | ---------------------- 73 | 74 | .. automodule:: usbcore.rx.nrzi 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | usbcore.rx.nrzi\_test module 80 | ---------------------------- 81 | 82 | .. automodule:: usbcore.rx.nrzi_test 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | usbcore.rx.pipeline module 88 | -------------------------- 89 | 90 | .. automodule:: usbcore.rx.pipeline 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | usbcore.rx.pipeline\_test module 96 | -------------------------------- 97 | 98 | .. automodule:: usbcore.rx.pipeline_test 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | usbcore.rx.shifter module 104 | ------------------------- 105 | 106 | .. automodule:: usbcore.rx.shifter 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | usbcore.rx.shifter\_test module 112 | ------------------------------- 113 | 114 | .. automodule:: usbcore.rx.shifter_test 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | 120 | Module contents 121 | --------------- 122 | 123 | .. automodule:: usbcore.rx 124 | :members: 125 | :undoc-members: 126 | :show-inheritance: 127 | -------------------------------------------------------------------------------- /docs/source/usbcore.sm.rst: -------------------------------------------------------------------------------- 1 | usbcore.sm package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.sm.header module 8 | ------------------------ 9 | 10 | .. automodule:: usbcore.sm.header 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.sm.header\_test module 16 | ------------------------------ 17 | 18 | .. automodule:: usbcore.sm.header_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | usbcore.sm.send module 24 | ---------------------- 25 | 26 | .. automodule:: usbcore.sm.send 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | usbcore.sm.send\_test module 32 | ---------------------------- 33 | 34 | .. automodule:: usbcore.sm.send_test 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | usbcore.sm.transfer module 40 | -------------------------- 41 | 42 | .. automodule:: usbcore.sm.transfer 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | usbcore.sm.transfer\_test module 48 | -------------------------------- 49 | 50 | .. automodule:: usbcore.sm.transfer_test 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | 56 | Module contents 57 | --------------- 58 | 59 | .. automodule:: usbcore.sm 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | -------------------------------------------------------------------------------- /docs/source/usbcore.test.rst: -------------------------------------------------------------------------------- 1 | usbcore.test package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.test.clock module 8 | ------------------------- 9 | 10 | .. automodule:: usbcore.test.clock 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.test.common module 16 | -------------------------- 17 | 18 | .. automodule:: usbcore.test.common 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: usbcore.test 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/usbcore.tx.rst: -------------------------------------------------------------------------------- 1 | usbcore.tx package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.tx.bitstuff module 8 | -------------------------- 9 | 10 | .. automodule:: usbcore.tx.bitstuff 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.tx.bitstuff\_test module 16 | -------------------------------- 17 | 18 | .. automodule:: usbcore.tx.bitstuff_test 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | usbcore.tx.crc module 24 | --------------------- 25 | 26 | .. automodule:: usbcore.tx.crc 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | usbcore.tx.crc\_test module 32 | --------------------------- 33 | 34 | .. automodule:: usbcore.tx.crc_test 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | usbcore.tx.nrzi module 40 | ---------------------- 41 | 42 | .. automodule:: usbcore.tx.nrzi 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | usbcore.tx.nrzi\_test module 48 | ---------------------------- 49 | 50 | .. automodule:: usbcore.tx.nrzi_test 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | usbcore.tx.pipeline module 56 | -------------------------- 57 | 58 | .. automodule:: usbcore.tx.pipeline 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | usbcore.tx.pipeline\_test module 64 | -------------------------------- 65 | 66 | .. automodule:: usbcore.tx.pipeline_test 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | usbcore.tx.shifter module 72 | ------------------------- 73 | 74 | .. automodule:: usbcore.tx.shifter 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | usbcore.tx.shifter\_test module 80 | ------------------------------- 81 | 82 | .. automodule:: usbcore.tx.shifter_test 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | usbcore.tx.tester module 88 | ------------------------ 89 | 90 | .. automodule:: usbcore.tx.tester 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | 96 | Module contents 97 | --------------- 98 | 99 | .. automodule:: usbcore.tx 100 | :members: 101 | :undoc-members: 102 | :show-inheritance: 103 | -------------------------------------------------------------------------------- /docs/source/usbcore.utils.rst: -------------------------------------------------------------------------------- 1 | usbcore.utils package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | usbcore.utils.CrcMoose3 module 8 | ------------------------------ 9 | 10 | .. automodule:: usbcore.utils.CrcMoose3 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | usbcore.utils.asserts module 16 | ---------------------------- 17 | 18 | .. automodule:: usbcore.utils.asserts 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | usbcore.utils.bits module 24 | ------------------------- 25 | 26 | .. automodule:: usbcore.utils.bits 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | usbcore.utils.packet module 32 | --------------------------- 33 | 34 | .. automodule:: usbcore.utils.packet 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | usbcore.utils.pprint module 40 | --------------------------- 41 | 42 | .. automodule:: usbcore.utils.pprint 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | usbcore.utils.sdiff module 48 | -------------------------- 49 | 50 | .. automodule:: usbcore.utils.sdiff 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | usbcore.utils.vcd module 56 | ------------------------ 57 | 58 | .. automodule:: usbcore.utils.vcd 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: usbcore.utils 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules 3 | doctest_optionflags = ALLOW_UNICODE ALLOW_BYTES 4 | norecursedirs = third_party .git vcd src 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | crcmod 2 | -e git+https://github.com/m-labs/migen@master#egg=migen 3 | -e git+https://github.com/mithro/litex@enable-lto#egg=litex 4 | pytest>=3.6.0 5 | pytest-pep8 6 | pytest-sugar 7 | pytest-travis-fold 8 | pytest-xdist 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | setup( 8 | name="valentyusb", 9 | version="0.3.4dev", 10 | description="FPGA USB stack written in LiteX", 11 | long_description=open("README.md").read(), 12 | author="Sean Cross", 13 | author_email="sean@xobs.io", 14 | url="https://github.com/im-tomu/valentyusb", 15 | download_url="https://github.com/im-tomu/valentyusb", 16 | license="BSD", 17 | platforms=["Any"], 18 | keywords=["HDL", "FPGA", "USB"], 19 | classifiers=[ 20 | "Development Status :: Alpha", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: BSD License", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python", 25 | ], 26 | packages=find_packages(exclude=("test-suite*", "sim*", "docs*")), 27 | install_requires=["litex"], 28 | ) 29 | -------------------------------------------------------------------------------- /sim/Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright (c) 2013, 2018 Potential Ventures Ltd 3 | # Copyright (c) 2013 SolarFlare Communications Inc 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of Potential Ventures Ltd, 14 | # SolarFlare Communications Inc nor the 15 | # names of its contributors may be used to endorse or promote products 16 | # derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL POTENTIAL VENTURES LTD BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | ############################################################################### 29 | 30 | # Default to verilog 31 | TOPLEVEL_LANG ?= verilog 32 | 33 | WPWD=$(shell sh -c 'pwd -W') 34 | PWD=$(shell pwd) 35 | 36 | ifeq ($(OS),Msys) 37 | WPWD=$(shell sh -c 'pwd -W') 38 | PYTHONPATH := $(PWD)/..;$(PYTHONPATH) 39 | else 40 | WPWD=$(shell pwd) 41 | PYTHONPATH := $(PWD)/..:$(PYTHONPATH) 42 | endif 43 | 44 | VERILOG_SOURCES = $(WPWD)/dut.v $(WPWD)/tb.v 45 | TOPLEVEL = tb 46 | MODULE = test-eptri 47 | #MODULE = test-dummyusb 48 | 49 | CUSTOM_COMPILE_DEPS = $(PWD)/dut.v 50 | 51 | include $(shell cocotb-config --makefiles)/Makefile.inc 52 | include $(shell cocotb-config --makefiles)/Makefile.sim 53 | 54 | $(PWD)/dut.v: generate_verilog.py ../valentyusb/usbcore/cpu/eptri.py 55 | cd .. 56 | PYTHONPATH=../../litex:../../migen:../../litedram:.. python3 generate_verilog.py eptri 57 | mv build/gateware/dut.v . 58 | 59 | #$(PWD)/dut.v: generate_verilog.py ../valentyusb/usbcore/cpu/dummyusb.py 60 | # cd .. 61 | # PYTHONPATH=../../litex:../../migen:../../litedram:.. python3 generate_verilog.py dummy 62 | # mv build/gateware/dut.v . 63 | # mv build/gateware/*.init . 64 | -------------------------------------------------------------------------------- /sim/README.md: -------------------------------------------------------------------------------- 1 | # ValentyUSB Simulation 2 | 3 | Simulation is incredibly important for validating hardware designs. This directory contains a [Cocotb](https://github.com/cocotb/cocotb) testbench for testing ValentyUSB. 4 | 5 | It also contains some support code to make it easier to view the resulting output. 6 | 7 | ## Running the Simulation 8 | 9 | You must ensure the following are all true: 10 | 11 | * litex and migen are checked out alongside this repository -- that is, `litex`, `migen`, `litedram`, and `valentyusb` are all in the same directory 12 | * You have `make` installed 13 | * You have installed cocotb by running either `pip install cocotb` or `pip3 install cocotb` 14 | * Icarus Verilog is installed, and you can run both `iverilog` and `vvp` (unless you're using a different simulator) 15 | * A C compiler is installed, since iverilog requires it 16 | * If you want to view vcd waveform diagrams, gtkwave is installed 17 | * If you want to decode USB signals, `sigrok-cli` is installed 18 | 19 | To run the simulation, ensure `iverilog` and `vvp` are in your PATH. You should also ensure cocotb is installed: 20 | 21 | ```sh 22 | $ pip install cocotb 23 | ``` 24 | 25 | You might need to also add `~/.local/bin` to your path, in order to have access to the `cocotb-config` script. 26 | 27 | If you want to use `python3` instead of `python`, specify PYTHON_BIN as an argument to make, either by setting it as an environment variable or specifying it on the command line: 28 | 29 | ```sh 30 | $ make PYTHON_BIN=python3 31 | ``` 32 | 33 | ## Viewing the output 34 | 35 | Cocotb will run the tests through the simulator. As part of the testbench, a file called `dump.vcd` is created. This contains all the signals from the simulation. You can view this using `gtkwave`. 36 | 37 | If you have `sigrok-cli` in your PATH, you can go one step further and run these signals through a logic analyzer. A script has been created that will set this up for you: 38 | 39 | ```sh 40 | $ gtkwave -S gtkwave.init dump.vcd 41 | ``` 42 | 43 | In order to get additional levels of decode, you can right-click on the area with signals and say `Add empty row`, and then drag this above the `usb_d_n` signal. This can be repeated up to four times. 44 | 45 | You may find the font to be a little small. Create ~/.gtkwaverc and add this: 46 | 47 | ``` 48 | fontname_signals Monospace 14 49 | fontname_waves Monospace 14 50 | ``` 51 | 52 | ## About test names 53 | 54 | Cocotb does not stop the simulator during the course of the run. In order to identify various sections of the simulation, you need to add the `test_name` signal and convert it to `Ascii`. The `gtkwave.init` script does this for you. 55 | 56 | ## FSM state names 57 | 58 | Migen has Finite State Machine support. The simulation engine adds additional signals to indicate which state the FSM is currently in. These states have signals whose names end in `_state_name`. You can add these signals to the decode output, right-click on them, select `Data Format` -> `Ascii` to get decoded state names. 59 | -------------------------------------------------------------------------------- /sim/dec-usb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export PYTHONHASHSEED=1 3 | exec `dirname $0`/gtkwave-sigrok-filter.py -P usb_signalling:signalling=full-speed:dm=usb_d_n:dp=usb_d_p,usb_packet:signalling=full-speed 4 | -------------------------------------------------------------------------------- /sim/gtkwave-sigrok-filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | gtkwave-sigrok-filter.py 5 | 6 | Use as a 'Transaction Filter Process' in gtkwave to apply signal 7 | 8 | Usage: 9 | - Group input signals in gtkwave with F4 10 | - Apply this script as a 'Transaction Filter Process' (Right click / Data Format) 11 | (note that sometime options don't work and you have to create a wrapper 12 | shell script calling this python file with options you want) 13 | - To get more decoding rows, add blank traces right below the first decoded 14 | trace. 15 | 16 | Options: 17 | - All options given to this script are passes as is to sigrok-cli, so 18 | refer to sigrok-cli doc for how to use protocol decoders 19 | - Examples: 20 | 21 | Wrapper script for USB full speed decoding : 22 | 23 | ``` 24 | #!/bin/bash 25 | exec `dirname $0`/gtkwave-sigrok-filter.py -P usb_signalling:signalling=full-speed,usb_packet:signalling=full-speed 26 | ``` 27 | 28 | Wrapper script for SPI decoding : 29 | 30 | ``` 31 | #!/bin/bash 32 | exec `dirname $0`/gtkwave-sigrok-filter.py -P spi 33 | ``` 34 | 35 | 36 | Copyright (C) 2019 Sylvain Munaut 37 | All rights reserved. 38 | 39 | Redistribution and use in source and binary forms, with or without 40 | modification, are permitted provided that the following conditions are met: 41 | 42 | 1. Redistributions of source code must retain the above copyright notice, this 43 | list of conditions and the following disclaimer. 44 | 2. Redistributions in binary form must reproduce the above copyright notice, 45 | this list of conditions and the following disclaimer in the documentation 46 | and/or other materials provided with the distribution. 47 | 48 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 49 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 50 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 51 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 52 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 53 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 54 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 55 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 56 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 57 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 58 | 59 | The views and conclusions contained in the software and documentation are those 60 | of the authors and should not be interpreted as representing official policies, 61 | either expressed or implied, of anyone. 62 | """ 63 | 64 | import subprocess 65 | import sys 66 | import tempfile 67 | import random 68 | import re 69 | 70 | 71 | COLORS = [ 72 | "black", 73 | "blue", 74 | "blue violet", 75 | "brown", 76 | "burlywood", 77 | "cadet blue", 78 | "chocolate", 79 | "cornflower blue", 80 | "dark blue", 81 | "dark cyan", 82 | "dark green", 83 | "dark khaki", 84 | "dark magenta", 85 | "dark olive green", 86 | "dark orange", 87 | "dark orchid", 88 | "dark red", 89 | "dark salmon", 90 | "dark sea green", 91 | "dark slate blue", 92 | "dark turquoise", 93 | "dark violet", 94 | "deep pink", 95 | "deep sky blue", 96 | "dodger blue", 97 | "firebrick", 98 | "forest green", 99 | "gold", 100 | "hot pink", 101 | "indian red", 102 | "khaki", 103 | "magenta", 104 | "maroon", 105 | "medium aquamarine", 106 | "medium blue", 107 | "medium orchid", 108 | "medium purple", 109 | "medium sea green", 110 | "medium slate blue", 111 | "medium spring green", 112 | "medium turquoise", 113 | "medium violet red", 114 | "midnight blue", 115 | "navy", 116 | "navy blue", 117 | "olive drab", 118 | "orange", 119 | "orange red", 120 | "orchid", 121 | "pale violet red", 122 | "peru", 123 | "pink", 124 | "plum", 125 | "purple", 126 | "red", 127 | "rosy brown", 128 | "royal blue", 129 | "saddle brown", 130 | "salmon", 131 | "sandy brown", 132 | "sienna", 133 | "sky blue", 134 | "slate blue", 135 | "steel blue", 136 | "tan", 137 | "thistle", 138 | "tomato", 139 | "turquoise", 140 | "violet", 141 | "violet red", 142 | "yellow green", 143 | ] 144 | 145 | def pick_color(): 146 | color = COLORS[random.randrange(0, len(COLORS)-1)] 147 | return color 148 | 149 | 150 | def get_decoders_infos(args): 151 | # Return value 152 | rv = {} 153 | 154 | # Run sigrok-cli 155 | pipe = subprocess.Popen([ 156 | 'sigrok-cli', '--show', 157 | ] + list(args), 158 | stdout=subprocess.PIPE, 159 | ) 160 | text = pipe.communicate()[0].decode('utf-8') 161 | 162 | # Parse output 163 | cur = None 164 | active = False 165 | for l in text.splitlines(): 166 | if l.startswith('ID: '): 167 | cur = rv.setdefault(l[4:], ({},{})) 168 | elif l == 'Annotation rows:': 169 | active = True 170 | elif not l.startswith('-'): 171 | active = False 172 | elif active: 173 | m = re.match('^- (.*) \((.*)\): (.*)$', l) 174 | for cn in m.group(3).split(','): 175 | cur[0][cn.strip()] = (m.group(1), pick_color()) 176 | cur[1][m.group(1)] = m.group(2) 177 | 178 | return rv 179 | 180 | 181 | def main(argv0, *args): 182 | decoders = get_decoders_infos(args) 183 | fh_in = sys.stdin 184 | fh_out = sys.stdout 185 | fh_err = sys.stderr 186 | with tempfile.NamedTemporaryFile() as fh_temp: 187 | # Repeat ... 188 | while True: 189 | # Read input until we get a full VCD input 190 | while True: 191 | l = fh_in.readline() 192 | if not l: 193 | return 0 194 | 195 | fh_temp.write(l.encode('utf-8')) 196 | 197 | if l.startswith('$comment data_end'): 198 | break 199 | 200 | fh_temp.flush() 201 | 202 | # Feed this to sigrok-cli and get output 203 | data = {} 204 | 205 | pipe = subprocess.Popen([ 206 | 'sigrok-cli', '-l', '4', '-O', 'ascii', 207 | '-i', fh_temp.name, '--input-format', 'vcd', 208 | '--protocol-decoder-samplenum', 209 | ] + list(args), 210 | stdout=subprocess.PIPE, 211 | universal_newlines=True 212 | ) 213 | text = pipe.communicate()[0] 214 | 215 | for l in text.splitlines(): 216 | # Parse 217 | l_t, l_d, l_c, l_v = l.strip().split(' ', 3) 218 | 219 | l_t = [int(x) for x in l_t.split('-')] # Time Span 220 | l_d = l_d.strip(':') # Decoder id 221 | l_c = l_c.strip(':') # Annotation class 222 | l_v = l_v.strip() # Value 223 | 224 | # Grab decoder infos 225 | d_id = l_d.split('-',1)[0] 226 | rowmap = decoders[d_id][0] 227 | 228 | # Map to a row 229 | row_id, color = rowmap[l_c] 230 | 231 | # Select one of the value 232 | l_v = re.split('" "', l_v[1:-1]) 233 | v = l_v[(len(l_v)-1)//2] 234 | 235 | # Save the start/stop event 236 | e = data.setdefault((l_d, row_id), {}) 237 | if l_t[1] not in e: 238 | e[l_t[1]] = None 239 | e[l_t[0]] = '?%s?%s' % (color, v) 240 | 241 | # Output 242 | first = True 243 | 244 | for k in sorted(data.keys()): 245 | if not first: 246 | fh_out.write("$next\n") 247 | first = False 248 | 249 | trace_name = k[0] + '/' + decoders[k[0].split('-',1)[0]][1][k[1]] 250 | fh_out.write("$name %s\n" % (trace_name,)) 251 | 252 | for t in sorted(data[k].keys()): 253 | v = data[k][t] 254 | fh_out.write("#%d %s\n" % (t, v if (v is not None) else '')) 255 | 256 | fh_out.write("$finish\n") 257 | fh_out.flush() 258 | 259 | # Reset 260 | fh_temp.seek(0) 261 | fh_temp.truncate() 262 | del data 263 | 264 | 265 | if __name__ == '__main__': 266 | try: 267 | sys.exit(main(*sys.argv)) 268 | except Exception as e: 269 | sys.stderr.write("Exception occurred: {}\n".format(e)) 270 | sys.stdout.write("$finish\n") 271 | sys.exit(1) 272 | -------------------------------------------------------------------------------- /sim/gtkwave.init: -------------------------------------------------------------------------------- 1 | # Add the signal indicating the test name, 2 | # and mark it as "ASCII" 3 | gtkwave::addSignalsFromList "test_name" 4 | gtkwave::/Edit/Highlight_All 5 | gtkwave::/Edit/Data_Format/ASCII 6 | 7 | # Add the USB decoder line 8 | set usb_sig [ list tb.usb_d_n tb.usb_d_p ] 9 | gtkwave::addSignalsFromList $usb_sig 10 | gtkwave::highlightSignalsFromList $usb_sig 11 | gtkwave::/Edit/Combine_Down "usb_decoded" 12 | gtkwave::highlightSignalsFromList "usb_decoded" 13 | gtkwave::setCurrentTranslateTransProc ./dec-usb.sh 14 | gtkwave::installTransFilter 1 15 | -------------------------------------------------------------------------------- /sim/tb.v: -------------------------------------------------------------------------------- 1 | `timescale 100ps / 100ps 2 | 3 | module tb( 4 | input clk48, 5 | output clk12, 6 | input reset, 7 | inout usb_d_p, 8 | inout usb_d_n, 9 | output usb_pullup, 10 | output usb_tx_en, 11 | input [29:0] wishbone_adr, 12 | output [31:0] wishbone_datrd, 13 | input [31:0] wishbone_datwr, 14 | input [3:0] wishbone_sel, 15 | input wishbone_cyc, 16 | input wishbone_stb, 17 | output wishbone_ack, 18 | input wishbone_we, 19 | input [2:0] wishbone_cti, 20 | input [1:0] wishbone_bte, 21 | input [4095:0] test_name, 22 | output wishbone_err 23 | ); 24 | 25 | dut dut ( 26 | .clk_clk48(clk48), 27 | .clk_clk12(clk12), 28 | .reset(reset), 29 | .usb_d_p(usb_d_p), 30 | .usb_d_n(usb_d_n), 31 | .usb_pullup(usb_pullup), 32 | .usb_tx_en(usb_tx_en), 33 | .wishbone_adr(wishbone_adr), 34 | .wishbone_dat_r(wishbone_datrd), 35 | .wishbone_dat_w(wishbone_datwr), 36 | .wishbone_sel(wishbone_sel), 37 | .wishbone_cyc(wishbone_cyc), 38 | .wishbone_stb(wishbone_stb), 39 | .wishbone_ack(wishbone_ack), 40 | .wishbone_we(wishbone_we), 41 | .wishbone_cti(wishbone_cti), 42 | .wishbone_bte(wishbone_bte), 43 | .wishbone_err(wishbone_err) 44 | ); 45 | 46 | // Dump waves 47 | initial begin 48 | $dumpfile("dump.vcd"); 49 | $dumpvars(0, tb); 50 | end 51 | 52 | endmodule 53 | -------------------------------------------------------------------------------- /test-suite/conf/environment.yml: -------------------------------------------------------------------------------- 1 | name: usb-test-suite-env 2 | channels: 3 | - conda-forge 4 | - symbiflow 5 | dependencies: 6 | - python=3.7 7 | - libzip 8 | - sigrok-cli 9 | - iverilog 10 | -------------------------------------------------------------------------------- /test-suite/conf/requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | cocotb 3 | wheel 4 | -------------------------------------------------------------------------------- /valentyusb/usbcore/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/cpu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/cpu/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/cpu/epfifo_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from unittest import TestCase 5 | 6 | from migen import * 7 | 8 | from ..endpoint import EndpointType, EndpointResponse 9 | from ..io_test import FakeIoBuf 10 | from ..pid import PIDTypes 11 | from ..utils.packet import crc16 12 | 13 | from ..test.common import BaseUsbTestCase, CommonUsbTestCase 14 | from ..test.clock import CommonTestMultiClockDomain 15 | 16 | from .epfifo import PerEndpointFifoInterface 17 | 18 | 19 | class TestPerEndpointFifoInterface( 20 | BaseUsbTestCase, 21 | CommonUsbTestCase, 22 | CommonTestMultiClockDomain, 23 | unittest.TestCase): 24 | 25 | maxDiff=None 26 | 27 | def get_endpoint(self, epaddr): 28 | epdir = EndpointType.epdir(epaddr) 29 | epnum = EndpointType.epnum(epaddr) 30 | if epdir == EndpointType.OUT: 31 | return getattr(self.dut, "ep_%s_out" % epnum) 32 | elif epdir == EndpointType.IN: 33 | return getattr(self.dut, "ep_%s_in" % epnum) 34 | else: 35 | raise SystemError("Unknown endpoint type: %r" % epdir) 36 | 37 | def on_usb_48_edge(self): 38 | if False: 39 | yield 40 | 41 | def on_usb_12_edge(self): 42 | if False: 43 | yield 44 | 45 | def setUp(self): 46 | CommonTestMultiClockDomain.setUp(self, ("usb_12", "usb_48")) 47 | 48 | # Only enable "debug" mode for tests with "debug" in their name 49 | if "debug" in self._testMethodName: 50 | debug = True 51 | else: 52 | debug = False 53 | 54 | self.iobuf = FakeIoBuf() 55 | self.endpoints = [EndpointType.BIDIR, EndpointType.IN, EndpointType.BIDIR] 56 | self.dut = PerEndpointFifoInterface(self.iobuf, self.endpoints, debug=debug) 57 | 58 | self.packet_h2d = Signal(1) 59 | self.packet_d2h = Signal(1) 60 | self.packet_idle = Signal(1) 61 | 62 | def run_sim(self, stim): 63 | def padfront(): 64 | yield 65 | yield 66 | yield 67 | yield 68 | yield 69 | yield 70 | # Make sure that the endpoints are currently blocked 71 | opending = yield self.dut.ep_0_out.ev.packet.pending 72 | self.assertTrue(opending) 73 | ipending = yield self.dut.ep_0_in.ev.packet.pending 74 | self.assertTrue(ipending) 75 | yield 76 | yield from self.dut.pullup._out.write(1) 77 | yield 78 | # Make sure that the endpoints are currently blocked but not being 79 | # triggered. 80 | opending = yield self.dut.ep_0_out.ev.packet.pending 81 | self.assertTrue(opending) 82 | ipending = yield self.dut.ep_0_in.ev.packet.pending 83 | self.assertTrue(ipending) 84 | 85 | otrigger = yield self.dut.ep_0_out.ev.packet.trigger 86 | self.assertFalse(otrigger) 87 | itrigger = yield self.dut.ep_0_in.ev.packet.trigger 88 | self.assertFalse(itrigger) 89 | 90 | yield 91 | yield from self.idle() 92 | yield from stim() 93 | 94 | CommonUsbTestCase.patch_csrs(self) 95 | run_simulation( 96 | self.dut, 97 | padfront(), 98 | vcd_name=self.make_vcd_name(), 99 | #clocks={ 100 | # "sys": 12, 101 | # "usb_48": 48, 102 | # "usb_12": 192, 103 | #}, 104 | clocks={ 105 | "sys": 2, 106 | "usb_48": 8, 107 | "usb_12": 32, 108 | }, 109 | ) 110 | print("-"*10) 111 | 112 | def tick_sys(self): 113 | yield from self.update_internal_signals() 114 | yield 115 | 116 | def tick_usb48(self): 117 | yield from self.wait_for_edge("usb_48") 118 | 119 | def tick_usb12(self): 120 | for i in range(0, 4): 121 | yield from self.tick_usb48() 122 | 123 | def update_internal_signals(self): 124 | yield from self.update_clocks() 125 | 126 | # IRQ / packet pending ----------------- 127 | def trigger(self, epaddr): 128 | endpoint = self.get_endpoint(epaddr) 129 | status = yield endpoint.ev.packet.trigger 130 | return bool(status) 131 | 132 | def pending(self, epaddr): 133 | endpoint = self.get_endpoint(epaddr) 134 | status = yield from endpoint.ev.pending.read() 135 | return bool(status & 0x2) 136 | 137 | def clear_pending(self, epaddr): 138 | # Can't clear pending while trigger is active. 139 | for i in range(0, 100): 140 | trigger = (yield from self.trigger(epaddr)) 141 | if not trigger: 142 | break 143 | yield from self.tick_sys() 144 | self.assertFalse(trigger) 145 | # Check the pending flag is raised 146 | self.assertTrue((yield from self.pending(epaddr))) 147 | # Clear pending flag 148 | endpoint = self.get_endpoint(epaddr) 149 | yield from endpoint.ev.pending.write(0xf) 150 | yield from self.tick_sys() 151 | # Check the pending flag has been cleared 152 | self.assertFalse((yield from self.trigger(epaddr))) 153 | self.assertFalse((yield from self.pending(epaddr))) 154 | 155 | # Endpoint state ----------------------- 156 | def response(self, epaddr): 157 | endpoint = self.get_endpoint(epaddr) 158 | response = yield endpoint.response 159 | return response 160 | 161 | def set_response(self, epaddr, v): 162 | endpoint = self.get_endpoint(epaddr) 163 | assert isinstance(v, EndpointResponse), v 164 | yield from endpoint.respond.write(v) 165 | 166 | def expect_last_tok(self, epaddr, value): 167 | endpoint = self.get_endpoint(epaddr) 168 | last_tok = yield from endpoint.last_tok.read() 169 | self.assertEqual(last_tok, value) 170 | 171 | # Get/set endpoint data ---------------- 172 | def set_data(self, epaddr, data): 173 | """Set an endpoints buffer to given data to be sent.""" 174 | assert isinstance(data, (list, tuple)) 175 | self.ep_print(epaddr, "Set: %r", data) 176 | 177 | endpoint = self.get_endpoint(epaddr) 178 | 179 | # Make sure the endpoint is empty 180 | empty = yield from endpoint.ibuf_empty.read() 181 | self.assertTrue( 182 | empty, "Device->Host buffer not empty when setting data!") 183 | 184 | # If we are writing multiple bytes of data, need to make sure we are 185 | # not going to ACK the packet until the data is ready. 186 | if len(data) > 1: 187 | response = yield endpoint.response 188 | self.assertNotEqual(response, EndpointResponse.ACK) 189 | 190 | for v in data: 191 | yield from endpoint.ibuf_head.write(v) 192 | yield from self.tick_sys() 193 | 194 | for i in range(0, 10): 195 | yield from self.tick_usb12() 196 | 197 | empty = yield from endpoint.ibuf_empty.read() 198 | if len(data) > 0: 199 | self.assertFalse( 200 | bool(empty), "Buffer not empty after setting zero data!") 201 | else: 202 | self.assertTrue( 203 | bool(empty), "Buffer empty after setting data!") 204 | 205 | def expect_data(self, epaddr, data): 206 | """Expect that an endpoints buffer has given contents.""" 207 | endpoint = self.get_endpoint(epaddr) 208 | 209 | # Make sure there is something pending 210 | self.assertTrue((yield from self.pending(epaddr))) 211 | 212 | actual_data = [] 213 | while range(0, 1024): 214 | yield from endpoint.obuf_head.write(0) 215 | empty = yield from endpoint.obuf_empty.read() 216 | if empty: 217 | break 218 | 219 | v = yield from endpoint.obuf_head.read() 220 | actual_data.append(v) 221 | yield 222 | 223 | assert len(actual_data) >= 2, actual_data 224 | actual_data, actual_crc16 = actual_data[:-2], actual_data[-2:] 225 | 226 | self.ep_print(epaddr, "Got: %r (expected: %r)", actual_data, data) 227 | self.assertSequenceEqual(data, actual_data) 228 | self.assertSequenceEqual(crc16(data), actual_crc16) 229 | 230 | def dtb(self, epaddr): 231 | endpoint = self.get_endpoint(epaddr) 232 | status = yield from endpoint.dtb.read() 233 | return bool(status) 234 | 235 | 236 | if __name__ == '__main__': 237 | unittest.main() 238 | -------------------------------------------------------------------------------- /valentyusb/usbcore/cpu/unififo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import fifo 5 | from migen.genlib import cdc 6 | 7 | from litex.soc.interconnect import csr_eventmanager as ev 8 | from litex.soc.interconnect.csr import * 9 | from litex.soc.cores.gpio import GPIOOut 10 | 11 | from ..endpoint import * 12 | from ..io import FakeIoBuf 13 | from ..rx.pipeline import RxPipeline 14 | from ..tx.pipeline import TxPipeline 15 | 16 | from ..utils.packet import * 17 | 18 | 19 | class UsbUniFifo(Module, AutoCSR): 20 | """ 21 | Presents the USB data stream as two FIFOs via CSR registers. 22 | """ 23 | 24 | def __init__(self, iobuf): 25 | self.submodules.ev = ev.EventManager() 26 | self.ev.submodules.rx = ev.EventSourcePulse() 27 | 28 | # --------------------- 29 | # RX side 30 | # --------------------- 31 | self.submodules.rx = rx = RxPipeline() 32 | self.byte_count = CSRStatus(8) 33 | 34 | obuf = fifo.AsyncFIFOBuffered(width=8, depth=128) 35 | self.submodules.obuf = ClockDomainsRenamer({"write": "usb_12", "read": "sys"})(obuf) 36 | 37 | # USB side (writing) 38 | self.comb += [ 39 | self.obuf.din.eq(self.rx.o_data_payload), 40 | self.obuf.we.eq(self.rx.o_data_strobe), 41 | ] 42 | self.sync.usb_12 += [ 43 | self.ev.rx.trigger.eq(self.rx.o_pkt_end), 44 | If(self.rx.o_data_strobe, self.byte_count.status.eq(self.byte_count.status + 1)) 45 | ] 46 | 47 | # System side (reading) 48 | self.obuf_head = CSR(8) 49 | self.obuf_empty = CSRStatus(1) 50 | self.comb += [ 51 | self.obuf_head.w.eq(self.obuf.dout), 52 | self.obuf.re.eq(self.obuf_head.re), 53 | self.obuf_empty.status.eq(~self.obuf.readable), 54 | ] 55 | 56 | # --------------------- 57 | # TX side 58 | # --------------------- 59 | self.submodules.tx = tx = TxPipeline() 60 | 61 | ibuf = fifo.AsyncFIFOBuffered(width=8, depth=128) 62 | self.submodules.ibuf = ClockDomainsRenamer({"write": "sys", "read": "usb_12"})(ibuf) 63 | 64 | # System side (writing) 65 | self.arm = CSRStorage(1) 66 | self.ibuf_head = CSR(8) 67 | self.ibuf_empty = CSRStatus(1) 68 | self.comb += [ 69 | self.ibuf.din.eq(self.ibuf_head.r), 70 | self.ibuf.we.eq(self.ibuf_head.re), 71 | self.ibuf_empty.status.eq(~self.ibuf.readable & ~tx.o_oe), 72 | ] 73 | 74 | # USB side (reading) 75 | self.comb += [ 76 | tx.i_data_payload.eq(self.ibuf.dout), 77 | self.ibuf.re.eq(tx.o_data_strobe & self.arm.storage), 78 | ] 79 | self.sync.usb_12 += [ 80 | tx.i_oe.eq(self.ibuf.readable & self.arm.storage), 81 | ] 82 | 83 | # ---------------------- 84 | # USB 48MHz bit strobe 85 | # ---------------------- 86 | self.comb += [ 87 | tx.i_bit_strobe.eq(rx.o_bit_strobe), 88 | ] 89 | 90 | # ---------------------- 91 | # Tristate 92 | # ---------------------- 93 | self.submodules.iobuf = iobuf 94 | self.comb += [ 95 | rx.i_usbp.eq(iobuf.usb_p_rx), 96 | rx.i_usbn.eq(iobuf.usb_n_rx), 97 | iobuf.usb_tx_en.eq(tx.o_oe), 98 | iobuf.usb_p_tx.eq(tx.o_usbp), 99 | iobuf.usb_n_tx.eq(tx.o_usbn), 100 | ] 101 | self.submodules.pullup = GPIOOut(iobuf.usb_pullup) 102 | -------------------------------------------------------------------------------- /valentyusb/usbcore/endpoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from enum import IntEnum 4 | 5 | 6 | class EndpointType(IntEnum): 7 | IN = 1 8 | OUT = 2 9 | BIDIR = IN | OUT 10 | 11 | @classmethod 12 | def epaddr(cls, ep_num, ep_dir): 13 | assert ep_dir != cls.BIDIR 14 | return ep_num << 1 | (ep_dir == cls.IN) 15 | 16 | @classmethod 17 | def epnum(cls, ep_addr): 18 | return ep_addr >> 1 19 | 20 | @classmethod 21 | def epdir(cls, ep_addr): 22 | if ep_addr & 0x1 == 0: 23 | return cls.OUT 24 | else: 25 | return cls.IN 26 | 27 | 28 | class EndpointResponse(IntEnum): 29 | """ 30 | >>> # Clearing top bit of STALL -> NAK 31 | >>> assert (EndpointResponse.STALL & EndpointResponse.RESET_MASK) == EndpointResponse.NAK 32 | """ 33 | STALL = 0b11 34 | ACK = 0b00 35 | NAK = 0b01 36 | NONE = 0b10 37 | 38 | RESET_MASK = 0b01 39 | 40 | 41 | if __name__ == "__main__": 42 | import doctest 43 | doctest.testmod() 44 | -------------------------------------------------------------------------------- /valentyusb/usbcore/io.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.genlib.cdc import MultiReg 7 | 8 | class Raw(Instance.PreformattedParam): 9 | def __init__(self, value): 10 | self.value = value 11 | 12 | 13 | class IoBuf(Module): 14 | def __init__(self, usbp_pin, usbn_pin, usb_pullup_pin=None): 15 | # tx/rx io interface 16 | self.usb_tx_en = Signal() 17 | self.usb_p_tx = Signal() 18 | self.usb_n_tx = Signal() 19 | 20 | self.usb_p_rx = Signal() 21 | self.usb_n_rx = Signal() 22 | self.usb_ls_rx = Signal() 23 | 24 | self.usb_p_rx_io = Signal() 25 | self.usb_n_rx_io = Signal() 26 | 27 | usb_p_t = TSTriple() 28 | usb_n_t = TSTriple() 29 | 30 | self.specials += usb_p_t.get_tristate(usbp_pin) 31 | self.specials += usb_n_t.get_tristate(usbn_pin) 32 | 33 | self.usb_pullup = Signal() 34 | if usb_pullup_pin is not None: 35 | self.comb += [ 36 | usb_pullup_pin.eq(self.usb_pullup), 37 | ] 38 | 39 | ####################################################################### 40 | ####################################################################### 41 | #### Mux the USB +/- pair with the TX and RX paths 42 | ####################################################################### 43 | ####################################################################### 44 | usb_p_t_i = Signal() 45 | usb_n_t_i = Signal() 46 | self.specials += [ 47 | MultiReg(usb_p_t.i, usb_p_t_i), 48 | MultiReg(usb_n_t.i, usb_n_t_i) 49 | ] 50 | self.comb += [ 51 | If(self.usb_tx_en, 52 | self.usb_p_rx.eq(0b1), 53 | self.usb_n_rx.eq(0b0), 54 | ).Elif(self.usb_ls_rx, 55 | self.usb_p_rx.eq(usb_n_t_i), 56 | self.usb_n_rx.eq(usb_p_t_i), 57 | ).Else( 58 | self.usb_p_rx.eq(usb_p_t_i), 59 | self.usb_n_rx.eq(usb_n_t_i), 60 | ), 61 | usb_p_t.oe.eq(self.usb_tx_en), 62 | usb_n_t.oe.eq(self.usb_tx_en), 63 | usb_p_t.o.eq(self.usb_p_tx), 64 | usb_n_t.o.eq(self.usb_n_tx), 65 | ] 66 | 67 | 68 | class FakeIoBuf(Module): 69 | def __init__(self): 70 | self.usb_pullup = Signal() 71 | 72 | self.usb_p = Signal() 73 | self.usb_n = Signal() 74 | 75 | self.usb_tx_en = Signal() 76 | self.usb_p_tx = Signal() 77 | self.usb_n_tx = Signal() 78 | 79 | self.usb_p_rx = Signal() 80 | self.usb_n_rx = Signal() 81 | 82 | self.usb_p_rx_io = Signal() 83 | self.usb_n_rx_io = Signal() 84 | 85 | self.comb += [ 86 | If(self.usb_tx_en, 87 | self.usb_p_rx.eq(0b1), 88 | self.usb_n_rx.eq(0b0) 89 | ).Else( 90 | self.usb_p_rx.eq(self.usb_p_rx_io), 91 | self.usb_n_rx.eq(self.usb_n_rx_io) 92 | ), 93 | ] 94 | self.comb += [ 95 | If(self.usb_tx_en, 96 | self.usb_p.eq(self.usb_p_tx), 97 | self.usb_n.eq(self.usb_n_tx), 98 | ).Else( 99 | self.usb_p.eq(self.usb_p_rx), 100 | self.usb_n.eq(self.usb_n_rx), 101 | ), 102 | ] 103 | 104 | def recv(self, v): 105 | tx_en = yield self.usb_tx_en 106 | assert not tx_en, "Currently transmitting!" 107 | 108 | if v == '0' or v == '_': 109 | # SE0 - both lines pulled low 110 | yield self.usb_p_rx_io.eq(0) 111 | yield self.usb_n_rx_io.eq(0) 112 | elif v == '1': 113 | # SE1 - illegal, should never occur 114 | yield self.usb_p_rx_io.eq(1) 115 | yield self.usb_n_rx_io.eq(1) 116 | elif v == '-' or v == 'I': 117 | # Idle 118 | yield self.usb_p_rx_io.eq(1) 119 | yield self.usb_n_rx_io.eq(0) 120 | elif v == 'J': 121 | yield self.usb_p_rx_io.eq(1) 122 | yield self.usb_n_rx_io.eq(0) 123 | elif v == 'K': 124 | yield self.usb_p_rx_io.eq(0) 125 | yield self.usb_n_rx_io.eq(1) 126 | else: 127 | assert False, "Unknown value: %s" % v 128 | 129 | def current(self): 130 | usb_p = yield self.usb_p 131 | usb_n = yield self.usb_n 132 | values = (usb_p, usb_n) 133 | 134 | if values == (0, 0): 135 | return '_' 136 | elif values == (1, 1): 137 | return '1' 138 | elif values == (1, 0): 139 | return 'J' 140 | elif values == (0, 1): 141 | return 'K' 142 | else: 143 | assert False, values 144 | -------------------------------------------------------------------------------- /valentyusb/usbcore/io_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from .io import FakeIoBuf 8 | 9 | class TestIoBuf(unittest.TestCase): 10 | pass 11 | 12 | 13 | class TestFakeIoBuf(unittest.TestCase): 14 | def setUp(self): 15 | self.dut = FakeIoBuf() 16 | 17 | def test_recv_idle(self): 18 | def stim(): 19 | yield 20 | yield from self.dut.recv('-') 21 | yield 22 | self.assertEqual((yield from self.dut.current()), 'J') 23 | yield 24 | self.assertEqual((yield from self.dut.current()), 'J') 25 | run_simulation(self.dut, stim()) 26 | 27 | def test_recv_se0(self): 28 | def stim(): 29 | yield 30 | yield from self.dut.recv('_') 31 | yield 32 | self.assertEqual((yield from self.dut.current()), '_') 33 | yield 34 | self.assertEqual((yield from self.dut.current()), '_') 35 | run_simulation(self.dut, stim()) 36 | 37 | def test_recv_se0_alias(self): 38 | def stim(): 39 | yield 40 | yield from self.dut.recv('0') 41 | yield 42 | self.assertEqual((yield from self.dut.current()), '_') 43 | yield 44 | self.assertEqual((yield from self.dut.current()), '_') 45 | run_simulation(self.dut, stim()) 46 | 47 | def test_recv_j(self): 48 | def stim(): 49 | yield 50 | yield from self.dut.recv('J') 51 | yield 52 | self.assertEqual((yield from self.dut.current()), 'J') 53 | yield 54 | self.assertEqual((yield from self.dut.current()), 'J') 55 | run_simulation(self.dut, stim()) 56 | 57 | def test_recv_k(self): 58 | def stim(): 59 | yield 60 | yield from self.dut.recv('K') 61 | yield 62 | self.assertEqual((yield from self.dut.current()), 'K') 63 | yield 64 | self.assertEqual((yield from self.dut.current()), 'K') 65 | run_simulation(self.dut, stim()) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /valentyusb/usbcore/pid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from enum import IntEnum 4 | 5 | 6 | class PID(IntEnum): 7 | # USB Packet IDs 8 | """ 9 | >>> bin(PID.SETUP.value) 10 | '0b1101' 11 | >>> PID.SETUP.encode() 12 | 'KKKKJJJJJJJJJJJJKKKKKKKKJJJJKKKK' 13 | 14 | >>> for p in PID: 15 | ... print("%-10s" % p, "%x" % p.value, "%02x" % p.byte(), p.encode(1)) 16 | PID.SETUP d 2d KJJJKKJK 17 | PID.OUT 1 e1 KJKJKKKK 18 | PID.IN 9 69 KJKKJJJK 19 | PID.SOF 5 a5 KJJKJJKK 20 | PID.DATA0 3 c3 KKJKJKKK 21 | PID.DATA1 b 4b KKJJKJJK 22 | PID.DATA2 7 87 KKKJKJKK 23 | PID.MDATA f 0f KKKKJKJK 24 | PID.ACK 2 d2 JJKJJKKK 25 | PID.NAK a 5a JJKKKJJK 26 | PID.STALL e 1e JJJJJKJK 27 | PID.NYET 6 96 JJJKKJKK 28 | PID.PRE c 3c JKKKKKJK 29 | PID.SPLIT 8 78 JKJJJJJK 30 | PID.PING 4 b4 JKKJJJKK 31 | PID.RESERVED 0 f0 JKJKKKKK 32 | """ 33 | 34 | # Token pids 35 | SETUP = 0b1101 # D 36 | OUT = 0b0001 # 1 37 | IN = 0b1001 # 9 38 | SOF = 0b0101 # 5 39 | 40 | # Data pid 41 | DATA0 = 0b0011 # 3 42 | DATA1 = 0b1011 # B 43 | # USB HS only 44 | DATA2 = 0b0111 # B 45 | MDATA = 0b1111 # F 46 | 47 | # Handshake pids 48 | ACK = 0b0010 # 2 49 | NAK = 0b1010 # A 50 | STALL = 0b1110 # E 51 | # USB HS only 52 | NYET = 0b0110 # 6 53 | 54 | # USB HS only 55 | PRE = 0b1100 # C 56 | ERR = 0b1100 # C 57 | SPLIT = 0b1000 # 8 58 | PING = 0b0100 # 4 59 | RESERVED = 0b0000 # 0 60 | 61 | def byte(self): 62 | v = self.value 63 | return v | ((0b1111 ^ v) << 4) 64 | 65 | def encode(self, cycles=4): 66 | # Prevent cyclic imports by importing here... 67 | from .utils.packet import nrzi, sync, encode_pid 68 | return nrzi(sync()+encode_pid(self.value),cycles)[cycles*len(sync()):] 69 | 70 | 71 | class PIDTypes(IntEnum): 72 | """ 73 | >>> # Token PIDs 74 | >>> PIDTypes.token(PID.SETUP), PIDTypes.data(PID.SETUP), PIDTypes.handshake(PID.SETUP) 75 | (True, False, False) 76 | >>> PIDTypes.token(PID.OUT), PIDTypes.data(PID.OUT), PIDTypes.handshake(PID.OUT) 77 | (True, False, False) 78 | >>> PIDTypes.token(PID.IN), PIDTypes.data(PID.IN), PIDTypes.handshake(PID.IN) 79 | (True, False, False) 80 | >>> PIDTypes.token(PID.SOF), PIDTypes.data(PID.SOF), PIDTypes.handshake(PID.SOF) 81 | (True, False, False) 82 | 83 | >>> # Data PIDs 84 | >>> PIDTypes.token(PID.DATA0), PIDTypes.data(PID.DATA0), PIDTypes.handshake(PID.DATA0) 85 | (False, True, False) 86 | >>> PIDTypes.token(PID.DATA1), PIDTypes.data(PID.DATA1), PIDTypes.handshake(PID.DATA1) 87 | (False, True, False) 88 | >>> # USB2.0 Data PIDs 89 | >>> PIDTypes.token(PID.DATA2), PIDTypes.data(PID.DATA2), PIDTypes.handshake(PID.DATA2) 90 | (False, True, False) 91 | >>> PIDTypes.token(PID.MDATA), PIDTypes.data(PID.MDATA), PIDTypes.handshake(PID.MDATA) 92 | (False, True, False) 93 | 94 | >>> # Handshake PIDs 95 | >>> PIDTypes.token(PID.ACK), PIDTypes.data(PID.ACK), PIDTypes.handshake(PID.ACK) 96 | (False, False, True) 97 | >>> PIDTypes.token(PID.NAK), PIDTypes.data(PID.NAK), PIDTypes.handshake(PID.NAK) 98 | (False, False, True) 99 | >>> PIDTypes.token(PID.STALL), PIDTypes.data(PID.STALL), PIDTypes.handshake(PID.STALL) 100 | (False, False, True) 101 | >>> # USB2.0 Handshake PIDs 102 | >>> PIDTypes.token(PID.NYET), PIDTypes.data(PID.NYET), PIDTypes.handshake(PID.NYET) 103 | (False, False, True) 104 | 105 | >>> # Special PIDs 106 | >>> PIDTypes.token(PID.PRE), PIDTypes.data(PID.PRE), PIDTypes.handshake(PID.PRE) 107 | (False, False, False) 108 | """ 109 | 110 | TOKEN = 0b0001 111 | DATA = 0b0011 112 | HANDSHAKE = 0b0010 113 | SPECIAL = 0b0000 114 | 115 | TYPE_MASK = 0b0011 116 | 117 | @staticmethod 118 | def token(p): 119 | assert isinstance(p, PID), repr(p) 120 | return (p & PIDTypes.TYPE_MASK) == PIDTypes.TOKEN 121 | 122 | @staticmethod 123 | def data(p): 124 | assert isinstance(p, PID), repr(p) 125 | return (p & PIDTypes.TYPE_MASK) == PIDTypes.DATA 126 | 127 | @staticmethod 128 | def handshake(p): 129 | assert isinstance(p, PID), repr(p) 130 | return (p & PIDTypes.TYPE_MASK) == PIDTypes.HANDSHAKE 131 | 132 | @staticmethod 133 | def special(p): 134 | assert isinstance(p, PID), repr(p) 135 | return (p & PIDTypes.TYPE_MASK) == PIDTypes.SPECIAL 136 | 137 | 138 | if __name__ == "__main__": 139 | import doctest 140 | doctest.testmod() 141 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/rx/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/bitstuff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.fhdl.decorators import ResetInserter 5 | 6 | from ..test.common import BaseUsbTestCase 7 | import unittest 8 | 9 | 10 | @ResetInserter() 11 | class RxBitstuffRemover(Module): 12 | """RX Bitstuff Removal 13 | 14 | Long sequences of 1's would cause the receiver to lose it's lock on the 15 | transmitter's clock. USB solves this with bitstuffing. A '0' is stuffed 16 | after every 6 consecutive 1's. This extra bit is required to recover the 17 | clock, but it should not be passed on to higher layers in the device. 18 | 19 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 7.1.9 20 | https://en.wikipedia.org/wiki/Bit_stuffing 21 | 22 | Clock Domain 23 | ------------ 24 | usb_12 : 12MHz 25 | 26 | Input Ports 27 | ------------ 28 | i_valid : Signal(1) 29 | Qualifier for all of the input signals. Indicates one bit of valid 30 | data is present on the inputs. 31 | 32 | i_data : Signal(1) 33 | Decoded data bit from USB bus. 34 | Qualified by valid. 35 | 36 | Output Ports 37 | ------------ 38 | o_data : Signal(1) 39 | Decoded data bit from USB bus. 40 | 41 | o_stall : Signal(1) 42 | Indicates the bit stuffer just removed an extra bit, so no data available. 43 | 44 | o_error : Signal(1) 45 | Indicates there has been a bitstuff error. A bitstuff error occurs 46 | when there should be a stuffed '0' after 6 consecutive 1's; but instead 47 | of a '0', there is an additional '1'. This is normal during IDLE, but 48 | should never happen within a packet. 49 | Qualified by valid. 50 | """ 51 | 52 | def __init__(self): 53 | self.i_valid = Signal() 54 | self.i_data = Signal() 55 | 56 | # This state machine recognizes sequences of 6 bits and drops the 7th 57 | # bit. The fsm implements a counter in a series of several states. 58 | # This is intentional to help absolutely minimize the levels of logic 59 | # used. 60 | self.submodules.stuff = stuff = FSM(reset_state="D0") 61 | 62 | drop_bit = Signal(1) 63 | 64 | for i in range(6): 65 | stuff.act("D%d" % i, 66 | If(self.i_valid, 67 | If(self.i_data, 68 | # Receiving '1' increments the bitstuff counter. 69 | NextState("D%d" % (i + 1)) 70 | ).Else( 71 | # Receiving '0' resets the bitstuff counter. 72 | NextState("D0") 73 | ) 74 | ), 75 | ) 76 | 77 | stuff.act("D6", 78 | If(self.i_valid, 79 | drop_bit.eq(1), 80 | # Reset the bitstuff counter, drop the data. 81 | NextState("D0") 82 | ) 83 | ) 84 | 85 | # pass all of the outputs through a pipe stage 86 | self.o_data = Signal() 87 | self.o_error = Signal() 88 | self.o_stall = Signal(reset=1) 89 | 90 | self.sync += [ 91 | self.o_data.eq(self.i_data), 92 | self.o_stall.eq(drop_bit | ~self.i_valid), 93 | self.o_error.eq(drop_bit & self.i_data & self.i_valid), 94 | ] 95 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/bitstuff_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.fhdl.decorators import ResetInserter 7 | 8 | from ..test.common import BaseUsbTestCase 9 | 10 | from .bitstuff import RxBitstuffRemover 11 | 12 | class TestRxBitstuffRemover(BaseUsbTestCase): 13 | def bitstuff_test(self, vector, short_name): 14 | def set_input(dut, r, d, v): 15 | yield dut.reset.eq(r == '1') 16 | yield dut.i_data.eq(d== '1') 17 | yield dut.i_valid.eq(v == '-') 18 | 19 | def get_output(dut): 20 | # Read outputs 21 | o_data = yield dut.o_data 22 | o_stall = yield dut.o_stall 23 | o_error = yield dut.o_error 24 | 25 | if o_error: 26 | return 'e' 27 | elif o_stall: 28 | return's' 29 | else: 30 | return str(o_data) 31 | 32 | def send(reset, value, valid): 33 | assert len(reset) == len(value) == len(valid), (reset, value, valid) 34 | output = "" 35 | for i in range(len(value)+1): 36 | if i < len(value): 37 | yield from set_input(dut, reset[i], value[i], valid[i]) 38 | 39 | yield 40 | 41 | if i > 0: 42 | if reset[i-1] == '1': 43 | output += '_' 44 | else: 45 | output += yield from get_output(dut) 46 | return output 47 | 48 | def stim(output, **kw): 49 | actual_output = yield from send(**kw) 50 | self.assertEqual(output, actual_output) 51 | 52 | with self.subTest(short_name=short_name, vector=vector): 53 | dut = RxBitstuffRemover() 54 | 55 | run_simulation( 56 | dut, 57 | stim(**vector), 58 | vcd_name=self.make_vcd_name(testsuffix=short_name), 59 | ) 60 | 61 | def test_no_bit_stuff(self): 62 | return self.bitstuff_test( 63 | # No bit stuff 64 | dict( 65 | reset = "00000000000000000000", 66 | value = "10110111011110111110", 67 | valid = "--------------------", 68 | output = "10110111011110111110", 69 | ), "no-bit-stuff") 70 | 71 | def test_bit_stuff(self): 72 | return self.bitstuff_test( 73 | # Bit stuff 74 | dict( 75 | reset = "0000000", 76 | value = "1111110", 77 | valid = "-------", 78 | output = "111111s", 79 | ), "bit-stuff") 80 | 81 | def test_bit_stuff_after_reset(self): 82 | return self.bitstuff_test( 83 | # Bit stuff after reset 84 | dict( 85 | reset = "00010000000", 86 | value = "11111111110", 87 | valid = "-----------", 88 | output = "111_111111s", 89 | ), "bit-stuff-after-reset") 90 | 91 | def test_bit_stuff_error(self): 92 | return self.bitstuff_test( 93 | # Bit stuff error 94 | dict( 95 | reset = "0000000", 96 | value = "1111111", 97 | valid = "-------", 98 | output = "111111e", 99 | ), "bit_stuff_error") 100 | 101 | def test_bit_stuff_error_after_reset(self): 102 | return self.bitstuff_test( 103 | # Bit stuff error after reset 104 | dict( 105 | reset = "00010000000", 106 | value = "11111111111", 107 | valid = "-----------", 108 | output = "111_111111e", 109 | ), "bit-stuff-error-after-reset") 110 | 111 | def test_multiple_bit_stuff_scenario(self): 112 | return self.bitstuff_test( 113 | dict( 114 | # Multiple bitstuff scenario 115 | reset = "000000000000000000000", 116 | value = "111111011111101111110", 117 | valid = "---------------------", 118 | output = "111111s111111s111111s", 119 | ), "multiple-bit-stuff-scenario") 120 | 121 | def test_mixed_bit_stuff_error(self): 122 | return self.bitstuff_test( 123 | dict( 124 | # Mixed bitstuff error 125 | reset = "000000000000000000000000000000000", 126 | value = "111111111111101111110111111111111", 127 | valid = "---------------------------------", 128 | output = "111111e111111s111111s111111e11111", 129 | ), "mixed-bit-stuff-error") 130 | 131 | def test_idle_packet_idle(self): 132 | return self.bitstuff_test( 133 | dict( 134 | # Idle, Packet, Idle 135 | reset = "0000000000000000000000001100000", 136 | value = "111110000000111111011101__11111", 137 | valid = "-------------------------------", 138 | output = "111110000000111111s11101__11111", 139 | ), "idle-packet-idle") 140 | 141 | def test_idle_packet_idle_packet_idle(self): 142 | return self.bitstuff_test( 143 | dict( 144 | # Idle, Packet, Idle, Packet, Idle 145 | reset = "00000000000000000000000011000000000000000000000000000001100000", 146 | value = "111110000000111111011101__11111111110000000111111011101__11111", 147 | valid = "--------------------------------------------------------------", 148 | output = "111110000000111111s11101__111111e1110000000111111s11101__11111", 149 | ), "idle-packet-idle-packet-idle") 150 | 151 | def test_captured_setup_packet_no_stuff(self): 152 | return self.bitstuff_test( 153 | dict( 154 | # Captured setup packet (no bitstuff) 155 | reset = "000000000000000000000000000000000110", 156 | value = "100000001101101000000000000001000__1", 157 | valid = "------------------------------------", 158 | output = "100000001101101000000000000001000__1" 159 | ), "captured-setup-packet-no-stuff") 160 | 161 | def test_valid_idle_packet_idle_packet_idle(self): 162 | return self.bitstuff_test( 163 | dict( 164 | # Idle, Packet, Idle, Packet, Idle 165 | reset = "00000000000000000000000000110000000000000000000000000000000001100000", 166 | value = "11111000000001111111011101__111101111110000100011110111011101__11111", 167 | valid = "--------_-------_---------------_----------_------__---------_------", 168 | output = "11111000s0000111s111s11101__1111s11e1110000s000111ss111s11101__11111", 169 | ), "valid-idle-packet-idle-packet-idle") 170 | 171 | def test_valid_captured_setup_packet_no_stuff(self): 172 | return self.bitstuff_test( 173 | dict( 174 | # Captured setup packet (no bitstuff) 175 | reset = "000000000000000000000000000000000000000110", 176 | value = "100000000110111010000000000000000001000__1", 177 | valid = "-----_--------_-----_----___--------------", 178 | output = "10000s00011011s01000s0000sss00000001000__1" 179 | ), "valid-captured-setup-packet-no-stuff") 180 | 181 | 182 | if __name__ == "__main__": 183 | unittest.main() 184 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import cdc 5 | 6 | from ..test.common import BaseUsbTestCase 7 | import unittest 8 | 9 | 10 | class RxClockDataRecovery(Module): 11 | """RX Clock Data Recovery module. 12 | 13 | RxClockDataRecovery synchronizes the USB differential pair with the FPGAs 14 | clocks, de-glitches the differential pair, and recovers the incoming clock 15 | and data. 16 | 17 | Clock Domain 18 | ------------ 19 | usb_48 : 48MHz 20 | 21 | Input Ports 22 | ----------- 23 | Input ports are passed in via the constructor. 24 | 25 | usbp_raw : Signal(1) 26 | Raw USB+ input from the FPGA IOs, no need to synchronize. 27 | 28 | usbn_raw : Signal(1) 29 | Raw USB- input from the FPGA IOs, no need to synchronize. 30 | 31 | Output Ports 32 | ------------ 33 | Output ports are data members of the module. All output ports are flopped. 34 | The line_state_dj/dk/se0/se1 outputs are 1-hot encoded. 35 | 36 | line_state_valid : Signal(1) 37 | Asserted for one clock when the output line state is ready to be sampled. 38 | 39 | line_state_dj : Signal(1) 40 | Represents Full Speed J-state on the incoming USB data pair. 41 | Qualify with line_state_valid. 42 | 43 | line_state_dk : Signal(1) 44 | Represents Full Speed K-state on the incoming USB data pair. 45 | Qualify with line_state_valid. 46 | 47 | line_state_se0 : Signal(1) 48 | Represents SE0 on the incoming USB data pair. 49 | Qualify with line_state_valid. 50 | 51 | line_state_se1 : Signal(1) 52 | Represents SE1 on the incoming USB data pair. 53 | Qualify with line_state_valid. 54 | """ 55 | def __init__(self, usbp_raw, usbn_raw, low_speed=None): 56 | if False: 57 | ####################################################################### 58 | # Synchronize raw USB signals 59 | # 60 | # We need to synchronize the raw USB signals with the usb_48 clock 61 | # domain. MultiReg implements a multi-stage shift register that takes 62 | # care of this for us. Without MultiReg we would have metastability 63 | # issues. 64 | # 65 | usbp = Signal(reset=1) 66 | usbn = Signal() 67 | 68 | self.specials += cdc.MultiReg(usbp_raw, usbp, n=1, reset=1) 69 | self.specials += cdc.MultiReg(usbn_raw, usbn, n=1) 70 | else: 71 | # Leave raw USB signals meta-stable. The synchronizer should clean 72 | # them up. 73 | usbp = usbp_raw 74 | usbn = usbn_raw 75 | 76 | ####################################################################### 77 | # Line State Recovery State Machine 78 | # 79 | # The receive path doesn't use a differential receiver. Because of 80 | # this there is a chance that one of the differential pairs will appear 81 | # to have changed to the new state while the other is still in the old 82 | # state. The following state machine detects transitions and waits an 83 | # extra sampling clock before decoding the state on the differential 84 | # pair. This transition period # will only ever last for one clock as 85 | # long as there is no noise on the line. If there is enough noise on 86 | # the line then the data may be corrupted and the packet will fail the 87 | # data integrity checks. 88 | # 89 | self.submodules.lsr = lsr = FSM() 90 | 91 | dpair = Signal(2) 92 | self.comb += dpair.eq(Cat(usbn, usbp)) 93 | 94 | # output signals for use by the clock recovery stage 95 | line_state_dt = Signal() 96 | line_state_dj = Signal() 97 | line_state_dk = Signal() 98 | line_state_se0 = Signal() 99 | line_state_se1 = Signal() 100 | 101 | # If we are in a transition state, then we can sample the pair and 102 | # move to the next corresponding line state. 103 | lsr.act("DT", 104 | line_state_dt.eq(1), 105 | Case(dpair, { 106 | 0b10 : NextState("DJ"), 107 | 0b01 : NextState("DK"), 108 | 0b00 : NextState("SE0"), 109 | 0b11 : NextState("SE1") 110 | }) 111 | ) 112 | 113 | # If we are in a valid line state and the value of the pair changes, 114 | # then we need to move to the transition state. 115 | lsr.act("DJ", line_state_dj.eq(1), If(dpair != 0b10, NextState("DT"))) 116 | lsr.act("DK", line_state_dk.eq(1), If(dpair != 0b01, NextState("DT"))) 117 | lsr.act("SE0", line_state_se0.eq(1), If(dpair != 0b00, NextState("DT"))) 118 | lsr.act("SE1", line_state_se1.eq(1), If(dpair != 0b11, NextState("DT"))) 119 | 120 | 121 | ####################################################################### 122 | # Clock and Data Recovery 123 | # 124 | # The DT state from the line state recovery state machine is used to align to 125 | # transmit clock. The line state is sampled in the middle of the bit time. 126 | # 127 | # Example of signal relationships 128 | # ------------------------------- 129 | # line_state DT DJ DJ DJ DT DK DK DK DK DK DK DT DJ DJ DJ 130 | # line_state_valid ________----____________----____________----________----____ 131 | # bit_phase 0 0 1 2 3 0 1 2 3 0 1 2 0 1 2 132 | # 133 | 134 | # We 4x oversample, so make the line_state_phase have 135 | # 4 possible values. 136 | line_state_phase = Signal(2 if low_speed is None else 5) 137 | 138 | self.line_state_valid = Signal() 139 | self.line_state_dj = Signal() 140 | self.line_state_dk = Signal() 141 | self.line_state_se0 = Signal() 142 | self.line_state_se1 = Signal() 143 | 144 | self.sync += [ 145 | self.line_state_valid.eq(line_state_phase == 1), 146 | 147 | If(line_state_dt, 148 | # re-align the phase with the incoming transition 149 | line_state_phase.eq(0), 150 | 151 | # make sure we never assert valid on a transition 152 | self.line_state_valid.eq(0), 153 | ).Elif(low_speed is not None and ~low_speed, 154 | # keep tracking the clock by incrementing the phase 155 | # (with a reduced period for full speed mode) 156 | line_state_phase.eq((line_state_phase + 1) & 3), 157 | ).Else( 158 | # keep tracking the clock by incrementing the phase 159 | line_state_phase.eq(line_state_phase + 1) 160 | ), 161 | 162 | # flop all the outputs to help with timing 163 | self.line_state_dj.eq(line_state_dj), 164 | self.line_state_dk.eq(line_state_dk), 165 | self.line_state_se0.eq(line_state_se0), 166 | self.line_state_se1.eq(line_state_se1), 167 | ] 168 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/clock_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.genlib import cdc 7 | 8 | from ..test.common import BaseUsbTestCase 9 | 10 | from .clock import RxClockDataRecovery 11 | 12 | class TestRxClockDataRecovery(BaseUsbTestCase): 13 | def test_j(self): 14 | return self.basic_recovery_test("j") 15 | 16 | def test_k(self): 17 | return self.basic_recovery_test("k") 18 | 19 | def test_0(self): 20 | return self.basic_recovery_test("0") 21 | 22 | def test_1(self): 23 | return self.basic_recovery_test("1") 24 | 25 | def test_jk01(self): 26 | return self.basic_recovery_test("jk01") 27 | 28 | def test_jjjkj0j1kjkkk0k10j0k00011j1k1011(self): 29 | return self.basic_recovery_test("jjjkj0j1kjkkk0k10j0k00011j1k1011") 30 | 31 | def test_jjjkj0j1kjkkk0k10j0k00011j1k1011(self): 32 | return self.basic_recovery_test("jjjkj0j1kjkkk0k10j0k00011j1k1011", True) 33 | 34 | def test_kkkkk0k0kjjjk0kkkkjjjkjkjkjjj0kj(self): 35 | return self.basic_recovery_test("kkkkk0k0kjjjk0kkkkjjjkjkjkjjj0kj", True) 36 | 37 | def basic_recovery_test(self, seq, short_test=True): 38 | """ 39 | This test covers basic clock and data recovery. 40 | """ 41 | 42 | def get_output(): 43 | """ 44 | Record data output when line_state_valid is asserted. 45 | """ 46 | valid = yield dut.line_state_valid 47 | if valid == 1: 48 | dj = yield dut.line_state_dj 49 | dk = yield dut.line_state_dk 50 | se0 = yield dut.line_state_se0 51 | se1 = yield dut.line_state_se1 52 | 53 | out = "%d%d%d%d" % (dj, dk, se0, se1) 54 | 55 | return { 56 | "1000" : "j", 57 | "0100" : "k", 58 | "0010" : "0", 59 | "0001" : "1", 60 | }[out] 61 | 62 | else: 63 | return "" 64 | 65 | def stim(glitch=-1): 66 | out_seq = "" 67 | clock = 0 68 | while len(out_seq) < len(seq): 69 | bit = (seq + "0")[clock >> 2] 70 | if clock != glitch: 71 | yield usbp_raw.eq({'j':1,'k':0,'0':0,'1':1}[bit]) 72 | yield usbn_raw.eq({'j':0,'k':1,'0':0,'1':1}[bit]) 73 | yield 74 | clock += 1 75 | out_seq += yield from get_output() 76 | self.assertEqual(out_seq, seq) 77 | 78 | 79 | if short_test: 80 | with self.subTest(seq=seq): 81 | usbp_raw = Signal() 82 | usbn_raw = Signal() 83 | 84 | dut = RxClockDataRecovery(usbp_raw, usbn_raw) 85 | 86 | run_simulation( 87 | dut, 88 | stim(), 89 | vcd_name=self.make_vcd_name( 90 | testsuffix="clock.basic_recovery_%s" % seq), 91 | ) 92 | 93 | else: 94 | for glitch in range(0, 32, 1): 95 | with self.subTest(seq=seq, glitch=glitch): 96 | usbp_raw = Signal() 97 | usbn_raw = Signal() 98 | 99 | dut = RxClockDataRecovery(usbp_raw, usbn_raw) 100 | 101 | run_simulation( 102 | dut, 103 | stim(glitch), 104 | vcd_name=self.make_vcd_name( 105 | testsuffix="basic_recovery_%s_%d" % ( 106 | seq, glitch)), 107 | ) 108 | 109 | 110 | if __name__ == "__main__": 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/crc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.fhdl.decorators import ResetInserter 5 | 6 | from ..test.common import BaseUsbTestCase 7 | import unittest 8 | 9 | 10 | @ResetInserter() 11 | class RxCrcChecker(Module): 12 | """CRC Checker 13 | 14 | Checks the CRC of a serial stream of data. 15 | 16 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 8.3.5 17 | https://en.wikipedia.org/wiki/Cyclic_redundancy_check 18 | 19 | Parameters 20 | ---------- 21 | Parameters are passed in via the constructor. 22 | 23 | width : int 24 | Width of the CRC. 25 | 26 | polynomial : int 27 | CRC polynomial in integer form. 28 | 29 | initial : int 30 | Initial value of the CRC register before data starts shifting in. 31 | 32 | residual : int 33 | Value of the CRC register if all the shifted in data is valid. 34 | 35 | Input Ports 36 | ------------ 37 | i_data : Signal(1) 38 | Decoded data bit from USB bus. 39 | Qualified by valid. 40 | 41 | i_reset : Signal(1) 42 | Resets the CRC calculation back to the initial state. 43 | 44 | i_valid : Signal(1) 45 | Indicate that i_data is valid and a CRC should be calculated 46 | 47 | Output Ports 48 | ------------ 49 | o_crc_good : Signal() 50 | CRC value is good. 51 | """ 52 | def __init__(self, width, polynomial, initial, residual): 53 | self.i_data = Signal() 54 | self.i_reset = Signal() 55 | self.i_valid = Signal() 56 | 57 | crc = Signal(width) 58 | crc_good = Signal(1) 59 | crc_invert = Signal(1) 60 | 61 | self.comb += [ 62 | crc_good.eq(crc == residual), 63 | crc_invert.eq(self.i_data ^ crc[width - 1]) 64 | ] 65 | 66 | for i in range(width): 67 | rhs = None 68 | if i == 0: 69 | rhs = crc_invert 70 | else: 71 | if (polynomial >> i) & 1: 72 | rhs = crc[i - 1] ^ crc_invert 73 | else: 74 | rhs = crc[i - 1] 75 | 76 | self.sync += [ 77 | If(self.i_reset, 78 | crc[i].eq((initial >> i) & 1) 79 | ).Elif(self.i_valid, 80 | crc[i].eq(rhs) 81 | ) 82 | ] 83 | 84 | # flop all outputs 85 | self.o_crc_good = Signal(1) 86 | 87 | self.sync += [ 88 | self.o_crc_good.eq(crc_good) 89 | ] 90 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/crc_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.fhdl.decorators import ResetInserter 7 | 8 | from ..test.common import BaseUsbTestCase 9 | from .crc import RxCrcChecker 10 | 11 | class TestRxCrcChecker(BaseUsbTestCase): 12 | def shifter_test(self, i, vector): 13 | def send(reset, valid, value): 14 | crc_good = "" 15 | for i in range(len(valid)): 16 | yield i_reset.eq(reset[i] == '-') 17 | yield i_valid.eq(valid[i] == '-') 18 | yield i_data.eq(value[i] == '1') 19 | yield 20 | 21 | o_crc_good = yield dut.o_crc_good 22 | 23 | out = "%d" % (o_crc_good) 24 | 25 | crc_good += { 26 | "1" : "-", 27 | "0" : "_", 28 | }[out] 29 | 30 | return crc_good 31 | 32 | 33 | def stim(width, polynomial, initial, residual, reset, valid, value, crc_good): 34 | actual_crc_good = yield from send(reset, valid, value) 35 | self.assertEqual(actual_crc_good, crc_good) 36 | 37 | with self.subTest(i=i, vector=vector): 38 | 39 | dut = RxCrcChecker( 40 | vector["width"], 41 | vector["polynomial"], 42 | vector["initial"], 43 | vector["residual"] 44 | ) 45 | i_valid = dut.i_valid 46 | i_data = dut.i_data 47 | i_reset = dut.i_reset 48 | 49 | run_simulation( 50 | dut, 51 | stim(**vector), 52 | vcd_name=self.make_vcd_name(testsuffix=str(i)), 53 | ) 54 | 55 | def test_usb2_token_with_good_crc5(self): 56 | return self.shifter_test(0, 57 | dict( 58 | # USB2 token with good CRC5 (1) 59 | width = 5, 60 | polynomial = 0b00101, 61 | initial = 0b11111, 62 | residual = 0b01100, 63 | reset = "-___________________", 64 | valid = "_----------------___", 65 | value = "00000000000001000000", 66 | crc_good = "_______-__________--" 67 | ) 68 | ) 69 | 70 | def test_usb2_token_with_good_crc5_and_pipeline_stalls(self): 71 | return self.shifter_test(1, 72 | dict( 73 | # USB2 token with good CRC5 and pipeline stalls (1) 74 | width = 5, 75 | polynomial = 0b00101, 76 | initial = 0b11111, 77 | residual = 0b01100, 78 | reset = "-_______________________________", 79 | valid = "_-___-___------------___-___-___", 80 | value = "00000011100000000001011100000000", 81 | crc_good = "_____________-________________--" 82 | ) 83 | ) 84 | 85 | def test_usb2_token_with_bad_crc5(self): 86 | return self.shifter_test(2, 87 | dict( 88 | # USB2 token with bad CRC5 (1) 89 | width = 5, 90 | polynomial = 0b00101, 91 | initial = 0b11111, 92 | residual = 0b01100, 93 | reset = "-___________________", 94 | valid = "_----------------___", 95 | value = "00010000000001000000", 96 | crc_good = "______-________-____" 97 | ) 98 | ) 99 | 100 | def test_usb2_token_with_good_crc5_2(self): 101 | return self.shifter_test(3, 102 | dict( 103 | # USB2 token with good CRC5 (2) 104 | width = 5, 105 | polynomial = 0b00101, 106 | initial = 0b11111, 107 | residual = 0b01100, 108 | reset = "-___________________", 109 | valid = "_----------------___", 110 | value = "00000011011011101000", 111 | crc_good = "_______-__________--" 112 | ) 113 | ) 114 | 115 | def test_usb2_token_with_bad_crc5_2(self): 116 | return self.shifter_test(4, 117 | dict( 118 | # USB2 token with bad CRC5 (2) 119 | width = 5, 120 | polynomial = 0b00101, 121 | initial = 0b11111, 122 | residual = 0b01100, 123 | reset = "-___________________", 124 | valid = "_----------------___", 125 | value = "00010011011011101000", 126 | crc_good = "______-_____________" 127 | ) 128 | ) 129 | 130 | def test_usb2_token_with_good_crc5_1_2(self): 131 | return self.shifter_test(5, 132 | dict( 133 | # Two USB2 token with good CRC5 (1,2) 134 | width = 5, 135 | polynomial = 0b00101, 136 | initial = 0b11111, 137 | residual = 0b01100, 138 | reset = "-________________________-___________________", 139 | valid = "_----------------_________----------------___", 140 | value = "000000000000010000000000000000011011011101000", 141 | crc_good = "_______-__________---------_____-__________--" 142 | ) 143 | ) 144 | 145 | def test_usb2_data_with_good_crc16(self): 146 | return self.shifter_test(6, 147 | dict( 148 | # USB2 data with good CRC16 (1) 149 | width = 16, 150 | polynomial = 0b1000000000000101, 151 | initial = 0b1111111111111111, 152 | residual = 0b1000000000001101, 153 | reset = "-______________________________________________________________________________________________", 154 | valid = "_--------_--------_--------_--------_--------_--------_--------_--------_----------------______", 155 | value = "00000000100110000000000000001000000000000000000000000000000001000000000001011101100101001000010", 156 | crc_good = "__________________________________________________________________________________________-----" 157 | ) 158 | ) 159 | 160 | def test_usb2_data_with_bad_crc16(self): 161 | return self.shifter_test(7, 162 | dict( 163 | # USB2 data with bad CRC16 (1) 164 | width = 16, 165 | polynomial = 0b1000000000000101, 166 | initial = 0b1111111111111111, 167 | residual = 0b1000000000001101, 168 | reset = "-______________________________________________________________________________________________", 169 | valid = "_--------_--------_--------_--------_--------_--------_--------_--------_----------------______", 170 | value = "00000000100110000000000000001000000000010000000000000000000001000000000001011101100101001000010", 171 | crc_good = "_______________________________________________________________________________________________" 172 | ) 173 | ) 174 | 175 | if __name__ == "__main__": 176 | unittest.main() 177 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/detect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import cdc 5 | 6 | from migen.fhdl.decorators import ResetInserter 7 | 8 | from ..test.common import BaseUsbTestCase 9 | import unittest 10 | 11 | 12 | @ResetInserter() 13 | class RxPacketDetect(Module): 14 | """Packet Detection 15 | 16 | Full Speed packets begin with the following sequence: 17 | 18 | KJKJKJKK 19 | 20 | This raw sequence corresponds to the following data: 21 | 22 | 00000001 23 | 24 | The bus idle condition is signaled with the J state: 25 | 26 | JJJJJJJJ 27 | 28 | This translates to a series of '1's since there are no transitions. Given 29 | this information, it is easy to detect the beginning of a packet by looking 30 | for 00000001. 31 | 32 | The end of a packet is even easier to detect. The end of a packet is 33 | signaled with two SE0 and one J. We can just look for the first SE0 to 34 | detect the end of the packet. 35 | 36 | Packet detection can occur in parallel with bitstuff removal. 37 | 38 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 7.1.10 39 | 40 | Input Ports 41 | ------------ 42 | i_valid : Signal(1) 43 | Qualifier for all of the input signals. Indicates one bit of valid 44 | data is present on the inputs. 45 | 46 | i_data : Signal(1) 47 | Decoded data bit from USB bus. 48 | Qualified by valid. 49 | 50 | i_se0 : Signal(1) 51 | Indicator for SE0 from USB bus. 52 | Qualified by valid. 53 | 54 | Output Ports 55 | ------------ 56 | o_pkt_start : Signal(1) 57 | Asserted for one clock on the last bit of the sync. 58 | 59 | o_pkt_active : Signal(1) 60 | Asserted while in the middle of a packet. 61 | 62 | o_pkt_end : Signal(1) 63 | Asserted for one clock after the last data bit of a packet was received. 64 | """ 65 | 66 | def __init__(self): 67 | self.i_valid = Signal() 68 | self.i_data = Signal() 69 | self.i_se0 = Signal() 70 | 71 | self.submodules.pkt = pkt = FSM() 72 | 73 | pkt_start = Signal() 74 | pkt_active = Signal() 75 | pkt_end = Signal() 76 | 77 | for i in range(5): 78 | pkt.act("D%d" % i, 79 | If(self.i_valid, 80 | If(self.i_data | self.i_se0, 81 | # Receiving '1' or SE0 early resets the packet start counter. 82 | NextState("D0") 83 | ).Else( 84 | # Receiving '0' increments the packet start counter. 85 | NextState("D%d" % (i + 1)) 86 | ) 87 | ) 88 | ) 89 | 90 | pkt.act("D5", 91 | If(self.i_valid, 92 | If(self.i_se0, 93 | NextState("D0") 94 | # once we get a '1', the packet is active 95 | ).Elif(self.i_data, 96 | pkt_start.eq(1), 97 | NextState("PKT_ACTIVE") 98 | ) 99 | ) 100 | ) 101 | 102 | pkt.act("PKT_ACTIVE", 103 | pkt_active.eq(1), 104 | If(self.i_valid & self.i_se0, 105 | NextState("D0"), 106 | pkt_active.eq(0), 107 | pkt_end.eq(1) 108 | ) 109 | ) 110 | 111 | # pass all of the outputs through a pipe stage 112 | self.o_pkt_start = Signal() 113 | self.o_pkt_active = Signal() 114 | self.o_pkt_end = Signal() 115 | self.comb += [ 116 | self.o_pkt_start.eq(pkt_start), 117 | self.o_pkt_active.eq(pkt_active), 118 | self.o_pkt_end.eq(pkt_end), 119 | ] 120 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/detect_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.genlib import cdc 7 | 8 | from migen.fhdl.decorators import ResetInserter 9 | 10 | from ..test.common import BaseUsbTestCase 11 | from .detect import RxPacketDetect 12 | 13 | 14 | class TestRxPacketDetect(BaseUsbTestCase): 15 | def packet_detect_test(self, vector, short_name): 16 | def send(value, valid): 17 | value += "_" 18 | pkt_start = "" 19 | pkt_active = "" 20 | for i in range(len(value)): 21 | if i < len(value): 22 | yield dut.i_data.eq(value[i] == '1') 23 | yield dut.i_valid.eq(valid[i] == '-') 24 | yield dut.reset.eq(value[i] == '_' and valid[i] == '-') 25 | 26 | yield 27 | 28 | pkt_start += { 29 | 1 : "S", 30 | 0 : " ", 31 | }[(yield dut.o_pkt_start)] 32 | 33 | pkt_active += { 34 | 1 : "-", 35 | 0 : "_", 36 | }[(yield dut.o_pkt_active)] 37 | 38 | return pkt_start, pkt_active 39 | 40 | def stim(value, valid, pkt_start, pkt_active): 41 | actual_pkt_start, actual_pkt_active = yield from send(value, valid) 42 | self.assertSequenceEqual(pkt_start, actual_pkt_start) 43 | self.assertSequenceEqual(pkt_active, actual_pkt_active) 44 | 45 | with self.subTest(short_name=short_name, vector=vector): 46 | dut = RxPacketDetect() 47 | 48 | run_simulation( 49 | dut, 50 | stim(**vector), 51 | vcd_name=self.make_vcd_name(testsuffix=short_name), 52 | ) 53 | 54 | def test_se0_idle(self): 55 | return self.packet_detect_test( 56 | dict( 57 | # SE0, Idle 58 | value = "______________111111111111111", 59 | valid = "------------------------------", 60 | pkt_start = " ", 61 | pkt_active = "______________________________" 62 | ), "se0-idle") 63 | 64 | def test_idle_packet_idle(self): 65 | return self.packet_detect_test( 66 | dict( 67 | # Idle, Packet, Idle 68 | value = "11111000000011111111101__11111", 69 | valid = "-------------------------------", 70 | pkt_start = " S ", 71 | pkt_active = "_____________-----------_______" 72 | ), "idle-packet-idle") 73 | 74 | def test_idle_packet_idle_stall(self): 75 | return self.packet_detect_test( 76 | dict( 77 | # Idle, Packet, Idle (pipeline stall) 78 | value = "111110000000111111111101__11111", 79 | valid = "--------------------------------", 80 | pkt_start = " S ", 81 | pkt_active = "_____________------------_______" 82 | ), "idle-packet-idle-stall") 83 | 84 | def test_idle_packet_idle_stalls(self): 85 | return self.packet_detect_test( 86 | dict( 87 | # Idle, Packet, Idle (pipeline stalls) 88 | value = "11111000000011111111111101__11111", 89 | valid = "----------------------------------", 90 | pkt_start = " S ", 91 | pkt_active = "_____________--------------_______" 92 | ), "idle-packet-idle-stalls") 93 | 94 | def test_idle_packet_idle_packet_idle(self): 95 | return self.packet_detect_test( 96 | dict( 97 | # Idle, Packet, Idle, Packet, Idle 98 | value = "11111000000011111111101__1111111111000000011111111101__11111", 99 | valid = "-------------------------------------------------------------", 100 | pkt_start = " S S ", 101 | pkt_active = "_____________-----------___________________-----------_______" 102 | ), "idle-packet-idle-packet-idle") 103 | 104 | def test_idle_sync_idle(self): 105 | return self.packet_detect_test( 106 | dict( 107 | # Idle, Short Sync Packet, Idle 108 | value = "111110000011111111101__11111", 109 | valid = "-----------------------------", 110 | pkt_start = " S ", 111 | pkt_active = "___________-----------_______" 112 | ), "idle-shortsyncpacket-idle") 113 | 114 | def test_idle_glitch(self): 115 | return self.packet_detect_test( 116 | dict( 117 | # Idle Glitch 118 | value = "11111111110011111111_1111__111", 119 | valid = "-------------------------------", 120 | pkt_start = " ", 121 | pkt_active = "_______________________________" 122 | ), "idle-glitch") 123 | 124 | def test_valid_idle_packet_idle_packet_idle(self): 125 | return self.packet_detect_test( 126 | dict( 127 | # Idle, Packet, Idle, Packet, Idle 128 | value = "11111100100000011111111110101___111111111110000000011111111101___11111", 129 | valid = "----_---_--_---_------_--_----_-----_----------_--------------_--------", 130 | pkt_start = " S S ", 131 | pkt_active = "_________________-------------______________________------------_______" 132 | ), "valid-idle-packet-idle-packet-idle") 133 | 134 | 135 | if __name__ == "__main__": 136 | unittest.main() 137 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/nrzi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from ..test.common import BaseUsbTestCase 5 | 6 | import unittest 7 | 8 | 9 | class RxNRZIDecoder(Module): 10 | """RX NRZI decoder. 11 | 12 | In order to ensure there are enough bit transitions for a receiver to recover 13 | the clock usb uses NRZI encoding. This module processes the incoming 14 | dj, dk, se0, and valid signals and decodes them to data values. It 15 | also pipelines the se0 signal and passes it through unmodified. 16 | 17 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 7.1.8 18 | https://en.wikipedia.org/wiki/Non-return-to-zero 19 | 20 | Clock Domain 21 | ------------ 22 | usb_48 : 48MHz 23 | 24 | Input Ports 25 | ----------- 26 | Input ports are passed in via the constructor. 27 | 28 | i_valid : Signal(1) 29 | Qualifier for all of the input signals. Indicates one bit of valid 30 | data is present on the inputs. 31 | 32 | i_dj : Signal(1) 33 | Indicates the bus is currently in a Full-Speed J-state. 34 | Qualified by valid. 35 | 36 | i_dk : Signal(1) 37 | Indicates the bus is currently in a Full-Speed K-state. 38 | Qualified by valid. 39 | 40 | i_se0 : Signal(1) 41 | Indicates the bus is currently in a SE0 state. 42 | Qualified by valid. 43 | 44 | Output Ports 45 | ------------ 46 | Output ports are data members of the module. All output ports are flopped. 47 | 48 | o_valid : Signal(1) 49 | Qualifier for all of the output signals. Indicates one bit of valid 50 | data is present on the outputs. 51 | 52 | o_data : Signal(1) 53 | Decoded data bit from USB bus. 54 | Qualified by valid. 55 | 56 | o_se0 : Signal(1) 57 | Indicates the bus is currently in a SE0 state. 58 | Qualified by valid. 59 | """ 60 | 61 | def __init__(self): 62 | self.i_valid = Signal() 63 | self.i_dj = Signal() 64 | self.i_dk = Signal() 65 | self.i_se0 = Signal() 66 | 67 | # pass all of the outputs through a pipe stage 68 | self.o_valid = Signal(1) 69 | self.o_data = Signal(1) 70 | self.o_se0 = Signal(1) 71 | 72 | if False: 73 | valid = Signal(1) 74 | data = Signal(1) 75 | 76 | # simple state machine decodes a JK transition as a '0' and no 77 | # transition as a '1'. se0 is ignored. 78 | self.submodules.nrzi = nrzi = FSM() 79 | 80 | nrzi.act("DJ", 81 | If(self.i_valid, 82 | valid.eq(1), 83 | 84 | If(self.i_dj, 85 | data.eq(1) 86 | ).Elif(self.i_dk, 87 | data.eq(0), 88 | NextState("DK") 89 | ) 90 | ) 91 | ) 92 | 93 | nrzi.act("DK", 94 | If(self.i_valid, 95 | valid.eq(1), 96 | 97 | If(self.i_dj, 98 | data.eq(0), 99 | NextState("DJ") 100 | ).Elif(self.i_dk, 101 | data.eq(1) 102 | ) 103 | ) 104 | ) 105 | 106 | self.sync += [ 107 | self.o_valid.eq(valid), 108 | If(valid, 109 | self.o_se0.eq(self.i_se0), 110 | self.o_data.eq(data), 111 | ), 112 | ] 113 | else: 114 | last_data = Signal() 115 | self.sync += [ 116 | If(self.i_valid, 117 | last_data.eq(self.i_dk), 118 | self.o_data.eq(~(self.i_dk ^ last_data)), 119 | self.o_se0.eq((~self.i_dj) & (~self.i_dk)), 120 | ), 121 | self.o_valid.eq(self.i_valid), 122 | ] 123 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/nrzi_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from ..test.common import BaseUsbTestCase 8 | from .nrzi import RxNRZIDecoder 9 | 10 | 11 | class TestRxNRZIDecoder(BaseUsbTestCase): 12 | def nrzi_test(self, vector, short_name): 13 | 14 | def send(valid, value): 15 | valid += "_" 16 | value += "_" 17 | output = "" 18 | for i in range(len(valid)): 19 | yield dut.i_valid.eq(valid[i] == '-') 20 | yield dut.i_dj.eq(value[i] == 'j') 21 | yield dut.i_dk.eq(value[i] == 'k') 22 | yield dut.i_se0.eq(value[i] == '_') 23 | yield 24 | 25 | o_valid = yield dut.o_valid 26 | if o_valid: 27 | data = yield dut.o_data 28 | se0 = yield dut.o_se0 29 | 30 | out = "%d%d" % (data, se0) 31 | 32 | output += { 33 | "10" : "1", 34 | "00" : "0", 35 | "01" : "_", 36 | "11" : "_" 37 | }[out] 38 | return output 39 | 40 | 41 | def stim(valid, value, output): 42 | actual_output = yield from send(valid, value) 43 | self.assertEqual(actual_output, output) 44 | 45 | with self.subTest(short_name=short_name, vector=vector): 46 | dut = RxNRZIDecoder() 47 | 48 | run_simulation( 49 | dut, 50 | stim(**vector), 51 | vcd_name=self.make_vcd_name(testsuffix=short_name), 52 | ) 53 | 54 | def test_usb2_spec_7_1_8(self): 55 | return self.nrzi_test( 56 | dict( 57 | # USB2 Spec, 7.1.8 58 | valid = "-----------------", 59 | value = "jkkkjjkkjkjjkjjjk", 60 | output = "10110101000100110" 61 | ), "usb2-spec-7.1.8") 62 | 63 | def test_usb2_spec_7_1_9_1(self): 64 | return self.nrzi_test( 65 | dict( 66 | # USB2 Spec, 7.1.9.1 67 | valid = "--------------------", 68 | value = "jkjkjkjkkkkkkkjjjjkk", 69 | output = "10000000111111011101" 70 | ), "usb2-spec-7.1.9.1") 71 | 72 | def test_usb2_spec_7_1_9_1_stalls(self): 73 | return self.nrzi_test( 74 | dict( 75 | # USB2 Spec, 7.1.9.1 (added pipeline stalls) 76 | valid = "------___--------------", 77 | value = "jkjkjkkkkjkkkkkkkjjjjkk", 78 | output = "10000000111111011101" 79 | ), "usb2-spec-7.1.9.1-stalls") 80 | 81 | def test_usb2_spec_7_1_9_1_stalls_2(self): 82 | return self.nrzi_test( 83 | dict( 84 | # USB2 Spec, 7.1.9.1 (added pipeline stalls 2) 85 | valid = "-------___-------------", 86 | value = "jkjkjkjjjjkkkkkkkjjjjkk", 87 | output = "10000000111111011101" 88 | ), "usb2-spec-7.1.9.1-stalls-2") 89 | 90 | def test_usb2_spec_7_1_9_1_stalls_3(self): 91 | return self.nrzi_test( 92 | dict( 93 | # USB2 Spec, 7.1.9.1 (added pipeline stalls 3) 94 | valid = "-------___-------------", 95 | value = "jkjkjkjkkkkkkkkkkjjjjkk", 96 | output = "10000000111111011101" 97 | ), "usb2-spec-7.1.9.1-stalls-3") 98 | 99 | def test_usb2_spec_7_1_9_1_stalls_se0_glitch(self): 100 | return self.nrzi_test( 101 | dict( 102 | # USB2 Spec, 7.1.9.1 (added pipeline stalls, se0 glitch) 103 | valid = "-------___-------------", 104 | value = "jkjkjkj__kkkkkkkkjjjjkk", 105 | output = "10000000111111011101" 106 | ), "usb2-spec-7.1.9.1-stalls-se0-glitch") 107 | 108 | def test_captured_setup_packet(self): 109 | return self.nrzi_test( 110 | dict( 111 | # Captured setup packet 112 | valid = "------------------------------------", 113 | value = "jkjkjkjkkkjjjkkjkjkjkjkjkjkjkkjkj__j", 114 | output = "100000001101101000000000000001000__1" 115 | ), "captured-setup-packet") 116 | 117 | def test_captured_setup_packet_stalls(self): 118 | return self.nrzi_test( 119 | dict( 120 | # Captured setup packet (pipeline stalls) 121 | valid = "-___----___--------___-___-___-___----------------___-___---", 122 | value = "jjjjkjkjjkkkjkkkjjjjjkkkkkkkkkjjjjkjkjkjkjkjkjkkjkkkkj_____j", 123 | output = "100000001101101000000000000001000__1" 124 | ), "test-captured-setup-packet-stalls") 125 | 126 | 127 | if __name__ == "__main__": 128 | unittest.main() 129 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/pipeline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import cdc 5 | 6 | import unittest 7 | 8 | from .bitstuff import RxBitstuffRemover 9 | from .clock import RxClockDataRecovery 10 | from .detect import RxPacketDetect 11 | from .nrzi import RxNRZIDecoder 12 | from .shifter import RxShifter 13 | from ..utils.packet import b, nrzi 14 | from ..test.common import BaseUsbTestCase 15 | 16 | 17 | class RxPipeline(Module): 18 | def __init__(self, low_speed_support=False): 19 | self.reset = Signal() 20 | 21 | # 12MHz USB alignment pulse in 48MHz clock domain 22 | self.o_bit_strobe = Signal() 23 | 24 | # Reset state is J 25 | self.i_usbp = Signal(reset=1) 26 | self.i_usbn = Signal(reset=0) 27 | 28 | if low_speed_support: 29 | self.i_low_speed = Signal() 30 | low_speed_48 = Signal() 31 | else: 32 | low_speed_48 = None 33 | 34 | self.o_data_strobe = Signal() 35 | self.o_data_payload = Signal(8) 36 | 37 | self.o_pkt_start = Signal() 38 | self.o_pkt_in_progress = Signal() 39 | self.o_pkt_end = Signal() 40 | 41 | # 48MHz domain 42 | # Clock recovery 43 | clock_data_recovery = RxClockDataRecovery(self.i_usbp, self.i_usbn, 44 | low_speed_48) 45 | self.submodules.clock_data_recovery = ClockDomainsRenamer("usb_48")(clock_data_recovery) 46 | self.comb += [ 47 | self.o_bit_strobe.eq(clock_data_recovery.line_state_valid), 48 | ] 49 | if low_speed_support: 50 | self.specials += cdc.MultiReg(self.i_low_speed, low_speed_48, odomain="usb_48") 51 | 52 | # A reset condition is one where the device is in SE0 for more 53 | # than 2.5 uS, which is ~30 bit times. 54 | self.o_reset = Signal() 55 | reset_counter = Signal(7) 56 | self.comb += self.o_reset.eq(reset_counter[6]) 57 | self.sync.usb_48 += [ 58 | If(clock_data_recovery.line_state_valid, 59 | If(clock_data_recovery.line_state_se0, 60 | If(~reset_counter[6], 61 | reset_counter.eq(reset_counter + 1), 62 | ) 63 | ).Else( 64 | reset_counter.eq(0), 65 | ) 66 | ) 67 | ] 68 | 69 | # NRZI decoding 70 | nrzi = RxNRZIDecoder() 71 | self.submodules.nrzi = nrzi = ClockDomainsRenamer("usb_48")(nrzi) 72 | self.comb += [ 73 | nrzi.i_valid.eq(self.o_bit_strobe), 74 | nrzi.i_dj.eq(clock_data_recovery.line_state_dj), 75 | nrzi.i_dk.eq(clock_data_recovery.line_state_dk), 76 | nrzi.i_se0.eq(clock_data_recovery.line_state_se0), 77 | ] 78 | 79 | # The packet detector asserts the reset of the pipeline. 80 | reset = Signal() 81 | detect = RxPacketDetect() 82 | self.submodules.detect = detect = ClockDomainsRenamer("usb_48")(detect) 83 | self.comb += [ 84 | detect.reset.eq(self.reset), 85 | detect.i_valid.eq(nrzi.o_valid), 86 | detect.i_se0.eq(nrzi.o_se0), 87 | detect.i_data.eq(nrzi.o_data), 88 | reset.eq(~detect.o_pkt_active), 89 | ] 90 | 91 | bitstuff = RxBitstuffRemover() 92 | self.submodules.bitstuff = ClockDomainsRenamer("usb_48")(bitstuff) 93 | self.comb += [ 94 | bitstuff.reset.eq(~detect.o_pkt_active | self.reset), 95 | bitstuff.i_valid.eq(nrzi.o_valid), 96 | bitstuff.i_data.eq(nrzi.o_data), 97 | ] 98 | 99 | last_reset = Signal() 100 | self.sync.usb_48 += [ 101 | last_reset.eq(reset), 102 | ] 103 | 104 | # 1bit->8bit (1byte) serial to parallel conversion 105 | shifter = RxShifter(width=8) 106 | self.submodules.shifter = shifter = ClockDomainsRenamer("usb_48")(shifter) 107 | self.comb += [ 108 | shifter.reset.eq(last_reset), 109 | shifter.i_data.eq(bitstuff.o_data), 110 | shifter.i_valid.eq(~bitstuff.o_stall & detect.o_pkt_active), 111 | ] 112 | 113 | # Cross the data from the 48MHz domain to the 12MHz domain 114 | flag_start = Signal() 115 | flag_end = Signal() 116 | flag_valid = Signal() 117 | payloadFifo = genlib.fifo.AsyncFIFO(8, 2) 118 | self.submodules.payloadFifo = payloadFifo = ClockDomainsRenamer({"write":"usb_48", "read":"usb_12"})(payloadFifo) 119 | 120 | self.comb += [ 121 | payloadFifo.din.eq(shifter.o_data[::-1]), 122 | payloadFifo.we.eq(shifter.o_put), 123 | self.o_data_payload.eq(payloadFifo.dout), 124 | self.o_data_strobe.eq(payloadFifo.readable), 125 | payloadFifo.re.eq(1), 126 | ] 127 | 128 | flagsFifo = genlib.fifo.AsyncFIFO(2, 2) 129 | self.submodules.flagsFifo = flagsFifo = ClockDomainsRenamer({"write":"usb_48", "read":"usb_12"})(flagsFifo) 130 | 131 | self.comb += [ 132 | flagsFifo.din[1].eq(detect.o_pkt_start), 133 | flagsFifo.din[0].eq(detect.o_pkt_end), 134 | flagsFifo.we.eq(detect.o_pkt_start | detect.o_pkt_end), 135 | flag_start.eq(flagsFifo.dout[1]), 136 | flag_end.eq(flagsFifo.dout[0]), 137 | flag_valid.eq(flagsFifo.readable), 138 | flagsFifo.re.eq(1), 139 | ] 140 | 141 | # Packet flag signals (in 12MHz domain) 142 | self.comb += [ 143 | self.o_pkt_start.eq(flag_start & flag_valid), 144 | self.o_pkt_end.eq(flag_end & flag_valid), 145 | ] 146 | 147 | self.sync.usb_12 += [ 148 | If(self.o_pkt_start, 149 | self.o_pkt_in_progress.eq(1), 150 | ).Elif(self.o_pkt_end, 151 | self.o_pkt_in_progress.eq(0), 152 | ), 153 | ] 154 | 155 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/pullup_detect.py: -------------------------------------------------------------------------------- 1 | from migen import * 2 | 3 | from migen.fhdl.decorators import ResetInserter 4 | 5 | @ResetInserter() 6 | class RxPullUpDetect(Module): 7 | 8 | def __init__(self, threshold=1000): 9 | self.i_d_p = Signal() 10 | self.i_d_n = Signal() 11 | self.i_tx_en = Signal() 12 | self.o_j_pullup_detect = Signal() 13 | self.o_k_pullup_detect = Signal() 14 | 15 | cnt = Signal(max=threshold+1) 16 | pull_mode_cur = Signal(2) 17 | pull_mode_last = Signal(2) 18 | pull_mode_latched = Signal(2) 19 | 20 | self.comb += [ 21 | self.o_j_pullup_detect.eq(pull_mode_latched[0]), 22 | self.o_k_pullup_detect.eq(pull_mode_latched[1]) 23 | ] 24 | 25 | self.sync += [ 26 | pull_mode_last.eq(pull_mode_cur), 27 | pull_mode_cur.eq(Cat(self.i_d_p, self.i_d_n)), 28 | If (self.i_tx_en | (pull_mode_cur != pull_mode_last) | 29 | (pull_mode_last == 0b11) | 30 | ((pull_mode_last ^ pull_mode_latched) == 0b11), 31 | cnt.eq(0) 32 | ).Elif (cnt == threshold, 33 | pull_mode_latched.eq(pull_mode_last) 34 | ).Else ( 35 | cnt.eq(cnt + 1) 36 | ) 37 | ] 38 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/shifter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.fhdl.decorators import ResetInserter 5 | from ..test.common import BaseUsbTestCase 6 | 7 | import unittest 8 | 9 | @ResetInserter() 10 | class RxShifter(Module): 11 | """RX Shifter 12 | 13 | A shifter is responsible for shifting in serial bits and presenting them 14 | as parallel data. The shifter knows how many bits to shift and has 15 | controls for resetting the shifter. 16 | 17 | Clock Domain 18 | ------------ 19 | usb_12 : 12MHz 20 | 21 | Parameters 22 | ---------- 23 | Parameters are passed in via the constructor. 24 | 25 | width : int 26 | Number of bits to shift in. 27 | 28 | Input Ports 29 | ----------- 30 | i_valid : Signal(1) 31 | Qualifier for all of the input signals. Indicates one bit of valid 32 | data is present on the inputs. 33 | 34 | i_data : Signal(1) 35 | Serial input data. 36 | Qualified by valid. 37 | 38 | Output Ports 39 | ------------ 40 | o_data : Signal(width) 41 | Shifted in data. 42 | 43 | o_put : Signal(1) 44 | Asserted for one clock once the register is full. 45 | """ 46 | def __init__(self, width): 47 | self.i_valid = Signal() 48 | self.i_data = Signal() 49 | 50 | self.o_data = Signal(width) 51 | self.o_put = Signal() 52 | 53 | # Instead of using a counter, we will use a sentinel bit in the shift 54 | # register to indicate when it is full. 55 | shift_reg = Signal(width+1, reset=0b1) 56 | 57 | self.comb += self.o_data.eq(shift_reg[0:width]) 58 | self.sync += [ 59 | self.o_put.eq(shift_reg[width-1] & ~shift_reg[width] & self.i_valid), 60 | If(self.i_valid, 61 | If(shift_reg[width], 62 | shift_reg.eq(Cat(self.i_data, shift_reg.reset[0:width])), 63 | ).Else( 64 | shift_reg.eq(Cat(self.i_data, shift_reg[0:width])), 65 | ), 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /valentyusb/usbcore/rx/shifter_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.fhdl.decorators import CEInserter, ResetInserter 7 | 8 | from ..test.common import BaseUsbTestCase 9 | 10 | from .shifter import RxShifter 11 | 12 | 13 | class TestRxShifter(BaseUsbTestCase): 14 | def shifter_test(self, vector, short_name): 15 | actual_output = [] 16 | def send(reset, data , put=None, output=None): 17 | for i in range(len(data)+2): 18 | if i < len(data): 19 | if data[i] == '|': 20 | assert reset[i] == '|', reset[i] 21 | assert put[i] == '|', put[i] 22 | yield dut.i_valid.eq(data[i] != '|') 23 | yield dut.reset.eq(reset[i] == '-') 24 | yield dut.i_data.eq(data[i] == '1') 25 | yield 26 | o_put = yield dut.o_put 27 | if o_put: 28 | last_output = yield dut.o_data 29 | actual_output.append(last_output) 30 | 31 | with self.subTest(short_name=short_name, vector=vector): 32 | dut = RxShifter(8) 33 | 34 | actual_output.clear() 35 | run_simulation( 36 | dut, 37 | send(**vector), 38 | vcd_name=self.make_vcd_name(testsuffix=short_name), 39 | ) 40 | self.assertListEqual(vector['output'], actual_output) 41 | 42 | def test_basic_shift_in(self): 43 | return self.shifter_test( 44 | # 0 45 | dict( 46 | # basic shift in 47 | reset = "-|________|", 48 | data = "1|00000000|", 49 | put = "_|_______-|", 50 | output = [0b00000000] 51 | ), "basic-shift-in") 52 | 53 | def test_basic_shift_in_1(self): 54 | return self.shifter_test( 55 | # 1 56 | dict( 57 | # basic shift in 58 | reset = "-|________|||________|", 59 | data = "1|00000001|||00000001|", 60 | put = "_|_______-|||_______-|", 61 | output = [0b00000001,0b00000001] 62 | ), "basic-shift-in-1") 63 | 64 | def test_basic_shift_in_2(self): 65 | return self.shifter_test( 66 | # 2 67 | dict( 68 | # basic shift in 69 | reset = "-|________|||________|", 70 | data = "1|10000000|||10000000|", 71 | put = "_|_______-|||_______-|", 72 | output = [0b10000000,0b10000000] 73 | ), "basic-shift-in-2") 74 | 75 | def test_basic_shift_in_3(self): 76 | return self.shifter_test( 77 | # 3 78 | dict( 79 | # basic shift in 80 | reset = "-|________|", 81 | data = "1|11111111|", 82 | put = "_|_______-|", 83 | output = [0b11111111] 84 | ), "basic-shift-in-3") 85 | 86 | def test_basic_shift_in_4(self): 87 | return self.shifter_test( 88 | # 4 89 | dict( 90 | # basic shift in 91 | reset = "-|________|", 92 | data = "1|10000000|", 93 | put = "_|_______-|", 94 | output = [0b10000000] 95 | ), "basic-shift-in-4") 96 | 97 | def test_basic_shift_in_5(self): 98 | return self.shifter_test( 99 | # 5 100 | dict( 101 | # basic shift in 102 | reset = "-|________|", 103 | data = "1|00000001|", 104 | put = "_|_______-|", 105 | output = [0b00000001] 106 | ), "basic-shift-in-5") 107 | 108 | def test_basic_shift_in_6(self): 109 | return self.shifter_test( 110 | # 6 111 | dict( 112 | # basic shift in 113 | reset = "-|________|", 114 | data = "1|01111110|", 115 | put = "_|_______-|", 116 | output = [0b01111110] 117 | ), "basic-shift-in-6") 118 | 119 | def test_basic_shift_in_7(self): 120 | return self.shifter_test( 121 | # 7 122 | dict( 123 | # basic shift in 124 | reset = "-|________|", 125 | data = "0|01110100|", 126 | put = "_|_______-|", 127 | output = [0b01110100] 128 | ), "basic-shift-in-7") 129 | 130 | def test_basic_shift_in_2_bytes(self): 131 | return self.shifter_test( 132 | # 8 133 | dict( 134 | # basic shift in, 2 bytes 135 | reset = "-|________|||________|", 136 | data = "0|01110100|||10101000|", 137 | put = "_|_______-|||_______-|", 138 | output = [0b01110100,0b10101000] 139 | ), "basic-shift-in-2-bytes") 140 | 141 | def test_multiple_resets(self): 142 | return self.shifter_test( 143 | # 9 144 | dict( 145 | # multiple resets 146 | reset = "-|________|______-|___-|________|", 147 | data = "0|01110100|0011010|0111|10101000|", 148 | put = "_|_______-|_______|____|_______-|", 149 | output = [0b01110100, 0b10101000] 150 | ), "multiple-resets") 151 | 152 | def test_multiple_resets_tight_timing(self): 153 | return self.shifter_test( 154 | # 10 155 | dict( 156 | # multiple resets (tight timing) 157 | reset = "-|________|-|________|", 158 | data = "0|01110100|1|00101000|", 159 | put = "_|_______-|_|_______-|", 160 | output = [0b01110100,0b00101000] 161 | ), "multiple-resets-tight-timing") 162 | 163 | if __name__ == "__main__": 164 | unittest.main() 165 | -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/sm/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from litex.soc.cores.gpio import GPIOOut 8 | 9 | from ..pid import PIDTypes 10 | from ..rx.pipeline import RxPipeline 11 | from ..tx.pipeline import TxPipeline 12 | from ..utils.packet import * 13 | from ..test.common import BaseUsbTestCase 14 | 15 | 16 | class PacketHeaderDecode(Module): 17 | def __init__(self, rx): 18 | self.submodules.rx = rx 19 | 20 | self.o_pid = Signal(4) 21 | self.o_addr = Signal(7) 22 | endp4 = Signal() 23 | self.o_endp = Signal(4) 24 | crc5 = Signal(5) 25 | self.o_decoded = Signal() 26 | 27 | # FIXME: This whole module should just be in the usb_12 clock domain? 28 | self.submodules.fsm = fsm = ClockDomainsRenamer("usb_12")(FSM()) 29 | fsm.act("IDLE", 30 | If(rx.o_pkt_start, 31 | NextState("WAIT_PID"), 32 | ), 33 | ) 34 | pid = rx.o_data_payload[0:4] 35 | fsm.act("WAIT_PID", 36 | If(rx.o_data_strobe, 37 | NextValue(self.o_pid, pid), 38 | Case(pid & PIDTypes.TYPE_MASK, { 39 | PIDTypes.TOKEN: NextState("WAIT_BYTE0"), 40 | PIDTypes.DATA: NextState("END"), 41 | PIDTypes.HANDSHAKE: NextState("END"), 42 | }), 43 | ), 44 | ) 45 | fsm.act("WAIT_BYTE0", 46 | If(rx.o_data_strobe, 47 | NextValue(self.o_addr[0:7], rx.o_data_payload[0:7]), 48 | NextValue(endp4, rx.o_data_payload[7]), 49 | NextState("WAIT_BYTE1"), 50 | ), 51 | ) 52 | fsm.act("WAIT_BYTE1", 53 | If(rx.o_data_strobe, 54 | NextValue(self.o_endp, Cat(endp4, rx.o_data_payload[0:3])), 55 | NextValue(crc5, rx.o_data_payload[4:]), 56 | NextState("END"), 57 | ), 58 | ) 59 | fsm.act("END", 60 | self.o_decoded.eq(1), 61 | NextState("IDLE"), 62 | ) 63 | -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/header_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from ..pid import PIDTypes 8 | from ..rx.pipeline import RxPipeline 9 | from ..utils.packet import * 10 | 11 | from ..test.common import BaseUsbTestCase 12 | 13 | from .header import PacketHeaderDecode 14 | 15 | 16 | class TestPacketHeaderDecode(BaseUsbTestCase): 17 | 18 | def sim(self, stim): 19 | rx = RxPipeline() 20 | dut = PacketHeaderDecode(rx) 21 | 22 | run_simulation( 23 | dut, stim(dut), 24 | vcd_name=self.make_vcd_name(), 25 | clocks={"sys": 12, "usb_48": 48, "usb_12": 192}, 26 | ) 27 | 28 | def recv_packet(self, dut, bits, tick): 29 | if not tick: 30 | def tick(): 31 | if False: 32 | yield 33 | 34 | for i in range(len(bits)): 35 | b = bits[i] 36 | if b == ' ': 37 | continue 38 | elif b == '_': 39 | # SE0 - both lines pulled low 40 | yield dut.rx.i_usbp.eq(0) 41 | yield dut.rx.i_usbn.eq(0) 42 | elif b == 'J': 43 | yield dut.rx.i_usbp.eq(1) 44 | yield dut.rx.i_usbn.eq(0) 45 | elif b == 'K': 46 | yield dut.rx.i_usbp.eq(0) 47 | yield dut.rx.i_usbn.eq(1) 48 | else: 49 | assert False, "Unknown value: %s" % v 50 | 51 | # four 48MHz cycles = 1 bit time 52 | for t in range(0, 4): 53 | yield 54 | 55 | continue_sim = yield from tick(dut) 56 | if not continue_sim: 57 | break 58 | 59 | MAX_ITER=10000 60 | if continue_sim: 61 | for i in range(0, MAX_ITER): 62 | continue_sim = yield from tick(dut) 63 | if not continue_sim: 64 | break 65 | yield 66 | self.assertFalse(continue_sim) 67 | self.assertLess(i, MAX_ITER-1) 68 | 69 | def check_packet(self, expected_pid, expected_addr, expected_endp, packet): 70 | def stim(dut): 71 | for i in range(100): 72 | yield 73 | 74 | def tick(dut): 75 | return not (yield dut.o_decoded) 76 | 77 | yield from self.recv_packet( 78 | dut, 79 | packet, 80 | tick, 81 | ) 82 | 83 | decoded = yield dut.o_decoded 84 | self.assertTrue(decoded) 85 | 86 | actual_pid = yield dut.o_pid 87 | self.assertEqual(expected_pid, actual_pid) 88 | 89 | actual_addr = yield dut.o_addr 90 | self.assertEqual(expected_addr, actual_addr) 91 | 92 | actual_endp = yield dut.o_endp 93 | self.assertEqual(expected_endp, actual_endp) 94 | 95 | for i in range(100): 96 | yield 97 | 98 | self.sim(stim) 99 | 100 | def check_token(self, expected_pid, expected_addr, expected_endp): 101 | self.check_packet( 102 | expected_pid, expected_addr, expected_endp, 103 | wrap_packet(token_packet(expected_pid, expected_addr, expected_endp)), 104 | ) 105 | 106 | def check_data(self, expected_pid, data): 107 | self.check_packet( 108 | expected_pid, 0, 0, 109 | wrap_packet(data_packet(expected_pid, data)), 110 | ) 111 | 112 | def check_status(self, expected_pid): 113 | # Status packet is a data_packet with no data. 114 | self.check_packet( 115 | expected_pid, 0, 0, 116 | wrap_packet(data_packet(expected_pid, [])), 117 | ) 118 | 119 | def check_handshake(self, expected_pid): 120 | self.check_packet( 121 | expected_pid, 0, 0, 122 | wrap_packet(handshake_packet(expected_pid)), 123 | ) 124 | 125 | def test_decode_setup_zero(self): 126 | self.check_token(PID.SETUP, 0x0, 0x0) 127 | 128 | def test_decode_in_ep1(self): 129 | self.check_token(PID.IN, 28, 1) 130 | 131 | def test_decode_out_ep8(self): 132 | self.check_token(PID.OUT, 12, 0xf) 133 | 134 | def test_decode_data0(self): 135 | self.check_status(PID.DATA0) 136 | 137 | def test_decode_data1(self): 138 | self.check_status(PID.DATA1) 139 | 140 | def test_decode_ack(self): 141 | self.check_handshake(PID.ACK) 142 | 143 | def test_decode_nak(self): 144 | self.check_handshake(PID.NAK) 145 | 146 | 147 | if __name__ == "__main__": 148 | unittest.main() 149 | -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/hosttransfer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import fsm 5 | from migen.genlib.cdc import MultiReg 6 | 7 | from ..endpoint import EndpointType, EndpointResponse 8 | from ..pid import PID, PIDTypes 9 | from ..rx.pipeline import RxPipeline 10 | from ..tx.pipeline import TxPipeline 11 | from ..sm.header import PacketHeaderDecode 12 | from ..sm.send import TxPacketSend 13 | 14 | class UsbHostTransfer(Module): 15 | 16 | def __init__(self, iobuf, auto_crc=True, cdc=False, low_speed_support=False): 17 | self.submodules.iobuf = iobuf = ClockDomainsRenamer("usb_48")(iobuf) 18 | if iobuf.usb_pullup is not None: 19 | self.comb += iobuf.usb_pullup.eq(0) 20 | self.submodules.tx = tx = TxPipeline(low_speed_support=low_speed_support) 21 | self.submodules.txstate = txstate = TxPacketSend(tx, auto_crc=auto_crc, token_support=True) 22 | self.submodules.rx = rx = RxPipeline(low_speed_support=low_speed_support) 23 | self.submodules.rxstate = rxstate = PacketHeaderDecode(rx) 24 | self.comb += [ 25 | tx.i_bit_strobe.eq(rx.o_bit_strobe), 26 | ] 27 | 28 | self.data_recv_put = Signal() 29 | self.data_recv_payload = Signal(8) 30 | 31 | self.data_send_get = Signal() 32 | self.data_send_have = Signal() 33 | self.data_send_payload = Signal(8) 34 | self.data_end = Signal() 35 | 36 | self.i_addr = Signal(7) 37 | self.i_ep = Signal(4) 38 | self.i_frame = Signal(11) 39 | self.comb += [ 40 | txstate.i_addr.eq(self.i_addr), 41 | txstate.i_ep.eq(self.i_ep), 42 | txstate.i_frame.eq(self.i_frame) 43 | ] 44 | 45 | self.i_reset = Signal() 46 | self.i_sof = Signal() 47 | 48 | self.i_cmd_setup = Signal() 49 | self.i_cmd_in = Signal() 50 | self.i_cmd_out = Signal() 51 | self.i_cmd_pre = Signal() 52 | self.i_cmd_data1 = Signal() 53 | self.i_cmd_iso = Signal() 54 | self.o_cmd_latched = Signal() 55 | 56 | self.o_got_ack = Signal() 57 | self.o_got_nak = Signal() 58 | self.o_got_stall = Signal() 59 | self.o_got_data0 = Signal() 60 | self.o_got_data1 = Signal() 61 | 62 | cmd_data1 = Signal() 63 | cmd_iso = Signal() 64 | low_speed_override = Signal() 65 | sof_latch = Signal() 66 | 67 | reset_out = Signal() 68 | self.specials += MultiReg(self.i_reset, reset_out, odomain="usb_48") 69 | 70 | if low_speed_support: 71 | self.i_low_speed = Signal() 72 | low_speed = Signal() 73 | self.comb += [ 74 | low_speed.eq(self.i_low_speed | low_speed_override), 75 | rx.i_low_speed.eq(low_speed), 76 | tx.i_low_speed.eq(low_speed), 77 | ] 78 | else: 79 | low_speed = 0 80 | 81 | self.comb += [ 82 | rx.i_usbp.eq(iobuf.usb_p_rx), 83 | rx.i_usbn.eq(iobuf.usb_n_rx), 84 | iobuf.usb_ls_rx.eq(low_speed), 85 | iobuf.usb_tx_en.eq(tx.o_oe | reset_out), 86 | iobuf.usb_p_tx.eq(tx.o_usbp & ~reset_out), 87 | iobuf.usb_n_tx.eq(tx.o_usbn & ~reset_out), 88 | ] 89 | 90 | self.sync.usb_12 += If(self.i_sof, sof_latch.eq(1)) 91 | 92 | fsm = ResetInserter()(FSM(reset_state='IDLE')) 93 | self.submodules.fsm = fsm = ClockDomainsRenamer('usb_12')(fsm) 94 | self.comb += fsm.reset.eq(self.i_reset | rx.o_reset) 95 | 96 | fsm.act('IDLE', 97 | NextValue(low_speed_override, 0), 98 | If (sof_latch, 99 | If (low_speed, 100 | NextState('KA' if low_speed_support else 'SOF') 101 | ).Else (NextState('SOF')) 102 | ).Elif (self.i_cmd_setup | self.i_cmd_in | self.i_cmd_out, 103 | If (self.i_cmd_pre, 104 | NextState('PREAMBLE') 105 | ).Else (NextState('START_TRANSFER')))) 106 | 107 | fsm.act('SOF', 108 | txstate.i_pkt_start.eq(1), 109 | txstate.i_pid.eq(PID.SOF), 110 | If (txstate.o_pkt_end, 111 | NextValue(sof_latch, 0), 112 | NextState('IDLE'))) 113 | 114 | fsm.act('PREAMBLE', 115 | txstate.i_pkt_start.eq(1), 116 | txstate.i_pid.eq(PID.PRE), 117 | If (txstate.o_pkt_end, 118 | NextValue(low_speed_override, 1), 119 | NextState('START_TRANSFER_AFTER_PREAMBLE'))) 120 | 121 | fsm.delayed_enter('START_TRANSFER_AFTER_PREAMBLE', 'START_TRANSFER', 4) 122 | 123 | fsm.act('START_TRANSFER', 124 | self.o_cmd_latched.eq(1), 125 | NextValue(cmd_data1, self.i_cmd_data1), 126 | NextValue(cmd_iso, self.i_cmd_iso), 127 | If (self.i_cmd_setup, 128 | NextState('SETUP') 129 | ).Elif (self.i_cmd_in, 130 | NextState('IN') 131 | ).Elif (self.i_cmd_out, 132 | NextState('OUT') 133 | ).Else (NextState('IDLE'))) 134 | 135 | fsm.act('SETUP', 136 | txstate.i_pkt_start.eq(1), 137 | txstate.i_pid.eq(PID.SETUP), 138 | If (txstate.o_pkt_end, 139 | NextState('SEND_DATA'))) 140 | 141 | fsm.act('IN', 142 | txstate.i_pkt_start.eq(1), 143 | txstate.i_pid.eq(PID.IN), 144 | If (txstate.o_pkt_end, 145 | NextState('WAIT_REPLY'))) 146 | 147 | fsm.act('OUT', 148 | txstate.i_pkt_start.eq(1), 149 | txstate.i_pid.eq(PID.OUT), 150 | If (txstate.o_pkt_end, 151 | NextState('SEND_DATA'))) 152 | 153 | fsm.act('SEND_DATA', 154 | txstate.i_pkt_start.eq(1), 155 | txstate.i_pid.eq(Mux(cmd_data1, PID.DATA1, PID.DATA0)), 156 | self.data_send_get.eq(txstate.o_data_ack), 157 | self.data_end.eq(txstate.o_pkt_end), 158 | If (txstate.o_pkt_end, 159 | If(cmd_iso, 160 | NextState('IDLE') 161 | ).Else(NextState('WAIT_REPLY')))) 162 | 163 | fsm.act('WAIT_REPLY', 164 | If (rxstate.o_decoded, 165 | If ((rxstate.o_pid & PIDTypes.TYPE_MASK) == PIDTypes.DATA, 166 | NextState('RECV_DATA') 167 | ).Else (NextState('IDLE'), 168 | self.o_got_ack.eq(rxstate.o_pid == PID.ACK), 169 | self.o_got_nak.eq(rxstate.o_pid == PID.NAK), 170 | self.o_got_stall.eq(rxstate.o_pid == PID.STALL)) 171 | ).Elif(self.i_cmd_setup | self.i_cmd_in | self.i_cmd_out, 172 | NextValue(low_speed_override, 0), 173 | If (self.i_cmd_pre, 174 | NextState('PREAMBLE') 175 | ).Else (NextState('START_TRANSFER')))) 176 | 177 | fsm.act('RECV_DATA', 178 | self.data_recv_put.eq(rx.o_data_strobe), 179 | If(rx.o_pkt_end, 180 | If (True, 181 | NextState('END_DATA_LS') 182 | ).Else (NextState('END_DATA')))) 183 | 184 | fsm.delayed_enter('END_DATA_LS', 'END_DATA', 8) 185 | 186 | fsm.act('END_DATA', 187 | # FIXME: Discard if CRC16 is incorrect 188 | self.o_got_data0.eq(rxstate.o_pid == PID.DATA0), 189 | self.o_got_data1.eq(rxstate.o_pid == PID.DATA1), 190 | If (cmd_iso, 191 | NextState('IDLE') 192 | ).Else (NextState('ACK'))) 193 | 194 | fsm.act('ACK', 195 | txstate.i_pkt_start.eq(1), 196 | txstate.i_pid.eq(PID.ACK), 197 | If (txstate.o_pkt_end, 198 | NextState('IDLE'))) 199 | 200 | if low_speed_support: 201 | fsm.act('KA', 202 | NextValue(tx.i_keepalive, 1), 203 | NextState('KA_WAIT1')) 204 | fsm.delayed_enter('KA_WAIT1', 'KA_MID', 10) 205 | fsm.act('KA_MID', 206 | NextValue(tx.i_keepalive, 0), 207 | NextState('KA_WAIT2')) 208 | fsm.delayed_enter('KA_WAIT2', 'KA_END', 10) 209 | fsm.act('KA_END', 210 | NextValue(sof_latch, 0), 211 | NextState('IDLE')) 212 | 213 | self.comb += [ 214 | self.data_recv_payload.eq(rx.o_data_payload), 215 | txstate.i_data_payload.eq(self.data_send_payload), 216 | txstate.i_data_ready.eq(self.data_send_have), 217 | ] 218 | -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/send.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | from migen.genlib import cdc 7 | 8 | from litex.soc.cores.gpio import GPIOOut 9 | 10 | from ..pid import PIDTypes 11 | from ..tx.pipeline import TxPipeline 12 | from ..tx.crc import TxParallelCrcGenerator 13 | from ..utils.asserts import assertMultiLineEqualSideBySide 14 | from ..utils.packet import * 15 | from ..utils.pprint import pp_packet 16 | from ..test.common import BaseUsbTestCase 17 | 18 | 19 | class TxPacketSend(Module): 20 | def __init__(self, tx, auto_crc=True, token_support=False): 21 | self.submodules.tx = tx 22 | 23 | self.i_pkt_start = Signal() 24 | self.o_pkt_end = Signal() 25 | 26 | self.i_pid = Signal(4) 27 | self.i_data_payload = Signal(8) 28 | self.i_data_ready = Signal() 29 | self.o_data_ack = Signal() 30 | 31 | o_oe12 = Signal() 32 | self.specials += cdc.MultiReg(tx.o_oe, o_oe12, odomain="usb_12", n=1) 33 | 34 | pid = Signal(4) 35 | 36 | if token_support: 37 | self.i_addr = Signal(7) 38 | self.i_ep = Signal(4) 39 | self.i_frame = Signal(11) 40 | 41 | fsm = FSM() 42 | self.submodules.fsm = fsm = ClockDomainsRenamer("usb_12")(fsm) 43 | fsm.act('IDLE', 44 | NextValue(tx.i_oe, self.i_pkt_start), 45 | If(self.i_pkt_start, 46 | # If i_pkt_start is set, then send the next packet. 47 | # We pre-queue the SYNC byte here to cut down on latency. 48 | NextState('QUEUE_SYNC'), 49 | ).Else( 50 | NextValue(tx.i_oe, 0), 51 | ) 52 | ) 53 | 54 | # Send the QUEUE_SYNC byte 55 | fsm.act('QUEUE_SYNC', 56 | # The PID might change mid-sync, because we're still figuring 57 | # out what the response ought to be. 58 | NextValue(pid, self.i_pid), 59 | tx.i_data_payload.eq(1), 60 | If(tx.o_data_strobe, 61 | NextState('QUEUE_PID'), 62 | ), 63 | ) 64 | 65 | # Send the PID byte 66 | fsm.act('QUEUE_PID', 67 | tx.i_data_payload.eq(Cat(pid, pid ^ 0b1111)), 68 | If(tx.o_data_strobe, 69 | If(pid & PIDTypes.TYPE_MASK == PIDTypes.HANDSHAKE, 70 | NextState('WAIT_TRANSMIT'), 71 | ).Elif(pid & PIDTypes.TYPE_MASK == PIDTypes.DATA, 72 | NextState('QUEUE_DATA0'), 73 | ).Elif(pid & PIDTypes.TYPE_MASK == PIDTypes.TOKEN, 74 | NextState('QUEUE_TOKEN0' if token_support else 'ERROR'), 75 | ).Else( 76 | NextState('ERROR'), 77 | ), 78 | ), 79 | ) 80 | 81 | nextstate = 'WAIT_TRANSMIT' 82 | if auto_crc: 83 | nextstate = 'QUEUE_CRC0' 84 | 85 | fsm.act('QUEUE_DATA0', 86 | If(~self.i_data_ready, 87 | NextState(nextstate), 88 | ).Else( 89 | NextState('QUEUE_DATAn'), 90 | ), 91 | ) 92 | 93 | # Keep transmitting data bytes until the i_data_ready signal is not 94 | # high on a o_data_strobe event. 95 | fsm.act('QUEUE_DATAn', 96 | tx.i_data_payload.eq(self.i_data_payload), 97 | self.o_data_ack.eq(tx.o_data_strobe), 98 | If(~self.i_data_ready, 99 | NextState(nextstate), 100 | ), 101 | ) 102 | 103 | if auto_crc: 104 | crc = TxParallelCrcGenerator( 105 | crc_width = 16, 106 | data_width = 8, 107 | polynomial = 0b1000000000000101, # polynomial = (16, 15, 2, 0) 108 | initial = 0b1111111111111111, # seed = 0xFFFF 109 | ) 110 | self.submodules.crc = crc = ClockDomainsRenamer("usb_12")(crc) 111 | 112 | self.comb += [ 113 | crc.i_data_payload.eq(self.i_data_payload), 114 | crc.reset.eq(fsm.ongoing('QUEUE_PID')), 115 | If(fsm.ongoing('QUEUE_DATAn'), 116 | crc.i_data_strobe.eq(tx.o_data_strobe), 117 | ), 118 | ] 119 | 120 | fsm.act('QUEUE_CRC0', 121 | tx.i_data_payload.eq(crc.o_crc[:8]), 122 | If(tx.o_data_strobe, 123 | NextState('QUEUE_CRC1'), 124 | ), 125 | ) 126 | fsm.act('QUEUE_CRC1', 127 | tx.i_data_payload.eq(crc.o_crc[8:]), 128 | If(tx.o_data_strobe, 129 | NextState('WAIT_TRANSMIT'), 130 | ), 131 | ) 132 | 133 | if token_support: 134 | token_data = Signal(11) 135 | crc5 = Signal(5) 136 | crc5_cnt = Signal(4, reset=0) 137 | self.sync.usb_12 += [ 138 | # Prepare the 11 bit token data and 5 bit CRC 139 | If (fsm.before_entering('QUEUE_SYNC'), 140 | crc5_cnt.eq(3)), 141 | If (crc5_cnt != 0, 142 | crc5_cnt.eq(crc5_cnt + 1), 143 | If (crc5_cnt == 4, 144 | crc5.eq(0b11111), 145 | token_data.eq(Mux(pid == PID.SOF, self.i_frame, 146 | Cat(self.i_addr, self.i_ep))) 147 | ).Else ( 148 | crc5.eq(Mux(crc5[0] ^ token_data[0], 149 | (crc5 >> 1) ^ 0b10100, 150 | (crc5 >> 1))), 151 | token_data.eq(Cat(token_data[1:], token_data[0])) 152 | )) 153 | ] 154 | fsm.act('QUEUE_TOKEN0', 155 | tx.i_data_payload.eq(token_data[:8]), 156 | If(tx.o_data_strobe, 157 | NextState('QUEUE_TOKEN1') 158 | )) 159 | fsm.act('QUEUE_TOKEN1', 160 | tx.i_data_payload.eq(Cat(token_data[8:], ~crc5)), 161 | If(tx.o_data_strobe, 162 | NextState('WAIT_TRANSMIT') 163 | )) 164 | 165 | fsm.act('WAIT_TRANSMIT', 166 | NextValue(tx.i_oe, 0), 167 | If(~o_oe12, 168 | self.o_pkt_end.eq(1), 169 | NextState('IDLE'), 170 | ), 171 | ) 172 | 173 | fsm.act('ERROR') 174 | -------------------------------------------------------------------------------- /valentyusb/usbcore/sm/send_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from ..pid import PIDTypes 8 | from ..tx.pipeline import TxPipeline 9 | from ..utils.asserts import assertMultiLineEqualSideBySide 10 | from ..utils.packet import * 11 | from ..utils.pprint import pp_packet 12 | from ..test.common import BaseUsbTestCase 13 | 14 | from .send import TxPacketSend 15 | 16 | 17 | class CommonTxPacketSendTestCase: 18 | maxDiff=None 19 | 20 | def assert_packet_sent(self, dut, pid, data=None, ndata=None): 21 | assert PIDTypes.handshake(pid) or PIDTypes.data(pid), pid 22 | 23 | yield dut.i_pid.eq(pid) 24 | yield 25 | yield 26 | yield 27 | 28 | if PIDTypes.handshake(pid): 29 | expected_packet = wrap_packet(handshake_packet(pid)) 30 | tick_data = None 31 | elif PIDTypes.data(pid): 32 | expected_packet = wrap_packet(data_packet(pid, data)) 33 | def tick_data(last_ack=[False]): 34 | if len(ndata) > 0: 35 | yield dut.i_data_payload.eq(ndata[0]) 36 | 37 | ack = yield dut.o_data_ack 38 | if ack: 39 | ndata.pop(0) 40 | last_ack[0] = ack 41 | else: 42 | yield dut.i_data_payload.eq(0) 43 | yield dut.i_data_ready.eq(len(ndata) > 0) 44 | 45 | actual_usb_p, actual_usb_n = yield from self.wait_for_packet(dut, tick_data) 46 | actual_packet = undiff(actual_usb_p, actual_usb_n) 47 | assertMultiLineEqualSideBySide( 48 | pp_packet(expected_packet), 49 | pp_packet(actual_packet), 50 | "%s packet (with data %r) send failed" % (pid, data), 51 | ) 52 | 53 | def wait_for_packet(self, dut, tick_data=None): 54 | PAD_FRONT = 8 55 | PAD_BACK = 8 56 | N = 4 57 | 58 | clk12 = ClockSignal("usb_12") 59 | clk48 = ClockSignal("usb_48") 60 | 61 | def clk12_edge(): 62 | start = yield dut.i_pkt_start 63 | if start: 64 | yield dut.i_pkt_start.eq(0) 65 | if not tick_data: 66 | return 67 | yield from tick_data() 68 | 69 | usb = { 70 | 'p': "", 71 | 'n': "", 72 | } 73 | def clk48_edge(clk48=[0]): 74 | j = clk48[0] 75 | 76 | if j % N == 0: 77 | yield dut.tx.i_bit_strobe.eq(1) 78 | else: 79 | yield dut.tx.i_bit_strobe.eq(0) 80 | 81 | usb['p'] += str((yield dut.tx.o_usbp)) 82 | usb['n'] += str((yield dut.tx.o_usbn)) 83 | 84 | clk48[0] += 1 85 | 86 | def tick(last={'clk12': None, 'clk48':None}): 87 | current_clk12 = yield clk12 88 | if current_clk12 and not last['clk12']: 89 | yield from clk12_edge() 90 | last['clk12'] = current_clk12 91 | 92 | current_clk48 = yield clk48 93 | if current_clk48 and not last['clk48']: 94 | yield from clk48_edge() 95 | last['clk48'] = current_clk48 96 | 97 | yield 98 | 99 | yield dut.i_pkt_start.eq(1) 100 | 101 | i = 0 102 | while usb['p'][PAD_FRONT*N:][-PAD_BACK*N:] != '1'*(PAD_BACK*N) and i < 10000: 103 | yield from tick() 104 | i += 1 105 | 106 | #assert usbn[20:] == 'J'*20 107 | start = usb['p'].find('0') 108 | end = usb['p'].rfind('0')+1+N 109 | usb_p = usb['p'][start:end] 110 | usb_n = usb['n'][start:end] 111 | # print() 112 | # print("---- ", self.id(), " ----", sep="") 113 | # print(usb['p']) 114 | # print(' '*(start-1), usb_p) 115 | # print(' '*(start-1), usb_n) 116 | # print(usb['n']) 117 | # print("-----", len(self.id())*"-", "-----", sep="") 118 | return usb_p, usb_n 119 | 120 | def test_ack(self): 121 | self.sim(PID.ACK) 122 | 123 | def test_nak(self): 124 | self.sim(PID.NAK) 125 | 126 | def test_stall(self): 127 | self.sim(PID.STALL) 128 | 129 | def test_status0(self): 130 | self.sim(PID.DATA0, []) 131 | 132 | def test_status1(self): 133 | self.sim(PID.DATA1, []) 134 | 135 | def test_data0_one_zero(self): 136 | self.sim(PID.DATA0, [0]) 137 | 138 | def test_data0_one_one(self): 139 | self.sim(PID.DATA0, [1]) 140 | 141 | def test_data0_one_one(self): 142 | self.sim(PID.DATA0, [0b10000001]) 143 | 144 | def test_data0_two_ones(self): 145 | self.sim(PID.DATA0, [1, 1]) 146 | 147 | def test_data0_two_ones(self): 148 | self.sim(PID.DATA0, [0, 1]) 149 | 150 | def test_data0_edges(self): 151 | self.sim(PID.DATA0, data=[0b00000001, 0, 0, 0b10000000]) 152 | 153 | def test_data0_all_zero(self): 154 | self.sim(PID.DATA0, data=[0, 0, 0, 0]) 155 | 156 | def test_data0_dat1234(self): 157 | self.sim(PID.DATA0, data=[1, 2, 3, 4]) 158 | 159 | def test_data1_all_zero(self): 160 | self.sim(PID.DATA1, data=[0, 0, 0, 0]) 161 | 162 | def test_data0_descriptor(self): 163 | self.sim(PID.DATA0, data=[ 164 | 0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00, 0xdd, 0x94, 165 | ]) 166 | 167 | 168 | class TestTxPacketSendNoCrc(BaseUsbTestCase, CommonTxPacketSendTestCase): 169 | maxDiff=None 170 | 171 | def sim(self, pid, data=None): 172 | tx = TxPipeline() 173 | dut = TxPacketSend(tx, auto_crc=False) 174 | 175 | if data is not None: 176 | ndata = data + crc16(data) 177 | else: 178 | ndata = None 179 | 180 | def stim(dut): 181 | yield from self.assert_packet_sent(dut, pid, data, ndata) 182 | 183 | run_simulation( 184 | dut, stim(dut), 185 | vcd_name=self.make_vcd_name(), 186 | clocks={"sys": 10, "usb_48": 40, "usb_12": 160}, 187 | ) 188 | 189 | 190 | class TestTxPacketSendAutoCrc(BaseUsbTestCase, CommonTxPacketSendTestCase): 191 | maxDiff=None 192 | 193 | def sim(self, pid, data=None): 194 | tx = TxPipeline() 195 | dut = TxPacketSend(tx, auto_crc=True) 196 | 197 | if data is not None: 198 | ndata = list(data) 199 | else: 200 | ndata = None 201 | 202 | def stim(dut): 203 | yield from self.assert_packet_sent(dut, pid, data, ndata) 204 | 205 | run_simulation( 206 | dut, stim(dut), 207 | vcd_name=self.make_vcd_name(), 208 | clocks={"sys": 10, "usb_48": 40, "usb_12": 160}, 209 | ) 210 | 211 | 212 | 213 | 214 | if __name__ == "__main__": 215 | unittest.main() 216 | -------------------------------------------------------------------------------- /valentyusb/usbcore/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/test/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/test/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | 5 | 6 | class CommonTestMultiClockDomain: 7 | def setUp(self, clock_names): 8 | self.signals = {} 9 | self.cycle_count = {} 10 | self.last_value = {} 11 | for n in clock_names: 12 | self.signals[n] = ClockSignal(n) 13 | self.cycle_count[n] = 0 14 | self.last_value[n] = 0 15 | 16 | def update_clocks(self): 17 | for n in self.signals: 18 | current_value = yield self.signals[n] 19 | # Run the callback 20 | if current_value and not self.last_value[n]: 21 | yield from getattr(self, "on_%s_edge" % n)() 22 | self.cycle_count[n] += 1 23 | self.last_value[n] = current_value 24 | 25 | def wait_for_edge(self, name): 26 | count_last = self.cycle_count[name] 27 | while True: 28 | yield from self.update_internal_signals() 29 | count_next = self.cycle_count[name] 30 | if count_last != count_next: 31 | break 32 | yield 33 | 34 | def update_internal_signals(self): 35 | yield from self.update_clocks() 36 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/tx/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/bitstuff.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from migen.fhdl.decorators import CEInserter, ResetInserter 8 | 9 | from .tester import module_tester 10 | from ..test.common import BaseUsbTestCase 11 | 12 | 13 | @ResetInserter() 14 | class TxBitstuffer(Module): 15 | """ 16 | Bitstuff Insertion 17 | 18 | Long sequences of 1's would cause the receiver to lose it's lock on the 19 | transmitter's clock. USB solves this with bitstuffing. A '0' is stuffed 20 | after every 6 consecutive 1's. 21 | 22 | The TxBitstuffer is the only component in the transmit pipeline that can 23 | delay transmission of serial data. It is therefore responsible for 24 | generating the bit_strobe signal that keeps the pipe moving forward. 25 | 26 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 7.1.9 27 | https://en.wikipedia.org/wiki/Bit_stuffing 28 | 29 | Clock Domain 30 | ------------ 31 | usb_12 : 48MHz 32 | 33 | Input Ports 34 | ------------ 35 | i_data : Signal(1) 36 | Data bit to be transmitted on USB. 37 | 38 | Output Ports 39 | ------------ 40 | o_data : Signal(1) 41 | Data bit to be transmitted on USB. 42 | 43 | o_stall : Signal(1) 44 | Used to apply backpressure on the tx pipeline. 45 | """ 46 | def __init__(self): 47 | self.i_data = Signal() 48 | 49 | self.o_stall = Signal(1) 50 | self.o_will_stall = Signal(1) 51 | self.o_data = Signal(1) 52 | 53 | self.submodules.stuff = stuff = FSM() 54 | 55 | stuff_bit = Signal(1) 56 | 57 | for i in range(5): 58 | stuff.act("D%d" % i, 59 | If(self.i_data, 60 | # Receiving '1' increments the bitstuff counter. 61 | NextState("D%d" % (i + 1)) 62 | ).Else( 63 | # Receiving '0' resets the bitstuff counter. 64 | NextState("D0") 65 | ) 66 | ) 67 | 68 | stuff.act("D5", 69 | If(self.i_data, 70 | # There's a '1', so indicate we might stall on the next loop. 71 | self.o_will_stall.eq(1), 72 | 73 | # Receiving '1' increments the bitstuff counter. 74 | NextState("D6") 75 | ).Else( 76 | # Receiving '0' resets the bitstuff counter. 77 | NextState("D0") 78 | ) 79 | ) 80 | 81 | stuff.act("D6", 82 | # stuff a bit 83 | stuff_bit.eq(1), 84 | 85 | # Reset the bitstuff counter 86 | NextState("D0") 87 | ) 88 | 89 | self.comb += [ 90 | self.o_stall.eq(stuff_bit) 91 | ] 92 | 93 | # flop outputs 94 | self.sync += [ 95 | If(stuff_bit, 96 | self.o_data.eq(0), 97 | ).Else( 98 | self.o_data.eq(self.i_data), 99 | ), 100 | ] 101 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/bitstuff_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from migen.fhdl.decorators import CEInserter, ResetInserter 8 | 9 | from .bitstuff import TxBitstuffer 10 | from .tester import module_tester 11 | from ..test.common import BaseUsbTestCase 12 | 13 | 14 | @module_tester( 15 | TxBitstuffer, 16 | 17 | i_data = (1,), 18 | 19 | o_stall = (1,), 20 | o_data = (1,), 21 | ) 22 | class TestTxBitstuffer(BaseUsbTestCase): 23 | def test_passthrough(self): 24 | self.do( 25 | i_data = "--___---__", 26 | 27 | o_stall = "__________", 28 | o_data = "_--___---_", 29 | ) 30 | 31 | def test_passthrough_se0(self): 32 | self.do( 33 | i_data = "--___---__", 34 | 35 | o_stall = "__________", 36 | o_data = "_--___---_", 37 | ) 38 | 39 | def test_bitstuff(self): 40 | self.do( 41 | i_data = "---------__", 42 | 43 | o_stall = "______-____", 44 | o_data = "_------_--_", 45 | ) 46 | 47 | def test_bitstuff_input_stall(self): 48 | self.do( 49 | i_data = "---------", 50 | 51 | o_stall = "______-__", 52 | o_data = "_------_-", 53 | ) 54 | 55 | def test_bitstuff_se0(self): 56 | self.do( 57 | i_data = "---------__-", 58 | 59 | o_stall = "______-_____", 60 | o_data = "_------_--__", 61 | ) 62 | 63 | def test_bitstuff_at_eop(self): 64 | self.do( 65 | i_data = "-------__", 66 | 67 | o_stall = "______-__", 68 | o_data = "_------__", 69 | ) 70 | 71 | def test_multi_bitstuff(self): 72 | self.do( 73 | i_data = "----------------", 74 | 75 | o_stall = "______-______-__", 76 | o_data = "_------_------_-", 77 | ) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/crc_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import functools 4 | import operator 5 | import unittest 6 | 7 | from migen import * 8 | 9 | from migen.fhdl.decorators import CEInserter, ResetInserter 10 | 11 | from ..test.common import BaseUsbTestCase 12 | from ..utils.CrcMoose3 import CrcAlgorithm 13 | from ..utils.bits import * 14 | from ..utils.packet import crc5, crc16, encode_data, b 15 | from .shifter import TxShifter 16 | from .tester import module_tester 17 | 18 | from .crc import TxSerialCrcGenerator, TxParallelCrcGenerator, TxCrcPipeline, bytes_to_int 19 | 20 | 21 | @module_tester( 22 | TxSerialCrcGenerator, 23 | 24 | width = None, 25 | polynomial = None, 26 | initial = None, 27 | 28 | reset = (1,), 29 | ce = (1,), 30 | i_data = (1,), 31 | 32 | o_crc = ("width",) 33 | ) 34 | class TestTxSerialCrcGenerator(BaseUsbTestCase): 35 | def test_token_crc5_zeroes(self): 36 | self.do( 37 | width = 5, 38 | polynomial = 0b00101, 39 | initial = 0b11111, 40 | 41 | reset = "-_______________", 42 | ce = "__-----------___", 43 | i_data = " 00000000000 ", 44 | o_crc = " 222" 45 | ) 46 | 47 | def test_token_crc5_zeroes_alt(self): 48 | self.do( 49 | width = 5, 50 | polynomial = 0b00101, 51 | initial = 0b11111, 52 | 53 | reset = "-______________", 54 | ce = "_-----------___", 55 | i_data = " 00000000000 ", 56 | o_crc = " 222" 57 | ) 58 | 59 | def test_token_crc5_nonzero(self): 60 | self.do( 61 | width = 5, 62 | polynomial = 0b00101, 63 | initial = 0b11111, 64 | 65 | reset = "-______________", 66 | ce = "_-----------___", 67 | i_data = " 01100000011 ", 68 | o_crc = " ccc" 69 | ) 70 | 71 | def test_token_crc5_nonzero_stall(self): 72 | self.do( 73 | width = 5, 74 | polynomial = 0b00101, # polynomial = (5, 2, 0) 75 | initial = 0b11111, # seed = 0x1F 76 | 77 | reset = "-_____________________________", 78 | ce = "_-___-___-___-___-___------___", 79 | i_data = " 0 1 111101110111000011 ", 80 | o_crc = " ccc" 81 | ) 82 | 83 | def test_data_crc16_nonzero(self): 84 | self.do( 85 | width = 16, 86 | polynomial = 0b1000000000000101, # polynomial = (16, 15, 2, 0) 87 | initial = 0b1111111111111111, # seed = 0xFFFF 88 | 89 | reset = "-________________________________________________________________________", 90 | ce = "_--------_--------_--------_--------_--------_--------_--------_--------_", 91 | i_data = " 00000001 01100000 00000000 10000000 00000000 00000000 00000010 00000000 ", 92 | o_crc =(" *", [0x94dd]) 93 | ) 94 | 95 | 96 | class TestTxParallelCrcGenerator(BaseUsbTestCase): 97 | def sim(self, name, dut, in_data, expected_crc): 98 | def stim(): 99 | yield dut.i_data_strobe.eq(1) 100 | for d in in_data: 101 | yield dut.i_data_payload.eq(d) 102 | yield 103 | o_crc = yield dut.o_crc 104 | print("{0} {1:04x} {1:016b} {2:04x} {2:016b}".format(name, expected_crc, o_crc)) 105 | yield 106 | o_crc = yield dut.o_crc 107 | print("{0} {1:04x} {1:016b} {2:04x} {2:016b}".format(name, expected_crc, o_crc)) 108 | self.assertEqual(hex(expected_crc), hex(o_crc)) 109 | 110 | run_simulation(dut, stim(), vcd_name=self.make_vcd_name()) 111 | 112 | def sim_crc16(self, in_data): 113 | expected_crc = bytes_to_int(crc16(in_data)) 114 | dut = TxParallelCrcGenerator( 115 | crc_width = 16, 116 | polynomial = 0b1000000000000101, 117 | initial = 0b1111111111111111, 118 | data_width = 8, 119 | ) 120 | mask = 0xff 121 | self.assertSequenceEqual(in_data, [x & mask for x in in_data]) 122 | self.sim("crc16", dut, in_data, expected_crc) 123 | 124 | def sim_crc5(self, in_data): 125 | expected_crc = crc5(in_data) 126 | dut = TxParallelCrcGenerator( 127 | crc_width = 5, 128 | polynomial = 0b00101, 129 | initial = 0b11111, 130 | data_width = 4, 131 | ) 132 | mask = 0x0f 133 | self.assertSequenceEqual(in_data, [x & mask for x in in_data]) 134 | self.sim("crc5", dut, in_data, expected_crc) 135 | 136 | def test_token_crc5_zeroes(self): 137 | self.sim_crc5([0, 0]) 138 | 139 | def test_token_crc5_nonzero1(self): 140 | self.sim_crc5([b("0110"), b("0000")]) 141 | 142 | def test_data_crc16_nonzero1(self): 143 | self.sim_crc16([ 144 | b("00000001"), b("01100000"), b("00000000"), b("10000000"), 145 | b("00000000"), b("00000000"), b("00000010"), b("00000000"), 146 | ]) 147 | 148 | def test_data_crc16_nonzero2(self): 149 | self.sim_crc16([ 150 | 0b00000001, 0b01100000, 0b00000000, 0b10000000, 151 | 0b00000000, 0b00000000, 0b00000010, 0b00000000, 152 | ]) 153 | 154 | 155 | class TestCrcPipeline(BaseUsbTestCase): 156 | maxDiff=None 157 | 158 | def sim(self, data): 159 | expected_crc = crc16(data) 160 | 161 | dut = TxCrcPipeline() 162 | dut.expected_crc = Signal(16) 163 | def stim(): 164 | MAX = 1000 165 | yield dut.expected_crc[:8].eq(expected_crc[0]) 166 | yield dut.expected_crc[8:].eq(expected_crc[1]) 167 | yield dut.reset.eq(1) 168 | yield dut.ce.eq(1) 169 | for i in range(MAX+1): 170 | if i > 10: 171 | yield dut.reset.eq(0) 172 | 173 | ack = yield dut.o_data_ack 174 | if ack: 175 | if len(data) == 0: 176 | yield dut.ce.eq(0) 177 | for i in range(5): 178 | yield 179 | crc16_value = yield dut.o_crc16 180 | 181 | encoded_expected_crc = encode_data(expected_crc) 182 | encoded_actual_crc = encode_data([crc16_value & 0xff, crc16_value >> 8]) 183 | self.assertSequenceEqual(encoded_expected_crc, encoded_actual_crc) 184 | return 185 | data.pop(0) 186 | if len(data) > 0: 187 | yield dut.i_data_payload.eq(data[0]) 188 | else: 189 | yield dut.i_data_payload.eq(0xff) 190 | yield 191 | self.assertLess(i, MAX) 192 | 193 | run_simulation(dut, stim(), vcd_name=self.make_vcd_name()) 194 | 195 | def test_00000001_byte(self): 196 | self.sim([0b00000001]) 197 | 198 | def test_10000000_byte(self): 199 | self.sim([0b10000000]) 200 | 201 | def test_00000000_byte(self): 202 | self.sim([0]) 203 | 204 | def test_11111111_byte(self): 205 | self.sim([0xff]) 206 | 207 | def test_10101010_byte(self): 208 | self.sim([0b10101010]) 209 | 210 | def test_zero_bytes(self): 211 | self.sim([0, 0, 0]) 212 | 213 | def test_sequential_bytes(self): 214 | self.sim([0, 1, 2]) 215 | 216 | def test_sequential_bytes2(self): 217 | self.sim([0, 1]) 218 | 219 | def test_sequential_bytes3(self): 220 | self.sim([1, 0]) 221 | 222 | 223 | if __name__ == "__main__": 224 | import doctest 225 | doctest.testmod() 226 | unittest.main() 227 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/nrzi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from .tester import module_tester 8 | from ..test.common import BaseUsbTestCase 9 | 10 | 11 | class TxNRZIEncoder(Module): 12 | """ 13 | NRZI Encode 14 | 15 | In order to ensure there are enough bit transitions for a receiver to recover 16 | the clock usb uses NRZI encoding. This module processes the incoming 17 | dj, dk, se0, and valid signals and decodes them to data values. It 18 | also pipelines the se0 signal and passes it through unmodified. 19 | 20 | https://www.pjrc.com/teensy/beta/usb20.pdf, USB2 Spec, 7.1.8 21 | https://en.wikipedia.org/wiki/Non-return-to-zero 22 | 23 | Clock Domain 24 | ------------ 25 | usb_48 : 48MHz 26 | 27 | Input Ports 28 | ----------- 29 | i_valid : Signal(1) 30 | Qualifies oe, data, and se0. 31 | 32 | i_oe : Signal(1) 33 | Indicates that the transmit pipeline should be driving USB. 34 | 35 | i_data : Signal(1) 36 | Data bit to be transmitted on USB. Qualified by o_valid. 37 | 38 | i_keepalive : Signal(1) 39 | Send a keepalive signal consisting of two SE0 bits. 40 | Qualified by o_valid. 41 | 42 | i_low_speed : Signal(1) 43 | Indicates that low speed encoding of J and K should be used. 44 | 45 | 46 | Output Ports 47 | ------------ 48 | o_usbp : Signal(1) 49 | Raw value of USB+ line. 50 | 51 | o_usbn : Signal(1) 52 | Raw value of USB- line. 53 | 54 | o_oe : Signal(1) 55 | When asserted it indicates that the tx pipeline should be driving USB. 56 | """ 57 | 58 | def __init__(self): 59 | self.i_valid = Signal() 60 | self.i_oe = Signal() 61 | self.i_data = Signal() 62 | self.i_keepalive = Signal() 63 | self.i_low_speed = Signal() 64 | 65 | # Simple state machine to perform NRZI encoding. 66 | self.submodules.nrzi = nrzi = FSM() 67 | 68 | usbp = Signal(1) 69 | usbn = Signal(1) 70 | oe = Signal(1) 71 | 72 | # wait for new packet to start 73 | nrzi.act("IDLE", 74 | usbp.eq(1), 75 | usbn.eq(0), 76 | oe.eq(0), 77 | 78 | If(self.i_valid, 79 | If(self.i_oe, 80 | # first bit of sync always forces a transition, we idle 81 | # in J so the first output bit is K. 82 | NextState("DK") 83 | ).Elif(self.i_keepalive, 84 | NextState("SE0A") 85 | ) 86 | ) 87 | ) 88 | 89 | # the output line is in state J 90 | nrzi.act("DJ", 91 | usbp.eq(1), 92 | usbn.eq(0), 93 | oe.eq(1), 94 | 95 | If(self.i_valid, 96 | If(~self.i_oe, 97 | NextState("SE0A") 98 | ).Elif(self.i_data, 99 | NextState("DJ") 100 | ).Else( 101 | NextState("DK") 102 | ) 103 | ) 104 | ) 105 | 106 | # the output line is in state K 107 | nrzi.act("DK", 108 | usbp.eq(0), 109 | usbn.eq(1), 110 | oe.eq(1), 111 | 112 | If(self.i_valid, 113 | If(~self.i_oe, 114 | NextState("SE0A") 115 | ).Elif(self.i_data, 116 | NextState("DK") 117 | ).Else( 118 | NextState("DJ") 119 | ) 120 | ) 121 | ) 122 | 123 | # first bit of the SE0 state 124 | nrzi.act("SE0A", 125 | usbp.eq(0), 126 | usbn.eq(0), 127 | oe.eq(1), 128 | 129 | If(self.i_valid, 130 | NextState("SE0B") 131 | ) 132 | ) 133 | # second bit of the SE0 state 134 | nrzi.act("SE0B", 135 | usbp.eq(0), 136 | usbn.eq(0), 137 | oe.eq(1), 138 | 139 | If(self.i_valid, 140 | NextState("EOPJ") 141 | ) 142 | ) 143 | 144 | # drive the bus back to J before relinquishing control 145 | nrzi.act("EOPJ", 146 | usbp.eq(1), 147 | usbn.eq(0), 148 | oe.eq(1), 149 | 150 | If(self.i_valid, 151 | NextState("IDLE") 152 | ) 153 | ) 154 | 155 | # flop all outputs 156 | self.o_usbp = Signal(1) 157 | self.o_usbn = Signal(1) 158 | self.o_oe = Signal(1) 159 | 160 | self.sync += [ 161 | self.o_oe.eq(oe), 162 | self.o_usbp.eq(Mux(self.i_low_speed, usbn, usbp)), 163 | self.o_usbn.eq(Mux(self.i_low_speed, usbp, usbn)), 164 | ] 165 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/nrzi_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from .tester import module_tester 8 | from ..test.common import BaseUsbTestCase 9 | 10 | from .nrzi import TxNRZIEncoder 11 | 12 | 13 | @module_tester( 14 | TxNRZIEncoder, 15 | 16 | i_valid = (1,), 17 | i_oe = (1,), 18 | i_data = (1,), 19 | 20 | o_usbp = (1,), 21 | o_usbn = (1,), 22 | o_oe = (1,) 23 | ) 24 | class TestTxNRZIEncoder(BaseUsbTestCase): 25 | def test_setup_token(self): 26 | self.do( 27 | i_valid = "_|--------|--------|--------|--------|--------", 28 | i_oe = "_|--------|--------|--------|--------|--______", 29 | i_data = "_|00000001|10110100|00000000|00001000|00______", 30 | 31 | # XXX|KJKJKJKK|KJJJKKJK|JKJKJKJK|JKJKKJKJ|KJ00JX 32 | o_oe = "___|--------|--------|--------|--------|-----_", 33 | o_usbp = " |_-_-_-__|_---__-_|-_-_-_-_|-_-__-_-|_-__- ", 34 | o_usbn = " |-_-_-_--|-___--_-|_-_-_-_-|_-_--_-_|-____ ", 35 | ) 36 | 37 | 38 | if __name__ == "__main__": 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/pipeline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from migen import * 4 | from migen.genlib import cdc 5 | from migen.genlib.fsm import FSM, NextState, NextValue 6 | 7 | import unittest 8 | 9 | from .bitstuff import TxBitstuffer 10 | from .nrzi import TxNRZIEncoder 11 | from .shifter import TxShifter 12 | from ..utils.packet import b, nrzi, diff 13 | from ..test.common import BaseUsbTestCase 14 | 15 | 16 | class TxPipeline(Module): 17 | def __init__(self, low_speed_support=False): 18 | self.i_bit_strobe = Signal() 19 | 20 | self.i_data_payload = Signal(8) 21 | self.o_data_strobe = Signal() 22 | 23 | self.i_oe = Signal() 24 | 25 | if low_speed_support: 26 | self.i_low_speed = Signal() 27 | self.i_keepalive = Signal() 28 | 29 | self.o_usbp = Signal() 30 | self.o_usbn = Signal() 31 | self.o_oe = Signal() 32 | 33 | self.o_pkt_end = Signal() 34 | 35 | tx_pipeline_fsm = CEInserter()(FSM()) 36 | self.submodules.tx_pipeline_fsm = ClockDomainsRenamer("usb_12")(tx_pipeline_fsm) 37 | shifter = TxShifter(width=8) 38 | self.submodules.shifter = ClockDomainsRenamer("usb_12")(shifter) 39 | bitstuff = CEInserter()(TxBitstuffer()) 40 | self.submodules.bitstuff = ClockDomainsRenamer("usb_12")(bitstuff) 41 | nrzi = TxNRZIEncoder() 42 | self.submodules.nrzi = ClockDomainsRenamer("usb_48")(nrzi) 43 | 44 | sync_pulse = Signal(8) 45 | 46 | self.fit_dat = fit_dat = Signal() 47 | self.fit_oe = fit_oe = Signal() 48 | 49 | da_reset_shifter = Signal() 50 | da_reset_bitstuff = Signal() # Need to reset the bit stuffer 1 cycle after the shifter. 51 | stall = Signal() 52 | 53 | # These signals are set during the sync pulse 54 | sp_reset_bitstuff = Signal() 55 | sp_reset_shifter = Signal() 56 | sp_bit = Signal() 57 | sp_o_data_strobe = Signal() 58 | 59 | # 12MHz domain 60 | 61 | da_stalled_reset = Signal() 62 | bitstuff_valid_data = Signal() 63 | 64 | if low_speed_support: 65 | new_bit = Signal() 66 | bit_strobe_sync = cdc.PulseSynchronizer('usb_48', 'usb_12') 67 | self.submodules += bit_strobe_sync 68 | self.comb += [ 69 | bit_strobe_sync.i.eq(self.i_bit_strobe), 70 | new_bit.eq(Mux(self.i_low_speed, bit_strobe_sync.o, 1)) 71 | ] 72 | else: 73 | new_bit = C(0b1) 74 | 75 | # Keep a Gray counter around to smoothly transition between states 76 | state_gray = Signal(2) 77 | state_data = Signal() 78 | state_sync = Signal() 79 | 80 | self.comb += [ 81 | tx_pipeline_fsm.ce.eq(new_bit), 82 | 83 | shifter.i_data.eq(self.i_data_payload), 84 | # Send a data strobe when we're two bits from the end of the sync pulse. 85 | # This is because the pipeline takes two bit times, and we want to ensure the pipeline 86 | # has spooled up enough by the time we're there. 87 | 88 | shifter.reset.eq(da_reset_shifter | sp_reset_shifter), 89 | shifter.ce.eq(new_bit & ~stall), 90 | 91 | bitstuff.reset.eq(da_reset_bitstuff), 92 | bitstuff.ce.eq(new_bit), 93 | bitstuff.i_data.eq(shifter.o_data), 94 | stall.eq(bitstuff.o_stall), 95 | 96 | sp_bit.eq(sync_pulse[0]), 97 | sp_reset_bitstuff.eq(sync_pulse[0]), 98 | 99 | # The shifter has one clock cycle of latency, so reset it 100 | # one cycle before the end of the sync byte. 101 | sp_reset_shifter.eq(sync_pulse[1]), 102 | 103 | sp_o_data_strobe.eq(sync_pulse[5]), 104 | 105 | state_data.eq(state_gray[0] & state_gray[1]), 106 | state_sync.eq(state_gray[0] & ~state_gray[1]), 107 | 108 | fit_oe.eq(state_data | state_sync), 109 | fit_dat.eq((state_data & shifter.o_data & ~bitstuff.o_stall) | sp_bit), 110 | self.o_data_strobe.eq(((state_data & shifter.o_get & ~stall & self.i_oe) | sp_o_data_strobe) & new_bit), 111 | ] 112 | 113 | # If we reset the shifter, then o_empty will go high on the next cycle. 114 | # 115 | 116 | self.sync.usb_12 += [ 117 | # If the shifter runs out of data, percolate the "reset" signal to the 118 | # shifter, and then down to the bitstuffer. 119 | # da_reset_shifter.eq(~stall & shifter.o_empty & ~da_stalled_reset), 120 | # da_stalled_reset.eq(da_reset_shifter), 121 | # da_reset_bitstuff.eq(~stall & da_reset_shifter), 122 | If(new_bit, 123 | bitstuff_valid_data.eq(~stall & shifter.o_get & self.i_oe)), 124 | ] 125 | 126 | tx_pipeline_fsm.act('IDLE', 127 | If(self.i_oe, 128 | NextState('SEND_SYNC'), 129 | NextValue(sync_pulse, 1 << 7), 130 | NextValue(state_gray, 0b01), 131 | ).Else( 132 | NextValue(state_gray, 0b00), 133 | ) 134 | ) 135 | 136 | tx_pipeline_fsm.act('SEND_SYNC', 137 | NextValue(sync_pulse, sync_pulse >> 1), 138 | 139 | If(sync_pulse[0], 140 | NextState('SEND_DATA'), 141 | NextValue(state_gray, 0b11), 142 | ).Else( 143 | NextValue(state_gray, 0b01), 144 | ), 145 | ) 146 | 147 | tx_pipeline_fsm.act('SEND_DATA', 148 | If(~self.i_oe & shifter.o_empty & ~bitstuff.o_stall, 149 | If(bitstuff.o_will_stall, 150 | NextState('STUFF_LAST_BIT') 151 | ).Else( 152 | NextValue(state_gray, 0b10), 153 | NextState('IDLE'), 154 | ) 155 | ).Else( 156 | NextValue(state_gray, 0b11), 157 | ), 158 | ) 159 | 160 | tx_pipeline_fsm.act('STUFF_LAST_BIT', 161 | NextValue(state_gray, 0b10), 162 | NextState('IDLE'), 163 | ) 164 | 165 | # 48MHz domain 166 | # NRZI encoding 167 | nrzi_dat = Signal() 168 | nrzi_oe = Signal() 169 | # Cross the data from the 12MHz domain to the 48MHz domain 170 | cdc_dat = cdc.MultiReg(fit_dat, nrzi_dat, odomain="usb_48", n=3) 171 | cdc_oe = cdc.MultiReg(fit_oe, nrzi_oe, odomain="usb_48", n=3) 172 | self.specials += [cdc_dat, cdc_oe] 173 | if low_speed_support: 174 | cdc_low_speed = cdc.MultiReg(self.i_low_speed, nrzi.i_low_speed, odomain="usb_48", n=3) 175 | cdc_keepalive = cdc.MultiReg(self.i_keepalive, nrzi.i_keepalive, odomain="usb_48", n=3) 176 | self.specials += [cdc_low_speed, cdc_keepalive] 177 | 178 | self.comb += [ 179 | nrzi.i_valid.eq(self.i_bit_strobe), 180 | nrzi.i_data.eq(nrzi_dat), 181 | nrzi.i_oe.eq(nrzi_oe), 182 | 183 | self.o_usbp.eq(nrzi.o_usbp), 184 | self.o_usbn.eq(nrzi.o_usbn), 185 | self.o_oe.eq(nrzi.o_oe), 186 | ] 187 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/shifter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from migen.fhdl.decorators import CEInserter, ResetInserter 8 | 9 | from ..utils.packet import b 10 | from ..test.common import BaseUsbTestCase 11 | 12 | 13 | @ResetInserter() 14 | @CEInserter() 15 | class TxShifter(Module): 16 | """Transmit Shifter 17 | 18 | TxShifter accepts parallel data and shifts it out serially. 19 | 20 | Parameters 21 | ---------- 22 | Parameters are passed in via the constructor. 23 | 24 | width : int 25 | Width of the data to be shifted. 26 | 27 | Input Ports 28 | ----------- 29 | Input ports are passed in via the constructor. 30 | 31 | i_data : Signal(width) 32 | Data to be transmitted. 33 | 34 | Output Ports 35 | ------------ 36 | Output ports are data members of the module. All outputs are flopped. 37 | 38 | o_data : Signal(1) 39 | Serial data output. 40 | 41 | o_empty : Signal(1) 42 | Asserted the cycle before the shifter loads in more i_data. 43 | 44 | o_get : Signal(1) 45 | Asserted the cycle after the shifter loads in i_data. 46 | 47 | """ 48 | def __init__(self, width): 49 | self.i_data = Signal(width) 50 | self.o_get = Signal(1) 51 | self.o_empty = Signal(1) 52 | 53 | self.o_data = Signal(1) 54 | 55 | shifter = Signal(width) 56 | pos = Signal(width, reset=0b1) 57 | 58 | empty = Signal(1) 59 | self.sync += [ 60 | pos.eq(pos >> 1), 61 | shifter.eq(shifter >> 1), 62 | If(empty, 63 | shifter.eq(self.i_data), 64 | pos.eq(1 << (width-1)), 65 | ), 66 | self.o_get.eq(empty), 67 | ] 68 | self.comb += [ 69 | empty.eq(pos[0]), 70 | self.o_empty.eq(empty), 71 | self.o_data.eq(shifter[0]), 72 | ] 73 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/shifter_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | 5 | from migen import * 6 | 7 | from migen.fhdl.decorators import CEInserter, ResetInserter 8 | 9 | from ..utils.packet import b 10 | from ..test.common import BaseUsbTestCase 11 | 12 | from .shifter import TxShifter 13 | 14 | 15 | class TestTxShifter(BaseUsbTestCase): 16 | def shifter_test(self, vector, name): 17 | def send(reset, ce, data): 18 | out = "" 19 | get = "" 20 | empty = "" 21 | 22 | yield dut.i_data.eq(data.pop(0)) 23 | for i in range(len(ce)+1): 24 | if i < len(ce): 25 | if ce[i] == "|": 26 | continue 27 | yield dut.reset.eq(reset[i] == '-') 28 | yield dut.ce.eq(ce[i] == '-') 29 | 30 | yield 31 | 32 | if i < 1: 33 | continue 34 | 35 | out += str((yield dut.o_data)) 36 | o_get = yield dut.o_get 37 | o_empty = yield dut.o_empty 38 | get += { 39 | 0: "_", 40 | 1: "-", 41 | }[o_get] 42 | empty += { 43 | 0: "_", 44 | 1: "-", 45 | }[o_empty] 46 | 47 | if o_get: 48 | if data: 49 | yield dut.i_data.eq(data.pop(0)) 50 | else: 51 | yield dut.i_data.eq(0) 52 | 53 | if ce[i-1] == "|": 54 | out += "|" 55 | get += "|" 56 | empty += "|" 57 | 58 | return out, empty, get 59 | 60 | def stim(width, data, reset, ce, out, empty, get): 61 | actual_out, actual_empty, actual_get = yield from send(reset, ce, data) 62 | self.assertSequenceEqual(out, actual_out) 63 | self.assertSequenceEqual(empty, actual_empty) 64 | self.assertSequenceEqual(get, actual_get) 65 | 66 | with self.subTest(name=name, vector=vector): 67 | fname = name.replace(' ', '_') 68 | dut = TxShifter(vector["width"]) 69 | 70 | run_simulation(dut, stim(**vector), 71 | vcd_name=self.make_vcd_name(testsuffix=fname)) 72 | 73 | def test_basic_shift_out_1(self): 74 | return self.shifter_test( 75 | dict( 76 | width = 8, 77 | data = [b("00000001"), b("00000001"), b("00000001"), 0], 78 | reset = "-|________|________|________", 79 | ce = "-|--------|--------|--------", 80 | out = "0|00000001|00000001|00000001", 81 | empty = "-|_______-|_______-|_______-", 82 | get = "_|-_______|-_______|-_______", 83 | ), "basic shift out 1") 84 | 85 | def test_basic_shift_out_2(self): 86 | return self.shifter_test( 87 | dict( 88 | width = 8, 89 | data = [b("10000000"), b("10000000"), b("10000000"), 0], 90 | reset = "-|________|________|________", 91 | ce = "-|--------|--------|--------", 92 | out = "0|10000000|10000000|10000000", 93 | empty = "-|_______-|_______-|_______-", 94 | get = "_|-_______|-_______|-_______", 95 | ), "basic shift out 2") 96 | 97 | def test_basic_shift_out_3(self): 98 | return self.shifter_test( 99 | dict( 100 | width = 8, 101 | data = [b("01100110"), b("10000001"), b("10000000"), 0], 102 | reset = "-|________|________|________", 103 | ce = "-|--------|--------|--------", 104 | out = "0|01100110|10000001|10000000", 105 | empty = "-|_______-|_______-|_______-", 106 | get = "_|-_______|-_______|-_______", 107 | ), "basic shift out 3") 108 | 109 | def test_stall_shift_out_1(self): 110 | return self.shifter_test( 111 | dict( 112 | width = 8, 113 | data = [b("00000001"), b("00000001"), b("00000001"), 0], 114 | reset = "-|_________|________|________", 115 | ce = "-|--_------|--------|--------", 116 | out = "0|000000001|00000001|00000001", 117 | empty = "-|________-|_______-|_______-", 118 | get = "_|-________|-_______|-_______", 119 | ), "stall shift out 1") 120 | 121 | def test_stall_shift_out_2(self): 122 | return self.shifter_test( 123 | dict( 124 | width = 8, 125 | data = [b("10000000"), b("10000000"), b("10000000"), 0], 126 | reset = "-|_________|________|________", 127 | ce = "-|---_-----|--------|--------", 128 | out = "0|100000000|10000000|10000000", 129 | empty = "-|________-|_______-|_______-", 130 | get = "_|-________|-_______|-_______", 131 | ), "stall shift out 2") 132 | 133 | def test_stall_shift_out_3(self): 134 | return self.shifter_test( 135 | dict( 136 | width = 8, 137 | data = [b("01100110"), b("10000001"), b("10000000"), 0], 138 | reset = "-|_________|________|________", 139 | ce = "-|---_-----|--------|--------", 140 | out = "0|011100110|10000001|10000000", 141 | empty = "-|________-|_______-|_______-", 142 | get = "_|-________|-_______|-_______", 143 | ), "stall shift out 3") 144 | 145 | def test_multistall_shift_out_1(self): 146 | return self.shifter_test( 147 | dict( 148 | width = 8, 149 | data = [b("00000001"), b("00000001"), b("00000001"), 0], 150 | reset = "-|___________|_________|_________", 151 | ce = "-|--___------|--------_|----_----", 152 | out = "0|00000000001|000000011|000000001", 153 | empty = "-|__________-|_______--|________-", 154 | get = "_|-__________|-________|-________", 155 | ), "mutlistall shift out 1") 156 | 157 | def test_multistall_shift_out_2(self): 158 | return self.shifter_test( 159 | dict( 160 | width = 8, 161 | data = [b("10000000"), b("10000000"), b("10000000"), 0], 162 | reset = "-|____________|________|__________", 163 | ce = "-|---____-----|--------|-_----_---", 164 | out = "0|100000000000|10000000|1100000000", 165 | empty = "-|___________-|_______-|_________-", 166 | get = "_|-___________|-_______|--________", 167 | ), "mutlistall shift out 2") 168 | 169 | def test_multistall_shift_out_3(self): 170 | return self.shifter_test( 171 | dict( 172 | width = 8, 173 | data = [b("01100110"), b("10000001"), b("10000000"), 0], 174 | reset = "-|____________|___________|_________", 175 | ce = "-|---____-----|--------___|-_-------", 176 | out = "0|011111100110|10000001111|110000000", 177 | empty = "-|___________-|_______----|________-", 178 | get = "_|-___________|-__________|--_______", 179 | ), "mutlistall shift out 3") 180 | 181 | if __name__ == "__main__": 182 | unittest.main() 183 | -------------------------------------------------------------------------------- /valentyusb/usbcore/tx/tester.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import inspect 4 | 5 | from migen import * 6 | 7 | MIGEN_SIGNALS = ("reset", "ce") 8 | 9 | 10 | def get_ultimate_caller_modulename(): 11 | """ 12 | Helper to find the ultimate caller's module name (extra level further up 13 | stack than case where test directly inherits from BaseUsbTestCase). 14 | """ 15 | caller = inspect.stack()[2] 16 | module = inspect.getmodule(caller[0]) 17 | return module.__spec__.name 18 | 19 | 20 | def create_tester(dut_type, **def_args): 21 | def run(self, **test_args): 22 | name = self.id() 23 | self.vcd_name = self.make_vcd_name( 24 | modulename=get_ultimate_caller_modulename()) 25 | 26 | self.inputs = dict() 27 | self.outputs = dict() 28 | self.params = set() 29 | self.dut_args = dict() 30 | 31 | # parse tester definition 32 | for key in def_args: 33 | if not key.startswith("i_") and not key.startswith("o_") and key not in MIGEN_SIGNALS: 34 | self.params.add(key) 35 | 36 | # create dut 37 | for p in self.params: 38 | self.dut_args[p] = test_args[p] 39 | 40 | dut = dut_type(**self.dut_args) 41 | 42 | # gather signal 43 | for key in def_args: 44 | if key.startswith("i_") or key in MIGEN_SIGNALS: 45 | self.inputs[key] = getattr(dut, key) 46 | elif key.startswith("o_"): 47 | self.outputs[key] = getattr(dut, key) 48 | 49 | # calc num clocks 50 | clocks = 0 51 | for i in set(self.inputs.keys()) | set(self.outputs.keys()): 52 | if isinstance(test_args[i], str): 53 | clocks = max(clocks, len(test_args[i])) 54 | 55 | # decode stimulus 56 | def decode(c): 57 | try: 58 | return int(c, 16) 59 | except: 60 | pass 61 | 62 | if c == "-": 63 | return 1 64 | 65 | return 0 66 | 67 | # error message debug helper 68 | def to_waveform(sigs): 69 | output = "" 70 | 71 | for name in sigs.keys(): 72 | output += "%20s: %s\n" % (name, sigs[name]) 73 | 74 | return output 75 | 76 | 77 | 78 | actual_output = dict() 79 | 80 | # setup stimulus 81 | def stim(): 82 | 83 | for signal_name in self.outputs.keys(): 84 | actual_output[signal_name] = "" 85 | 86 | j = 0 87 | for i in range(clocks): 88 | for input_signal in self.inputs.keys(): 89 | v = test_args[input_signal][i] 90 | if v == '|': 91 | continue 92 | yield self.inputs[input_signal].eq(decode(v)) 93 | if v == '|': 94 | continue 95 | 96 | yield 97 | 98 | skip = True 99 | while True: 100 | if test_args[list(self.outputs.keys())[0]][j] != '|': 101 | skip = False 102 | if not skip: 103 | break 104 | 105 | for output_signal in self.outputs.keys(): 106 | actual_output[output_signal] += '|' 107 | j += 1 108 | 109 | for output_signal in self.outputs.keys(): 110 | assert len(actual_output[output_signal]) == j 111 | 112 | actual_value = yield self.outputs[output_signal] 113 | actual_output[output_signal] += str(actual_value) 114 | 115 | expected_value = test_args[output_signal][j] 116 | if expected_value == ' ': 117 | continue 118 | expected_value = decode(expected_value) 119 | actual_value = decode(actual_output[output_signal][j]) 120 | 121 | details = "\n" 122 | if expected_value != actual_value: 123 | details += " %s\n" % (output_signal, ) 124 | details += "\n" 125 | details += " Actual: %s\n" % (actual_output[output_signal]) 126 | details += " Expected: %s\n" % (test_args[output_signal]) 127 | details += " " + (" " * j) + "^\n" 128 | details += to_waveform(actual_output) 129 | self.assertEqual(expected_value, actual_value, msg = ("%s:%s:%d" % (name, output_signal, j)) + details) 130 | 131 | j += 1 132 | 133 | 134 | 135 | # run simulation 136 | run_simulation(dut, stim(), vcd_name=self.vcd_name) 137 | 138 | return actual_output 139 | 140 | return run 141 | 142 | 143 | def module_tester(dut_type, **def_args): 144 | def wrapper(class_type): 145 | class_type.do = create_tester(dut_type, **def_args) 146 | return class_type 147 | 148 | return wrapper 149 | -------------------------------------------------------------------------------- /valentyusb/usbcore/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/im-tomu/valentyusb/e8f08aae553e8fd7ae40ac62439d37360b9d1d67/valentyusb/usbcore/utils/__init__.py -------------------------------------------------------------------------------- /valentyusb/usbcore/utils/asserts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import tempfile 4 | import subprocess 5 | from .sdiff import Differ, getTerminalSize, original_diff 6 | import sys 7 | 8 | def assertMultiLineEqualSideBySide(expected, actual, msg): 9 | # print("data1: {}".format(data1.splitlines(1))) 10 | if expected == actual: 11 | return 12 | 13 | withcolor = True 14 | if not sys.stdout.isatty(): 15 | withcolor = False 16 | 17 | (columns, lines) = getTerminalSize() 18 | 19 | # Print out header 20 | expected = expected.splitlines(1) 21 | actual = actual.splitlines(1) 22 | differ = Differ(linejunk=None, charjunk=None, 23 | cutoff=0.1, fuzzy=0, 24 | cutoffchar=False, context=5) 25 | for line in differ.formattext(' ', 26 | None, "expected", None, "actual", columns, 27 | withcolor=withcolor, linediff=None): 28 | msg = msg + '\n' + line 29 | for line in differ.formattext(' ', 30 | None, "--------", None, "--------", columns, 31 | withcolor=withcolor, linediff=None): 32 | msg = msg + '\n' + line 33 | 34 | # Print out body 35 | lines = original_diff(expected, actual, 36 | linejunk=None, charjunk=None, 37 | cutoff=0, fuzzy=1, 38 | cutoffchar=False, context=5, 39 | width=columns, 40 | withcolor=withcolor) 41 | for line in lines: 42 | msg = msg + '\n' + line 43 | 44 | assert False, msg 45 | -------------------------------------------------------------------------------- /valentyusb/usbcore/utils/bits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def int_to_bits(i, width=None): 5 | """Convert an int to list of bits (LSB first). 6 | 7 | l[0] == LSB 8 | l[-1] == MSB 9 | 10 | 0b10000 11 | | | 12 | | \\--- LSB 13 | | 14 | \\------ MSB 15 | 16 | >>> int_to_bits(0b1) 17 | [1] 18 | >>> int_to_bits(0b0) 19 | [0] 20 | >>> int_to_bits(0b100) 21 | [0, 0, 1] 22 | >>> int_to_bits(0b100, 4) 23 | [0, 0, 1, 0] 24 | >>> int_to_bits(0b100, 8) 25 | [0, 0, 1, 0, 0, 0, 0, 0] 26 | """ 27 | if width is None: 28 | width='' 29 | return [int(i) for i in "{0:0{w}b}".format(i,w=width)[::-1]] 30 | 31 | 32 | def bits_to_int(bits): 33 | """Convert a list of bits (LSB first) to an int. 34 | 35 | l[0] == LSB 36 | l[-1] == MSB 37 | 38 | 0b10000 39 | | | 40 | | \\--- LSB 41 | | 42 | \\------ MSB 43 | 44 | >>> bits_to_int([1]) 45 | 1 46 | >>> bits_to_int([0]) 47 | 0 48 | >>> bin(bits_to_int([0, 0, 1])) 49 | '0b100' 50 | >>> bin(bits_to_int([0, 0, 1, 0])) 51 | '0b100' 52 | >>> bin(bits_to_int([0, 1, 0, 1])) 53 | '0b1010' 54 | >>> bin(bits_to_int([0, 1, 0, 1, 0, 0, 0])) 55 | '0b1010' 56 | >>> bin(bits_to_int([0, 0, 0, 0, 0, 1, 0, 1])) 57 | '0b10100000' 58 | """ 59 | v = 0 60 | for i in range(0, len(bits)): 61 | v |= bits[i] << i 62 | return v 63 | 64 | 65 | def int_to_rbits(i, width=None): 66 | """Convert an int to list of bits (MSB first). 67 | 68 | l[0] == MSB 69 | l[-1] == LSB 70 | 71 | 0b10000 72 | | | 73 | | \\--- LSB 74 | | 75 | \\------ MSB 76 | 77 | >>> int_to_rbits(0b1) 78 | [1] 79 | >>> int_to_rbits(0b0) 80 | [0] 81 | >>> int_to_rbits(0b100) 82 | [1, 0, 0] 83 | >>> int_to_rbits(0b100, 4) 84 | [0, 1, 0, 0] 85 | >>> int_to_rbits(0b100, 8) 86 | [0, 0, 0, 0, 0, 1, 0, 0] 87 | """ 88 | if width is None: 89 | width='' 90 | return [int(i) for i in "{0:0{w}b}".format(i,w=width)] 91 | 92 | 93 | def rbits_to_int(rbits): 94 | """Convert a list of bits (MSB first) to an int. 95 | 96 | l[0] == MSB 97 | l[-1] == LSB 98 | 99 | 0b10000 100 | | | 101 | | \\--- LSB 102 | | 103 | \\------ MSB 104 | 105 | >>> rbits_to_int([1]) 106 | 1 107 | >>> rbits_to_int([0]) 108 | 0 109 | >>> bin(rbits_to_int([1, 0, 0])) 110 | '0b100' 111 | >>> bin(rbits_to_int([1, 0, 1, 0])) 112 | '0b1010' 113 | >>> bin(rbits_to_int([1, 0, 1, 0, 0, 0, 0, 0])) 114 | '0b10100000' 115 | """ 116 | v = 0 117 | for i in range(0, len(rbits)): 118 | v |= rbits[i] << len(rbits)-i-1 119 | return v 120 | 121 | 122 | def get_bit(epaddr, v): 123 | """ 124 | >>> get_bit(0, 0b11) 125 | True 126 | >>> get_bit(0, 0b10) 127 | False 128 | >>> get_bit(0, 0b101) 129 | True 130 | >>> get_bit(1, 0b101) 131 | False 132 | """ 133 | return bool(1 << epaddr & v) 134 | 135 | 136 | def set_bit(current, epaddr, v): 137 | """ 138 | >>> bin(set_bit(0, 0, 1)) 139 | '0b1' 140 | >>> bin(set_bit(0, 2, 1)) 141 | '0b100' 142 | >>> bin(set_bit(0b1000, 2, 1)) 143 | '0b1100' 144 | >>> bin(set_bit(0b1100, 2, 0)) 145 | '0b1000' 146 | >>> bin(set_bit(0b1101, 2, 0)) 147 | '0b1001' 148 | """ 149 | if v: 150 | return current | 1 << epaddr 151 | else: 152 | return current & ~(1 << epaddr) 153 | -------------------------------------------------------------------------------- /valentyusb/usbcore/utils/vcd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tempfile 4 | 5 | def write_gtkwave_file(vcd_filename): 6 | 7 | basename, ext = os.path.splitext(vcd_filename) 8 | gtkw_filename = basename + '.gtkw' 9 | topdir = os.path.abspath(os.path.join('..', os.path.dirname(__file__))) 10 | 11 | with open(gtkw_filename, 'w') as f: 12 | f.write(""" 13 | [*] 14 | [*] GTKWave Analyzer v3.3.86 (w)1999-2017 BSI 15 | [*] Fri Jan 4 10:39:59 2019 16 | [*] 17 | [dumpfile] "{vcd_filename}" 18 | [dumpfile_mtime] "Fri Jan 4 09:20:43 2019" 19 | [dumpfile_size] 31260 20 | [savefile] "{gtkw_filename}" 21 | [timestart] 0 22 | [size] 1920 1019 23 | [pos] -1 -1 24 | *-19.442974 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 25 | [sst_width] 204 26 | [signals_width] 296 27 | [sst_expanded] 1 28 | [sst_vpaned_height] 302 29 | @10800028 30 | [transaction_args] "" 31 | ^<1 {topdir}/utils/dec-usb.sh 32 | #{usb} usbn usbp 33 | @200 34 | - 35 | - 36 | - 37 | - 38 | @28 39 | usbp 40 | usbn 41 | @1001200 42 | -group_end 43 | @200 44 | - 45 | @10800028 46 | [transaction_args] "" 47 | ^<1 {topdir}/utils/dec-usb.sh 48 | #{usb} i_usbp i_usbn 49 | @200 50 | - 51 | - 52 | - 53 | @29 54 | i_usbp 55 | @28 56 | i_usbn 57 | @1001200 58 | -group_end 59 | [pattern_trace] 1 60 | [pattern_trace] 0 61 | """.format(**locals())) 62 | 63 | 64 | def add_vcd_timescale(filename, timescale=435): 65 | tempfile = tempfile.SpooledTemporaryFile() 66 | with open(filename) as f: 67 | data = f.read() 68 | with open(filename, 'w') as f: 69 | f.write("$timescale %ips $end\n" % timescale) 70 | f.write(data) 71 | -------------------------------------------------------------------------------- /valentyusb/utils/dec-usb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec `dirname $0`/gtkwave-sigrok-filter.py -P usb_signalling:signalling=full-speed,usb_packet:signalling=full-speed 4 | -------------------------------------------------------------------------------- /valentyusb/utils/gtkwave-sigrok-filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | import tempfile 6 | import random 7 | import re 8 | 9 | 10 | COLORS = [ 11 | "dark blue", 12 | "dark cyan", 13 | "dark goldenrod", 14 | "dark green", 15 | "dark khaki", 16 | "dark magenta", 17 | "dark olive green", 18 | "dark orange", 19 | "dark orchid", 20 | "dark red", 21 | "dark salmon", 22 | "dark sea green", 23 | "dark slate blue", 24 | "dark turquoise", 25 | "dark violet", 26 | "deep pink", 27 | "deep sky blue", 28 | "dodger blue", 29 | "firebrick", 30 | "gainsboro", 31 | "gold", 32 | "goldenrod", 33 | "indian red", 34 | "lemon chiffon", 35 | "light blue", 36 | "light coral", 37 | "light cyan", 38 | "light goldenrod", 39 | "light goldenrod yellow", 40 | "light green", 41 | "light pink", 42 | "light salmon", 43 | "light sea green", 44 | "light sky blue", 45 | "light slate blue", 46 | "light steel blue", 47 | "light yellow", 48 | "lime green", 49 | "linen", 50 | "magenta", 51 | "maroon", 52 | "medium aquamarine", 53 | "medium blue", 54 | "medium orchid", 55 | "medium purple", 56 | "medium sea green", 57 | "medium slate blue", 58 | "medium spring green", 59 | "medium turquoise", 60 | "medium violet red", 61 | "midnight blue", 62 | "mint cream", 63 | "misty rose", 64 | "moccasin", 65 | "navajo white", 66 | "navy", 67 | "navy blue", 68 | "old lace", 69 | "olive drab", 70 | "orange", 71 | "orange red", 72 | "orchid", 73 | "pale goldenrod", 74 | "pale green", 75 | "pale turquoise", 76 | "pale violet red", 77 | "papaya whip", 78 | "peach puff", 79 | "peru", 80 | "pink", 81 | "plum", 82 | "powder blue", 83 | "purple", 84 | "red", 85 | "rosy brown", 86 | "royal blue", 87 | "saddle brown", 88 | "salmon", 89 | "sandy brown", 90 | "sea green", 91 | "seashell", 92 | "sienna", 93 | "sky blue", 94 | "slate blue", 95 | "snow", 96 | "spring green", 97 | "steel blue", 98 | "tan", 99 | "thistle", 100 | "tomato", 101 | "turquoise", 102 | "violet", 103 | "violet red", 104 | "wheat", 105 | "white", 106 | "white smoke", 107 | "yellow", 108 | "yellow green", 109 | ] 110 | 111 | def pick_color(): 112 | return COLORS.pop(0) 113 | 114 | 115 | def get_decoders_infos(args): 116 | # Return value 117 | rv = {} 118 | 119 | # Run sigrok-cli 120 | pipe = subprocess.Popen([ 121 | 'sigrok-cli', '--show', 122 | ] + list(args), 123 | stdout=subprocess.PIPE, 124 | ) 125 | text = pipe.communicate()[0].decode('utf-8') 126 | 127 | # Parse output 128 | cur = None 129 | active = False 130 | for l in text.splitlines(): 131 | if l.startswith('ID: '): 132 | cur = rv.setdefault(l[4:], ({},{})) 133 | elif l == 'Annotation rows:': 134 | active = True 135 | elif not l.startswith('-'): 136 | active = False 137 | elif active: 138 | m = re.match('^- (.*) \((.*)\): (.*)$', l) 139 | for cn in m.group(3).split(','): 140 | cur[0][cn.strip()] = (m.group(1), pick_color()) 141 | cur[1][m.group(1)] = m.group(2) 142 | 143 | return rv 144 | 145 | 146 | def main(argv0, *args): 147 | decoders = get_decoders_infos(args) 148 | fh_in = sys.stdin 149 | fh_out = sys.stdout 150 | with tempfile.NamedTemporaryFile() as fh_temp: 151 | # Repeat ... 152 | while True: 153 | # Read input until we get a full VCD input 154 | while True: 155 | l = fh_in.readline() 156 | if not l: 157 | return 0 158 | 159 | fh_temp.write(l.encode('utf-8')) 160 | 161 | if l.startswith('$comment data_end'): 162 | break 163 | 164 | fh_temp.flush() 165 | 166 | # Feed this to sigrok-cli and get output 167 | data = {} 168 | 169 | pipe = subprocess.Popen([ 170 | 'sigrok-cli', '-l', '4', '-O', 'ascii', 171 | '-i', fh_temp.name, '--input-format', 'vcd', 172 | '--protocol-decoder-samplenum', 173 | ] + list(args), 174 | stdout=subprocess.PIPE, 175 | universal_newlines=True 176 | ) 177 | text = pipe.communicate()[0] 178 | 179 | for l in text.splitlines(): 180 | # Parse 181 | l_t, l_d, l_c, l_v = l.strip().split(' ', 3) 182 | 183 | l_t = [int(x) for x in l_t.split('-')] # Time Span 184 | l_d = l_d.strip(':') # Decoder id 185 | l_c = l_c.strip(':') # Annotation class 186 | l_v = l_v.strip() # Value 187 | 188 | # Grab decoder infos 189 | d_id = l_d.split('-',1)[0] 190 | rowmap = decoders[d_id][0] 191 | 192 | # Map to a row 193 | row_id, color = rowmap[l_c] 194 | 195 | # Select one of the value 196 | l_v = re.split('" "', l_v[1:-1]) 197 | v = l_v[(len(l_v)-1)//2] 198 | 199 | # Save the start/stop event 200 | e = data.setdefault((l_d, row_id), {}) 201 | if l_t[1] not in e: 202 | e[l_t[1]] = None 203 | e[l_t[0]] = '?%s?%s' % (color, v) 204 | 205 | # Output 206 | first = True 207 | 208 | for k in data.keys(): 209 | if not first: 210 | fh_out.write("$next\n") 211 | first = False 212 | 213 | trace_name = k[0] + '/' + decoders[k[0].split('-',1)[0]][1][k[1]] 214 | fh_out.write("$name %s\n" % (trace_name,)) 215 | 216 | for t in sorted(data[k].keys()): 217 | v = data[k][t] 218 | fh_out.write("#%d %s\n" % (t, v if (v is not None) else '')) 219 | 220 | fh_out.write("$finish\n") 221 | fh_out.flush() 222 | 223 | # Reset 224 | fh_temp.seek(0) 225 | fh_temp.truncate() 226 | del data 227 | 228 | 229 | if __name__ == '__main__': 230 | sys.exit(main(*sys.argv)) 231 | -------------------------------------------------------------------------------- /vcd/README.md: -------------------------------------------------------------------------------- 1 | Output of the unit tests are written to this directory. 2 | --------------------------------------------------------------------------------