├── .github └── workflows │ └── release.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── bin ├── migratev2.py ├── release.sh └── serialize_shuttle.py ├── config.md ├── diagram.json ├── images ├── cocotb-testrun.png └── demoboard_bootbutton.jpg ├── pyproject.toml ├── src ├── config.ini ├── examples │ ├── README.md │ ├── __init__.py │ ├── basic │ │ ├── __init__.py │ │ └── tb.py │ ├── tt_um_factory_test │ │ ├── __init__.py │ │ └── tt_um_factory_test.py │ ├── tt_um_psychogenic_neptuneproportional │ │ ├── __init__.py │ │ └── tt_um_psychogenic_neptuneproportional.py │ ├── tt_um_psychogenic_shaman │ │ ├── __init__.py │ │ ├── shaman.py │ │ └── tb.py │ ├── tt_um_rejunity_sn76489 │ │ ├── __init__.py │ │ └── tt_um_rejunity_sn76489.py │ └── tt_um_rgbled_decoder │ │ ├── __init__.py │ │ └── tt_um_rgbled_decoder.py ├── main.py ├── tests │ ├── __init__.py │ ├── counter_read.py │ ├── counter_speed.py │ ├── dffram.py │ ├── sram.py │ └── test_anton.py └── ttboard │ ├── __init__.py │ ├── boot │ ├── __init__.py │ ├── demoboard_detect.py │ ├── rom.py │ └── shuttle_properties.py │ ├── cocotb │ ├── __init__.py │ └── dut.py │ ├── config │ ├── __init__.py │ ├── config_file.py │ ├── parser.py │ └── user_config.py │ ├── demoboard.py │ ├── globals.py │ ├── log.py │ ├── mode.py │ ├── pins │ ├── __init__.py │ ├── desktop_pin.py │ ├── gpio_map.py │ ├── mux_control.py │ ├── muxed.py │ ├── pins.py │ ├── standard.py │ └── upython.py │ ├── ports │ ├── __init__.py │ ├── io.py │ └── oe.py │ ├── project_design.py │ ├── project_mux.py │ └── util │ ├── __init__.py │ ├── colors.py │ ├── platform.py │ ├── shuttle_tests.py │ └── time.py ├── test ├── conftest.py └── test_serialized_shuttle.py └── wokwi.toml /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: UF2 Generate and Release 2 | on: 3 | workflow_dispatch: 4 | push: 5 | jobs: 6 | create-release: 7 | env: 8 | RPOS_UF2FILE: RPI_PICO-20241025-v1.24.0.uf2 9 | TT_RUNS_SUPPORTED: "unknown tt04 tt05 tt06 tt07 tt08" 10 | runs-on: ubuntu-24.04 11 | steps: 12 | 13 | - name: checkout repo 14 | uses: actions/checkout@v4 15 | with: 16 | submodules: recursive 17 | path: sdk 18 | 19 | - name: Download stock MicroPython UF2 20 | run: wget -O /tmp/rp2-pico.uf2 "https://micropython.org/resources/firmware/$RPOS_UF2FILE" 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.10' 26 | cache: 'pip' 27 | 28 | - name: Move ucocotb 29 | run: | 30 | cp -r $GITHUB_WORKSPACE/sdk/microcotb/src/microcotb $GITHUB_WORKSPACE/sdk/src 31 | 32 | - name: Get shuttle indices 33 | run: | 34 | mkdir $GITHUB_WORKSPACE/sdk/src/shuttles 35 | for chip in $TT_RUNS_SUPPORTED; do wget -O $GITHUB_WORKSPACE/sdk/src/shuttles/$chip.json "https://index.tinytapeout.com/$chip.json?fields=repo,address,commit,clock_hz,title,danger_level"; done 36 | - name: Prepare shuttle bin files 37 | run: | 38 | cp -R $GITHUB_WORKSPACE/sdk/src $GITHUB_WORKSPACE/sdk/convwork 39 | RUNPATH="$GITHUB_WORKSPACE/sdk/convwork:$PYTHONPATH" 40 | echo "Path is $RUNPATH" 41 | for chip in $TT_RUNS_SUPPORTED; do PYTHONPATH=$RUNPATH python3 $GITHUB_WORKSPACE/sdk/bin/serialize_shuttle.py $GITHUB_WORKSPACE/sdk/src/shuttles/$chip.json; done 42 | 43 | - name: Run PyTests 44 | run: | 45 | pip install pytest 46 | RUNPATH="$GITHUB_WORKSPACE/sdk/convwork:$PYTHONPATH" 47 | cd $GITHUB_WORKSPACE/sdk/test 48 | for chip in $TT_RUNS_SUPPORTED; do PYTHONPATH=$RUNPATH pytest --shuttle $chip --shuttlepath $GITHUB_WORKSPACE/sdk/src/shuttles test_serialized_shuttle.py; done 49 | 50 | - name: Build the final UF2 51 | run: | 52 | pip install uf2utils 53 | touch $GITHUB_WORKSPACE/sdk/src/release_${{ github.ref_name }} 54 | python -m uf2utils.examples.custom_pico --fs_root $GITHUB_WORKSPACE/sdk/src --upython /tmp/rp2-pico.uf2 --out /tmp/tt-demo-rp2040-${{ github.ref_name }}.uf2 55 | 56 | - name: Upload Artifact 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: tt-demo-board-firmware 60 | path: /tmp/tt-demo-rp2040-${{ github.ref_name }}.uf2 61 | 62 | - name: Upload Release 63 | uses: ncipollo/release-action@v1 64 | if: startsWith(github.ref, 'refs/tags/') 65 | with: 66 | artifacts: "/tmp/tt-demo-rp2040-${{ github.ref_name }}.uf2" 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | generateReleaseNotes: true 69 | 70 | simulate: 71 | runs-on: ubuntu-24.04 72 | needs: create-release 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v4 76 | 77 | - name: Download UF2 78 | uses: actions/download-artifact@v4 79 | with: 80 | name: tt-demo-board-firmware 81 | path: /tmp 82 | 83 | - name: Copy UF2 to workspace 84 | run: | 85 | cp /tmp/tt-demo-rp2040-${{ github.ref_name }}.uf2 $GITHUB_WORKSPACE/release.uf2 86 | 87 | - name: Run a Wokwi CI server 88 | uses: wokwi/wokwi-ci-server-action@v1 89 | 90 | - name: Test with Wokwi 91 | uses: wokwi/wokwi-ci-action@v1 92 | with: 93 | token: ${{ secrets.WOKWI_CLI_TOKEN }} 94 | path: / 95 | expect_text: 'boot done:' 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/shuttles/ 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 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "microcotb"] 2 | path = microcotb 3 | url = https://github.com/psychogenic/microcotb.git 4 | -------------------------------------------------------------------------------- /bin/migratev2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | TT SDK v2.0 migration script 4 | @copyright: (C) 2024 Pat Deegan, https://psychogenic.com 5 | 6 | Tries to do lots of the grunt work of migrating code to the new 7 | cocotb-like SDK. 8 | 9 | Use --help to see: 10 | usage: migratev2.py [-h] [--outdir OUTDIR] [--overwrite] infile [infile ...] 11 | 12 | SDK v2 migration tool 13 | 14 | positional arguments: 15 | infile files to migrate 16 | 17 | options: 18 | -h, --help show this help message and exit 19 | --outdir OUTDIR Destination directory for migrated file(s) 20 | --overwrite Allow overwriting files when outputting 21 | 22 | 23 | Sample use cases 24 | 25 | 1) Look at a single file 26 | ./migratev2.py path/to/file.py 27 | 28 | 2) Write out files 29 | Specify a directory in which to dump the contents using --outdir 30 | ./migratev2 --outdir /tmp/new path/to/*.py path/to/more/*.py 31 | 32 | All files will endup under /tmp/new/path/to/... in exact 33 | reflection of relative path used, to any depth. 34 | 35 | ''' 36 | import re 37 | import argparse 38 | import os 39 | import sys 40 | 41 | substitutions = [ 42 | 43 | ('\.input_byte\s*=', '.ui_in.value ='), 44 | ('(return\s+|=)\s*([\w\.]+)\.input_byte', '\g<1> \g<2>.ui_in.value'), 45 | ('\.input_byte', '.ui_in.value'), 46 | ('(return\s+|=)\s*([^\s]+)\.output_byte', '\g<1> \g<2>.uo_out.value'), 47 | ('\.output_byte', '.uo_out.value'), 48 | # order matters 49 | ('\.bidir_byte\s*=', '.uio_in.value ='), 50 | ('(return\s+|=)\s*([^\s]+)\.bidir_byte', '\g<1> \g<2>.uio_out.value'), 51 | ('([^\s]+)\.bidir_byte', '\g<1>.uio_out.value'), 52 | ('\.bidir_mode', '.uio_oe[:]'), # 53 | ('\.project_nrst', '.rst_n'), 54 | ('\.project_clk([^_]+)', '.clk\g<1>') 55 | ] 56 | 57 | 58 | 59 | special_cases = [ 60 | ('individual_pin_attrib', '\.(in|out|uio)(\d+)'), 61 | ('individual_pin_write', '\.(in|out|uio)(\d+)\(([^)]+)\)'), 62 | ('individual_pin_read', '\.(in|out|uio)(\d+)\((\s*)\)'), 63 | 64 | ] 65 | 66 | class Replacer: 67 | def __init__(self): 68 | self.substitutions = [] 69 | for v in substitutions: 70 | self.substitutions.append( [re.compile(v[0]), v[1]]) 71 | 72 | #self.special_cases = [] 73 | for sc in special_cases: 74 | #spec = [re.compile(sc[1]), sc[2]] 75 | #self.special_cases.append(spec) 76 | setattr(self, sc[0], re.compile(sc[1], re.MULTILINE)) 77 | 78 | def read(self, fpath:str): 79 | with open(fpath, 'r') as f: 80 | return ''.join(f.readlines()) 81 | 82 | def basic_substitutions(self, contents:str): 83 | for s in self.substitutions: 84 | contents = s[0].sub(s[1], contents) 85 | 86 | return contents 87 | 88 | def special_substitutions(self, contents:str): 89 | 90 | set_bitmap = { 91 | 'in': 'ui_in', 92 | 'out': 'uo_out', 93 | 'uio': 'uio_in', 94 | } 95 | 96 | read_bitmat = { 97 | 'in': 'ui_in', 98 | 'out': 'uo_out', 99 | 'uio': 'uio_out', 100 | 101 | } 102 | 103 | seen = dict() 104 | 105 | for p in self.individual_pin_write.findall(contents): 106 | subre = f'\.{p[0]}{p[1]}\({p[2]}\)' 107 | repl = f'.{set_bitmap[p[0]]}[{p[1]}] = {p[2]}' 108 | print(f"'{subre}', '{repl}'") 109 | contents = re.sub(subre, repl, contents, 0, re.MULTILINE) 110 | 111 | for p in self.individual_pin_read.findall(contents): 112 | subre = f'\.{p[0]}{p[1]}\({p[2]}\)' 113 | repl = f'.{read_bitmap[p[0]]}[{p[1]}]' 114 | print(f"'{subre}', '{repl}'") 115 | contents = re.sub(subre, repl, contents, 0, re.MULTILINE) 116 | 117 | for p in self.individual_pin_attrib.findall(contents): 118 | subre = f'\.{p[0]}{p[1]}' 119 | repl = f'.pins.{set_bitmap[p[0]]}{p[1]}' 120 | print(f"PINATTR '{subre}', '{repl}'") 121 | contents = re.sub(subre, repl, contents, 0, re.MULTILINE) 122 | 123 | 124 | 125 | 126 | return contents 127 | 128 | def migrate(self, contents:str): 129 | contents = self.basic_substitutions(contents) 130 | contents = self.special_substitutions(contents) 131 | return contents 132 | 133 | def migrate_file(self, fpath:str): 134 | c = self.read(fpath) 135 | return self.migrate(c) 136 | 137 | # 138 | #f = r.read('src/examples/tt_um_psychogenic_neptuneproportional/tt_um_psychogenic_neptuneproportional.py') 139 | def getArgsParser(): 140 | parser = argparse.ArgumentParser(description='SDK v2 migration tool') 141 | 142 | parser.add_argument('--outdir', required=False, 143 | type=str, 144 | help="Destination directory for migrated file(s)") 145 | parser.add_argument('--overwrite', required=False, 146 | action='store_true', 147 | help="Allow overwriting files when outputting") 148 | parser.add_argument('infile', nargs='+', help='files to migrate') 149 | return parser 150 | 151 | 152 | def mkdir_if_needed(dirpath:str): 153 | if not os.path.exists(dirpath): 154 | print(f'Creating directory: {dirpath}') 155 | os.makedirs(dirpath) 156 | 157 | def main(): 158 | 159 | parser = getArgsParser() 160 | args = parser.parse_args() 161 | 162 | 163 | if not args.outdir: 164 | if len(args.infile) != 1: 165 | print("You MUST specify --outdir if more than one file is to be migrated") 166 | return 167 | 168 | rep = Replacer() 169 | for infile in args.infile: 170 | if not os.path.exists(infile): 171 | print(f"Can't find '{infile}'") 172 | continue 173 | print(f"Processing '{infile}'", file=sys.stderr) 174 | contents = rep.migrate_file(infile) 175 | if not args.outdir: 176 | print(contents) 177 | else: 178 | destpathdir = os.path.join(args.outdir, os.path.dirname(infile)) 179 | mkdir_if_needed(destpathdir) 180 | fpath = os.path.join(destpathdir, os.path.basename(infile)) 181 | if os.path.exists(fpath) and not args.overwrite: 182 | print(f"{fpath} exists and NO --overwrite, skip", file=sys.stderr) 183 | else: 184 | print(f"Writing {fpath}", file=sys.stderr) 185 | with open(fpath, 'w') as f: 186 | f.write(contents) 187 | 188 | main() 189 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Simple script to package up the TT SDK as an 4 | # installable UF2 for the RP2040 5 | # 6 | # Copyright (C) 2024 Pat Deegan, https://psychogenic.com 7 | # 8 | # To run: 9 | # 1) ensure you have uf2utils 10 | # pip install uf2utils 11 | # 2) run from top dir using 12 | # ./bin/release.sh VERSION [SDKSRCDIR] 13 | # e.g. 14 | # ./bin/release.sh 1.2.4 15 | # or 16 | # ./bin/release.sh 1.2.4 /path/to/sdk/src 17 | # 18 | # 19 | 20 | 21 | RPOS_UF2FILE=RPI_PICO-20241025-v1.24.0.uf2 22 | TT_RUNS_SUPPORTED="unknown tt05 tt06 tt07 tt08" 23 | 24 | VERSION=$1 25 | SRCDIR=$2 26 | 27 | # check for uf2utils 28 | UF2INFOPATH=`which uf2info` 29 | if [ "$?" == "1" ] 30 | then 31 | echo "uf2utils is required (https://pypi.org/project/uf2utils/)" 32 | exit 1 33 | fi 34 | 35 | # check version was specified 36 | if [ "x$VERSION" == "x" ] 37 | then 38 | echo "USAGE: $0 VERSION [SDKSRCDIR]" 39 | exit 2 40 | fi 41 | 42 | # check and test source dir 43 | if [ "x$SRCDIR" == "x" ] 44 | then 45 | SRCDIR=./src 46 | fi 47 | 48 | if [ -d $SRCDIR ] 49 | then 50 | echo "Using SDK from $SRCDIR" 51 | else 52 | echo "Can't find SDK in $SRCDIR" 53 | exit 3 54 | fi 55 | 56 | if [ -e ./bin/serialize_shuttle.py ] 57 | then 58 | echo "Downloading and serializing shuttles" 59 | else 60 | echo "Run this script from repo topdir, ./bin/release.sh" 61 | fi 62 | echo "Download shuttles for $TT_RUNS_SUPPORTED" 63 | mkdir $SRCDIR/shuttles 64 | for chip in $TT_RUNS_SUPPORTED; do echo "get shuttle $chip"; wget -O $SRCDIR/shuttles/$chip.json "https://index.tinytapeout.com/$chip.json?fields=address,clock_hz,title,danger_level"; done 65 | for chip in $TT_RUNS_SUPPORTED; do echo "serialize $chip shuttle"; rm $SRCDIR/shuttles/$chip.json.bin; PYTHONPATH="./src/:./microcotb/src:$PYTHONPATH" python ./bin/serialize_shuttle.py $SRCDIR/shuttles/$chip.json; done 66 | 67 | # create some temp stuff 68 | BUILDDIR=`mktemp -d -t ttupython-XXXXX` 69 | RPEXISTING=`ls /tmp/rp2-pico-????.uf2` 70 | 71 | echo "Download $RPOS_UF2FILE" 72 | if [ "x$RPEXISTING" == "x" ] 73 | then 74 | RPUF2=`mktemp -t rp2-pico-XXXX.uf2` 75 | echo "Getting $RPOS_UF2FILE" 76 | wget -O $RPUF2 -c "https://micropython.org/resources/firmware/$RPOS_UF2FILE" 77 | else 78 | echo "already have $RPOS_UF2FILE (as $RPEXISTING)" 79 | RPUF2=$RPEXISTING 80 | fi 81 | 82 | touch $BUILDDIR/release_v$VERSION 83 | 84 | echo "Including SDK from $SRCDIR" 85 | cp -Ra $SRCDIR/* $BUILDDIR 86 | echo "Including microcotb" 87 | cp -Ra $SRCDIR/../microcotb/src/microcotb $BUILDDIR 88 | for pcd in `find $BUILDDIR -type d -name "__pycache__"` 89 | do 90 | echo "cleaning up $pcd" 91 | rm -rf $pcd 92 | done 93 | 94 | 95 | echo "Generating UF2" 96 | OUTFILE=/tmp/tt-demo-rp2040-$VERSION.uf2 97 | python -m uf2utils.examples.custom_pico --fs_root $BUILDDIR --upython $RPUF2 --out $OUTFILE 98 | echo 99 | uf2info $OUTFILE 100 | 101 | rm -rf $BUILDDIR 102 | # echo $BUILDDIR 103 | #rm $RPUF2 104 | echo 105 | echo "Done: $OUTFILE created" 106 | echo 107 | exit 0 108 | -------------------------------------------------------------------------------- /bin/serialize_shuttle.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from ttboard.project_mux import DesignIndex 4 | BinFileSuffix = 'bin' 5 | 6 | def main(): 7 | if len(sys.argv) < 2: 8 | print("MUST pass SHUTTLE.JSON argument") 9 | return False 10 | fname = sys.argv[1] 11 | if not os.path.exists(fname): 12 | print(f'Cannot find {fname}?') 13 | return False 14 | 15 | basename = os.path.basename(fname) 16 | destdir = os.path.dirname(fname) 17 | dest_file = os.path.join(destdir, f'{basename}.{BinFileSuffix}') 18 | 19 | d = DesignIndex(None, fname) 20 | d.to_bin_file(dest_file) 21 | 22 | if os.path.exists(dest_file): 23 | print(f'Wrote out {dest_file}') 24 | return True 25 | 26 | print(f'Cannot find resulting file {dest_file}?') 27 | return False 28 | 29 | if __name__ == '__main__': 30 | if not main(): 31 | print("Prawblemz") 32 | #from ttboard.project_mux import * 33 | #d = DesignIndex(None, 'ho.json') 34 | -------------------------------------------------------------------------------- /config.md: -------------------------------------------------------------------------------- 1 | # TT demo board configuration 2 | 3 | A *config.ini* file, in the filesystem root, specifies default behaviour and, optionally, project-specific settings. 4 | 5 | ### Automatic Load and Default Config 6 | 7 | The [config.ini](src/config.ini) file has a **DEFAULT** section that may be used to specify the demo board mode, default project to enable and other things 8 | 9 | ``` 10 | [DEFAULT] 11 | # project: project to load by default 12 | project = tt_um_factory_test 13 | 14 | # start in reset (bool) 15 | start_in_reset = no 16 | 17 | # mode can be any of 18 | # - SAFE: all RP2040 pins inputs 19 | # - ASIC_RP_CONTROL: TT inputs,nrst and clock driven, outputs monitored 20 | # - ASIC_MANUAL_INPUTS: basically same as safe, but intent is clear 21 | mode = ASIC_RP_CONTROL 22 | 23 | # log_level can be one of 24 | # - DEBUG 25 | # - INFO 26 | # - WARN 27 | # - ERROR 28 | log_level = INFO 29 | 30 | 31 | # default RP2040 system clock 32 | rp_clock_frequency = 125e6 33 | 34 | ``` 35 | 36 | Each project on the shuttle may have it's own section as well, with additional attributes. All attributes are optional. 37 | See the config section, below, for details. 38 | 39 | ## Configuration 40 | 41 | A `config.ini` file may be used to setup defaults (e.g. default mode or project to load on boot) as well as specific configuration to apply when loading a project in particular. See the included `config.ini` for samples with commentary. 42 | 43 | Projects may use their own sections in this file to do preliminary setup, like configure clocking, direction and state of bidir pins, etc. 44 | 45 | If you're connected to the REPL, the configuration can be probed just by looking at the repr string or printing the object out 46 | 47 | 48 | ``` 49 | >>> tt.user_config 50 | 51 | >>> 52 | >>> print(tt.user_config) 53 | UserConfig config.ini, Defaults: 54 | project: tt_um_factory_test 55 | mode: ASIC_RP_CONTROL 56 | tt_um_psychogenic_neptuneproportional 57 | clock_frequency: 4000 58 | mode: ASIC_RP_CONTROL 59 | ui_in: 200 60 | tt_um_urish_simon 61 | clock_frequency: 50000 62 | mode: ASIC_MANUAL_INPUTS 63 | ... 64 | 65 | ``` 66 | 67 | 68 | If any override sections are present in the file, sections will show you which are there, and these are present as 69 | attributes you can just looking at to see summary info, or print out to see everything the section actually does. 70 | 71 | ``` 72 | >>> tt.user_config.sections 73 | ['tt_um_factory_test', 'tt_um_urish_simon', 'tt_um_psychogenic_neptuneproportional', 'tt_um_test', 'tt_um_loopback', 'tt_um_vga_clock', 'tt_um_algofoogle_solo_squash'] 74 | >>> 75 | >>> tt.user_config.tt_um_urish_simon 76 | 77 | >>> 78 | >>> tt.user_config.tt_um_psychogenic_neptuneproportional 79 | 80 | >>> 81 | >>> print(tt.user_config.tt_um_vga_clock) 82 | tt_um_vga_clock 83 | clock_frequency: 3.15e+07 84 | mode: ASIC_RP_CONTROL 85 | rp_clock_frequency: 1.26e+08 86 | 87 | ``` 88 | 89 | 90 | ### Sections and Values 91 | 92 | This is *similar* to, but not the same (because hand-crufted) as the python config parser. 93 | 94 | Sections are simply name `[SECTION]`. 95 | 96 | Values are 97 | 98 | ``` 99 | key = value 100 | ``` 101 | 102 | Where the value may be 103 | 104 | * a string 105 | * a numerical value (an int, float, 0xnn or 0bnnnnnnnn representation) 106 | * a boolean (true, false, yes, no) 107 | * a comment (a line beginning with #) 108 | 109 | 110 | ### System Defaults 111 | 112 | System-wide default settings supported are 113 | 114 | project: (string) name of project to load on boot, e.g. *tt_um_loopback* 115 | 116 | mode: (string) ASIC_RP_CONTROL, ASIC_MANUAL_INPUTS (to use the on-board switches/buttons), or SAFE 117 | 118 | start_in_reset: (bool) whether projects should have their nRESET pin held low when enabled 119 | 120 | rp_clock_frequency: system clock frequency 121 | 122 | 123 | ### Project-specific 124 | 125 | Some values may be auto-configured when enabling a project, by having them specified in their own section. 126 | The section name is 127 | 128 | ``` 129 | [PROJECT_NAME] 130 | ``` 131 | 132 | as specified in the shuttle, for instance 133 | 134 | ``` 135 | [tt_um_psychogenic_neptuneproportional] 136 | 137 | ``` 138 | 139 | Values that may be set are 140 | * clock_frequency: Frequency, in Hz, to auto-clock on project clock pin (ignored if in ASIC_MANUAL_INPUTS) 141 | * rp_clock_frequency: system clock frequency -- useful if you need precision for your project clock PWM 142 | * ui_in: value to set for inputs on startup (ignored if in ASIC_MANUAL_INPUTS) 143 | * uio_oe_pico: bidirectional pin direction, bits set to 1 are driven by RP2040 144 | * uio_in: actual value to set on bidirectional pins (only applies to outputs) 145 | * mode: tt mode to set for this project 146 | 147 | 148 | Values unspecified in a configuration are left as-is on project enable(). 149 | 150 | Project auto-clocking is stopped by default when a project is loaded. If the clock_frequency is set, then 151 | it will be setup accordingly (*after* the rp_clock_frequency has been configured if that's present). 152 | 153 | Bi-directional pins (uio*) are reset to inputs when enabling another project. 154 | 155 | 156 | -------------------------------------------------------------------------------- /diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "author": "Uri Shaked", 4 | "editor": "wokwi", 5 | "parts": [ 6 | { 7 | "type": "wokwi-pi-pico", 8 | "id": "pico" 9 | } 10 | ], 11 | "connections": [] 12 | } 13 | -------------------------------------------------------------------------------- /images/cocotb-testrun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/images/cocotb-testrun.png -------------------------------------------------------------------------------- /images/demoboard_bootbutton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/images/demoboard_bootbutton.jpg -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ttboard_tinytapeout" 7 | version = "2.0.0" 8 | authors = [ 9 | { name="Pat Deegan" }, 10 | ] 11 | description = "This library provides the DemoBoard class, which is the primary entry point to all the TinyTapeout demo pcb's RP2040 functionality, and supporting components" 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [project.urls] 21 | Homepage = "https://tinytapeout.com/" 22 | Issues = "https://github.com/TinyTapeout/tt-micropython-firmware/issues" 23 | -------------------------------------------------------------------------------- /src/config.ini: -------------------------------------------------------------------------------- 1 | # TT 3.5 shuttle user config file 2 | # DEFAULT is system-wide section 3 | # [PROJECT_NAME] is tt_um_whatever for that project 4 | # comment out lines by starting with # 5 | # empty values are ok, e.g. 6 | # project = 7 | # will load nothing by default 8 | # numbers can be int, float, scientific, bin or hex 9 | 10 | 11 | 12 | #### DEFAULT Section #### 13 | 14 | [DEFAULT] 15 | # project: project to load by default 16 | project = tt_um_factory_test 17 | 18 | # start in reset (bool) 19 | start_in_reset = no 20 | 21 | # mode can be any of 22 | # - SAFE: all RP2040 pins inputs 23 | # - ASIC_RP_CONTROL: TT inputs,nrst and clock driven, outputs monitored 24 | # - ASIC_MANUAL_INPUTS: basically same as safe, but intent is clear 25 | mode = ASIC_RP_CONTROL 26 | 27 | # log_level can be one of 28 | # - DEBUG 29 | # - INFO 30 | # - WARN 31 | # - ERROR 32 | log_level = INFO 33 | 34 | 35 | # default RP2040 system clock 36 | rp_clock_frequency = 125e6 37 | 38 | 39 | # force_shuttle 40 | # by default, system attempts to figure out which ASIC is on board 41 | # using the chip ROM. This can be a problem if you have something 42 | # connected to the demoboard. If you want to bypass this step and 43 | # manually set the shuttle, uncomment this and set the option to 44 | # a valid shuttle 45 | # force_shuttle = tt06 46 | 47 | 48 | # force_demoboard 49 | # System does its best to determine the version of demoboard 50 | # its running on. Override this here, using tt0* 51 | # force_demoboard = tt06 52 | 53 | #### PROJECT OVERRIDES #### 54 | 55 | 56 | [tt_um_test] 57 | clock_frequency = 10 58 | start_in_reset = no 59 | ui_in = 1 60 | 61 | [tt_um_factory_test] 62 | clock_frequency = 10 63 | start_in_reset = no 64 | ui_in = 1 65 | 66 | 67 | [tt_um_psychogenic_neptuneproportional] 68 | # set clock to 4kHz 69 | clock_frequency = 4000 70 | # clock config 4k, disp single bits 71 | ui_in = 0b11001000 72 | mode = ASIC_RP_CONTROL 73 | 74 | 75 | [wokwi_7seg_tiny_tapeout_display] 76 | rp_clock_frequency = 50_000_000 77 | clock_frequency = 5 78 | mode = ASIC_RP_CONTROL 79 | 80 | [tt_um_seven_segment_seconds] 81 | rp_clock_frequency = 120e6 82 | clock_frequency = 10e6 83 | ui_in = 0 84 | mode = ASIC_RP_CONTROL 85 | 86 | 87 | 88 | 89 | [tt_um_loopback] 90 | # ui_in[0] == 1 means bidirs on output 91 | clock_frequency = 1000 92 | ui_in = 1 93 | 94 | # uio_oe_pico, 1 bit means we will 95 | # write to it (RP pin is output), 96 | # 0 means read from (RP is input) 97 | # set to all output 98 | uio_oe_pico = 0xff 99 | uio_in = 0b110010101 100 | 101 | [tt_um_vga_clock] 102 | rp_clock_frequency = 126e6 103 | clock_frequency = 31.5e6 104 | mode = ASIC_RP_CONTROL 105 | 106 | 107 | [tt_um_urish_simon] 108 | clock_frequency = 50000 109 | mode = ASIC_MANUAL_INPUTS 110 | 111 | 112 | [tt_um_algofoogle_solo_squash] 113 | mode = ASIC_RP_CONTROL 114 | 115 | # start inactive (all ins 0) 116 | ui_in = 0 117 | 118 | # Ensure we are *reading* from all of the ASIC's bidir pins, 119 | # so bidirs all inputs: 120 | uio_oe_pico = 0 121 | 122 | 123 | 124 | [tt_um_psychogenic_shaman] 125 | mode = ASIC_RP_CONTROL 126 | clock_frequency = 1e6 127 | # shaman uses a mix of in and out on bidir 128 | uio_oe_pico = 0b11001100 129 | uio_in = 0 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/examples/README.md: -------------------------------------------------------------------------------- 1 | # SDK project interaction examples and tests 2 | 3 | This is the home of sample interaction scripts and tests specific to given projects. 4 | 5 | The idea is to have a standardized place for sample code and to allow people to try out projects with ease. 6 | 7 | As of version 2.0 of the SDK, the demoboard API has been standardized to match that used in Verilog (uo_out, ui_in, etc) and made such that existing [cocotb version 2.0](https://www.cocotb.org/) tests should be usable, in many cases as-is. 8 | 9 | 10 | 11 | ## Quick start 12 | 13 | 14 | A system is in place to support [cocotb](https://www.cocotb.org/) style tests, such that the code used during development simulation should be usable (almost) as-is with hardware-in-the-loop, directly on the demoboard. 15 | 16 | For example 17 | 18 | ``` 19 | @cocotb.test() 20 | async def test_counter(dut): 21 | dut._log.info("Start") 22 | clock = Clock(dut.clk, 10, units="us") 23 | cocotb.start_soon(clock.start()) 24 | dut.uio_oe_pico.value = 0 # all inputs on our side 25 | 26 | dut.ui_in.value = 0b1 27 | dut.rst_n.value = 0 28 | await ClockCycles(dut.clk, 10) 29 | dut.rst_n.value = 1 30 | await ClockCycles(dut.clk, 1) 31 | 32 | dut._log.info("Testing counter") 33 | for i in range(256): 34 | assert dut.uo_out.value == dut.uio_out.value, f"uo_out != uio_out" 35 | assert int(dut.uo_out.value) == i, f"uio value not incremented correctly {dut.uio_out.value} != {i}" 36 | await ClockCycles(dut.clk, 1) 37 | 38 | 39 | dut._log.info("test_counter passed") 40 | ``` 41 | 42 | will be detected as a test case and run as part of the testbench. You can see the full sample in [tt_um_factory_test.py](tt_um_factory_test/tt_um_factory_test.py). 43 | 44 | To run an existing test, import the module and call its `run()` function: 45 | 46 | ``` 47 | >>> import examples.tt_um_factory_test as test 48 | >>> test.run() 49 | ``` 50 | 51 | ![factory test output](../../images/cocotb-testrun.png) 52 | 53 | # Adding samples 54 | 55 | If you have a project on a chip and would like to run your own tests, you can re-use much of your cocotb testbench (you did have a testbench, right?). 56 | 57 | To start 58 | 59 | * create a package using the name of your project as per the submitted info.yaml. This is a subdirectory of that name with an `__init__.py` file. 60 | 61 | * create modules that include your `@cocotb.test()` functions 62 | 63 | * import those modules and expose a `run()` method in your top level `__init__.py`. 64 | 65 | 66 | 67 | ### cocotb.test() 68 | 69 | The `cocotb.test()` functions should work as-is, with the following caveats 70 | 71 | * We don't have access to internals, so restrict the tests to treating the *dut* as a blackbox, meaning you can only play with I/O (writing to `dut.ui_in` and `dut.uio_in`, reading from `dut.uo_out` and `dut.uio_out`) 72 | 73 | * The bidirectional pin direction is actually set by the design inside the chip. We are outside the ASIC, so you must **mirror** that (i.e. if the project sets a pin as an input, we want to set the corresponding pin as an output to be able to write to it within tests) 74 | 75 | To make the fact that you are setting bidir pin direction from the RP2040 side, the API uses the name `uio_oe_pico`. 76 | 77 | If, for example, the project verilog sets 78 | 79 | ``` 80 | assign uio_oe = 0xf0 /* 0b11110000 high nibble out, low nibble in*/ 81 | ``` 82 | Then, in your test, you would set 83 | 84 | ``` 85 | dut.uio_oe_pico = 0x0f # 0b00001111 high nibble in, low nibble out 86 | ``` 87 | 88 | 89 | ### functionality 90 | 91 | The SDK cocotb implementation supports 92 | 93 | * @cocotb.test() detection, with all optional parameters (as of now name, expect_fail, timeout_* and skip are respected) 94 | 95 | * setting up one or more clocks using Clock() and cocotb.start_soon() 96 | 97 | * get_sim_time() 98 | 99 | * await on Timer, ClockCycles, RisingEdge, and FallingEdge 100 | 101 | 102 | Within tests, you may read 103 | 104 | * dut.uo_out.value 105 | 106 | * dut.uio_out.value 107 | 108 | and write to 109 | 110 | * dut.ui_in.value 111 | 112 | * dut.uio_in.value 113 | 114 | and do the usual things, like 115 | 116 | ``` 117 | assert dut.uo_out.value == 4, f"output should be 4!" 118 | # or 119 | dut.ui_in.value = 0xff 120 | await ClockCycles(dut.clk, 2) 121 | ``` 122 | 123 | In addition, though this is as of yet unsupported in cocotb v2 (I've submitted patches and there's an ongoing discussion), value bit and slice access is fully supported, for example 124 | 125 | ``` 126 | dut.ui_in.value[0] = 1 127 | dut.ui_in.value[3:2] = 0b11 128 | assert dut.uo_out.value[7] == 1, "high bit should be TRUE" 129 | ``` 130 | 131 | 132 | 133 | ### test runner 134 | 135 | This is the area with the greatest delta from standard cocotb functionality. 136 | 137 | We need a function to: 138 | 139 | * load and enable the design 140 | 141 | * create a suitable DUT instance 142 | 143 | * get the test runner and run all the detected cocotb.test()s. 144 | 145 | 146 | ``` 147 | from ttboard.demoboard import DemoBoard 148 | from ttboard.cocotb.dut import DUT 149 | def main(): 150 | tt = DemoBoard.get() 151 | 152 | # make certain this chip has the project 153 | if not tt.shuttle.has('tt_um_factory_test'): 154 | print("This shuttle doesn't have mah project??!!") 155 | return 156 | 157 | # enable the project 158 | tt.shuttle.tt_um_factory_test.enable() 159 | 160 | dut = DUT() 161 | dut._log.info("enabled project, running, running tests") 162 | runner = cocotb.get_runner() 163 | runner.test(dut) 164 | 165 | ``` 166 | 167 | 168 | ### DUT extensions 169 | 170 | If your tests are *only* using the ui_in/uo_out/uio_in/uio_out ports, then the runner above using the default DUT class will just work. 171 | 172 | There are cases where tests are safe, in that they do not access any internals of the design, but you've added convenience functionality or renaming to the verilog tb, and your cocotb tests reflect that. 173 | 174 | For example, my old neptune testbench looks like this in verilog 175 | 176 | 177 | ``` 178 | 179 | // testbench is controlled by test.py 180 | module tb ( 181 | input [2:0] clk_config, 182 | input input_pulse, 183 | input display_single_enable, 184 | input display_single_select, 185 | output [6:0] segments, 186 | output prox_select 187 | ); 188 | 189 | // this part dumps the trace to a vcd file that can be viewed with GTKWave 190 | initial begin 191 | $dumpfile ("tb.vcd"); 192 | $dumpvars (0, tb); 193 | #1; 194 | end 195 | 196 | // wire up the inputs and outputs 197 | reg clk; 198 | reg rst_n; 199 | reg ena; 200 | // reg [7:0] ui_in; 201 | reg [7:0] uio_in; 202 | wire [7:0] uo_out; 203 | wire [7:0] uio_out; 204 | wire [7:0] uio_oe; 205 | 206 | assign prox_select = uo_out[7]; 207 | assign segments = uo_out[6:0]; 208 | 209 | wire [7:0] ui_in = {display_single_select, 210 | display_single_enable, 211 | input_pulse, 212 | clk_config[2], clk_config[1], clk_config[0], 213 | 1'b0,1'b0}; 214 | 215 | /* ... */ 216 | ``` 217 | 218 | and my cocotb tests use the nicely named `input_pulse` (a bit), `clk_config` (3 bits), etc. 219 | 220 | The first option would be to re-write all the cocotb.test() stuff to use only ui_in and such. Yuk. 221 | 222 | Rather than do all that work, and have ugly `tt.ui_in.value[5]` stuff everywhere as a bonus, you can extend the DUT class to add in wrappers to these values. 223 | 224 | To do this, you just derive a new class from `ttboard.cocotb.dut.DUT`, create the attributes using `add_bit_attribute` or `add_slice_attribute` (for things like `tt.ui_in[3:1]`). 225 | 226 | In my neptune case, this looks like: 227 | 228 | ``` 229 | import ttboard.cocotb.dut 230 | 231 | 232 | class DUT(ttboard.cocotb.dut.DUT): 233 | def __init__(self): 234 | super().__init__('Neptune') 235 | self.tt = DemoBoard.get() 236 | # inputs 237 | self.add_bit_attribute('display_single_select', self.tt.ui_in, 7) 238 | self.add_bit_attribute('display_single_enable', self.tt.ui_in, 6) 239 | self.add_bit_attribute('input_pulse', self.tt.ui_in, 5) 240 | self.add_slice_attribute('clk_config', self.tt.ui_in, 4, 2) # tt.ui_in[4:2] 241 | # outputs 242 | self.add_bit_attribute('prox_select', self.tt.uo_out, 7) 243 | self.add_slice_attribute('segments', self.tt.uo_out, 6, 0) # tt.uo_out[6:0] 244 | 245 | ```` 246 | 247 | After instantiation, the DUT object will now have all the requisite attributes, e.g. `dut.segments`, `dut.clk_config` etc. 248 | 249 | Using that class to construct my dut, things like 250 | 251 | ``` 252 | 253 | pulseClock = Clock(dut.input_pulse, 1000*(1.0/tunerInputFreqHz), units='ms') 254 | cocotb.start_soon(pulseClock.start()) 255 | # or 256 | val = int(dut.segments.value) << 1 257 | ``` 258 | 259 | will justwork(tm) in the tests. 260 | 261 | 262 | -------------------------------------------------------------------------------- /src/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/examples/__init__.py -------------------------------------------------------------------------------- /src/examples/basic/__init__.py: -------------------------------------------------------------------------------- 1 | from .tb import run 2 | 3 | -------------------------------------------------------------------------------- /src/examples/basic/tb.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Dec 6, 2024 3 | 4 | A basic set of samples to get going with 5 | cocotb tests on TT demoboards. 6 | 7 | You need: 8 | * a few @cocotb.test() functions 9 | * a little bit of setup, and to call the runner 10 | 11 | The @cocotb.test() functions are pretty regular. 12 | 13 | The run() function is where more TT-specific stuff happens. 14 | Check at the bottom. 15 | 16 | 17 | @see: ttboard.cocotb.dut for the base class you 18 | can override for custom DUTs (has utility methods 19 | and nifty callbacks) 20 | 21 | @author: Pat Deegan 22 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 23 | ''' 24 | 25 | import microcotb as cocotb 26 | from microcotb.clock import Clock 27 | from microcotb.triggers import RisingEdge, FallingEdge, ClockCycles, Timer 28 | from microcotb.utils import get_sim_time 29 | 30 | # get the detected @cocotb tests into a namespace 31 | # so we can load multiple such modules 32 | cocotb.set_runner_scope(__name__) 33 | 34 | from ttboard.demoboard import DemoBoard, RPMode 35 | from ttboard.cocotb.dut import DUT 36 | 37 | # utility method, called by actual tests 38 | async def do_reset(dut:DUT, num_cycles:int=10): 39 | # Reset 40 | dut._log.info("Reset") 41 | dut.ena.value = 1 42 | 43 | dut.uio_oe_pico.value = 0 # all inputs on RP2 side 44 | dut.ui_in.value = 0 45 | dut.rst_n.value = 0 46 | await ClockCycles(dut.clk, num_cycles) 47 | dut.rst_n.value = 1 48 | 49 | 50 | 51 | # all tests are detected with @cocotb.test(): 52 | @cocotb.test() 53 | async def test_timer(dut:DUT): 54 | 55 | # start up a clock, on the clk signal 56 | clock = Clock(dut.clk, 10, units="us") 57 | cocotb.start_soon(clock.start()) 58 | 59 | await do_reset(dut) # always the same, so in its own async function 60 | 61 | await Timer(120, 'us') 62 | 63 | assert dut.rst_n.value == 1, "rst_n should be HIGH" 64 | 65 | 66 | 67 | 68 | @cocotb.test() 69 | async def test_clockcycles(dut:DUT): 70 | 71 | # start up a clock, on the clk signal 72 | clock = Clock(dut.clk, 10, units="us") 73 | cocotb.start_soon(clock.start()) 74 | 75 | await do_reset(dut) # always the same, so in its own async function 76 | 77 | for i in range(256): 78 | dut.ui_in.value = i 79 | await ClockCycles(dut.clk, 5) 80 | 81 | assert dut.ui_in.value == i, "ui_in should be our last i" 82 | 83 | 84 | dut._log.info(f"Total sim 'runtime': {get_sim_time('ms')}ms") 85 | 86 | @cocotb.test() 87 | async def test_multiclocks(dut:DUT): 88 | # start up a clock, on the clk signal 89 | clock = Clock(dut.clk, 10, units="us") 90 | cocotb.start_soon(clock.start()) 91 | 92 | # startup another clock, on our aliased bit "signal" (ui_in[0]) 93 | clock = Clock(dut.input_pulse, 1, units="ms") 94 | cocotb.start_soon(clock.start()) 95 | 96 | await do_reset(dut) # always the same, so in its own async function 97 | 98 | # wait for it to go up and down a few times 99 | for i in range(5): 100 | await RisingEdge(dut.input_pulse) 101 | dut._log.info(f"on {i} uio_in is: {int(dut.uio_in.value)}") 102 | await FallingEdge(dut.input_pulse) 103 | dut._log.info(f"off") 104 | 105 | keepWaiting = True 106 | while keepWaiting: 107 | # we want to have just started counting up 108 | await RisingEdge(dut.input_pulse) 109 | 110 | # and we want to make sure the value is small 111 | # enough that we won't wrap around 112 | if dut.uo_out.value < 100: 113 | keepWaiting = False 114 | 115 | # ok, we're good... capture the value of uo_out 116 | # right now 117 | out_val = dut.uo_out.value 118 | 119 | # and clock the project a few times to see it increment 120 | for i in range(10): 121 | assert dut.uo_out.value == (out_val + i), "out val increments" 122 | dut._log.info(f"uo_out is now {(out_val + i)}") 123 | await ClockCycles(dut.clk, 1) 124 | 125 | 126 | 127 | def run(): 128 | 129 | # get the demoboard singleton 130 | tt = DemoBoard.get() 131 | # We are testing a project, check it's on 132 | # this board 133 | if not tt.shuttle.has('tt_um_factory_test'): 134 | print("My project's not here!") 135 | return False 136 | 137 | # enable it 138 | tt.shuttle.tt_um_factory_test.enable() 139 | 140 | # we want to be able to control the I/O 141 | # set the mode 142 | if tt.mode != RPMode.ASIC_RP_CONTROL: 143 | print("Setting mode to ASIC_RP_CONTROL") 144 | tt.mode = RPMode.ASIC_RP_CONTROL 145 | 146 | # bidirs all inputs 147 | tt.uio_oe_pico.value = 0 # all inputs 148 | 149 | # get a runner 150 | runner = cocotb.get_runner(__name__) 151 | 152 | # here's our DUT... you could subclass this and 153 | # do cool things, like rename signals or access 154 | # bits and slices 155 | dut = DUT() # basic TT DUT, with dut._log, dut.ui_in, etc 156 | 157 | # say we want to treat a single bit, ui_in[0], as a signal, 158 | # to use as a named input, so we can do 159 | # dut.input_pulse.value = 1, or use it as a clock... easy: 160 | dut.add_bit_attribute('input_pulse', tt.ui_in, 0) 161 | # now dut.input_pulse can be used like any other dut wire 162 | # there's also an add_slice_attribute to access chunks[4:2] 163 | # by name. 164 | 165 | # run all the @cocotb.test() 166 | runner.test(dut) 167 | -------------------------------------------------------------------------------- /src/examples/tt_um_factory_test/__init__.py: -------------------------------------------------------------------------------- 1 | from .tt_um_factory_test import main as run -------------------------------------------------------------------------------- /src/examples/tt_um_factory_test/tt_um_factory_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 22, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | import gc 9 | from ttboard.demoboard import DemoBoard 10 | from ttboard.mode import RPMode 11 | from microcotb.clock import Clock 12 | from microcotb.triggers import RisingEdge, FallingEdge, ClockCycles, Timer 13 | from microcotb.time.value import TimeValue 14 | import microcotb as cocotb 15 | from microcotb.utils import get_sim_time 16 | gc.collect() 17 | 18 | 19 | # get the detected @cocotb tests into a namespace 20 | # so we can load multiple such modules 21 | cocotb.set_runner_scope(__name__) 22 | 23 | @cocotb.test() 24 | async def test_loopback(dut): 25 | dut._log.info("Start") 26 | 27 | clock = Clock(dut.clk, 10, units="us") 28 | cocotb.start_soon(clock.start()) 29 | 30 | # Reset 31 | dut._log.info("Reset") 32 | dut.ena.value = 1 33 | 34 | # ui_in[0] == 0: Copy bidirectional pins to outputs 35 | dut.uio_oe_pico.value = 0xff # all outputs from us 36 | dut.ui_in.value = 0b0 37 | dut.uio_in.value = 0 38 | dut.rst_n.value = 0 39 | await ClockCycles(dut.clk, 10) 40 | dut.rst_n.value = 1 41 | 42 | for i in range(256): 43 | dut.uio_in.value = i 44 | await ClockCycles(dut.clk, 1) 45 | assert dut.uo_out.value == i, f"uio value unstable {dut.uio_out.value} != {i}" 46 | 47 | dut._log.info("test_loopback passed") 48 | 49 | 50 | @cocotb.test(timeout_time=100, timeout_unit='us', expect_fail=True) 51 | @cocotb.parametrize( 52 | clk_period=[10,125], 53 | timer_t=[101, 200]) 54 | async def test_timeout(dut, clk_period:int, timer_t:int): 55 | clock = Clock(dut.clk, clk_period, units="us") 56 | cocotb.start_soon(clock.start()) 57 | # will timeout before the timer expires, hence expect_fail=True above 58 | await Timer(timer_t, 'us') 59 | 60 | @cocotb.test(expect_fail=True) 61 | async def test_should_fail(dut): 62 | 63 | dut._log.info("Will fail with msg") 64 | 65 | assert dut.rst_n.value == 0, f"rst_n ({dut.rst_n.value}) == 0" 66 | 67 | 68 | 69 | 70 | 71 | 72 | @cocotb.test() 73 | async def test_counter(dut): 74 | dut._log.info("Start") 75 | 76 | clock = Clock(dut.clk, 10, units="us") 77 | cocotb.start_soon(clock.start()) 78 | dut.uio_oe_pico.value = 0 # all inputs on our side 79 | 80 | dut.ui_in.value = 0b1 81 | dut.rst_n.value = 0 82 | await ClockCycles(dut.clk, 10) 83 | dut.rst_n.value = 1 84 | await ClockCycles(dut.clk, 1) 85 | 86 | dut._log.info("Testing counter") 87 | for i in range(256): 88 | assert dut.uo_out.value == dut.uio_out.value, f"uo_out != uio_out" 89 | assert int(dut.uo_out.value) == i, f"uio value not incremented correctly {dut.uio_out.value} != {i}" 90 | await ClockCycles(dut.clk, 1) 91 | 92 | 93 | dut._log.info("test_counter passed") 94 | 95 | @cocotb.test() 96 | async def test_edge_triggers(dut): 97 | dut._log.info("Start") 98 | 99 | clock = Clock(dut.clk, 10, units="us") 100 | cocotb.start_soon(clock.start()) 101 | dut.uio_oe_pico.value = 0 # all inputs on our side 102 | 103 | dut.ui_in.value = 0b1 104 | dut.rst_n.value = 0 105 | await ClockCycles(dut.clk, 10) 106 | dut.rst_n.value = 1 107 | await ClockCycles(dut.clk, 1) 108 | 109 | dut._log.info(f"Testing counter, waiting on rising edge of bit 5 at {get_sim_time('us')}us") 110 | await RisingEdge(dut.some_bit) 111 | dut._log.info(f"Got rising edge, now {get_sim_time('us')}us value is {hex(dut.uo_out.value)}") 112 | 113 | dut._log.info(f"Now await falling edge") 114 | await FallingEdge(dut.some_bit) 115 | dut._log.info(f"Got rising edge, now {get_sim_time('us')}us value is {hex(dut.uo_out.value)}") 116 | 117 | dut._log.info("test_edge_triggers passed") 118 | 119 | 120 | 121 | @cocotb.test(skip=True) 122 | async def test_will_skip(dut): 123 | dut._log.info("This should not be output!") 124 | 125 | 126 | def main(): 127 | import ttboard.cocotb.dut 128 | 129 | class DUT(ttboard.cocotb.dut.DUT): 130 | def __init__(self): 131 | super().__init__('FactoryTest') 132 | self.tt = DemoBoard.get() 133 | # inputs 134 | self.add_bit_attribute('some_bit', self.tt.uo_out, 5) 135 | 136 | tt = DemoBoard.get() 137 | tt.shuttle.tt_um_factory_test.enable() 138 | 139 | if tt.mode != RPMode.ASIC_RP_CONTROL: 140 | print("Setting mode to ASIC_RP_CONTROL") 141 | tt.mode = RPMode.ASIC_RP_CONTROL 142 | 143 | tt.uio_oe_pico.value = 0 # all inputs 144 | 145 | 146 | TimeValue.ReBaseStringUnits = True # I like pretty strings 147 | 148 | 149 | runner = cocotb.get_runner(__name__) 150 | 151 | dut = DUT() 152 | dut._log.info(f"enabled factory test project. Will test with {runner}") 153 | 154 | runner.test(dut) -------------------------------------------------------------------------------- /src/examples/tt_um_psychogenic_neptuneproportional/__init__.py: -------------------------------------------------------------------------------- 1 | from .tt_um_psychogenic_neptuneproportional import main as run -------------------------------------------------------------------------------- /src/examples/tt_um_psychogenic_neptuneproportional/tt_um_psychogenic_neptuneproportional.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 21, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | import microcotb as cocotb 8 | from microcotb.clock import Clock 9 | from microcotb.triggers import Timer, ClockCycles # RisingEdge, FallingEdge, Timer, ClockCycles 10 | 11 | 12 | # get the detected @cocotb tests into a namespace 13 | # so we can load multiple such modules 14 | cocotb.set_runner_scope(__name__) 15 | 16 | from ttboard.demoboard import DemoBoard, RPMode 17 | 18 | displayNotes = { 19 | 'NA': 0b00000010, # - 20 | 'A': 0b11101110, # A 21 | 'B': 0b00111110, # b 22 | 'C': 0b10011100, # C 23 | 'D': 0b01111010, # d 24 | 'E': 0b10011110, # E 25 | 'F': 0b10001110, # F 26 | 'G': 0b11110110, # g 27 | } 28 | 29 | displayProx = { 30 | 'lowfar': 0b00111000, 31 | 'lowclose': 0b00101010, 32 | 'exact': 0b00000001, 33 | 'hiclose': 0b01000110, 34 | 'hifar': 0b11000100 35 | 36 | } 37 | 38 | SegmentMask = 0xFF 39 | ProxSegMask = 0xFE 40 | 41 | 42 | 43 | @cocotb.test() 44 | async def note_a_exact(dut): 45 | dispValues = await note_a(dut, delta=0, msg="A exact") 46 | 47 | target_value = (displayProx['exact'] & ProxSegMask) 48 | assert dispValues[0] == target_value, f"exact fail {dispValues[0]} != {target_value}" 49 | dut._log.info("Note A full pass") 50 | 51 | 52 | 53 | 54 | @cocotb.test(skip=False) 55 | async def note_e_highfar(dut): 56 | dispValues = await note_e(dut, eFreq=330, delta=12, msg="little E high/far") 57 | target_value = (displayProx['hifar'] & ProxSegMask) 58 | assert dispValues[0] == target_value, f"high/far fail {dispValues[0]} != {target_value}" 59 | dut._log.info("Note E full pass") 60 | 61 | 62 | @cocotb.test(skip=False) 63 | async def note_g_highclose(dut): 64 | dispValues = await note_g(dut, delta=3, msg="High/close") 65 | target_value = (displayProx['hiclose'] & ProxSegMask) 66 | assert dispValues[0] == target_value, f"High/close fail {dispValues[0]} != {target_value}" 67 | dut._log.info("Note G full pass") 68 | 69 | 70 | 71 | async def reset(dut): 72 | dut._log.info(f"reset(dut)") 73 | dut.display_single_enable.value = 0 74 | dut.display_single_select.value = 0 75 | dut.input_pulse.value = 0 76 | dut.rst_n.value = 0 77 | dut.clk_config.value = 1 # 2khz clock 78 | dut._log.debug("hold in reset") 79 | dut.rst_n.value = 0 80 | await ClockCycles(dut.clk, 5) 81 | dut.rst_n.value = 1 82 | await ClockCycles(dut.clk, 1) 83 | dut._log.info("reset done") 84 | 85 | 86 | async def startup(dut): 87 | dut._log.info("starting clock") 88 | clock = Clock(dut.clk, 500, units="us") 89 | cocotb.start_soon(clock.start()) 90 | await reset(dut) 91 | 92 | async def getDisplayValues(dut): 93 | displayedValues = [None, None] 94 | attemptCount = 0 95 | while None in displayedValues or attemptCount < 3: 96 | displayedValues[int(dut.prox_select.value)] = int(dut.segments.value) << 1 97 | 98 | await ClockCycles(dut.clk, 1) 99 | 100 | attemptCount += 1 101 | if attemptCount > 100: 102 | dut._log.error(f"NEVER HAVE {displayedValues}") 103 | return displayedValues 104 | 105 | # dut._log.info(f'Display Segments: {displayedValues} ( [ {bin(displayedValues[0])} , {bin(displayedValues[1])}])') 106 | return displayedValues 107 | 108 | async def inputPulsesFor(dut, tunerInputFreqHz:int, inputTimeSecs=0.51): 109 | 110 | pulseClock = Clock(dut.input_pulse, 1000.0*(1.0/tunerInputFreqHz), units='ms') 111 | cocotb.start_soon(pulseClock.start()) 112 | await Timer(inputTimeSecs, 'sec') 113 | dispV = await getDisplayValues(dut) 114 | 115 | return dispV 116 | 117 | 118 | 119 | async def setup_tuner(dut): 120 | dut._log.info("start") 121 | await startup(dut) 122 | 123 | 124 | async def note_toggle(dut, freq, delta=0, msg="", toggleTime=0.58): 125 | dut._log.info(msg) 126 | await startup(dut) 127 | dispValues = await inputPulsesFor(dut, freq + delta, toggleTime) 128 | return dispValues 129 | 130 | 131 | 132 | async def note_e(dut, eFreq=330, delta=0, msg=""): 133 | dut._log.info(f"E @ {eFreq} delta {delta}") 134 | dispValues = await note_toggle(dut, freq=eFreq, delta=delta, msg=msg); 135 | note_target = (displayNotes['E'] & SegmentMask) 136 | assert dispValues[1] == note_target, f"Note E FAIL: {dispValues[1]} != {note_target}" 137 | dut._log.info(f"Note E @ {eFreq} pass ({bin(dispValues[1])})") 138 | return dispValues 139 | 140 | 141 | 142 | 143 | async def note_g(dut, delta=0, msg=""): 144 | gFreq = 196 145 | 146 | dut._log.info(f"G delta {delta}") 147 | dispValues = await note_toggle(dut, freq=gFreq, delta=delta, msg=msg); 148 | 149 | note_target = (displayNotes['G'] & SegmentMask) 150 | assert dispValues[1] == note_target, f"Note G FAIL: {dispValues[1]} != {note_target}" 151 | dut._log.info(f"Note G: PASS ({bin(dispValues[1])})") 152 | return dispValues 153 | 154 | 155 | async def note_a(dut, delta=0, msg=""): 156 | aFreq = 110 157 | 158 | dut._log.info(f"A delta {delta}") 159 | dispValues = await note_toggle(dut, freq=aFreq, delta=delta, msg=msg); 160 | 161 | note_target = (displayNotes['A'] & SegmentMask) 162 | assert dispValues[1] == note_target, f"Note A FAIL: {dispValues[1]} != {note_target}" 163 | dut._log.info(f"Note A pass ({bin(dispValues[1])})") 164 | return dispValues 165 | 166 | 167 | 168 | 169 | ### DUT class override, so I can get nicely-named aliases 170 | ### that match my verilog testbench 171 | import ttboard.cocotb.dut 172 | class DUT(ttboard.cocotb.dut.DUT): 173 | def __init__(self): 174 | super().__init__('Neptune') 175 | 176 | # inputs 177 | self.add_bit_attribute('display_single_select', 178 | self.tt.ui_in, 7) 179 | self.add_bit_attribute('display_single_enable', 180 | self.tt.ui_in, 6) 181 | self.add_bit_attribute('input_pulse', 182 | self.tt.ui_in, 5) 183 | # tt.ui_in[4:2] 184 | self.add_slice_attribute('clk_config', 185 | self.tt.ui_in, 4, 2) 186 | # outputs 187 | self.add_bit_attribute('prox_select', self.tt.uo_out, 7) 188 | # tt.uo_out[6:0] 189 | self.add_slice_attribute('segments', self.tt.uo_out, 6, 0) 190 | 191 | 192 | def main(): 193 | from microcotb.time.value import TimeValue 194 | 195 | tt = DemoBoard.get() 196 | tt.shuttle.tt_um_psychogenic_neptuneproportional.enable() 197 | 198 | if tt.mode != RPMode.ASIC_RP_CONTROL: 199 | print("Setting mode to ASIC_RP_CONTROL") 200 | tt.mode = RPMode.ASIC_RP_CONTROL 201 | 202 | # I'll spend the cycles to get pretty timestamps 203 | TimeValue.ReBaseStringUnits = True 204 | 205 | # create runner and DUT, and get tests going 206 | runner = cocotb.get_runner(__name__) 207 | dut = DUT() 208 | dut._log.info(f"enabled neptune project, will test with {runner}") 209 | runner.test(dut) 210 | 211 | 212 | if __name__ == '__main__': 213 | main() 214 | -------------------------------------------------------------------------------- /src/examples/tt_um_psychogenic_shaman/__init__.py: -------------------------------------------------------------------------------- 1 | from .tb import main as run 2 | -------------------------------------------------------------------------------- /src/examples/tt_um_psychogenic_shaman/shaman.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Apr 28, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.demoboard import DemoBoard 8 | from examples.tt_um_psychogenic_shaman.util import wait_clocks 9 | 10 | import ttboard.log as logging 11 | log = logging.getLogger(__name__) 12 | 13 | class Shaman: 14 | ''' 15 | A little wrapper to use the TT pins in a way 16 | that makes sense with this project 17 | 18 | ''' 19 | def __init__(self, tt:DemoBoard): 20 | self.tt = tt 21 | 22 | @property 23 | def data(self): 24 | return self.tt.ui_in.value 25 | 26 | @data.setter 27 | def data(self, set_to:int): 28 | self.tt.ui_in.value = set_to 29 | 30 | 31 | 32 | @property 33 | def result(self): 34 | return self.tt.uo_out.value 35 | 36 | 37 | @property 38 | def result_ready(self): 39 | # bidir bit 0 40 | return self.tt.uio_out[0] 41 | 42 | @property 43 | def begin_processing(self): 44 | # bidir bit 1 45 | return self.tt.uio_out[1] 46 | 47 | @property 48 | def parallel_load(self): 49 | return self.uio_in[2] 50 | 51 | @parallel_load.setter 52 | def parallel_load(self, set_to:int): 53 | self.tt.uio_in[2] = set_to 54 | 55 | @property 56 | def result_next(self): 57 | return self.tt.uio_in[3] 58 | 59 | @result_next.setter 60 | def result_next(self, set_to:int): 61 | self.tt.uio_in[3] = set_to 62 | 63 | @property 64 | def busy(self): 65 | return self.tt.uio_out[4] 66 | 67 | @property 68 | def processing(self): 69 | return self.tt.uio_out[5] 70 | 71 | @property 72 | def start(self): 73 | return self.tt.uio_in[6] 74 | 75 | @start.setter 76 | def start(self, set_to:int): 77 | self.tt.uio_in[6] = set_to 78 | 79 | @property 80 | def data_clock(self): 81 | return self.tt.uio_in[7] 82 | 83 | @data_clock.setter 84 | def data_clock(self, set_to:int): 85 | self.tt.uio_in[7] = set_to 86 | 87 | def clock_in_data(self, data_byte:int): 88 | self.data = data_byte 89 | self.data_clock = 1 90 | wait_clocks(1) 91 | self.data_clock = 0 92 | wait_clocks(1) 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/examples/tt_um_rejunity_sn76489/__init__.py: -------------------------------------------------------------------------------- 1 | from .tt_um_rejunity_sn76489 import main as run -------------------------------------------------------------------------------- /src/examples/tt_um_rgbled_decoder/__init__.py: -------------------------------------------------------------------------------- 1 | from .tt_um_rgbled_decoder import main as run -------------------------------------------------------------------------------- /src/examples/tt_um_rgbled_decoder/tt_um_rgbled_decoder.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 23, 2024 3 | 4 | Adaptation of Andreas Scharnreitner's testbench to run on 5 | TT demoboard with SDK 6 | @author: Pat Deegan 7 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 8 | ''' 9 | 10 | # Copyright 2023 Andreas Scharnreitner 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); 13 | # you may not use this file except in compliance with the License. 14 | # You may obtain a copy of the License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the License is distributed on an "AS IS" BASIS, 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | # See the License for the specific language governing permissions and 22 | # limitations under the License. 23 | 24 | import microcotb as cocotb 25 | from microcotb.clock import Clock 26 | from microcotb.triggers import RisingEdge, FallingEdge, Timer, ClockCycles 27 | from microcotb.utils import get_sim_time 28 | 29 | # get the detected @cocotb tests into a namespace 30 | # so we can load multiple such modules 31 | cocotb.set_runner_scope(__name__) 32 | 33 | 34 | def time_delta_not(cond:str): 35 | return f'sim time delta not {cond}' 36 | 37 | @cocotb.test() 38 | async def test_spi(dut): 39 | dut._log.info("Start SPI test") 40 | clock = Clock(dut.tbspi.sclk, 10, units="us") 41 | cocotb.start_soon(clock.start()) 42 | 43 | #setup 44 | dut.tbspi.nsel.value = 1 45 | dut.tbspi.mosi.value = 0 46 | 47 | # reset 48 | dut._log.info("Reset SPI") 49 | dut.tbspi.nreset.value = 0 50 | await ClockCycles(dut.tbspi.sclk, 5) 51 | dut.tbspi.nreset.value = 1 52 | await ClockCycles(dut.tbspi.sclk, 5) 53 | 54 | #after reset the data should be 0 55 | assert dut.tbspi.data.value == 0 56 | #without nsel the data_rdy should be 1 (ready) 57 | assert dut.tbspi.data_rdy.value == 1, "not ready" 58 | 59 | await ClockCycles(dut.tbspi.sclk, 10) 60 | 61 | await FallingEdge(dut.tbspi.sclk) 62 | dut.tbspi.nsel.value = 0 63 | await FallingEdge(dut.tbspi.data_rdy) 64 | dut._log.info("SPI: Writing 0xAA") 65 | for i in range(8): 66 | dut.tbspi.mosi.value = i%2 67 | await ClockCycles(dut.tbspi.sclk, 1) 68 | 69 | dut.tbspi.nsel.value = 1 70 | await RisingEdge(dut.tbspi.data_rdy) 71 | 72 | assert dut.tbspi.data.value == 0xAA 73 | 74 | await ClockCycles(dut.tbspi.sclk, 10) 75 | 76 | await FallingEdge(dut.tbspi.sclk) 77 | dut.tbspi.nsel.value = 0 78 | await FallingEdge(dut.tbspi.data_rdy) 79 | dut._log.info("SPI: Writing 0xB3") 80 | for i in range(8): 81 | dut.tbspi.mosi.value = (0xB3 >> i)&1 82 | await ClockCycles(dut.tbspi.sclk, 1) 83 | 84 | dut.tbspi.nsel.value = 1 85 | await RisingEdge(dut.tbspi.data_rdy) 86 | 87 | assert dut.tbspi.data.value == 0xB3 88 | 89 | await ClockCycles(dut.tbspi.sclk, 10) 90 | 91 | @cocotb.test() 92 | async def test_rgbled(dut): 93 | dut._log.info("Start RGBLED test") 94 | clock = Clock(dut.tbrgbled.clk, 40, units="ns") 95 | cocotb.start_soon(clock.start()) 96 | 97 | #setup 98 | dut.tbrgbled.data_rdy.value = 0 99 | 100 | dut._log.info("Reset RGBLED") 101 | dut.tbrgbled.nreset.value = 0 102 | await ClockCycles(dut.tbrgbled.clk, 5) 103 | dut.tbrgbled.nreset.value = 1 104 | await ClockCycles(dut.tbrgbled.clk, 5) 105 | 106 | dut._log.info("RGBLED Output Test") 107 | dut.tbrgbled.data.value = 0x112233445566AA00FF 108 | dut.tbrgbled.data_rdy.value = 1 109 | 110 | await RisingEdge(dut.tbrgbled.data_rdy) 111 | 112 | tim_start = get_sim_time('us') 113 | 114 | await RisingEdge(dut.tbrgbled.led) 115 | 116 | assert (get_sim_time('us') - tim_start) > 50, time_delta_not("> 50") 117 | tim_start = get_sim_time('ns') 118 | 119 | await FallingEdge(dut.tbrgbled.led) 120 | 121 | tim_mid = get_sim_time('ns') 122 | assert (tim_mid - tim_start) > 650, time_delta_not("> 650") 123 | assert (tim_mid - tim_start) < 950, time_delta_not("< 950") 124 | 125 | await RisingEdge(dut.tbrgbled.led) 126 | 127 | assert (get_sim_time('ns') - tim_mid) > 300, time_delta_not("> 300") 128 | assert (get_sim_time('ns') - tim_mid) < 600, time_delta_not("< 600") 129 | 130 | assert (get_sim_time('ns') - tim_start) > 650, time_delta_not("> 650") 131 | assert (get_sim_time('ns') - tim_start) < 1850, time_delta_not("< 1850") 132 | 133 | for i in range(8): 134 | await RisingEdge(dut.tbrgbled.led) 135 | 136 | tim_start = get_sim_time('ns') 137 | 138 | await FallingEdge(dut.tbrgbled.led) 139 | 140 | tim_mid = get_sim_time('ns') 141 | assert (tim_mid - tim_start) > 250, time_delta_not("> 250") 142 | assert (tim_mid - tim_start) < 550, time_delta_not("< 550") 143 | 144 | await RisingEdge(dut.tbrgbled.led) 145 | 146 | assert (get_sim_time('ns') - tim_mid) > 700, time_delta_not("> 700") 147 | assert (get_sim_time('ns') - tim_mid) < 1000, time_delta_not("< 1000") 148 | 149 | assert (get_sim_time('ns') - tim_start) > 650, time_delta_not("> 650") 150 | assert (get_sim_time('ns') - tim_start) < 1850, time_delta_not("< 1850") 151 | 152 | # problem here: waiting on internal signal dut.tbrgbled.rgbled_dut.do_res 153 | # await RisingEdge(dut.tbrgbled.rgbled_dut.do_res) 154 | # 155 | # tim_start = get_sim_time('us') 156 | # 157 | # await ClockCycles(dut.tbrgbled.clk, 10) 158 | # await RisingEdge(dut.tbrgbled.led) 159 | # 160 | # assert (get_sim_time('us') - tim_start) > 50 161 | # 162 | # await ClockCycles(dut.tbrgbled.clk, 10) 163 | 164 | # simplified version 165 | tim_start = get_sim_time('us') 166 | await ClockCycles(dut.tbrgbled.clk, 10) 167 | await RisingEdge(dut.tbrgbled.led) 168 | assert (get_sim_time('us') - tim_start) > 50, time_delta_not("> 50") 169 | 170 | await ClockCycles(dut.tbrgbled.clk, 10) 171 | 172 | 173 | 174 | 175 | 176 | from ttboard.demoboard import DemoBoard, RPMode 177 | import ttboard.cocotb.dut as basedut 178 | from microcotb.dut import Wire 179 | 180 | class RGBLED(basedut.DUT): 181 | def __init__(self, data:Wire, data_rdy:Wire): 182 | super().__init__('RGBLED') 183 | self.data = data 184 | self.data_rdy = data_rdy 185 | self.led = self.new_slice_attribute(self.tt.uo_out, 0) 186 | self.nreset = self.rst_n 187 | 188 | class TBSPI(basedut.DUT): 189 | 190 | def __init__(self, data:Wire, data_rdy:Wire): 191 | super().__init__('SPI') 192 | self.data = data 193 | self.data_rdy = data_rdy 194 | self.nreset = self.rst_n 195 | self.add_bit_attribute('mosi', self.tt.ui_in, 0) 196 | self.add_bit_attribute('sclk', self.tt.ui_in, 1) 197 | self.add_bit_attribute('nsel', self.tt.ui_in, 2) 198 | 199 | class DUT(basedut.DUT): 200 | def __init__(self): 201 | super().__init__('RGBDUT') 202 | self.data = Wire() 203 | self.add_bit_attribute('data_rdy', self.tt.ui_in, 2) 204 | self.tbrgbled = RGBLED(self.data, self.data_rdy) 205 | self.tbspi = TBSPI(self.data, self.data_rdy) 206 | 207 | 208 | def load_project(tt:DemoBoard): 209 | 210 | if not tt.shuttle.has('tt_um_rgbled_decoder'): 211 | print("No tt_um_rgbled_decoder available in shuttle?") 212 | return False 213 | 214 | tt.shuttle.tt_um_rgbled_decoder.enable() 215 | 216 | if tt.mode != RPMode.ASIC_RP_CONTROL: 217 | print("Setting mode to ASIC_RP_CONTROL") 218 | tt.mode = RPMode.ASIC_RP_CONTROL 219 | 220 | return True 221 | 222 | def main(): 223 | tt = DemoBoard.get() 224 | 225 | if not load_project(tt): 226 | return 227 | 228 | tt.uio_oe_pico.value = 0 # all inputs 229 | 230 | 231 | dut = DUT() 232 | dut._log.info("enabled rgbled project, running") 233 | runner = cocotb.get_runner(__name__) 234 | runner.test(dut) 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 9, 2024 3 | 4 | Code here, in main.py, runs on every power-up. 5 | 6 | You can put anything you like in here, including any utility functions 7 | you might want to have access to when connecting to the REPL. 8 | 9 | If you want to use the SDK, all 10 | you really need is something like 11 | 12 | tt = DemoBoard() 13 | 14 | Then you can 15 | # enable test project 16 | tt.shuttle.tt_um_factory_test.enable() 17 | 18 | and play with i/o as desired. 19 | 20 | @author: Pat Deegan 21 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 22 | ''' 23 | print("BOOT: Tiny Tapeout SDK") 24 | import gc 25 | 26 | # stash the current value for garbage 27 | # collection threshold (is -1/when full, by default) 28 | GCThreshold = gc.threshold() 29 | # start very aggressive, to keep thing defragged 30 | # as we read in ini and JSON files, etc 31 | gc.threshold(80000) 32 | 33 | import ttboard.log as logging 34 | # logging.ticksStart() # start-up tick delta counter 35 | 36 | logging.basicConfig(level=logging.DEBUG, filename='boot.log') 37 | 38 | 39 | import micropython 40 | from ttboard.boot.demoboard_detect import DemoboardDetect 41 | from ttboard.demoboard import DemoBoard 42 | import ttboard.util.colors as colors 43 | 44 | # logging.dumpTicksMsDelta('import') 45 | 46 | gc.collect() 47 | 48 | tt = None 49 | def startup(): 50 | # construct DemoBoard 51 | # either pass an appropriate RPMode, e.g. RPMode.ASIC_RP_CONTROL 52 | # or have "mode = ASIC_RP_CONTROL" in ini DEFAULT section 53 | ttdemoboard = DemoBoard() 54 | print("\n\n") 55 | print(f"The '{colors.color('tt', 'red')}' object is available.") 56 | print() 57 | print(f"Projects may be enabled with {colors.bold('tt.shuttle.PROJECT_NAME.enable()')}, e.g.") 58 | print("tt.shuttle.tt_um_urish_simon.enable()") 59 | print() 60 | print(f"The io ports are named as in Verilog, {colors.bold('tt.ui_in')}, {colors.bold('tt.uo_out')}...") 61 | print(f"and behave as with cocotb, e.g. {colors.bold('tt.uo_out.value = 0xAA')} or {colors.bold('print(tt.ui_in.value)')}") 62 | print(f"Bits may be accessed by index, e.g. {colors.bold('tt.uo_out[7]')} (note: that's the {colors.color('high bit!', 'red')}) to read or {colors.bold('tt.ui_in[5] = 1')} to write.") 63 | print(f"Direction of the bidir pins is set using {colors.bold('tt.uio_oe_pico')}, used in the same manner as the io ports themselves.") 64 | print("\n") 65 | print(f"{colors.color('TT SDK v' + ttdemoboard.version, 'cyan')}") 66 | print("\n\n") 67 | gc.collect() 68 | return ttdemoboard 69 | 70 | def autoClockProject(freqHz:int): 71 | tt.clock_project_PWM(freqHz) 72 | 73 | def stopClocking(): 74 | tt.clock_project_stop() 75 | 76 | 77 | # Detect the demoboard version 78 | detection_result = '(best guess)' 79 | detection_color = 'red' 80 | if DemoboardDetect.probe(): 81 | # detection was conclusive 82 | detection_result = '' 83 | detection_color = 'cyan' 84 | detection_message = 'Detected ' + DemoboardDetect.PCB_str() + ' demoboard ' + detection_result 85 | print(f"{colors.color(detection_message, detection_color)}") 86 | 87 | 88 | tt = startup() 89 | 90 | 91 | logging.basicConfig(filename=None) 92 | gc.collect() 93 | colors.color_start('magenta', False) 94 | print("Mem info") 95 | micropython.mem_info() 96 | colors.color_end() 97 | 98 | 99 | print(tt) 100 | print() 101 | 102 | logging.dumpTicksMsDelta('boot done') 103 | print(f"tt.sdk_version={tt.version}") 104 | # end by being so aggressive on collection 105 | gc.threshold(GCThreshold) 106 | 107 | # to run tests easily import a module of interest, as below, and then 108 | # run() it 109 | 110 | 111 | def run_testbench_basic(): 112 | import microcotb 113 | import examples.basic as test 114 | test.run() 115 | return test 116 | 117 | def run_testbench_factorytest(): 118 | import microcotb 119 | import examples.tt_um_factory_test as test 120 | test.run() 121 | return test 122 | 123 | def run_testbench_neptune(): 124 | import microcotb 125 | import examples.tt_um_psychogenic_neptuneproportional as test 126 | test.run() 127 | return test 128 | 129 | # run_testbench_factorytest() 130 | # or 131 | # import examples.tt_um_psychogenic_shaman as test 132 | # import examples.tt_um_rejunity_sn76489 as test 133 | # import examples.tt_um_factory_test as test 134 | # import examples.tt_um_psychogenic_neptuneproportional as test 135 | # dut = test.tt_um_psychogenic_neptuneproportional.DUT() 136 | # import examples.tt_um_rgbled_decoder as test 137 | # test.run() 138 | # from examples.basic import run 139 | 140 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/counter_read.py: -------------------------------------------------------------------------------- 1 | # Clock speed test, Michael Bell 2 | # This test clocks the tt_um_test design at a high frequency 3 | # and checks the counter has incremented by the correct amount 4 | 5 | import machine 6 | import rp2 7 | import time 8 | 9 | from ttboard.mode import RPMode 10 | from ttboard.demoboard import DemoBoard 11 | 12 | # PIO program to drive the clock. Put a value n and it clocks n+1 times 13 | # Reads 0 when done. 14 | @rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, autopull=True, pull_thresh=32, autopush=True, push_thresh=32) 15 | def clock_prog(): 16 | out(x, 32) .side(0) 17 | label("clock_loop") 18 | irq(4) .side(1) 19 | jmp(x_dec, "clock_loop").side(0) 20 | irq(clear, 4) .side(0) 21 | in_(null, 32) .side(0) 22 | 23 | @rp2.asm_pio(autopush=True, push_thresh=32, in_shiftdir=rp2.PIO.SHIFT_RIGHT, fifo_join=rp2.PIO.JOIN_RX) 24 | def read_prog(): 25 | in_(pins, 2) 26 | 27 | # Select design, don't apply config so the PWM doesn't start. 28 | tt = DemoBoard(apply_user_config=False) 29 | tt.shuttle.tt_um_test.enable() 30 | 31 | # Setup the PIO clock driver 32 | sm = rp2.StateMachine(0, clock_prog, sideset_base=machine.Pin(0)) 33 | sm.exec("irq(clear, 4)") 34 | sm.active(1) 35 | 36 | # Setup the PIO counter read 37 | sm_rx = rp2.StateMachine(1, read_prog, in_base=machine.Pin(3)) 38 | 39 | # Setup read DMA 40 | dst_data = bytearray(8192) 41 | d = rp2.DMA() 42 | 43 | # Read using the SM1 RX DREQ 44 | c = d.pack_ctrl(inc_read=False, treq_sel=5) 45 | 46 | # Read from the SM1 RX FIFO 47 | d.config( 48 | read=0x5020_0024, 49 | write=dst_data, 50 | count=len(dst_data)//4, 51 | ctrl=c, 52 | trigger=False 53 | ) 54 | 55 | def start_rx(): 56 | # Reset the SM 57 | sm_rx.active(0) 58 | while sm_rx.rx_fifo() > 0: sm_rx.get() 59 | sm_rx.restart() 60 | 61 | # Wait until out0 changes from its current value 62 | if machine.Pin(3).value(): 63 | sm_rx.exec("wait(0, pin, 0)") 64 | else: 65 | sm_rx.exec("wait(1, pin, 0)") 66 | 67 | # Re-activate SM, it will block until the wait command completes 68 | sm_rx.active(1) 69 | 70 | 71 | # Frequency for the RP2040, the design is clocked at half this frequency 72 | def run_test(freq): 73 | # Multiply requested project clock frequency by 2 to get RP2040 clock 74 | freq *= 2 75 | 76 | if freq > 266_000_000: 77 | raise ValueError("Too high a frequency requested") 78 | 79 | machine.freq(freq) 80 | 81 | try: 82 | # Run 64 clocks 83 | print("Clock test... ", end ="") 84 | start_rx() 85 | sm.put(63) 86 | sm.get() 87 | print(f" done. Value now: {tt.uo_out.value}") 88 | 89 | # Print the values read back for inspection 90 | for j in range(4): 91 | readings = sm_rx.get() 92 | for i in range(16): 93 | val = (readings >> (i*2)) & 0x3 94 | print(val, end = " ") 95 | print() 96 | sm_rx.active(0) 97 | 98 | total_errors = 0 99 | 100 | for _ in range(10): 101 | last = tt.uo_out.value 102 | 103 | # Setup the read SM and DMA transfer into the verification buffer 104 | start_rx() 105 | d.config(write=dst_data, trigger=True) 106 | 107 | # Run clock for enough time to fill the buffer 108 | t = time.ticks_us() 109 | sm.put(1024*17) 110 | sm.get() 111 | t = time.ticks_us() - t 112 | print(f"Clocked for {t}us: ", end = "") 113 | 114 | # Print the first 16 values in the DMA'd buffer 115 | for j in range(0,4): 116 | readings = dst_data[j] 117 | for i in range(4): 118 | val = (readings >> (i*2)) & 0x3 119 | print(val, end = " ") 120 | 121 | # Check the counter has incremented by 1, as we sent a 122 | # multiple of 256 clocks plus one more 123 | if tt.uo_out.value != (last + 1) & 0xFF: 124 | print("Error: ", end="") 125 | print(tt.uo_out.value) 126 | 127 | # Check the read data from the counter continuously increases 128 | def verify(count, expected_val, retry): 129 | errors = 0 130 | 131 | for j in range(2,len(dst_data)): 132 | readings = dst_data[j] 133 | for i in range(4): 134 | val = (readings >> (i*2)) & 0x3 135 | if count == 1 and val != expected_val: 136 | if retry: 137 | return -1 138 | else: 139 | print(f"Error at {j}:{i} {val} should be {expected_val}") 140 | errors += 1 141 | count += 1 142 | if count == 2: 143 | expected_val = (expected_val + 1) & 0x3 144 | count = 0 145 | if errors > 10: break 146 | return errors 147 | 148 | expected_val = dst_data[2] & 0x3 149 | errors = verify(1, expected_val, True) 150 | if errors == -1: 151 | expected_val = (dst_data[2] >> 2) & 0x3 152 | errors = verify(0, expected_val, False) 153 | 154 | total_errors += errors 155 | if errors > 10: 156 | return total_errors 157 | 158 | finally: 159 | # Remove overclock 160 | if freq > 133_000_000: 161 | machine.freq(133_000_000) 162 | 163 | return total_errors 164 | 165 | if __name__ == "__main__": 166 | freq = 50_000_000 167 | while True: 168 | print(f"\nRun at {freq/1000000}MHz project clock\n") 169 | errors = run_test(freq) 170 | if errors > 10: break 171 | freq += 1_000_000 172 | -------------------------------------------------------------------------------- /src/tests/counter_speed.py: -------------------------------------------------------------------------------- 1 | # Clock speed test, Michael Bell 2 | # This test clocks the tt_um_test design at a high frequency 3 | # and checks the counter has incremented by the correct amount 4 | # 5 | 6 | import machine 7 | import rp2 8 | import time 9 | 10 | from ttboard.mode import RPMode 11 | from ttboard.demoboard import DemoBoard 12 | 13 | # PIO program to drive the clock. Put a value n and it clocks n+1 times 14 | # Reads 0 when done. 15 | @rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, autopull=True, pull_thresh=32, autopush=True, push_thresh=32) 16 | def clock_prog(): 17 | out(x, 32) .side(0) 18 | label("clock_loop") 19 | nop() .side(1) 20 | jmp(x_dec, "clock_loop").side(0) 21 | in_(null, 32) .side(0) 22 | 23 | # Select design, don't apply config so the PWM doesn't start. 24 | tt = DemoBoard(apply_user_config=False) 25 | tt.shuttle.tt_um_test.enable() 26 | 27 | # Setup the PIO clock driver 28 | sm = rp2.StateMachine(0, clock_prog, sideset_base=machine.Pin(0)) 29 | sm.active(1) 30 | 31 | def run_test(freq, fast=False): 32 | # Multiply requested project clock frequency by 2 to get RP2040 clock 33 | freq *= 2 34 | 35 | if freq > 350_000_000: 36 | raise ValueError("Too high a frequency requested") 37 | 38 | if freq > 266_000_000: 39 | rp2.Flash().set_divisor(4) 40 | 41 | machine.freq(freq) 42 | 43 | try: 44 | # Run 1 clock 45 | print("Clock test... ", end ="") 46 | sm.put(1) 47 | sm.get() 48 | print(f" done. Value: {tt.uo_out.value}") 49 | 50 | errors = 0 51 | for _ in range(10): 52 | last = tt.uo_out.value 53 | 54 | # Run clock for approx 0.25 or 1 second, sending a multiple of 256 clocks plus 1. 55 | clocks = (freq // 2048) * 256 if fast else (freq // 512) * 256 56 | t = time.ticks_us() 57 | sm.put(clocks) 58 | sm.get() 59 | t = time.ticks_us() - t 60 | print(f"Clocked for {t}us: ", end = "") 61 | 62 | # Check the counter has incremented by 1. 63 | if tt.uo_out.value != (last + 1) & 0xFF: 64 | print("Error: ", end="") 65 | errors += 1 66 | print(tt.uo_out.value) 67 | 68 | if not fast: 69 | # Sleep so the 7-seg display can be read 70 | time.sleep(0.5) 71 | finally: 72 | if freq > 133_000_000: 73 | machine.freq(133_000_000) 74 | if freq > 266_000_000: 75 | rp2.Flash().set_divisor(2) 76 | 77 | return errors 78 | 79 | if __name__ == "__main__": 80 | freq = 66_000_000 81 | while True: 82 | print(f"\nRun at {freq/1000000}MHz project clock\n") 83 | errors = run_test(freq, True) 84 | if errors > 0: break 85 | freq += 2_000_000 -------------------------------------------------------------------------------- /src/tests/dffram.py: -------------------------------------------------------------------------------- 1 | ''' 2 | DFFRAM class and test for tt_um_urish_dffram 3 | 4 | ''' 5 | 6 | import random 7 | 8 | from ttboard.pins import Pins 9 | from ttboard.demoboard import DemoBoard 10 | import ttboard.util.time as time 11 | class DFFRAM: 12 | 13 | def __init__(self, pins:Pins): 14 | self.p = pins 15 | self._data_in_write = 0 16 | 17 | for p in self.p.bidirs: 18 | p.mode = Pins.OUT 19 | 20 | self.p.ui_in.value = 0 21 | self.p.uio_in.value = 0 22 | 23 | @property 24 | def we(self): 25 | return self.p.in7() 26 | 27 | @we.setter 28 | def we(self, v:int): 29 | if v: 30 | self.p.ui_in[7] = 1 31 | else: 32 | self.p.ui_in[7] = 0 33 | 34 | @property 35 | def addr(self): 36 | return self.p.ui_in.value & 0x7f 37 | 38 | @addr.setter 39 | def addr(self, v:int): 40 | self.p.ui_in.value = (self.p.ui_in.value & ~0x7f) | (v & 0x7f) 41 | 42 | @property 43 | def data_out(self): 44 | return self.p.uo_out.value 45 | 46 | @property 47 | def data_in(self): 48 | return self.p.uio_in.value 49 | 50 | @data_in.setter 51 | def data_in(self, v:int): 52 | self.p.uio_in.value = v 53 | 54 | 55 | 56 | def setup(): 57 | tt = DemoBoard() 58 | 59 | tt.shuttle.tt_um_urish_dffram.enable() 60 | 61 | dffram = DFFRAM(tt.pins) 62 | 63 | tt.reset_project(True) 64 | tt.clock_project_PWM(1e4) 65 | time.sleep_ms(1) 66 | 67 | tt.reset_project(False) 68 | time.sleep_ms(1) 69 | return tt, dffram 70 | 71 | def test(): 72 | tt, dffram = setup() 73 | 74 | tt.clock_project_stop() 75 | 76 | # Outputs only valid when clock is low, so start clock low 77 | tt.clk.off() 78 | 79 | print("Writing RAM") 80 | for i in range(0,128): 81 | dffram.addr = i 82 | 83 | dffram.data_in = i 84 | dffram.we = 1 85 | tt.clock_project_once() 86 | 87 | # Not sure why but writes need a cycle with we = 0 to take? 88 | dffram.we = 0 89 | tt.clock_project_once() 90 | 91 | print('Reading RAM') 92 | dffram.we = 0 93 | for i in range(0,128): 94 | dffram.addr = i 95 | 96 | tt.clock_project_once() 97 | #print(f'Read addr {i} as {dffram.data_out}') 98 | if dffram.data_out != i: 99 | print(f"Error at {i}: {dffram.data_out} != {i}") 100 | 101 | print('Verify random reads and writes') 102 | ram = [i for i in range(128)] 103 | for i in range(1000): 104 | addr = random.randint(0, 63) 105 | dffram.addr = addr 106 | 107 | if random.randint(0, 1) == 1: 108 | data = random.randint(0, 255) 109 | ram[addr] = data 110 | dffram.data_in = data 111 | dffram.we = 1 112 | tt.clock_project_once() 113 | dffram.we = 0 114 | tt.clock_project_once() 115 | else: 116 | tt.clock_project_once() 117 | if dffram.data_out != ram[addr]: 118 | print(f"Error at {addr}: {dffram.data_out} != {ram[addr]}") 119 | 120 | print('Verify RAM contents') 121 | for i in range(0,128): 122 | dffram.addr = i 123 | tt.clock_project_once() 124 | if dffram.data_out != ram[i]: 125 | print(f"Error at {i}: {dffram.data_out} != {ram[i]}") 126 | 127 | print("Test done") 128 | return tt, dffram 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/tests/sram.py: -------------------------------------------------------------------------------- 1 | ''' 2 | SRAM class and test for tt_um_urish_sram_poc project 3 | 4 | Uses 5 | wire bank_select = ui_in[6]; 6 | wire [5:0] addr_low = ui_in[5:0]; 7 | wire [4:0] addr_high_in = uio_in[4:0]; 8 | wire [10:0] addr = {bank_select ? addr_high_in : addr_high_reg, addr_low}; 9 | wire [1:0] byte_index = addr[1:0]; 10 | wire [8:0] word_index = addr[10:2]; 11 | 12 | assign uio_oe = 8'b0; // All bidirectional IOs are inputs 13 | assign uio_out = 8'b0; 14 | 15 | wire WE = ui_in[7] && !bank_select; 16 | ''' 17 | 18 | import random 19 | 20 | from ttboard.pins import Pins 21 | from ttboard.demoboard import DemoBoard 22 | import ttboard.util.time as time 23 | class SRAM: 24 | 25 | def __init__(self, pins:Pins): 26 | self.p = pins 27 | self._data_in_write = 0 28 | 29 | for p in self.p.bidirs: 30 | p.mode = Pins.OUT 31 | 32 | self.p.ui_in.value = 0 33 | self.p.uio_in.value = 0 34 | 35 | @property 36 | def bank_select(self): 37 | return self.p.in6() 38 | 39 | @bank_select.setter 40 | def bank_select(self, v:int): 41 | self.p.ui_in[6] = v 42 | 43 | @property 44 | def we(self): 45 | return self.p.in7() 46 | 47 | @we.setter 48 | def we(self, v:int): 49 | if v: 50 | self.p.ui_in[7] = 1 51 | else: 52 | self.p.ui_in[7] = 0 53 | 54 | 55 | 56 | 57 | @property 58 | def addr_low(self): 59 | return self.p.ui_in.value & 0x3f 60 | 61 | @addr_low.setter 62 | def addr_low(self, v:int): 63 | self.p.ui_in.value = (self.p.ui_in.value & ~0x3f) | (v & 0x3f) 64 | 65 | @property 66 | def addr_high_in(self): 67 | return self.p.uio_in.value & 0x1f 68 | 69 | @addr_high_in.setter 70 | def addr_high_in(self, v:int): 71 | self.p.uio_in.value = (self.p.uio_in.value & ~0x1f) | (v & 0x1f) 72 | 73 | @property 74 | def data_out(self): 75 | return self.p.uo_out.value 76 | 77 | @property 78 | def data_in(self): 79 | return self.p.uio_out.value 80 | 81 | @data_in.setter 82 | def data_in(self, v:int): 83 | self.p.uio_in.value = v 84 | 85 | 86 | 87 | def setup(): 88 | tt = DemoBoard() 89 | 90 | tt.shuttle.tt_um_urish_sram_poc.enable() 91 | 92 | sram = SRAM(tt.pins) 93 | 94 | tt.reset_project(True) 95 | tt.clock_project_PWM(1e4) 96 | time.sleep_ms(1) 97 | 98 | tt.reset_project(False) 99 | time.sleep_ms(1) 100 | return tt, sram 101 | 102 | def test(): 103 | tt, sram = setup() 104 | tt.clock_project_stop() 105 | 106 | # Outputs only valid when clock is low, so start clock low 107 | tt.clk.off() 108 | 109 | print("Writing RAM") 110 | for i in range(0,64): 111 | sram.addr_low = i 112 | """sram.addr_hi_in = i >> 6 113 | sram.bank_select = 1 114 | tt.clock_project_once() 115 | tt.clock_project_once() 116 | 117 | sram.bank_select = 0 118 | tt.clock_project_once() 119 | tt.clock_project_once()""" 120 | 121 | sram.data_in = i & 0xFF 122 | sram.we = 1 123 | tt.clock_project_once() 124 | 125 | #print(f'Wrote {i} @ addr {i}') 126 | #sram.we = 0 127 | #tt.clock_project_once() 128 | 129 | print('Reading RAM') 130 | sram.we = 0 131 | for i in range(0,64): 132 | sram.addr_low = i 133 | 134 | """sram.addr_hi_in = i >> 6 135 | sram.bank_select = 1 136 | tt.clock_project_once() 137 | tt.clock_project_once() 138 | 139 | sram.bank_select = 0 140 | tt.clock_project_once()""" 141 | 142 | tt.clock_project_once() 143 | #print(f'Read addr {i} as {sram.data_out}') 144 | if sram.data_out != i: 145 | print(f"Error at {i}: {sram.data_out} != {i}") 146 | 147 | print('Verify random reads and writes') 148 | ram = [i for i in range(64)] 149 | for i in range(1000): 150 | addr = random.randint(0, 63) 151 | sram.addr_low = addr 152 | 153 | if random.randint(0, 1) == 1: 154 | data = random.randint(0, 255) 155 | ram[addr] = data 156 | sram.data_in = data 157 | sram.we = 1 158 | tt.clock_project_once() 159 | sram.we = 0 160 | else: 161 | tt.clock_project_once() 162 | if sram.data_out != ram[addr]: 163 | print(f"Error at {addr}: {sram.data_out} != {ram[addr]}") 164 | 165 | print('Verify RAM contents') 166 | for i in range(0,64): 167 | sram.addr_low = i 168 | tt.clock_project_once() 169 | if sram.data_out != ram[i]: 170 | print(f"Error at {i}: {sram.data_out} != {ram[i]}") 171 | 172 | print("Test done") 173 | return tt, sram 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/tests/test_anton.py: -------------------------------------------------------------------------------- 1 | from ttboard.pins.upython import Pin # rather than from machine, allows dev on desktop 2 | import ttboard.util.time as time # for time.sleep_ms() 3 | from ttboard.demoboard import DemoBoard 4 | 5 | # ========= Config ========= 6 | 7 | # These are 'do nothing defaults' that get overridden further below: 8 | PAUSE_DELAY = 0 9 | EXTRA_DELAY = 0 10 | BASIC_TEST = False 11 | FRAME_TEST = False 12 | PRINT_VSYNC_ERRORS = False 13 | PRINT_LZC_ERRORS = False 14 | 15 | # Lazy... override the above. Done this way so we can very quickly comment 16 | # out as required, to disable those tests/configs: 17 | PAUSE_DELAY = 3000 # Milliseconds to pause before each test. 18 | EXTRA_DELAY = 0 # Extra milliseconds to sleep between clock pulses. 19 | BASIC_TEST = True 20 | FRAME_TEST = True 21 | # PRINT_VSYNC_ERRORS = True 22 | # PRINT_LZC_ERRORS = True 23 | 24 | OUTPUT_TIMING = False 25 | 26 | # ========= Helper functions ========= 27 | 28 | # print_tt_outputs(): 29 | # Show the state of the board's 8 uo_outs, and 8 bidir pins (treated also as outputs). 30 | # Optionally also takes what they are *expected* to be, just for visual comparison. 31 | def print_tt_outputs(tt:DemoBoard, expect_out:int=None, expect_bidir:int=None, prefix=''): 32 | print(f'{prefix} uo_out= {tt.uo_out.value:08b} bidir= {tt.uio_in.value:08b}', end='') 33 | if [expect_out,expect_bidir] != [None,None]: 34 | print('; expected', end='') 35 | if expect_out is not None: 36 | print(f' uo_out={expect_out:08b}', end='') 37 | if expect_bidir is not None: 38 | print(f' bidir={expect_bidir:08b}', end='') 39 | print() 40 | 41 | # print_progress(): 42 | # Prints dots to show progress based on X (and optionally Y) input. 43 | # Effectively gives a progress update every 16 pixels. 44 | #NOTE: If 'ok' is False, and a progress character is due to be printed, it will be '!' 45 | # instead of a dot, and True will be returned to show that we've 'reset' from an otherwise 46 | # erroneous batch. If a progress character is NOT due, just return whatever 'ok' is currently. 47 | def print_progress(x:int, y:int=0, ok:bool=True): 48 | if x&0b1111 == 0: 49 | # extra keyword arguments given print('.' if ok else '!', end='', flush=True) 50 | print('.' if ok else '!', end='') 51 | return True 52 | else: 53 | return ok 54 | 55 | # announce(): 56 | # Print basic header for the next test, and (optionally) how long 57 | # until it starts (given PAUSE_DELAY): 58 | def announce(test_header:str): 59 | print(f'\n{test_header}') 60 | if PAUSE_DELAY > 0: 61 | print(f'(Will continue in {PAUSE_DELAY}ms...)') 62 | time.sleep_ms(PAUSE_DELAY) 63 | 64 | # pulse_clock(): 65 | # Quick helper to pulse the clock and optionally wait out configured extra delay. 66 | def pulse_clock(tt:DemoBoard): 67 | tt.clock_project_once() 68 | if EXTRA_DELAY > 0: 69 | time.sleep_ms(EXTRA_DELAY) 70 | 71 | # do_reset(): 72 | # Go through a reset sequence by asserting reset, pulsing clock 3 times, 73 | # then releasing reset again: 74 | def do_reset(tt:DemoBoard): 75 | print('Resetting design...') 76 | tt.reset_project(True) 77 | for _i in range(3): 78 | tt.clock_project_once() 79 | tt.reset_project(False) 80 | 81 | # calc_lzc(): 82 | # Given h (x) position, v (y) position, and frame number, 83 | # calculates the value that the design's LZC should be returning 84 | # via the bidir pins. Note that the typical output value is in the range 85 | # [0,24] (5 bits) with an extra MSB (bit 5) being set to 1 if all 86 | # 24 input bits are 0. 87 | def calc_lzc(h:int, v:int, frame:int = 0): 88 | # Constrain input ranges: 89 | h &= 0x3FF # 10 bits 90 | v &= 0x3FF # 10 bits 91 | frame &= 0xF # 4 bits 92 | # Determine the input to the LZC (ffff'vvvv'vvvv'vvhh'hhhh'hhhh): 93 | lzc_in = (frame << 20) | (v << 10) | (h) 94 | # Count leading zeros: 95 | #NOTE: if lzc_in is 0, not only is out[4:0]==24, but also out[5] is 1 96 | # to indicate that all 24 bits are 0 (hence 24|32). 97 | return (24|32) if lzc_in==0 else (24-len(f'{lzc_in:b}')) 98 | 99 | 100 | # ========= Main test code ========= 101 | 102 | # Get a handle to the base board: config.ini selects ASIC_RP_CONTROL mode to configure 103 | # our RP2040 GPIOs for proper cooperation with an attached TT chip: 104 | tt = DemoBoard() 105 | 106 | # Enable my tt03p5-solo-squash design: 107 | #NOTE: This might not be needed given our config.ini 108 | tt.shuttle.tt_um_algofoogle_solo_squash.enable() 109 | print(f'Project {tt.shuttle.enabled.name} selected ({tt.shuttle.enabled.repo})') 110 | 111 | # Ensure we are *reading* from all of the ASIC's bidir pins: 112 | for pin in tt.bidirs: 113 | pin.mode = Pin.IN 114 | 115 | # Start with project clock low, and reset NOT asserted: 116 | tt.clk(0) 117 | tt.reset_project(False) 118 | 119 | # By default all inputs to the ASIC should be low: 120 | tt.ui_in.value = 0 121 | 122 | # Print initial state of all outputs; likely to be somewhat random: 123 | print_tt_outputs(tt, prefix='Pre-reset state:') 124 | 125 | # Reset the design: 126 | do_reset(tt) 127 | 128 | # Now show the state of all outputs again. 129 | # Expected outputs are what the design should always assert while held in reset: 130 | print_tt_outputs(tt, expect_out=0b11011110, expect_bidir=0b11111000, prefix='Post-reset state:') 131 | 132 | if BASIC_TEST: 133 | error_count = 0 134 | announce('BASIC_TEST: Basic test of first 2 video lines...') 135 | # Now, the design should render immediately from the first line of the visible 136 | # display area, and the first two lines that it renders should be fully yellow... 137 | #NOTE: RGB outs are registered (hence 'next_color' and 'color') 138 | # but the rest are not (i.e. hsync, vsync, speaker, col0, row0). 139 | next_color = 0b110 # Yellow 140 | for y in range(2): 141 | print(f'Line {y}:') 142 | bulk_ok = True # 'bulk' because it keeps track of ANY error within a progress update batch. 143 | 144 | t_start = time.ticks_us() 145 | for x in range(800): 146 | # Make sure outputs match what we expect... 147 | color = next_color 148 | next_color = 0b110 if x<640 else 0 # Yellow 149 | hsync = 0 if x>=640+16 and x<640+16+96 else 1 # (HSYNC is active low) 150 | vsync = 0 if y>=480+10 and y<480+10+2 else 1 # (VSYNC is active low) 151 | speaker = 0 # Off 152 | col0 = 1 if x==0 else 0 153 | row0 = 1 if y==0 else 0 154 | expect_out = (row0<<7) | (col0<<6) | (speaker<<5) | (vsync<<4) | (hsync<<3) | color 155 | if tt.uo_out.value != expect_out: 156 | error_count += 1 157 | print_tt_outputs(tt, expect_out, prefix=f'[{x},{y}] Error:') 158 | bulk_ok = False 159 | bulk_ok = print_progress(x, ok=bulk_ok) 160 | pulse_clock(tt) 161 | 162 | t_end = time.ticks_us() 163 | if OUTPUT_TIMING: 164 | print(f'{int( (t_end - t_start)/1000 )}ms') 165 | else: 166 | print() 167 | print(f'\nBASIC_TEST done. Error rate: {error_count}/1600\n') 168 | 169 | 170 | if FRAME_TEST: 171 | vsync_errors = 0 172 | lzc_errors = 0 173 | announce('FRAME_TEST: Content test of the first full frame...') 174 | do_reset(tt) 175 | # I'm lazy, so let's just count how many pixels there are of each colour in a full frame, 176 | # and compare with a prediction using the following guide... 177 | # Blue (varies): 178 | # ~ 18.75%+/-1% of ((640-32)*(480-64)-32*64-16*16, i.e. playfield area minus paddle and ball. 179 | # = 44485..49498 180 | # Green: 181 | # + 28*28*20*2 - Interior of each block in the top and bottom walls 182 | # + 28*28*13 - Interior of each block in the RHS wall 183 | # + 16*16 - Ball #NOTE: Ball area is less if the ball is off-screen or intersecting paddle 184 | # = 41808 185 | # Red: 186 | # + 32*64 - Paddle 187 | # = 2048 188 | # Yellow: 189 | # + 640*4 - Top/bottom borders for each of top and bottom walls 190 | # + 28*4*20*2 - Left/right borders for each block of top/bottom walls 191 | # + (32*32-28*28)*13 - All borders for each block of RHS wall 192 | # = 10160 193 | # Cyan, Magenta, White: all 0 194 | cols=800 195 | rows=525 196 | total_pixels = rows*cols 197 | # With 3 colour bits, there are 8 possible colours to count: 198 | color_stats = [ 199 | # [0]Name [1]Actual [2]Expected 200 | ['Black', 0, None ], # Colour 0 # Irrelevant if others are (about) right. 201 | ['Blue', 0, range(44485, 49499) ], # Colour 1 202 | ['Green', 0, 41808 ], # Colour 2 203 | ['Cyan', 0, 0 ], # Colour 3 204 | ['Red', 0, 2048 ], # Colour 4 205 | ['Magenta', 0, 0 ], # Colour 5 206 | ['Yellow', 0, 10160 ], # Colour 6 207 | ['White', 0, 0 ] # Colour 7 208 | ] 209 | paddle_y = None 210 | ball_x = None 211 | ball_y = None 212 | for y in range(rows): 213 | print(f'Line {y}:') 214 | bulk_ok = True 215 | t_start = time.ticks_us() 216 | for x in range(cols): 217 | # Increment the count in the bin of the current pixel colour: 218 | color = tt.uo_out.value & 0b111 219 | color_stats[color][1] += 1 220 | # Try to detect the paddle's Y position (i.e. the first line to have a red pixel): 221 | if paddle_y is None and color == 0b100: # Red. 222 | paddle_y = y 223 | print(f'Detected paddle_y: {paddle_y}') 224 | # Try to detect the ball position: 225 | if ball_x is None and color == 0b010 and y>=32 and y<480-32 and x<640-32: # Green. 226 | ball_x = x-1 # Registered, so delayed by 1. 227 | ball_y = y 228 | print(f'Detected ball position: ({ball_x},{ball_y})') 229 | # Make sure vsync is asserted at the right times: 230 | expected_vsync = 0 if y>=480+10 and y<480+10+2 else 1 # (VSYNC is active low) 231 | actual_vsync = (tt.uo_out.value & 0b10000) >> 4 232 | expected_lzc = calc_lzc(x, y) 233 | actual_lzc = tt.uio_in.value & 0b111111 234 | if actual_vsync != expected_vsync: 235 | bulk_ok = False 236 | vsync_errors += 1 237 | if PRINT_VSYNC_ERRORS: 238 | print_tt_outputs(tt, expect_out, prefix=f'[{x},{y}] VSYNC error:') 239 | if actual_lzc != expected_lzc: 240 | bulk_ok = False 241 | lzc_errors += 1 242 | if PRINT_LZC_ERRORS: 243 | print_tt_outputs(tt, expect_out, prefix=f'[{x},{y}] LZC error:') 244 | bulk_ok = print_progress(x, y, ok=bulk_ok) 245 | pulse_clock(tt) 246 | t_end = time.ticks_us() 247 | if OUTPUT_TIMING: 248 | print(f'{int( (t_end - t_start)/1000 )}ms') 249 | else: 250 | print() 251 | 252 | print() 253 | if vsync_errors==0: 254 | print('No VSYNC errors.') 255 | else: 256 | print(f'VSYNC error rate: {vsync_errors}/{total_pixels}') 257 | 258 | if lzc_errors==0: 259 | print('No LZC errors.') 260 | else: 261 | print(f'LZC error rate: {lzc_errors}/{total_pixels}') 262 | 263 | if paddle_y is None: 264 | print('ERROR: Paddle was NOT detected') 265 | else: 266 | print(f'Paddle was detected; top edge is Y={paddle_y}') 267 | 268 | if ball_x is None: 269 | print('ERROR: Ball was NOT detected') 270 | else: 271 | print(f'Ball was detected at position ({ball_x},{ball_y})') 272 | 273 | print('\nCounted pixel colours:') 274 | print(f'=============================================') 275 | print(f"{'RGB':5s}{'Color':10s}{'Actual':>10s}{'Expected':>10s}") 276 | print(f'---------------------------------------------') 277 | for i in range(len(color_stats)): 278 | name = color_stats[0] 279 | actual = color_stats[1] 280 | expected = color_stats[2] 281 | print(f'{i:03b} {name:10s}{actual:>10d}', end='') 282 | if expected is None: 283 | print('-') 284 | continue 285 | elif isinstance(expected, range): 286 | print(expected, end='') 287 | fail = actual not in expected 288 | else: 289 | print(f"{expected:>10d}", end='') 290 | fail = actual != expected 291 | 292 | if fail: 293 | print(' - ERROR') 294 | else: 295 | print() 296 | print(f'=============================================') 297 | 298 | print('\FRAME_TEST done\n') 299 | 300 | 301 | """ 302 | TODO: 303 | - Try running on upython/RP2040 304 | - Is there a good way to write inputs/outputs to a file, e.g. VCD? 305 | - Can we make mapped pin names to suit actual pin names for our design? 306 | - Test running the design at full speed... IRQs to check expected conditions? 307 | - Print '*' instead of '.' for any batch that contains at least 1 error 308 | """ 309 | -------------------------------------------------------------------------------- /src/ttboard/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 6, 2024 3 | 4 | The TinyTapeout Demo Board PCB RPi SDK 5 | 6 | @author: Pat Deegan 7 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 8 | ''' 9 | import os 10 | 11 | VERSION='0.0.0' 12 | 13 | 14 | relfiles = list( 15 | map(lambda v: v.replace('release_v', ''), 16 | filter(lambda f: f.startswith('release_v'), os.listdir('/'))) ) 17 | if len(relfiles): 18 | VERSION = relfiles[0] 19 | -------------------------------------------------------------------------------- /src/ttboard/boot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/ttboard/boot/__init__.py -------------------------------------------------------------------------------- /src/ttboard/boot/demoboard_detect.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 30, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | import ttboard.util.platform as platform 8 | from ttboard.pins.upython import Pin 9 | import ttboard.pins.gpio_map 10 | from ttboard.pins.gpio_map import GPIOMapTT04, GPIOMapTT06 11 | 12 | import ttboard.log as logging 13 | log = logging.getLogger(__name__) 14 | 15 | class DemoboardVersion: 16 | ''' 17 | Simple wrapper for an 'enum' type deal with db versions. 18 | Supported are TT04/TT05 and TT06+ 19 | ''' 20 | UNKNOWN = 0 21 | TT04 = 1 22 | TT06 = 2 23 | 24 | @classmethod 25 | def to_string(cls, v:int): 26 | asStr = { 27 | cls.UNKNOWN: 'UNKNOWN', 28 | cls.TT04: 'TT04/TT05', 29 | cls.TT06: 'TT06+' 30 | } 31 | if v in asStr: 32 | return asStr[v] 33 | return 'N/A' 34 | 35 | class DemoboardDetect: 36 | ''' 37 | DemoboardDetect 38 | centralizes and implements strategies for detecting 39 | the version of the demoboard. 40 | 41 | Because the TT demoboards have had disruptive changes in the 42 | migration to TT06+ chips, namely in terms of 43 | GPIO mapping and the removal of the demoboard MUX, 44 | and because the presence or absence of a carrier board on the 45 | db can make a difference, we use a combination of strategies. 46 | 47 | TT06+ boards have a mix of pull-up/pull-downs on the ASIC mux 48 | control lines, which allow detection of both: 49 | * the fact this is a TT06+ demoboard; and 50 | * the fact that the carrier is present 51 | However, this still looks identical to TT04 for a db with 52 | no carrier inserted. 53 | 54 | TT04 boards have an on-board MUX, so if we play with that, 55 | we should have different values showing on the ASIC mux lines. 56 | If it has no impact (this pin is mapped to project reset, so 57 | it shouldn't unless a project is selected--so this is only assured 58 | to work on powerup) 59 | 60 | This class has: 61 | a probe() method, to encapsulate all the action, 62 | PCB, CarrierPresent class attribs to hold the results 63 | 64 | 65 | 66 | ''' 67 | PCB = DemoboardVersion.UNKNOWN 68 | CarrierPresent = None 69 | CarrierVersion = None 70 | 71 | 72 | @classmethod 73 | def PCB_str(cls): 74 | return DemoboardVersion.to_string(cls.PCB) 75 | 76 | @classmethod 77 | def probe_pullups(cls): 78 | cena_pin = GPIOMapTT06.get_raw_pin(GPIOMapTT06.ctrl_enable(), Pin.IN) 79 | # cinc_pin = GPIOMapTT06.get_raw_pin(GPIOMapTT06.ctrl_increment(), Pin.IN) 80 | crst_pin = GPIOMapTT06.get_raw_pin(GPIOMapTT06.ctrl_reset(), Pin.IN) 81 | 82 | crst = crst_pin() 83 | cena = cena_pin() 84 | 85 | 86 | if (not crst) and (not cena): 87 | log.debug("ctrl mux lines pulled to indicate TT06+ carrier present--tt06+ db") 88 | log.info("TT06+ demoboard with carrier present") 89 | cls.PCB = DemoboardVersion.TT06 90 | cls.CarrierPresent = True 91 | return True 92 | 93 | if crst and cena: 94 | log.info("probing ctrl mux lines gives no info, unable to determine db version") 95 | log.warn("TT04 demoboard OR TT06 No carrier present") 96 | cls.PCB = DemoboardVersion.UNKNOWN 97 | cls.CarrierPresent = None 98 | 99 | return False 100 | 101 | @classmethod 102 | def probe_tt04mux(cls): 103 | mux_pin = GPIOMapTT04.get_raw_pin(GPIOMapTT04.mux_select(), Pin.OUT) 104 | cena_pin = GPIOMapTT04.get_raw_pin(GPIOMapTT04.ctrl_enable(), Pin.IN) 105 | cinc_pin = GPIOMapTT04.get_raw_pin(GPIOMapTT04.ctrl_increment(), Pin.IN) 106 | crst_pin = GPIOMapTT04.get_raw_pin(GPIOMapTT04.ctrl_reset(), Pin.IN) 107 | 108 | mux_pin(0) 109 | mux_0 = [cena_pin(), cinc_pin(), crst_pin()] 110 | 111 | mux_pin(1) 112 | mux_1 = [cena_pin(), cinc_pin(), crst_pin()] 113 | if mux_1 != mux_0: 114 | log.info("DB seems to have on-board MUX: TT04+") 115 | cls.PCB = DemoboardVersion.TT04 116 | return True 117 | 118 | log.debug("Mux twiddle has no effect, probably not TT04 db") 119 | return False 120 | @classmethod 121 | def rp_all_inputs(cls): 122 | log.debug("Setting all RP GPIO to INPUTS") 123 | pins = [] 124 | for i in range(29): 125 | pins.append(platform.pin_as_input(i, Pin.PULL_DOWN)) 126 | 127 | return pins 128 | 129 | @classmethod 130 | def probe(cls): 131 | result = False 132 | cls.rp_all_inputs() 133 | if cls.probe_tt04mux(): 134 | cls._configure_gpiomap() 135 | result = True 136 | elif cls.probe_pullups(): 137 | cls._configure_gpiomap() 138 | result = True 139 | else: 140 | log.debug("Neither pullup nor tt04mux tests conclusive, assuming TT06+ board") 141 | cls.PCB = DemoboardVersion.TT06 142 | cls._configure_gpiomap() 143 | result = False 144 | 145 | # clear out boot prefix 146 | return result 147 | 148 | @classmethod 149 | def force_detection(cls, dbversion:int): 150 | cls.PCB = dbversion 151 | cls._configure_gpiomap() 152 | 153 | @classmethod 154 | def _configure_gpiomap(cls): 155 | mapToUse = { 156 | 157 | DemoboardVersion.TT04: GPIOMapTT04, 158 | DemoboardVersion.TT06: GPIOMapTT06 159 | 160 | } 161 | if cls.PCB in mapToUse: 162 | log.debug(f'Setting GPIOMap to {mapToUse[cls.PCB]}') 163 | ttboard.pins.gpio_map.GPIOMap = mapToUse[cls.PCB] 164 | else: 165 | raise RuntimeError('Cannot set GPIO map') 166 | 167 | 168 | -------------------------------------------------------------------------------- /src/ttboard/boot/rom.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Apr 26, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | import ttboard.util.time as time 9 | from ttboard.boot.shuttle_properties import ShuttleProperties 10 | 11 | 12 | import ttboard.log as logging 13 | log = logging.getLogger(__name__) 14 | 15 | class ChipROM(ShuttleProperties): 16 | def __init__(self, project_mux): 17 | super().__init__() 18 | self.project_mux = project_mux 19 | self._contents = None 20 | self._pins = project_mux.pins 21 | self._rom_data = None 22 | 23 | 24 | def _send_and_rcv(self, send:int): 25 | self._pins.ui_in.value = send 26 | time.sleep_ms(1) 27 | return self._pins.uo_out.value 28 | 29 | @property 30 | def shuttle(self): 31 | try: 32 | return self.contents['shuttle'] 33 | except: 34 | log.error("ROM has no 'shuttle'") 35 | return '' 36 | 37 | @property 38 | def repo(self): 39 | try: 40 | return self.contents['repo'] 41 | except: 42 | log.error("ROM has no 'repo'") 43 | return '' 44 | 45 | @property 46 | def commit(self): 47 | try: 48 | return self.contents['commit'] 49 | except: 50 | log.error("ROM has no 'commit'") 51 | return '' 52 | 53 | @property 54 | def contents(self): 55 | if self._contents is not None: 56 | return self._contents 57 | 58 | # select project 0 59 | self.project_mux.reset_and_clock_mux(0) 60 | 61 | self._contents = { 62 | 'shuttle': 'unknown', 63 | 'repo': '', 64 | 'commit': '' 65 | } 66 | 67 | # list of expected outputs as 68 | # (SEND, RCV) 69 | magic_expects = [(0, 0x78), (129, 0x0)] 70 | 71 | for magic_pairs in magic_expects: 72 | magic = self._send_and_rcv(magic_pairs[0]) 73 | if magic != magic_pairs[1]: 74 | log.warn(f"No chip rom here? Got 'magic' {hex(magic)} @ {magic_pairs[0]}") 75 | log.info('Fake reporting at tt04 chip') 76 | self.project_mux.disable() 77 | return self._contents 78 | 79 | rom_data = '' 80 | for i in range(32, 128): 81 | byte = self._send_and_rcv(i) 82 | if byte == 0: 83 | break 84 | rom_data += chr(byte) 85 | self._rom_data = rom_data 86 | 87 | if not len(rom_data): 88 | log.warn("ROM data empty") 89 | else: 90 | log.info(f'Got ROM data\n{rom_data}') 91 | for l in rom_data.splitlines(): 92 | try: 93 | k,v = l.split('=') 94 | self._contents[k] = v 95 | except: 96 | log.warn(f"Issue splitting {l}") 97 | pass 98 | log.debug(f"Parsed ROM contents: {self._contents}") 99 | self.project_mux.disable() 100 | return self._contents 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/ttboard/boot/shuttle_properties.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 5, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | 9 | class ShuttleProperties: 10 | def __init__(self, shuttle:str='n/a', repo:str='n/a', commit:str='n/a'): 11 | self._shuttle = shuttle 12 | self._repo = repo 13 | self._commit = commit 14 | 15 | 16 | @property 17 | def shuttle(self): 18 | return self._shuttle 19 | 20 | @property 21 | def repo(self): 22 | return self._repo 23 | 24 | @property 25 | def commit(self): 26 | return self._commit 27 | 28 | 29 | class HardcodedShuttle(ShuttleProperties): 30 | 31 | def __init__(self, shuttle:str, repo:str='', commit:str=''): 32 | super().__init__(shuttle, repo, commit) 33 | 34 | -------------------------------------------------------------------------------- /src/ttboard/cocotb/__init__.py: -------------------------------------------------------------------------------- 1 | from microcotb import * -------------------------------------------------------------------------------- /src/ttboard/cocotb/dut.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 21, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.demoboard import DemoBoard, Pins 8 | import ttboard.util.platform as plat 9 | import microcotb.dut 10 | from microcotb.testcase import TestCase 11 | from microcotb.dut import NoopSignal 12 | from microcotb.dut import Wire 13 | import ttboard.log as logging 14 | 15 | 16 | class PinWrapper(microcotb.dut.PinWrapper): 17 | def __init__(self, name:str, pin): 18 | super().__init__(name, pin) 19 | 20 | @property 21 | def value(self): 22 | return self._pin.value() 23 | 24 | @value.setter 25 | def value(self, set_to:int): 26 | if self._pin.mode != Pins.OUT: 27 | self._pin.mode = Pins.OUT 28 | self._pin.value(set_to) 29 | 30 | class ClockPin(microcotb.dut.PinWrapper): 31 | ''' 32 | clock pin is use *a lot*, needs 33 | to be optimized a little by 34 | calling the low level platform func 35 | ''' 36 | 37 | def __init__(self, name:str, pin): 38 | super().__init__(name, pin) 39 | 40 | @property 41 | def value(self): 42 | return plat.read_clock() 43 | 44 | @value.setter 45 | def value(self, set_to:int): 46 | plat.write_clock(set_to) 47 | 48 | 49 | 50 | class DUT(microcotb.dut.DUT): 51 | TTIOPortNames = ['uo_out', 'ui_in', 'uio_in', 52 | 'uio_out', 'uio_oe_pico'] 53 | 54 | def __init__(self, name:str='DUT'): 55 | super().__init__(name) 56 | tt:DemoBoard = DemoBoard.get() 57 | self.tt = tt # give ourselves access to demoboard object 58 | 59 | # wrap the bare clock pin 60 | self.clk = ClockPin('clk', self.tt.pins.rp_projclk) 61 | self.rst_n = PinWrapper('rst_n', self.tt.rst_n) 62 | 63 | 64 | # provide the I/O ports from DemoBoard 65 | # as attribs here, so we have dut.ui_in.value etc. 66 | 67 | for p in self.TTIOPortNames: 68 | setattr(self, p, getattr(self.tt, p)) 69 | self._log = logging.getLogger(name) 70 | 71 | # ena may be used in existing tests, does nothing 72 | self.ena = NoopSignal('ena', 1) 73 | 74 | 75 | def testing_will_begin(self): 76 | self._log.debug('About to start a test run') 77 | # you should absolutely do this if you override: 78 | if self.tt.is_auto_clocking: 79 | self._log.info('autoclocking... will stop it.') 80 | self.tt.clock_project_stop() 81 | 82 | # and this: make sure is an output 83 | self.tt.pins.rp_projclk.mode = Pins.OUT 84 | 85 | def testing_unit_start(self, test:TestCase): 86 | # override if desired 87 | self._log.debug(f'Test {test.name} about to start') 88 | 89 | 90 | def testing_unit_done(self, test:TestCase): 91 | # override if desired 92 | 93 | if test.failed: 94 | self._log.debug(f'{test.name} failed because: {test.failed_msg}') 95 | else: 96 | self._log.debug(f'{test.name} passed!') 97 | 98 | 99 | def testing_done(self): 100 | # override if desired, but good idea to reset clock pin mode 101 | # or just call super().testing_unit_done(test) to get it done 102 | # make sure is an input 103 | self.tt.pins.rp_projclk.mode = Pins.IN 104 | 105 | self._log.debug('All testing done') 106 | 107 | 108 | def __setattr__(self, name:str, value): 109 | if hasattr(self, name) and name in self.TTIOPortNames: 110 | port = getattr(self, name) 111 | port.value = value 112 | return 113 | super().__setattr__(name, value) 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/ttboard/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/ttboard/config/__init__.py -------------------------------------------------------------------------------- /src/ttboard/config/config_file.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Mar 20, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | from ttboard.config.parser import ConfigParser 9 | 10 | import ttboard.log as logging 11 | log = logging.getLogger(__name__) 12 | 13 | class ConfigFile: 14 | 15 | @classmethod 16 | def string_to_loglevel(cls, loglevname:str): 17 | conv_map = { 18 | 'debug': logging.DEBUG, 19 | 'info': logging.INFO, 20 | 'warn': logging.WARN, 21 | 'warning': logging.WARN, 22 | 'error': logging.ERROR 23 | } 24 | 25 | if loglevname.lower() in conv_map: 26 | return conv_map[loglevname.lower()] 27 | 28 | return None 29 | 30 | def __init__(self, ini_file:str): 31 | self._inifile_path = ini_file 32 | self._ini_file = ConfigParser() 33 | self._ini_file_loaded = False 34 | self.load(ini_file) 35 | 36 | def load(self, filepath:str): 37 | self._inifile_path = filepath 38 | self._ini_file_loaded = False 39 | try: 40 | self._ini_file.read(filepath) 41 | self._ini_file_loaded = True 42 | log.info(f'Loaded config {filepath}') 43 | except: # no FileNotFoundError on uPython 44 | log.warn(f'Could not load config file {filepath}') 45 | 46 | @property 47 | def filepath(self): 48 | return self._inifile_path 49 | 50 | @filepath.setter 51 | def filepath(self, set_to:str): 52 | self.load(set_to) 53 | @property 54 | def is_loaded(self): 55 | return self._ini_file_loaded 56 | 57 | @property 58 | def ini_file(self) -> ConfigParser: 59 | return self._ini_file 60 | 61 | @property 62 | def sections(self): 63 | return self.ini_file.sections() 64 | 65 | def has_section(self, section_name:str) -> bool: 66 | return self.ini_file.has_section(section_name) 67 | 68 | def has_option(self, section_name:str, option_name:str) -> bool: 69 | return self.ini_file.has_option(section_name, option_name) 70 | 71 | def get(self, section_name:str, option_name:str): 72 | return self.ini_file.get(section_name, option_name) 73 | 74 | @property 75 | def log_level(self): 76 | if not self.ini_file.has_option('DEFAULT', 'log_level'): 77 | return None 78 | 79 | levstr = self.ini_file.get('DEFAULT', 'log_level') 80 | return self.string_to_loglevel(levstr) 81 | 82 | -------------------------------------------------------------------------------- /src/ttboard/config/parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 22, 2024 3 | 4 | Minimal and functional version of CPython's ConfigParser module. 5 | 6 | This is a module reimplemented specifically for MicroPython standard library, 7 | with efficient and lean design in mind. Note that this module is likely work 8 | in progress and likely supports just a subset of CPython's corresponding 9 | module. Please help with the development if you are interested in this 10 | module. 11 | 12 | From unmerged PR to uPython, 13 | https://github.com/micropython/micropython-lib/pull/265/files 14 | https://github.com/Mika64/micropython-lib/tree/master/configparser 15 | 16 | @author: Michaël Ricart 17 | 18 | Augmented by Pat Deegan, 2024-01-22, to provide auto conversions to ints/floats/bools, 19 | support comments, etc. 20 | 21 | For TT shuttles, a DEFAULT section has defaults, including (optional) a project 22 | to load on startup. 23 | 24 | Each following section may be defined using 25 | [PROJECT_NAME] 26 | and include 27 | * clock_frequency: Freq, in Hz, to auto-clock 28 | * input_byte: value to set for inputs on startup 29 | * bidir_direction: bits set to one are driven by RP2040 30 | * bidir_byte: actual value to set (only applies to outputs) 31 | * mode: tt mode to set 32 | 33 | ============= sample config =============== 34 | # TT 3.5 shuttle init file 35 | # comment out lines by starting with # 36 | [DEFAULT] 37 | # project: project to load by default 38 | project = tt_um_test 39 | 40 | 41 | 42 | [tt_um_test] 43 | clock_frequency = 10 44 | 45 | [tt_um_psychogenic_neptuneproportional] 46 | clock_frequency = 4e3 47 | input_byte = 0b11001000 48 | 49 | 50 | ============= /sample config =============== 51 | 52 | ''' 53 | 54 | class ConfigParser: 55 | 56 | def __init__(self): 57 | self.convertToBools = { 58 | 'true': True, 59 | 'yes': True, 60 | 'no': False, 61 | 'false': False 62 | } 63 | self.config_dict = {} 64 | 65 | def sections(self): 66 | """Return a list of section names, excluding [DEFAULT]""" 67 | to_return = [section for section in self.config_dict.keys() if not section in "DEFAULT"] 68 | return to_return 69 | 70 | def add_section(self, section): 71 | """Create a new section in the configuration.""" 72 | self.config_dict[section] = {} 73 | 74 | def has_section(self, section): 75 | """Indicate whether the named section is present in the configuration.""" 76 | if section in self.config_dict.keys(): 77 | return True 78 | else: 79 | return False 80 | 81 | def add_option(self, section, option): 82 | """Create a new option in the configuration.""" 83 | if self.has_section(section) and not option in self.config_dict[section]: 84 | self.config_dict[section][option] = None 85 | else: 86 | raise 87 | 88 | def options(self, section): 89 | """Return a list of option names for the given section name.""" 90 | if not section in self.config_dict: 91 | raise 92 | return self.config_dict[section].keys() 93 | 94 | def read(self, filename=None, fp=None): 95 | """Read and parse a filename or a list of filenames.""" 96 | if not fp and not filename: 97 | print("ERROR : no filename and no fp") 98 | raise 99 | elif not fp and filename: 100 | fp = open(filename) 101 | 102 | content = fp.read() 103 | fp.close() 104 | self.config_dict = {line.replace('[','').replace(']',''):{} for line in content.split('\n')\ 105 | if line.startswith('[') and line.endswith(']') 106 | } 107 | 108 | striped_content = [line.strip() for line in content.split('\n')] 109 | for section in self.config_dict.keys(): 110 | start_index = striped_content.index('[%s]' % section) 111 | end_flag = [line for line in striped_content[start_index + 1:] if line.startswith('[')] 112 | if not end_flag: 113 | end_index = None 114 | else: 115 | end_index = striped_content.index(end_flag[0]) 116 | block = striped_content[start_index + 1 : end_index] 117 | commentless_block = [] 118 | for line in block: 119 | if line.startswith('#'): 120 | continue 121 | commentless_block.append(line) 122 | 123 | block = commentless_block 124 | options = [line.split('=')[0].strip() for line in block if '=' in line] 125 | for option in options: 126 | if option.startswith('#'): 127 | continue 128 | start_flag = [line for line in block if line.startswith(option) and '=' in line] 129 | start_index = block.index(start_flag[0]) 130 | end_flag = [line for line in block[start_index + 1:] if '=' in line] 131 | if not end_flag: 132 | end_index = None 133 | else: 134 | end_index = block.index(end_flag[0]) 135 | values = [value.split('=',1)[-1].strip() for value in block[start_index:end_index] if value] 136 | if not values: 137 | values = None 138 | elif len(values) == 1: 139 | values = values[0] 140 | 141 | if isinstance(values, str): 142 | commentPos = values.find('#') 143 | if commentPos >= 0: 144 | values = values[:commentPos].strip() 145 | 146 | 147 | 148 | vInt = None 149 | vFloat = None 150 | try: 151 | radix = 10 152 | if values.startswith('0b'): 153 | radix = 2 154 | elif values.startswith('0x'): 155 | radix = 16 156 | vInt = int(values, radix) 157 | values = vInt 158 | except ValueError: 159 | pass 160 | if vInt is None: 161 | try: 162 | vFloat = float(values) 163 | values = vFloat 164 | except ValueError: 165 | pass 166 | 167 | if vInt is None and vFloat is None: 168 | if values in self.convertToBools: 169 | values = self.convertToBools[values] 170 | 171 | self.config_dict[section][option] = values 172 | 173 | def get(self, section, option): 174 | """Get value of a givenoption in a given section.""" 175 | if not self.has_section(section) \ 176 | or not self.has_option(section,option): 177 | return None 178 | return self.config_dict[section][option] 179 | 180 | def has_option(self, section, option): 181 | """Check for the existence of a given option in a given section.""" 182 | if not section in self.config_dict: 183 | return False 184 | if option in self.config_dict[section].keys(): 185 | return True 186 | else: 187 | return False 188 | 189 | def write(self, filename = None, fp = None): 190 | """Write an .ini-format representation of the configuration state.""" 191 | if not fp and not filename: 192 | print("ERROR : no filename and no fp") 193 | raise 194 | elif not fp and filename: 195 | fp = open(filename,'w') 196 | 197 | for section in self.config_dict.keys(): 198 | fp.write('[%s]\n' % section) 199 | for option in self.config_dict[section].keys(): 200 | fp.write('\n%s =' % option) 201 | values = self.config_dict[section][option] 202 | if type(values) == type([]): 203 | fp.write('\n ') 204 | values = '\n '.join(values) 205 | else: 206 | fp.write(' ') 207 | fp.write(values) 208 | fp.write('\n') 209 | fp.write('\n') 210 | 211 | 212 | def remove_option(self, section, option): 213 | """Remove an option.""" 214 | if not self.has_section(section) \ 215 | or not self.has_option(section,option): 216 | raise 217 | del self.config_dict[section][option] 218 | 219 | def remove_section(self, section): 220 | """Remove a file section.""" 221 | if not self.has_section(section): 222 | raise 223 | del self.config_dict[section] 224 | -------------------------------------------------------------------------------- /src/ttboard/config/user_config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 22, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | import gc 8 | from ttboard.config.parser import ConfigParser 9 | from ttboard.mode import RPMode 10 | 11 | import ttboard.log as logging 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class UserProjectConfig: 16 | ''' 17 | Configuration specific to a project, held in a section with the 18 | project's shuttle name, e.g. 19 | 20 | [tt_um_psychogenic_neptuneproportional] 21 | # set clock to 4kHz 22 | clock_frequency = 4000 23 | # clock config 4k, disp single bits 24 | ui_in = 0b11001000 25 | mode = ASIC_RP_CONTROL 26 | 27 | You can use this to set: 28 | - mode (str) 29 | - start_in_reset (bool) 30 | - ui_in (int) 31 | - uio_oe_pico (int) 32 | - uio_in (int) 33 | - clock_frequency (int) project clock 34 | - rp_clock_frequency (int) RP2040 system clock frequency 35 | 36 | all keys are optional. 37 | 38 | A repr function lets you see the basics, do a print(tt.user_config.tt_um_someproject) to 39 | see more info. 40 | 41 | ''' 42 | opts = ['mode', 'start_in_reset', 'ui_in', 43 | 'uio_oe_pico', 44 | 'uio_in', 45 | 'clock_frequency', 46 | 'rp_clock_frequency'] 47 | 48 | 49 | def __init__(self, section:str, conf:ConfigParser): 50 | self.name = section 51 | for opt in self.opts: 52 | val = None 53 | if conf.has_option(section, opt): 54 | val = conf.get(section, opt) 55 | 56 | setattr(self, opt, val) 57 | 58 | 59 | def has(self, name:str): 60 | return self.get(name) is not None 61 | 62 | def get(self, name:str): 63 | if not hasattr(self, name): 64 | return None 65 | 66 | return getattr(self, name) 67 | 68 | def _properties_dict(self, include_unset:bool=False): 69 | ret = dict() 70 | known_attribs = self.opts 71 | for atr in known_attribs: 72 | v = self.get(atr) 73 | if v is not None or include_unset: 74 | ret[atr] = v 75 | 76 | return ret 77 | 78 | def __repr__(self): 79 | props = self._properties_dict(True) 80 | return f'' 81 | 82 | def __str__(self): 83 | property_strs = [] 84 | pdict = self._properties_dict() 85 | for k in sorted(pdict.keys()): 86 | property_strs.append(f' {k}: {pdict[k]}') 87 | 88 | properties = '\n'.join(property_strs) 89 | return f'{self.name}\n{properties}' 90 | 91 | class UserConfig: 92 | ''' 93 | Encapsulates the configuration for defaults and all the projects, in sections. 94 | The DEFAULT section holds system wide defaults and the default project to load 95 | on startup. 96 | 97 | DEFAULT section may have 98 | 99 | # project: project to load by default 100 | project = tt_um_test 101 | 102 | # start in reset (bool) 103 | start_in_reset = no 104 | 105 | # mode can be any of 106 | # - SAFE: all RP2040 pins inputs 107 | # - ASIC_RP_CONTROL: TT inputs,nrst and clock driven, outputs monitored 108 | # - ASIC_MANUAL_INPUTS: basically same as safe, but intent is clear 109 | mode = ASIC_RP_CONTROL 110 | 111 | # log_level can be one of 112 | # - DEBUG 113 | # - INFO 114 | # - WARN 115 | # - ERROR 116 | log_level = INFO 117 | 118 | 119 | # force_shuttle 120 | # by default, system attempts to figure out which ASIC is on board 121 | # using the chip ROM. This can be a problem if you have something 122 | # connected to the demoboard. If you want to bypass this step and 123 | # manually set the shuttle, uncomment this and set the option to 124 | # a valid shuttle 125 | # force_shuttle = tt05 126 | force_shuttle = tt04 127 | 128 | 129 | # force_demoboard 130 | # System does its best to determine the version of demoboard 131 | # its running on. Override this with 132 | # force_demoboard = tt0* 133 | force_demoboard = tt06 134 | 135 | 136 | Each project section is named [SHUTTLE_PROJECT_NAME] 137 | and will be an instance of, and described by, UserProjectConfig 138 | ''' 139 | 140 | def __init__(self, ini_filepath:str='config.ini'): 141 | self.inifile_path = ini_filepath 142 | conf = ConfigParser() 143 | conf.read(ini_filepath) 144 | self._proj_configs = dict() 145 | for section in conf.sections(): 146 | if section == 'DEFAULT': 147 | continue 148 | self._proj_configs[section] = None # UserProjectConfig(section, conf) 149 | 150 | 151 | def_opts = ['mode', 'project', 'start_in_reset', 'log_level', 152 | 'rp_clock_frequency', 'force_shuttle', 'force_demoboard'] 153 | for opt in def_opts: 154 | val = None 155 | if conf.has_option('DEFAULT', opt): 156 | val = conf.get('DEFAULT', opt) 157 | setattr(self, f'_{opt}', val) 158 | 159 | 160 | conf = None 161 | gc.collect() 162 | 163 | 164 | 165 | def _get_default_option(self, name:str, def_value=None): 166 | v = getattr(self, f'_{name}') 167 | if v is None: 168 | return def_value 169 | return v 170 | @property 171 | def filepath(self): 172 | return self.inifile_path 173 | @property 174 | def default_mode(self): 175 | mode_str = self._get_default_option('mode') 176 | if mode_str is None: 177 | return None 178 | 179 | return RPMode.from_string(mode_str) 180 | 181 | @property 182 | def default_project(self): 183 | return self._get_default_option('project') 184 | 185 | @property 186 | def default_start_in_reset(self): 187 | return self._get_default_option('start_in_reset') 188 | 189 | @property 190 | def default_rp_clock(self): 191 | return self._get_default_option('rp_clock_frequency') 192 | 193 | @property 194 | def force_shuttle(self): 195 | return self._get_default_option('force_shuttle') 196 | 197 | 198 | @property 199 | def force_demoboard(self): 200 | return self._get_default_option('force_demoboard') 201 | 202 | 203 | @classmethod 204 | def string_to_loglevel(cls, loglevname:str): 205 | conv_map = { 206 | 'debug': logging.DEBUG, 207 | 'info': logging.INFO, 208 | 'warn': logging.WARN, 209 | 'warning': logging.WARN, 210 | 'error': logging.ERROR 211 | } 212 | 213 | if loglevname.lower() in conv_map: 214 | return conv_map[loglevname.lower()] 215 | 216 | return None 217 | 218 | 219 | 220 | @property 221 | def log_level(self): 222 | lev_str = self._get_default_option('log_level', 'INFO') 223 | return self.string_to_loglevel(lev_str) 224 | 225 | def has_project(self, name:str): 226 | if name in self._proj_configs: 227 | return True 228 | return False 229 | 230 | def project(self, name:str): 231 | if not self.has_project(name): 232 | return None 233 | 234 | if self._proj_configs[name] is None: 235 | conf = ConfigParser() 236 | conf.read(self.inifile_path) 237 | self._proj_configs[name] = UserProjectConfig(name, conf) 238 | conf = None 239 | gc.collect() 240 | return self._proj_configs[name] 241 | 242 | def __getattr__(self, name): 243 | if self.has_project(name): 244 | return self.project(name) 245 | 246 | @property 247 | def sections(self): 248 | return list(self._proj_configs.keys()) 249 | 250 | def __dir__(self): 251 | return self.sections 252 | 253 | def __repr__(self): 254 | return f'' 255 | 256 | def __str__(self): 257 | def_mode = self._get_default_option('mode') 258 | section_props = '\n'.join(map(lambda psect: str(self.project(psect)), 259 | filter(lambda s: s != 'DEFAULT', self.sections))) 260 | return f'UserConfig {self.filepath}, Defaults:\nproject: {self.default_project}\nmode: {def_mode}\n{section_props}' 261 | 262 | -------------------------------------------------------------------------------- /src/ttboard/globals.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Apr 26, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.mode import RPMode 8 | from ttboard.pins.pins import Pins 9 | from ttboard.project_mux import ProjectMux 10 | 11 | class Globals: 12 | Pins_Singleton = None 13 | ProjectMux_Singleton = None 14 | 15 | @classmethod 16 | def pins(cls, mode=None) -> Pins: 17 | if cls.Pins_Singleton is None: 18 | if mode is None: 19 | mode = RPMode.SAFE 20 | cls.Pins_Singleton = Pins(mode=mode) 21 | 22 | if mode is not None and mode != cls.Pins_Singleton.mode: 23 | cls.Pins_Singleton.mode = mode 24 | 25 | return cls.Pins_Singleton 26 | 27 | 28 | @classmethod 29 | def project_mux(cls, for_shuttle_run:str=None) -> ProjectMux: 30 | 31 | if cls.ProjectMux_Singleton is None: 32 | cls.ProjectMux_Singleton = ProjectMux(cls.pins(), for_shuttle_run) 33 | elif for_shuttle_run is not None: 34 | raise RuntimeError('Only expecting a shuttle on first call of Globals.project_mux') 35 | 36 | return cls.ProjectMux_Singleton 37 | -------------------------------------------------------------------------------- /src/ttboard/log.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 22, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.util.platform import IsRP2040 8 | import ttboard.util.colors as colors 9 | import ttboard.util.time as time 10 | import gc 11 | RPLoggers = dict() 12 | DefaultLogLevel = 20 # info by default 13 | LoggingPrefix = 'BOOT' 14 | if IsRP2040: 15 | # no logging support, add something basic 16 | DEBUG = 10 17 | INFO = 20 18 | WARN = 30 19 | WARNING = 30 20 | ERROR = 40 21 | class Logger: 22 | OutFile = None 23 | colorMap = { 24 | 10: 'yellow', 25 | 20: 'green', 26 | 30: 'yellow', 27 | 40: 'red' 28 | } 29 | 30 | @classmethod 31 | def set_out_file(cls, path_to:str=None): 32 | if cls.OutFile is not None: 33 | cls.OutFile.close() 34 | cls.OutFile = None 35 | 36 | if path_to is not None: 37 | cls.OutFile = open(path_to, 'w') 38 | 39 | 40 | def __init__(self, name): 41 | self.name = name 42 | self.loglevel = DefaultLogLevel 43 | 44 | def out(self, s, level:int): 45 | global LoggingPrefix 46 | if self.loglevel <= level: 47 | if LoggingPrefix: 48 | prefix = LoggingPrefix 49 | else: 50 | prefix = self.name 51 | if self.OutFile is not None: 52 | print(f'{prefix}: {s}', file=self.OutFile) 53 | 54 | print(f'{prefix}: {colors.color(s, self.colorMap[level])}') 55 | 56 | def debug(self, s): 57 | self.out(s, DEBUG) 58 | def info(self, s): 59 | self.out(s, INFO) 60 | def warn(self, s): 61 | self.out(s, WARN) 62 | def warning(self, s): 63 | self.out(s, WARNING) 64 | def error(self, s): 65 | self.out(s, ERROR) 66 | 67 | def getChild(self, nm): 68 | return getLogger(f'{self.name}.{nm}') 69 | 70 | def dumpMem(prefix:str='Free mem'): 71 | print(f"{prefix}: {gc.mem_free()}") 72 | 73 | DeltaTicksStart = time.ticks_ms() 74 | def dumpTicksMs(msg:str='ticks'): 75 | print(f"{msg}: {time.ticks_ms()}") 76 | 77 | def ticksStart(): 78 | global DeltaTicksStart 79 | DeltaTicksStart = time.ticks_ms() 80 | 81 | def dumpTicksMsDelta(msg:str='ticks'): 82 | tnow = time.ticks_ms() 83 | print(f"{msg}: {time.ticks_diff(tnow, DeltaTicksStart)}") 84 | 85 | def getLogger(name:str): 86 | global RPLoggers 87 | if name not in RPLoggers: 88 | RPLoggers[name] = Logger(name) 89 | return RPLoggers[name] 90 | 91 | def basicConfig(level:int=None, filename:str=None): 92 | global DefaultLogLevel 93 | global RPLoggers 94 | 95 | if level is not None: 96 | DefaultLogLevel = level 97 | for logger in RPLoggers.values(): 98 | logger.loglevel = level 99 | 100 | Logger.set_out_file(filename) 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | else: 110 | from logging import * 111 | def dumpMem(prefix:str='Free mem'): 112 | print(f'{prefix}: infinity') 113 | def dumpTicksMs(msg:str='ticks ms'): 114 | print(f"{msg}: 0") 115 | 116 | def ticksStart(): 117 | return 118 | 119 | def dumpTicksMsDelta(msg:str='ticks'): 120 | print(f"{msg}: 0") -------------------------------------------------------------------------------- /src/ttboard/mode.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | class ModeBase: 8 | SAFE = 0 9 | ASIC_RP_CONTROL = 1 10 | ASIC_MANUAL_INPUTS = 2 11 | 12 | @classmethod 13 | def modemap(cls): 14 | modeMap = { 15 | 'SAFE': cls.SAFE, 16 | 'ASIC_RP_CONTROL': cls.ASIC_RP_CONTROL, 17 | 'ASIC_MANUAL_INPUTS': cls.ASIC_MANUAL_INPUTS 18 | } 19 | return modeMap 20 | 21 | @classmethod 22 | def from_string(cls, s:str): 23 | modeMap = cls.modemap() 24 | if s is None or not hasattr(s, 'upper'): 25 | return None 26 | sup = s.upper() 27 | if sup not in modeMap: 28 | # should we raise here? 29 | return None 30 | 31 | return modeMap[sup] 32 | 33 | @classmethod 34 | def namemap(cls): 35 | nameMap = { 36 | cls.SAFE: 'SAFE', 37 | cls.ASIC_RP_CONTROL: 'ASIC_RP_CONTROL', 38 | cls.ASIC_MANUAL_INPUTS: 'ASIC_MANUAL_INPUTS', 39 | } 40 | return nameMap 41 | @classmethod 42 | def to_string(cls, mode:int): 43 | nameMap = cls.namemap() 44 | if mode in nameMap: 45 | return nameMap[mode] 46 | 47 | return 'UNKNOWN' 48 | 49 | class RPMode(ModeBase): 50 | ''' 51 | Poor man's enum, allowing for 52 | RPMode.MODE notation and code completion 53 | where MODE is one of: 54 | SAFE 55 | ASIC_RP_CONTROL 56 | ASIC_MANUAL_INPUTS 57 | ''' 58 | pass 59 | 60 | class RPModeDEVELOPMENT(ModeBase): 61 | ''' 62 | Danger zone. Includes the 63 | STANDALONE mode, which drives outputs, conflicting with any ASIC present, 64 | so moved here. 65 | ''' 66 | STANDALONE = 3 67 | 68 | 69 | @classmethod 70 | def modemap(cls): 71 | modeMap = { 72 | 'SAFE': cls.SAFE, 73 | 'ASIC_RP_CONTROL': cls.ASIC_RP_CONTROL, 74 | 'ASIC_MANUAL_INPUTS': cls.ASIC_MANUAL_INPUTS, 75 | 'STANDALONE': cls.STANDALONE 76 | } 77 | return modeMap 78 | 79 | @classmethod 80 | def namemap(cls): 81 | nameMap = { 82 | cls.SAFE: 'SAFE', 83 | cls.ASIC_RP_CONTROL: 'ASIC_RP_CONTROL', 84 | cls.ASIC_MANUAL_INPUTS: 'ASIC_MANUAL_INPUTS', 85 | cls.STANDALONE: 'STANDALONE' 86 | } 87 | return nameMap 88 | -------------------------------------------------------------------------------- /src/ttboard/pins/__init__.py: -------------------------------------------------------------------------------- 1 | # from ttboard.pins.pins import Pins -------------------------------------------------------------------------------- /src/ttboard/pins/desktop_pin.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 22, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | import logging 9 | log = logging.getLogger(__name__) 10 | 11 | class Pin: 12 | ''' 13 | Stub class for desktop testing, 14 | i.e. where machine module DNE 15 | ''' 16 | OUT = 1 17 | IN = 2 18 | IRQ_FALLING = 3 19 | IRQ_RISING = 4 20 | PULL_DOWN = 5 21 | PULL_UP = 6 22 | OPEN_DRAIN = 7 23 | def __init__(self, gpio:int, direction:int=0, mode:int=0, pull:int=0): 24 | self.gpio = gpio 25 | self.dir = direction 26 | self.val = 0 27 | 28 | def value(self, setTo:int = None): 29 | if setTo is not None: 30 | log.debug(f'Setting GPIO {self.gpio} to {setTo}') 31 | self.val = setTo 32 | return self.val 33 | 34 | def init(self, direction:int, pull:int=None): 35 | log.debug(f'Setting GPIO {self.gpio} to direction {direction}') 36 | self.dir = direction 37 | 38 | def toggle(self): 39 | if self.val: 40 | self.val = 0 41 | else: 42 | self.val = 1 43 | 44 | def __call__(self, value:int=None): 45 | if value is not None: 46 | self.val = value 47 | return 48 | return self.val 49 | -------------------------------------------------------------------------------- /src/ttboard/pins/gpio_map.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | from ttboard.pins.upython import Pin 9 | from ttboard.mode import RPMode, RPModeDEVELOPMENT 10 | class GPIOMapBase: 11 | 12 | @classmethod 13 | def project_clock(cls): 14 | raise RuntimeError('not implemented') 15 | 16 | @classmethod 17 | def project_reset(cls): 18 | raise RuntimeError('not implemented') 19 | 20 | @classmethod 21 | def ctrl_increment(cls): 22 | raise RuntimeError('not implemented') 23 | 24 | @classmethod 25 | def ctrl_enable(cls): 26 | raise RuntimeError('not implemented') 27 | 28 | @classmethod 29 | def ctrl_reset(cls): 30 | raise RuntimeError('not implemented') 31 | 32 | 33 | @classmethod 34 | def demoboard_uses_mux(cls): 35 | return False 36 | 37 | @classmethod 38 | def mux_select(cls): 39 | raise RuntimeError('not implemented') 40 | 41 | @classmethod 42 | def muxed_pairs(cls): 43 | raise RuntimeError('not implemented') 44 | 45 | @classmethod 46 | def muxed_pinmode_map(cls, rpmode:int): 47 | raise RuntimeError('not implemented') 48 | 49 | 50 | @classmethod 51 | def always_outputs(cls): 52 | return [ 53 | # 'nproject_rst', 54 | # 'rp_projclk', -- don't do this during "safe" operation 55 | #'ctrl_ena' 56 | ] 57 | 58 | @classmethod 59 | def default_pull(cls, pin): 60 | # both of these now go through MUX and 61 | # must therefore rely on external/physical 62 | # pull-ups. the nProject reset has PU in 63 | # switch debounce, cena... may be a problem 64 | # (seems it has a pull down on board?) 65 | #if pin in ["nproject_rst", "ctrl_ena"]: 66 | # return Pin.PULL_UP 67 | return Pin.PULL_DOWN 68 | 69 | @classmethod 70 | def get_raw_pin(cls, pin:str, direction:int) -> Pin: 71 | 72 | pin_ionum = None 73 | if isinstance(pin, int): 74 | pin_ionum = pin 75 | else: 76 | pin_name_to_io = cls.all() 77 | if pin not in pin_name_to_io: 78 | return None 79 | pin_ionum = pin_name_to_io[pin] 80 | 81 | return Pin(pin_ionum, direction) 82 | 83 | 84 | @classmethod 85 | def all_common(cls): 86 | retDict = { 87 | "rp_projclk": cls.RP_PROJCLK, 88 | "ui_in0": cls.UI_IN0, 89 | "ui_in1": cls.UI_IN1, 90 | "ui_in2": cls.UI_IN2, 91 | "ui_in3": cls.UI_IN3, 92 | "uo_out4": cls.UO_OUT4, 93 | "uo_out5": cls.UO_OUT5, 94 | "uo_out6": cls.UO_OUT6, 95 | "uo_out7": cls.UO_OUT7, 96 | "ui_in4": cls.UI_IN4, 97 | "ui_in5": cls.UI_IN5, 98 | "ui_in6": cls.UI_IN6, 99 | "ui_in7": cls.UI_IN7, 100 | "uio0": cls.UIO0, 101 | "uio1": cls.UIO1, 102 | "uio2": cls.UIO2, 103 | "uio3": cls.UIO3, 104 | "uio4": cls.UIO4, 105 | "uio5": cls.UIO5, 106 | "uio6": cls.UIO6, 107 | "uio7": cls.UIO7, 108 | "rpio29": cls.RPIO29 109 | } 110 | return retDict 111 | 112 | class GPIOMapTT04(GPIOMapBase): 113 | ''' 114 | A place to store 115 | name -> GPIO # 116 | along with some class-level utilities, mainly for internal use. 117 | 118 | This allows for low-level control, if you wish, e.g. 119 | 120 | myrawpin = machine.Pin(GPIOMap.UO_OUT4, machine.Pin.OUT) 121 | 122 | The only caveat is that some of these are duplexed through 123 | the MUX, and named accordingly (e.g. nCRST_UO_OUT2) 124 | ''' 125 | RP_PROJCLK = 0 126 | HK_CSB = 1 127 | HK_SCK = 2 128 | SDI_nPROJECT_RST = 3 # SDI_UO_OUT0 = 3 129 | HK_SDO = 4 # SDO_UO_OUT1 = 4 130 | UO_OUT0 = 5 # nPROJECT_RST = 5 131 | CTRL_ENA_UO_OUT1 = 6 # CTRL_ENA = 6 132 | nCRST_UO_OUT2 = 7 133 | CINC_UO_OUT3 = 8 134 | UI_IN0 = 9 135 | UI_IN1 = 10 136 | UI_IN2 = 11 137 | UI_IN3 = 12 138 | UO_OUT4 = 13 139 | UO_OUT5 = 14 140 | UO_OUT6 = 15 141 | UO_OUT7 = 16 142 | UI_IN4 = 17 143 | UI_IN5 = 18 144 | UI_IN6 = 19 145 | UI_IN7 = 20 146 | UIO0 = 21 147 | UIO1 = 22 148 | UIO2 = 23 149 | UIO3 = 24 150 | UIO4 = 25 151 | UIO5 = 26 152 | UIO6 = 27 153 | UIO7 = 28 154 | RPIO29 = 29 155 | 156 | 157 | @classmethod 158 | def project_clock(cls): 159 | return cls.RP_PROJCLK 160 | 161 | @classmethod 162 | def project_reset(cls): 163 | return cls.SDI_nPROJECT_RST 164 | 165 | @classmethod 166 | def ctrl_increment(cls): 167 | return cls.CINC_UO_OUT3 168 | 169 | @classmethod 170 | def ctrl_enable(cls): 171 | return cls.CTRL_ENA_UO_OUT1 172 | 173 | @classmethod 174 | def ctrl_reset(cls): 175 | return cls.nCRST_UO_OUT2 176 | 177 | 178 | @classmethod 179 | def demoboard_uses_mux(cls): 180 | return True 181 | 182 | 183 | @classmethod 184 | def mux_select(cls): 185 | return cls.HK_CSB 186 | 187 | 188 | 189 | @classmethod 190 | def all(cls): 191 | retDict = cls.all_common() 192 | retDict.update({ 193 | "hk_csb": cls.HK_CSB, 194 | "hk_sck": cls.HK_SCK, 195 | "sdi_nprojectrst": cls.SDI_nPROJECT_RST, # "sdi_out0": cls.SDI_UO_OUT0, 196 | "hk_sdo": cls.HK_SDO, # "sdo_out1": cls.SDO_UO_OUT1, 197 | "uo_out0": cls.UO_OUT0, 198 | "cena_uo_out1": cls.CTRL_ENA_UO_OUT1, # "ctrl_ena": cls.CTRL_ENA, 199 | "ncrst_uo_out2": cls.nCRST_UO_OUT2, 200 | "cinc_uo_out3": cls.CINC_UO_OUT3, 201 | }) 202 | return retDict 203 | @classmethod 204 | def muxed_pairs(cls): 205 | mpairnames = [ 206 | 'sdi_nprojectrst', 207 | 'cena_uo_out1', 208 | 'ncrst_uo_out2', 209 | 'cinc_uo_out3' 210 | ] 211 | retVals = {} 212 | for mpair in mpairnames: 213 | retVals[mpair] = mpair.split('_', 1) 214 | 215 | return retVals; 216 | 217 | 218 | @classmethod 219 | def muxed_pinmode_map(cls, rpmode:int): 220 | 221 | pinModeMap = { 222 | 'nprojectrst': Pin.IN, # "special" pin -- In == pulled-up, NOT reset 223 | 'sdi': Pin.OUT, 224 | 'cena': Pin.OUT, 225 | 'uo_out1': Pin.IN, 226 | 227 | 'ncrst': Pin.OUT, 228 | 'uo_out2': Pin.IN, 229 | 230 | 231 | 'cinc': Pin.OUT, 232 | 'uo_out3': Pin.IN 233 | } 234 | if rpmode == RPModeDEVELOPMENT.STANDALONE: 235 | for k in pinModeMap.keys(): 236 | if k.startswith('uo_out'): 237 | pinModeMap[k] = Pin.OUT 238 | 239 | 240 | return pinModeMap 241 | 242 | 243 | 244 | class GPIOMapTT06(GPIOMapBase): 245 | RP_PROJCLK = 0 246 | PROJECT_nRST = 1 247 | CTRL_SEL_nRST = 2 248 | CTRL_SEL_INC = 3 249 | CTRL_SEL_ENA = 4 250 | UO_OUT0 = 5 251 | UO_OUT1 = 6 252 | UO_OUT2 = 7 253 | UO_OUT3 = 8 254 | UI_IN0 = 9 255 | UI_IN1 = 10 256 | UI_IN2 = 11 257 | UI_IN3 = 12 258 | UO_OUT4 = 13 259 | UO_OUT5 = 14 260 | UO_OUT6 = 15 261 | UO_OUT7 = 16 262 | UI_IN4 = 17 263 | UI_IN5 = 18 264 | UI_IN6 = 19 265 | UI_IN7 = 20 266 | UIO0 = 21 267 | UIO1 = 22 268 | UIO2 = 23 269 | UIO3 = 24 270 | UIO4 = 25 271 | UIO5 = 26 272 | UIO6 = 27 273 | UIO7 = 28 274 | RPIO29 = 29 275 | 276 | # Enable a workaround for a PCB error in TT07 carrier board, which swapped the ctrl_sel_inc and ctrl_sel_nrst lines: 277 | tt07_cb_fix = False 278 | 279 | @classmethod 280 | def project_clock(cls): 281 | return cls.RP_PROJCLK 282 | 283 | @classmethod 284 | def project_reset(cls): 285 | return cls.PROJECT_nRST 286 | 287 | 288 | @classmethod 289 | def ctrl_increment(cls): 290 | return cls.CTRL_SEL_INC 291 | 292 | @classmethod 293 | def ctrl_enable(cls): 294 | return cls.CTRL_SEL_ENA 295 | 296 | @classmethod 297 | def ctrl_reset(cls): 298 | return cls.CTRL_SEL_nRST 299 | 300 | @classmethod 301 | def always_outputs(cls): 302 | return [ 303 | 'cinc', 304 | 'cena', 305 | 'ncrst' 306 | ] 307 | @classmethod 308 | def all(cls): 309 | retDict = cls.all_common() 310 | 311 | retDict.update({ 312 | 'nprojectrst': cls.PROJECT_nRST, 313 | 'cinc': cls.CTRL_SEL_INC, 314 | 'cena': cls.CTRL_SEL_ENA, 315 | 'ncrst': cls.CTRL_SEL_nRST, 316 | 'uo_out0': cls.UO_OUT0, 317 | 'uo_out1': cls.UO_OUT1, 318 | 'uo_out2': cls.UO_OUT2, 319 | 'uo_out3': cls.UO_OUT3 320 | }) 321 | 322 | if cls.tt07_cb_fix: 323 | retDict['cinc'], retDict['ncrst'] = retDict['ncrst'], retDict['cinc'] 324 | 325 | return retDict 326 | 327 | GPIOMap = GPIOMapTT04 328 | -------------------------------------------------------------------------------- /src/ttboard/pins/mux_control.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.pins.standard import StandardPin 8 | from ttboard.pins.upython import Pin 9 | 10 | 11 | import ttboard.log as logging 12 | log = logging.getLogger(__name__) 13 | 14 | class MuxControl: 15 | ''' 16 | The MUX is a 4-Bit 1-of-2, so has 4 pairs of 17 | GPIO that are selected in unison by a single 18 | signal. 19 | 20 | The circuit is organized such that either: 21 | * all the outputs tied to the mux are selected; or 22 | * all the control signals are selected 23 | 24 | and this selector is actually the HK SPI nCS line. 25 | 26 | These facts are included here for reference, the 27 | MuxControl object is actually unaware of the 28 | specifics and this stuff happens either transparently 29 | or at higher levels 30 | 31 | ''' 32 | def __init__(self, name:str, gpioIdx, defValue:int=1): 33 | self.ctrlpin = StandardPin(name, gpioIdx, Pin.OUT) 34 | self.ctrlpin(defValue) 35 | self.currentValue = defValue 36 | self._muxedPins = [] 37 | 38 | def mode_project_IO(self): 39 | self.select_high() 40 | def mode_admin(self): 41 | self.select_low() 42 | 43 | def add_muxed(self, muxd): 44 | self._muxedPins.append(muxd) 45 | 46 | @property 47 | def selected(self): 48 | return self.currentValue 49 | 50 | def select(self, value:int): 51 | if value == self.currentValue: 52 | return 53 | 54 | # set the control pin according to 55 | # value. Note that we need to make 56 | # sure we switch ALL muxed pins over 57 | # otherwise we might end up with contention 58 | # as two sides think they're outputs 59 | # safety 60 | for mp in self._muxedPins: 61 | mp.current_dir = Pin.IN 62 | 63 | if value: 64 | log.debug('Mux CTRL selecting HIGH set (proj IO)') 65 | self.ctrlpin(1) 66 | for mp in self._muxedPins: 67 | pDeets = mp.high_pin 68 | mp.current_dir = pDeets.dir 69 | else: 70 | log.debug('Mux CTRL selecting LOW set (admin)') 71 | self.ctrlpin(0) 72 | for mp in self._muxedPins: 73 | pDeets = mp.low_pin 74 | mp.current_dir = pDeets.dir 75 | 76 | self.currentValue = value 77 | 78 | def select_high(self): 79 | self.select(1) 80 | 81 | def select_low(self): 82 | self.select(0) 83 | 84 | -------------------------------------------------------------------------------- /src/ttboard/pins/muxed.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | from ttboard.pins.mux_control import MuxControl 9 | from ttboard.pins.standard import StandardPin 10 | from ttboard.pins.upython import Pin 11 | 12 | import ttboard.log as logging 13 | log = logging.getLogger(__name__) 14 | class MuxedPinInfo: 15 | ''' 16 | MuxedPinInfo 17 | Details about a pin that is behind the 2:1 MUX 18 | ''' 19 | def __init__(self, name:str, muxSelect:bool, direction): 20 | self.name = name 21 | self.select = muxSelect 22 | self.dir = direction 23 | 24 | 25 | class MuxedSelection: 26 | def __init__(self, parent_pin, pInfo:MuxedPinInfo): 27 | self._parent = parent_pin 28 | self.info = pInfo 29 | 30 | @property 31 | def name(self): 32 | return self.info.name 33 | 34 | @property 35 | def direction(self): 36 | return self.info.dir 37 | 38 | 39 | @direction.setter 40 | def direction(self, set_to:int): 41 | cur_dir = self.info.dir 42 | 43 | if set_to == Pin.IN or set_to == Pin.OUT: 44 | self.info.dir = set_to 45 | else: 46 | raise ValueError(f'Invalid direction {set_to}') 47 | 48 | if set_to != cur_dir: 49 | if self._parent.selected == self.info.select: 50 | # we are selected and have changed direction 51 | self._parent.current_dir = set_to 52 | 53 | @property 54 | def info_string(self): 55 | direction = 'OUT' 56 | if self.direction == Pin.IN: 57 | direction = 'IN' 58 | return f'{self.info.name}[{direction}]' 59 | 60 | 61 | @property 62 | def mode(self): 63 | return self.direction 64 | 65 | @mode.setter 66 | def mode(self, setMode:int): 67 | # we override mode here to use direction 68 | # such that if you manually tweak this 69 | # it will be remembered as we switch 70 | # the mux back and forth 71 | self.direction = setMode 72 | 73 | @property 74 | def mode_str(self): 75 | return self._parent.mode_str 76 | 77 | 78 | # punting low-level pin things to parent 79 | # but this is risky-business -- not currently 80 | # maintained as part of the muxed pin state 81 | # just happening on the real GPIO, so use 82 | # wisely 83 | @property 84 | def pull(self): 85 | return self._parent.pull 86 | 87 | @pull.setter 88 | def pull(self, set_pull:int): 89 | self._parent.pull = set_pull 90 | 91 | @property 92 | def drive(self): 93 | return self._parent.drive 94 | 95 | @drive.setter 96 | def drive(self, set_drive:int): 97 | self._parent.driver = set_drive 98 | 99 | @property 100 | def gpio_num(self): 101 | return self._parent.gpio_num 102 | 103 | def value(self, value:int=None): 104 | return self(value) 105 | 106 | 107 | def __call__(self, value:int=None): 108 | self._parent.select_pin(self.info) 109 | return self._parent(value) 110 | 111 | def __repr__(self): 112 | return f'' 113 | 114 | class MuxedPin(StandardPin): 115 | ''' 116 | A GPIO that actually maps to two logical pins, 117 | through the MUX, e.g. GPIO 8 which goes through 118 | MUX to either cinc or out3. 119 | 120 | The purpose is to allow transparent auto-switching of mux via 121 | access so they will behave as the other pins in the system. 122 | 123 | E.g. reading Pins.out0() will automatically switch the MUX over 124 | if required before returning the read value. 125 | 126 | ''' 127 | def __init__(self, name:str, muxCtrl:MuxControl, 128 | gpio:int, pinL:MuxedPinInfo, pinH:MuxedPinInfo): 129 | super().__init__(name, gpio, pinH.dir) 130 | self.ctrl = muxCtrl 131 | self._current_dir = None 132 | 133 | #self._muxHighPin = pinH 134 | #self._muxLowPin = pinL 135 | self._sel_high = MuxedSelection(self, pinH) 136 | self._sel_low = MuxedSelection(self, pinL) 137 | setattr(self, self._sel_high.name, self._sel_high) 138 | #setattr(self, self._muxHighPin.name, 139 | # self._pinFunc(self._muxHighPin)) 140 | 141 | 142 | setattr(self, self._sel_low.name, self._sel_low) 143 | #setattr(self, self._muxLowPin.name, self._pinFunc(self._muxLowPin)) 144 | 145 | @property 146 | def high_pin(self) -> MuxedPinInfo: 147 | return self._sel_high.info 148 | 149 | @property 150 | def low_pin(self) -> MuxedPinInfo: 151 | return self._sel_low.info 152 | 153 | @property 154 | def current_dir(self): 155 | return self._current_dir 156 | 157 | @current_dir.setter 158 | def current_dir(self, setTo): 159 | if self._current_dir == setTo: 160 | return 161 | self._current_dir = setTo 162 | self.mode = setTo 163 | 164 | log.debug(f'Set dir to {self.mode_str}') 165 | 166 | def select_pin(self, pInfo:MuxedPinInfo): 167 | self.ctrl.select(pInfo.select) 168 | self.current_dir = pInfo.dir 169 | 170 | @property 171 | def selected(self): 172 | return self.ctrl.selected 173 | 174 | @property 175 | def selected_str(self): 176 | sel = self.selected 177 | if sel == self.high_pin.select: 178 | return 'HPIN' 179 | elif sel == self.low_pin.select: 180 | return 'LPIN' 181 | else: 182 | return '???PIN' 183 | 184 | def __repr__(self): 185 | return f'' 186 | 187 | def __str__(self): 188 | return f'MuxedPin {self.name} {self.gpio_num} ({self.selected_str} pin selected, now as {self.mode_str}) {self._sel_low.info_string}/{self._sel_high.info_string}' 189 | -------------------------------------------------------------------------------- /src/ttboard/pins/pins.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 6, 2024 3 | 4 | Main purposes of this module are to: 5 | 6 | * provide named access to pins 7 | * provide a consistent and transparent interface 8 | to standard and MUXed pins 9 | * provide utilities to handle logically related pins as ports (e.g. all the 10 | INn pins as a list or a byte) 11 | * augment the machine.Pin to give us access to mode, pull etc 12 | * handle init sanely 13 | 14 | TLDR 15 | 1) get pins 16 | p = Pins(RPMode.ASIC_RP_CONTROL) # monitor/control ASIC 17 | 18 | 2) play with pins 19 | print(p.pins.uo_out2()) # read 20 | p.ui_in[3] = 1 # set 21 | p.ui_in.value = 0x42 # set all INn 22 | p.pins.uio_in1.mode = Pins.OUT # set mode 23 | p.uio_in[1] = 1 # set output 24 | 25 | 26 | 27 | @author: Pat Deegan 28 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 29 | ''' 30 | 31 | from ttboard.mode import RPMode, RPModeDEVELOPMENT 32 | 33 | import ttboard.util.platform as platform 34 | from ttboard.pins.upython import Pin 35 | import ttboard.pins.gpio_map as gp 36 | from ttboard.pins.standard import StandardPin 37 | from ttboard.pins.muxed import MuxedPin, MuxedPinInfo 38 | from ttboard.pins.mux_control import MuxControl 39 | 40 | from ttboard.ports.io import IO as VerilogIOPort 41 | from ttboard.ports.oe import OutputEnable as VerilogOEPort 42 | 43 | 44 | import ttboard.log as logging 45 | log = logging.getLogger(__name__) 46 | 47 | 48 | 49 | class Pins: 50 | ''' 51 | This object handles setup and provides uniform named 52 | access to all logical pins, along with some utilities. 53 | See below for actual direction configuration of various pins. 54 | 55 | Tab-completion in a REPL will show you all the matching 56 | named attributes, and auto-complete as usual. 57 | 58 | # Pins: 59 | For regular read/writes to pins, access them on this object 60 | by name, as a function. An empty call is a read, a call with 61 | a parameter is a write. E.g. 62 | 63 | bp = Pins(...) 64 | bp.pins.uo_out1() # reads the value 65 | bp.ui_in[3] = 1 # sets the value 66 | # can also use normal machine.Pin functions like 67 | bp.pins.ui_in3.off() 68 | # or 69 | bp.pins.ui_in3.irq(...) 70 | 71 | Though you shouldn't need it (the pin objects support everything 72 | machine.Pin does), if you want low-level access to the 73 | bare machine.Pin object, it is also available by simply 74 | prepending the name with "pin_", e.g. 75 | 76 | bp.pin_out1.irq(handler=whatever, trigger=Pin.IRQ_FALLING) 77 | 78 | Just beware if accessing the muxed pins (e.g. cinc_out3). 79 | 80 | # Named Ports and Utilities: 81 | In addition to single pin access, named ports are available for 82 | input, output and bidirectional pins. 83 | 84 | bp.inputs is an array of [in0, in1, ... in7] 85 | bp.outputs is an array of [out0, out1, ... out7] 86 | bp.bidir is an array of [uio0, uio1, ... uio7] 87 | 88 | You may also access arrays of the raw machine.Pin by using _pins, e.g 89 | bp.input_pins 90 | 91 | Finally, the _byte properties allow you to read or set the entire 92 | port as a byte 93 | 94 | print(bp.uo_out.value) 95 | # or set 96 | bp.ui_in.value = 0xAA 97 | 98 | # Pin DIRECTION 99 | So, from the RP2040's perspective, is out2 configured to read (an 100 | input) or to write (an output)? 101 | 102 | These signals are all named according to the TT ASIC. So, 103 | under normal/expected operation, it is the ASIC that writes to OUTn 104 | and reads from INn. The bidirs... who knows. 105 | 106 | What you DON'T want is contention, e.g. the ASIC trying to 107 | drive out5 HIGH and the RP shorting it LOW. 108 | 109 | So this class has 3 modes of pin init at startup: 110 | * RPMode.SAFE, the default, which has every pin as an INPUT, no pulls 111 | * RPMode.ASIC_RP_CONTROL, for use with ASICs, where it watches the OUTn 112 | (configured as inputs) and can drive the INn and tickle the 113 | ASIC inputs (configured as outputs) 114 | * RPMode.STANDALONE: where OUTn is an OUTPUT, INn is an input, useful 115 | for playing with the board _without_ an ASIC onboard 116 | 117 | To override the safe mode default, create the instance using 118 | p = Pins(mode=Pins.MODE_LISTENER) # for example. 119 | 120 | 121 | 122 | ''' 123 | # convenience: aliasing here 124 | IN = Pin.IN 125 | IRQ_FALLING = Pin.IRQ_FALLING 126 | IRQ_RISING = Pin.IRQ_RISING 127 | OPEN_DRAIN = Pin.OPEN_DRAIN 128 | OUT = Pin.OUT 129 | PULL_DOWN = Pin.PULL_DOWN 130 | PULL_UP = Pin.PULL_UP 131 | 132 | # MUX pin is especial... 133 | muxName = 'hk_csb' # special pin 134 | 135 | 136 | def __init__(self, mode:int=RPMode.SAFE): 137 | self.dieOnInputControlSwitchHigh = True 138 | self._mode = None 139 | self._allpins = {} 140 | if gp.GPIOMap.demoboard_uses_mux(): 141 | self.muxCtrl = MuxControl(self.muxName, gp.GPIOMap.mux_select(), Pin.OUT) 142 | # special case: give access to mux control/HK nCS pin 143 | self.hk_csb = self.muxCtrl.ctrlpin 144 | self.pin_hk_csb = self.muxCtrl.ctrlpin.raw_pin 145 | self._allpins['hk_csb'] = self.hk_csb 146 | 147 | self._init_ioports() 148 | self.mode = mode 149 | 150 | 151 | 152 | def _init_ioports(self): 153 | # Note: these are named according the the ASICs point of view 154 | # we can write ui_in, we read uo_out 155 | port_defs = [ 156 | ('uo_out', 8, platform.read_uo_out_byte, None), 157 | ('ui_in', 8, platform.read_ui_in_byte, platform.write_ui_in_byte), 158 | ('uio_in', 8, platform.read_uio_byte, platform.write_uio_byte), 159 | ('uio_out', 8, platform.read_uio_byte, None) 160 | ] 161 | self._ports = dict() 162 | for pd in port_defs: 163 | setattr(self, pd[0], VerilogIOPort(*pd)) 164 | 165 | 166 | self.uio_oe_pico = VerilogOEPort('uio_oe_pico', 8, 167 | platform.read_uio_outputenable, 168 | platform.write_uio_outputenable) 169 | 170 | 171 | 172 | @property 173 | def demoboard_uses_mux(self): 174 | return gp.GPIOMap.demoboard_uses_mux() 175 | 176 | @property 177 | def all(self): 178 | return list(self._allpins.values()) 179 | 180 | 181 | @property 182 | def mode(self): 183 | return self._mode 184 | 185 | @mode.setter 186 | def mode(self, set_mode:int): 187 | startupMap = { 188 | RPModeDEVELOPMENT.STANDALONE: self.begin_standalone, 189 | RPMode.ASIC_RP_CONTROL: self.begin_asiconboard, 190 | RPMode.ASIC_MANUAL_INPUTS: self.begin_asic_manual_inputs, 191 | RPMode.SAFE: self.begin_safe 192 | } 193 | 194 | if set_mode not in startupMap: 195 | set_mode = RPMode.SAFE 196 | 197 | self._mode = set_mode 198 | log.info(f'Setting mode to {RPMode.to_string(set_mode)}') 199 | beginFunc = startupMap[set_mode] 200 | beginFunc() 201 | if set_mode == RPMode.ASIC_RP_CONTROL: 202 | self.ui_in.byte_write = platform.write_ui_in_byte 203 | self.uio_in.byte_write = platform.write_uio_byte 204 | else: 205 | self.ui_in.byte_write = None 206 | self.uio_in.byte_write = None 207 | 208 | 209 | def begin_inputs_all(self): 210 | 211 | log.debug(f'Begin inputs all with {gp.GPIOMap}') 212 | always_out = gp.GPIOMap.always_outputs() 213 | for name,gpio in gp.GPIOMap.all().items(): 214 | if name == self.muxName: 215 | continue 216 | p_type = Pin.IN 217 | if always_out.count(name) > 0: 218 | p_type = Pin.OUT 219 | p = StandardPin(name, gpio, p_type, pull=gp.GPIOMap.default_pull(name)) 220 | setattr(self, f'pin_{name}', p.raw_pin) 221 | setattr(self, name, p) # self._pinFunc(p)) 222 | self._allpins[name] = p 223 | 224 | return 225 | 226 | def safe_bidir(self): 227 | ''' 228 | Reset bidirection pins to safe mode, i.e. inputs 229 | 230 | ''' 231 | log.debug('Setting bidirs to safe mode (inputs)') 232 | for pname in gp.GPIOMap.all().keys(): 233 | if pname.startswith('uio'): 234 | p = getattr(self, pname) 235 | p.mode = Pin.IN 236 | 237 | 238 | 239 | def begin_safe(self): 240 | log.debug('begin: SAFE') 241 | self.begin_inputs_all() 242 | self._begin_alwaysOut() 243 | self._begin_muxPins() 244 | 245 | 246 | def begin_asiconboard(self): 247 | log.debug('begin: ASIC_RP_CONTROL') 248 | self.begin_inputs_all() 249 | self._begin_alwaysOut() 250 | unconfigured_pins = [] 251 | for pname in gp.GPIOMap.all().keys(): 252 | if pname.startswith('ui_in'): 253 | p = getattr(self, pname) 254 | if self.dieOnInputControlSwitchHigh: 255 | if p(): 256 | log.warn(f'Trying to control {pname} but possible contention (it is reading HIGH)') 257 | unconfigured_pins.append(pname) 258 | continue 259 | p.mode = Pin.OUT 260 | 261 | if len(unconfigured_pins): 262 | log.error(f'Following pins have not be set as outputs owing to contention: {",".join(unconfigured_pins)}') 263 | self._begin_muxPins() 264 | # needs to be after mux because reset now muxed 265 | self.project_clk_driven_by_RP2040(True) 266 | 267 | def begin_asic_manual_inputs(self): 268 | log.debug('begin: ASIC + MANUAL INPUTS') 269 | self.begin_inputs_all() 270 | self._begin_alwaysOut() 271 | # leave in* as inputs 272 | self._begin_muxPins() 273 | # leave clk and reset as inputs, for manual operation 274 | # needs to be after mux, because reset now muxed 275 | self.project_clk_driven_by_RP2040(False) 276 | 277 | 278 | 279 | def begin_standalone(self): 280 | log.debug('begin: STANDALONE') 281 | self.begin_inputs_all() 282 | self._begin_alwaysOut() 283 | 284 | for pname in gp.GPIOMap.all().keys(): 285 | if pname.startswith('uo_out'): 286 | p = getattr(self, pname) 287 | p.mode = Pin.OUT 288 | 289 | if pname.startswith('ui_in'): 290 | p = getattr(self, pname) 291 | p.pull = Pin.PULL_DOWN 292 | 293 | self._begin_muxPins() 294 | # needs to be after mux, because reset now muxed 295 | self.project_clk_driven_by_RP2040(True) 296 | 297 | def project_clk_driven_by_RP2040(self, rpControlled:bool): 298 | for pname in ['rp_projclk']: 299 | p = getattr(self, pname) 300 | if rpControlled: 301 | p.mode = Pin.OUT 302 | else: 303 | p.mode = Pin.IN 304 | 305 | 306 | def _begin_alwaysOut(self): 307 | for pname in gp.GPIOMap.always_outputs(): 308 | p = getattr(self, pname) 309 | p.mode = Pin.OUT 310 | 311 | def _begin_muxPins(self): 312 | if not gp.GPIOMap.demoboard_uses_mux(): 313 | return 314 | muxedPins = gp.GPIOMap.muxed_pairs() 315 | modeMap = gp.GPIOMap.muxed_pinmode_map(self.mode) 316 | for pname, muxPair in muxedPins.items(): 317 | log.debug(f'Creating muxed pin {pname}') 318 | mp = MuxedPin(pname, self.muxCtrl, 319 | getattr(self, pname), 320 | MuxedPinInfo(muxPair[0], 321 | 0, modeMap[muxPair[0]]), 322 | MuxedPinInfo(muxPair[1], 323 | 1, modeMap[muxPair[1]]) 324 | ) 325 | self.muxCtrl.add_muxed(mp) 326 | self._allpins[pname] = mp 327 | setattr(self, muxPair[0], getattr(mp, muxPair[0])) 328 | setattr(self, muxPair[1], getattr(mp, muxPair[1])) 329 | # override bare pin attrib 330 | setattr(self, pname, mp) 331 | 332 | # aliases 333 | @property 334 | def clk(self): 335 | return self.rp_projclk 336 | 337 | @property 338 | def nproject_rst(self): 339 | # had to munge the name because nproject_rst 340 | # is now in hardware MUX group, alias 341 | # allows use of old name with_underscore 342 | return self.nprojectrst 343 | 344 | @property 345 | def ctrl_ena(self): 346 | # had to munge name, now going through hw mux 347 | return self.cena 348 | 349 | def _dumpPin(self, p:StandardPin): 350 | print(f' {p.name} {p.mode_str} {p()}') 351 | def dump(self): 352 | print(f'Pins configured in mode {RPMode.to_string(self.mode)}') 353 | print(f'Currently:') 354 | for pname in sorted(gp.GPIOMap.all().keys()): 355 | self._dumpPin(getattr(self, pname)) 356 | 357 | 358 | 359 | def list_port(self, basename:str): 360 | retVal = [] 361 | 362 | for i in range(8): 363 | pname = f'{basename}{i}' 364 | if hasattr(self, pname): 365 | retVal.append(getattr(self,pname)) 366 | 367 | return retVal 368 | 369 | def _read_byte(self, pinList:list): 370 | v = 0 371 | for i in range(8): 372 | bit = pinList[i]() 373 | if bit: 374 | v |= (1 << i) 375 | 376 | return v 377 | 378 | def _write_byte(self, pinList:list, value:int): 379 | v = int(value) 380 | for i in range(8): 381 | if v & (1 << i): 382 | pinList[i](1) 383 | else: 384 | pinList[i](0) 385 | 386 | -------------------------------------------------------------------------------- /src/ttboard/pins/standard.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | from ttboard.util.platform import IsRP2040 9 | from ttboard.pins.upython import Pin 10 | 11 | if IsRP2040: 12 | import machine 13 | 14 | import ttboard.log as logging 15 | log = logging.getLogger(__name__) 16 | 17 | class StandardPin: 18 | ''' 19 | Augmented machine.Pin 20 | Holds the raw machine.Pin object, provides a callable interface 21 | obj() # to read 22 | obj(1) # to write 23 | and maintains/allows setting pull, mode and drive attributes 24 | 25 | # read/write attribs 26 | p.mode = Pin.OUT # direction 27 | p.pull = Pin.PULL_UP # pu 28 | print(p.drive) # may not be avail on any pins here, I dunno 29 | 30 | ''' 31 | def __init__(self, name:str, gpio:int, mode:int=Pin.IN, pull:int=-1, drive:int=0): 32 | self._name = name 33 | self._mode = mode 34 | self._pull = pull 35 | self._drive = drive 36 | self._gpio_num = None 37 | self._pwm = None 38 | if isinstance(gpio, StandardPin): 39 | self.raw_pin = gpio.raw_pin 40 | self._gpio_num = gpio.gpio_num 41 | self.mode = mode 42 | elif type(gpio) != int: 43 | self.raw_pin = gpio 44 | else: 45 | self.raw_pin = Pin(gpio, mode=mode, pull=pull) 46 | self._gpio_num = gpio 47 | 48 | @property 49 | def name(self): 50 | return self._name 51 | 52 | @property 53 | def is_input(self): 54 | return self._mode == Pin.IN 55 | 56 | @property 57 | def mode(self): 58 | return self._mode 59 | 60 | @mode.setter 61 | def mode(self, setMode:int): 62 | self._mode = setMode 63 | log.debug(f'Setting pin {self.name} to {self.mode_str}') 64 | self.raw_pin.init(setMode, pull=self._pull) 65 | 66 | @property 67 | def mode_str(self): 68 | modestr = 'OUT' 69 | if self.is_input: 70 | modestr = 'IN' 71 | return modestr 72 | @property 73 | def pull(self): 74 | return self._pull 75 | 76 | @pull.setter 77 | def pull(self, setPull:int): 78 | self._pull = setPull 79 | self.raw_pin.init(pull=setPull) 80 | 81 | @property 82 | def drive(self): 83 | return self._drive 84 | 85 | @drive.setter 86 | def drive(self, setDrive:int): 87 | self._drive = setDrive 88 | self.raw_pin.init(drive=setDrive) 89 | 90 | @property 91 | def gpio_num(self): 92 | return self._gpio_num 93 | 94 | def pwm(self, freq:int=None, duty_u16:int=0xffff/2): 95 | if self.is_input: 96 | log.error(f'Trying to twiddle PWM on pin {self.name} that is an input') 97 | return 98 | 99 | if freq is not None and freq < 1: 100 | log.info(f'Disabling pwm on {self.name}') 101 | if self._pwm is not None: 102 | self._pwm.deinit() 103 | self._pwm = None 104 | self.mode = Pin.OUT 105 | return None 106 | 107 | log.debug(f"Setting PWM on {self.name} to {freq}Hz") 108 | 109 | if IsRP2040: 110 | self._pwm = machine.PWM(self.raw_pin) 111 | else: 112 | log.warn('Not on RP2040--no PWM') 113 | return None 114 | 115 | if freq is not None and freq > 0: 116 | self._pwm.freq(int(freq)) 117 | 118 | if duty_u16 is not None and duty_u16 >= 0: 119 | self._pwm.duty_u16(int(duty_u16)) 120 | 121 | return self._pwm 122 | 123 | 124 | def __call__(self, value:int=None): 125 | if value is not None: 126 | return self.raw_pin.value(value) 127 | return self.raw_pin.value() 128 | 129 | def __getattr__(self, name): 130 | if hasattr(self.raw_pin, name): 131 | return getattr(self.raw_pin, name) 132 | raise AttributeError(f'no attr {name}') 133 | 134 | def __repr__(self): 135 | outval = '' 136 | if not self.is_input: 137 | outval = f' {self.raw_pin.value()}' 138 | return f'' 139 | 140 | 141 | def __str__(self): 142 | outval = '' 143 | if not self.is_input: 144 | outval = f' {self.raw_pin.value()}' 145 | return f'Standard pin {self.name} (GPIO {self.gpio_num}), configured as {self.mode_str}{outval}' 146 | 147 | -------------------------------------------------------------------------------- /src/ttboard/pins/upython.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.util.platform import IsRP2040 8 | if IsRP2040: 9 | from machine import Pin 10 | else: 11 | # give us some fake Pin to play with 12 | from ttboard.pins.desktop_pin import Pin 13 | -------------------------------------------------------------------------------- /src/ttboard/ports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/ttboard/ports/__init__.py -------------------------------------------------------------------------------- /src/ttboard/ports/io.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 21, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | import microcotb.ports.io 8 | 9 | class IO(microcotb.ports.io.IO): 10 | pass -------------------------------------------------------------------------------- /src/ttboard/ports/oe.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 21, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from microcotb.types.handle import LogicObject 8 | from microcotb.types.ioport import IOPort 9 | 10 | 11 | 12 | class OutputEnable(LogicObject): 13 | def __init__(self, name:str, width:int, read_byte_fn=None, write_byte_fn=None): 14 | port = IOPort(name, width, read_byte_fn, write_byte_fn) 15 | super().__init__(port) 16 | self.port = port 17 | 18 | 19 | def __repr__(self): 20 | val = hex(int(self.value)) if self.port.is_readable else '' 21 | return f'' -------------------------------------------------------------------------------- /src/ttboard/project_design.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Dec 4, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | import ttboard.log as logging 8 | log = logging.getLogger(__name__) 9 | 10 | class DangerLevel: 11 | SAFE=0 12 | UNKNOWN=1 13 | MEDIUM=2 14 | HIGH=3 15 | 16 | @classmethod 17 | def string_to_level(cls, s:str): 18 | smap = { 19 | 'safe': cls.SAFE, 20 | 'unknown': cls.UNKNOWN, 21 | 'medium': cls.MEDIUM, 22 | 'HIGH': cls.HIGH 23 | } 24 | if s in smap: 25 | return smap[s] 26 | return cls.UNKNOWN 27 | 28 | @classmethod 29 | def level_to_str(cls, level:int): 30 | strs = [ 31 | 'safe', 32 | 'unknown', 33 | 'medium', 34 | 'high' 35 | 36 | ] 37 | if level >= len(strs): 38 | return 'high' 39 | return strs[level] 40 | 41 | class Serializable: 42 | SerializerVersion = 1 43 | BytesForStringLen = 1 44 | StringEncoding = 'ascii' 45 | ByteOrder = 'big' 46 | 47 | def __init__(self): 48 | pass 49 | 50 | def to_bin_file(self, fpath:str): 51 | with open(fpath, 'wb') as f: 52 | f.write(self.serialize_int(self.SerializerVersion, 1)) 53 | f.write(b'TTSER') 54 | f.write(self.serialize()) 55 | f.close() 56 | 57 | def bin_header_valid(self, bytestream): 58 | version = self.deserialize_int(bytestream, 1) 59 | header = bytestream.read(5) 60 | if header == b'TTSER': 61 | return version 62 | 63 | def from_bin_file(self, fpath:str): 64 | with open(fpath, 'rb') as f: 65 | version = self.bin_header_valid(f) 66 | if not version: 67 | raise ValueError(f'bad header in {fpath}') 68 | log.info(f'Deserializing from v{version} file {fpath}') 69 | self.deserialize(f) 70 | f.close() 71 | 72 | 73 | @classmethod 74 | def serializable_string(cls, s:str): 75 | try: 76 | _enc = bytearray(s, cls.StringEncoding) 77 | return s 78 | except: 79 | news = '' 80 | for i in range(len(s)): 81 | c = s[i] 82 | cval = ord(c) 83 | if cval >= ord('0') and cval <= ord('9'): 84 | news += c 85 | elif cval < ord('A') or cval > ord('z'): 86 | news += '_' 87 | else: 88 | news += c 89 | return news 90 | 91 | @classmethod 92 | def deserialize_string(cls, bytestream): 93 | slen = cls.deserialize_int(bytestream, cls.BytesForStringLen) 94 | sbytes = bytestream.read(slen) 95 | try: 96 | return sbytes.decode(cls.StringEncoding) 97 | except Exception as e: 98 | log.error(f"Error deser string {e} (len {slen}) @ position {bytestream.tell()}: {sbytes}") 99 | return '' 100 | 101 | @classmethod 102 | def deserialize_int(cls, bytestream, num_bytes): 103 | bts = bytestream.read(num_bytes) 104 | if len(bts) != num_bytes: 105 | raise ValueError('empty') 106 | v = int.from_bytes(bts, cls.ByteOrder) 107 | return v 108 | 109 | 110 | @classmethod 111 | def serialize_string(cls, s:str): 112 | slen = len(s) 113 | bts = slen.to_bytes(cls.BytesForStringLen, cls.ByteOrder) 114 | 115 | try: 116 | enc = bytearray(s, cls.StringEncoding) 117 | except: 118 | enc = bytearray(cls.serializable_string(s), cls.StringEncoding) 119 | bts += enc 120 | return bts 121 | 122 | @classmethod 123 | def serialize_int(cls, i:int, num_bytes): 124 | try: 125 | return i.to_bytes(num_bytes, cls.ByteOrder) 126 | except OverflowError as e: 127 | log.error(f'OVERFLOW during conversion using {num_bytes} bytes for {i}: {e}') 128 | raise e 129 | 130 | 131 | @classmethod 132 | def serialize_list(cls, l:list): 133 | bts = bytearray() 134 | for element in l: 135 | if isinstance(element, str): 136 | bts += cls.serialize_string(element) 137 | elif isinstance(element, int): 138 | try: 139 | bts += cls.serialize_int(element, 1) 140 | except OverflowError as e: 141 | log.error(f'Issue serializing: {e}') 142 | log.error(f'Setting as 0') 143 | i = 0 144 | bts += i.to_bytes(1, cls.ByteOrder) 145 | elif isinstance(element, list): 146 | if len(element) != 2: 147 | raise RuntimeError(f'Expecting 2 elements in {element}') 148 | if not isinstance(element[0], int): 149 | raise RuntimeError(f'Expecting int as first in {element}') 150 | if not isinstance(element[1], int): 151 | raise RuntimeError(f'Expecting size as second in {element}') 152 | try: 153 | bts += cls.serialize_int(element[0], element[1]) 154 | except OverflowError as e: 155 | log.error(f'Issue serializing: {e}') 156 | log.error(f'Setting as 0') 157 | i = 0 158 | bts += i.to_bytes(element[1], cls.ByteOrder) 159 | else: 160 | RuntimeError(f'Unknown serialize {element}') 161 | return bts 162 | 163 | 164 | 165 | def serialize(self): 166 | raise RuntimeError('Override me') 167 | 168 | def deserialize(self, bytestream): 169 | raise RuntimeError('Override me') 170 | 171 | class Design(Serializable): 172 | SerializeClockBytes = 4 173 | SerializePayloadSizeBytes = 1 174 | SerializeAddressBytes = 2 175 | def __init__(self, projectMux, projname:str='NOTSET', projindex:int=0, info:dict=None): 176 | super().__init__() 177 | self.mux = projectMux 178 | self.count = int(projindex) 179 | self.name = projname 180 | 181 | self.danger_level = DangerLevel.HIGH 182 | self.macro = projname 183 | self.repo = '' 184 | self.commit = '' 185 | self.clock_hz = -1 186 | self._all = info 187 | if info is None: 188 | return 189 | 190 | self.macro = info['macro'] 191 | 192 | if 'danger_level' in info: 193 | self.danger_level = DangerLevel.string_to_level(info['danger_level']) 194 | else: 195 | self.danger_level = DangerLevel.SAFE 196 | 197 | if 'repo' in info: 198 | self.repo = info['repo'] 199 | 200 | if 'commit' in info: 201 | self.commit = info['commit'] 202 | self.clock_hz = int(info['clock_hz']) 203 | 204 | @classmethod 205 | def get_address_and_size_from(cls, bytestream): 206 | 207 | addr = cls.deserialize_int(bytestream, cls.SerializeAddressBytes) 208 | size = cls.deserialize_int(bytestream, cls.SerializePayloadSizeBytes) 209 | return (addr, size) 210 | 211 | @property 212 | def project_index(self): 213 | return self.count 214 | 215 | @property 216 | def danger_level_str(self): 217 | return DangerLevel.level_to_str(self.danger_level) 218 | 219 | def enable(self, force:bool=False): 220 | return self.mux.enable(self, force) 221 | 222 | def disable(self): 223 | self.mux.disable() 224 | 225 | def serialize(self): 226 | payload_data = [ 227 | self.name, 228 | self.danger_level, 229 | [self.clock_hz, self.SerializeClockBytes] 230 | 231 | ] 232 | 233 | payload_bytes = self.serialize_list(payload_data) 234 | 235 | header = [ 236 | [self.project_index, self.SerializeAddressBytes], 237 | [len(payload_bytes), self.SerializePayloadSizeBytes], 238 | ] 239 | all_data = self.serialize_list(header) + payload_bytes 240 | return all_data 241 | 242 | def deserialize(self, bytestream): 243 | 244 | addr, _size = self.get_address_and_size_from(bytestream) 245 | self.count = addr 246 | self.name = self.deserialize_string(bytestream) 247 | self.macro = self.name 248 | self.danger_level = self.deserialize_int(bytestream, 1) 249 | self.clock_hz = self.deserialize_int(bytestream, self.SerializeClockBytes) 250 | 251 | def __str__(self): 252 | return f'{self.name} ({self.count}) @ {self.repo}' 253 | 254 | def __repr__(self): 255 | if self.danger_level == DangerLevel.SAFE: 256 | dangermsg = '' 257 | else: 258 | dangermsg = f' danger={self.danger_level_str}' 259 | 260 | return f'' 261 | 262 | 263 | class DesignStub: 264 | ''' 265 | A yet-to-be-loaded design, just a pointer that will 266 | auto-load the design if accessed. 267 | Has a side effect of replacing itself as an attribute 268 | in the design index so this only happens once. 269 | ''' 270 | def __init__(self, design_index, address:int): 271 | self.design_index = design_index 272 | self.count = address 273 | # self.name = projname 274 | self._des = None 275 | 276 | def _lazy_load(self): 277 | des = self.design_index.load_project(self.project_index) 278 | setattr(self.design_index, des.name, des) 279 | self._des = des 280 | return des 281 | 282 | @property 283 | def project_index(self): 284 | return self.count 285 | 286 | def __getattr__(self, name:str): 287 | if hasattr(self, '_des') and self._des is not None: 288 | des = self._des 289 | else: 290 | des = self._lazy_load() 291 | return getattr(des, name) 292 | 293 | def __repr__(self): 294 | return f'' 295 | -------------------------------------------------------------------------------- /src/ttboard/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TinyTapeout/tt-micropython-firmware/0fedc59a74053d0b664c48cd13015034d11e3f09/src/ttboard/util/__init__.py -------------------------------------------------------------------------------- /src/ttboard/util/colors.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 5, 2024 3 | 4 | @author: Uri Shaked 5 | ''' 6 | Enable = True 7 | COLORS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"] 8 | 9 | def bold(s): 10 | if Enable: 11 | return f"\033[1m{s}\033[0m" 12 | return s 13 | 14 | def underline(s): 15 | if Enable: 16 | return f"\033[4m{s}\033[0m" 17 | return s 18 | 19 | def inverse(s): 20 | if Enable: 21 | return f"\033[7m{s}\033[0m" 22 | 23 | def color(s, color, bright = True): 24 | if not Enable: 25 | return s 26 | 27 | return color_start_code(color, bright) + s + color_end_code() 28 | 29 | def color_start(color, bright:bool = True): 30 | print(color_start_code(color, bright), end='') 31 | def color_end(): 32 | print(color_end_code()) 33 | def color_start_code(color, bright:bool = True): 34 | if not Enable: 35 | return '' 36 | 37 | code= str(COLORS.index(color)) 38 | suffix = ";1" if bright else "" 39 | return f"\033[3{code}{suffix}m" 40 | 41 | def color_end_code(): 42 | if not Enable: 43 | return '' 44 | 45 | return '\033[0m' 46 | -------------------------------------------------------------------------------- /src/ttboard/util/platform.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 23, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | 9 | RP2040SystemClockDefaultHz = 125000000 10 | 11 | import microcotb.platform 12 | IsRP2040 = microcotb.platform.IsRP2040 13 | 14 | 15 | if IsRP2040: 16 | ''' 17 | low-level machine related methods. 18 | @note: Register magic is based on "2.3.1.7. List of Registers" 19 | from the rp2040_datasheet.pdf 20 | 21 | 22 | Have some read_ and write_ for in/bidir/out ports 23 | NOTE: [mappings] that these rely on GPIO pin mappings that 24 | are specific to TT 4/5 demoboard layout 25 | 26 | The read_ will only return bits for GPIO that are set 27 | as INPUTs 28 | The write_ will write values to all GPIO but these will only 29 | impact GPIO that are setup as OUTPUTs. For input gpio, this 30 | value will be remembered and apply should the pin be turned 31 | into an output. 32 | 33 | NOTE: [muxing] there is a MUX on some pins of the low 34 | output nibble on TT04/05 demoboards, that could in theory 35 | interfere with this. 36 | If you use the tt.shuttle to select/enable projects, it 37 | ensures that the mux is set to let these pins out as you'd 38 | expect. If you're down in the weeds and want to make sure 39 | or play with this, you can use 40 | tt.muxCtrl.mode_admin() 41 | to send out 1/2/3 to the asic mux, and 42 | tt.muxCtrl.mode_project_IO() 43 | to behave normally. 44 | 45 | The machine native stuff below uses 46 | direct access to mem32 to go fastfastfast 47 | but this is pretty opaque. For ref, here are 48 | relevant registers from 2.3.1.7. List of Registers 49 | 0x004 GPIO_IN Input value for GPIO pins 50 | 0x010 GPIO_OUT GPIO output value 51 | 0x014 GPIO_OUT_SET GPIO output value set 52 | 0x018 GPIO_OUT_CLR GPIO output value clear 53 | 0x01c GPIO_OUT_XOR GPIO output value XOR 54 | 0x020 GPIO_OE GPIO output enable 55 | 0x024 GPIO_OE_SET GPIO output enable set 56 | 0x028 GPIO_OE_CLR GPIO output enable clear 57 | 0x02c GPIO_OE_XOR GPIO output enable XOR 58 | 59 | ''' 60 | import rp2 61 | import machine 62 | 63 | 64 | def pin_as_input(gpio_index:int, pull:int=None): 65 | if pull is not None: 66 | return machine.Pin(gpio_index, machine.Pin.IN, pull) 67 | else: 68 | return machine.Pin(gpio_index, machine.Pin.IN) 69 | def dump_portset(p:str, v:int): 70 | print(f'ps {p}: {bin(v)}') 71 | return 72 | @rp2.asm_pio(set_init=rp2.PIO.OUT_HIGH) 73 | def _pio_toggle_pin(): 74 | wrap_target() 75 | set(pins, 1) 76 | mov(y, osr) 77 | label("delay1") 78 | jmp(y_dec, "delay1") # Delay 79 | set(pins, 0) 80 | mov(y, osr) 81 | label("delay2") 82 | jmp(y_dec, "delay2") # Delay 83 | wrap() 84 | 85 | class PIOClock: 86 | def __init__(self, pin): 87 | self.freq = 0 88 | self.pin = pin 89 | self._current_pio = None 90 | 91 | def start(self, freq_hz:int): 92 | self.freq = freq_hz 93 | 94 | self.stop() 95 | if self.freq <= 0: 96 | return 97 | 98 | set_RP_system_clock(100_000_000) 99 | if self._current_pio is None: 100 | self._current_pio = rp2.StateMachine( 101 | 0, 102 | _pio_toggle_pin, 103 | freq=2000, 104 | set_base=self.pin, 105 | ) 106 | 107 | # Set the delay: 1000 cycles per hz minus 2 cycles for the set/mov instructions 108 | self._current_pio.put(int(500 * (2 / self.freq) - 2)) 109 | self._current_pio.exec("pull()") 110 | self._current_pio.active(1) 111 | 112 | def stop(self): 113 | if self._current_pio is None or not self.freq: 114 | return 115 | 116 | self._current_pio.active(0) 117 | self.freq = 0 118 | self._current_pio = None 119 | self.pin.init(machine.Pin.IN) 120 | 121 | def isfile(file_path:str): 122 | try: 123 | f = open(file_path, 'r') 124 | except OSError: 125 | return False 126 | f.close() 127 | return True 128 | def get_RP_system_clock(): 129 | return machine.freq() 130 | def set_RP_system_clock(freqHz:int): 131 | machine.freq(int(freqHz)) 132 | 133 | @micropython.native 134 | def write_ui_in_byte(val): 135 | # dump_portset('ui_in', val) 136 | # low level machine stuff 137 | # move the value bits to GPIO spots 138 | # low nibble starts at 9 | high nibble at 17 (-4 'cause high nibble) 139 | val = ((val & 0xF) << 9) | ((val & 0xF0) << 17-4) 140 | # xor with current GPIO values, and mask to keep only input bits 141 | # 0x1E1E00 == 0b111100001111000000000 so GPIO 9-12 and 17-20 142 | val = (machine.mem32[0xd0000010] ^ val) & 0x1E1E00 143 | # val is now be all the input bits that have CHANGED: 144 | # writing to 0xd000001c will flip any GPIO where a 1 is found 145 | machine.mem32[0xd000001c] = val 146 | 147 | @micropython.native 148 | def read_ui_in_byte(): 149 | # just read the high and low nibbles from GPIO and combine into a byte 150 | return ( (machine.mem32[0xd0000004] & (0xf << 17)) >> (17-4)) | ((machine.mem32[0xd0000004] & (0xf << 9)) >> 9) 151 | 152 | 153 | @micropython.native 154 | def write_uio_byte(val): 155 | # dump_portset('uio', val) 156 | # low level machine stuff 157 | # move the value bits to GPIO spots 158 | # for bidir, all uio bits are in a line starting 159 | # at GPIO 21 160 | val = (val << 21) 161 | val = (machine.mem32[0xd0000010] ^ val) & 0x1FE00000 162 | # val is now be all the bits that have CHANGED: 163 | # writing to 0xd000001c will flip any GPIO where a 1 is found, 164 | # only applies immediately to pins set as output 165 | machine.mem32[0xd000001c] = val 166 | 167 | 168 | @micropython.native 169 | def read_uio_byte(): 170 | return (machine.mem32[0xd0000004] & (0xff << 21)) >> 21 171 | 172 | @micropython.native 173 | def read_uio_outputenable(): 174 | # GPIO_OE register, masked for our bidir pins 175 | return (machine.mem32[0xd0000020] & 0x1FE00000) >> 21 176 | 177 | 178 | @micropython.native 179 | def write_uio_outputenable(val): 180 | # dump_portset('uio_oe', val) 181 | # GPIO_OE register, clearing bidir pins and setting any enabled 182 | val = (val << 21) 183 | machine.mem32[0xd0000020] = (machine.mem32[0xd0000020] & ((1 << 21) - 1)) | val 184 | 185 | @micropython.native 186 | def write_uo_out_byte(val): 187 | # dump_portset('uo_out', val) 188 | # low level machine stuff 189 | # move the value bits to GPIO spots 190 | 191 | val = ((val & 0xF) << 5) | ((val & 0xF0) << 13-4) 192 | val = (machine.mem32[0xd0000010] ^ val) & 0x1E1E0 193 | # val is now be all the bits that have CHANGED: 194 | # writing to 0xd000001c will flip any GPIO where a 1 is found, 195 | # only applies immediately to pins set as output 196 | machine.mem32[0xd000001c] = val 197 | 198 | @micropython.native 199 | def read_uo_out_byte(): 200 | 201 | # sample code to deal with differences between 202 | # PCBs, not actually required as we didn't move anything 203 | # after all! 204 | # global PCBVERSION_TT06 205 | # all_io = machine.mem32[0xd0000004] 206 | #if PCBVERSION_TT06 is None: 207 | # import ttboard.boot.demoboard_detect as dbdet 208 | # PCBVERSION_TT06 = True if dbdet.DemoboardDetect.PCB == dbdet.DemoboardVersion.TT06 else False 209 | 210 | #if PCBVERSION_TT06: 211 | # # gpio output bits are 212 | # # 0x1e1e0 == 0b11110000111100000 so GPIO5-8 and GPIO 13-17 213 | # val = ((all_io & (0xf << 13)) >> (13 - 4)) | ((all_io & (0xf << 5)) >> 5) 214 | #else: 215 | # just read the high and low nibbles from GPIO and combine into a byte 216 | return ( (machine.mem32[0xd0000004] & (0xf << 13)) >> (13-4)) | ((machine.mem32[0xd0000004] & (0xf << 5)) >> 5) 217 | 218 | 219 | @micropython.native 220 | def read_clock(): 221 | # clock is on GPIO 0 222 | return (machine.mem32[0xd0000010] & 1) 223 | 224 | @micropython.native 225 | def write_clock(val): 226 | # not a huge optimization, as this is a single bit, 227 | # but 5% or so counts when using the microcotb tests 228 | if val: 229 | machine.mem32[0xd0000014] = 1 # set bit 0 230 | else: 231 | machine.mem32[0xd0000018] = 1 # clear bit 0 232 | 233 | 234 | 235 | else: 236 | import os.path 237 | isfile = os.path.isfile 238 | 239 | class PIOClock: 240 | def __init__(self, pin): 241 | self.freq = 0 242 | self.pin = pin 243 | 244 | def start(self, freq_hz:int): 245 | self.freq = freq_hz 246 | print(f"(mock) PIO clock @ {freq_hz}Hz") 247 | 248 | def stop(self): 249 | self.freq = 0 250 | print("PIO clock stop") 251 | def pin_as_input(gpio_index:int, pull:int=None): 252 | from ttboard.pins.upython import Pin 253 | return Pin(gpio_index, Pin.IN, pull=pull) 254 | def get_RP_system_clock(): 255 | return RP2040SystemClockDefaultHz 256 | def set_RP_system_clock(freqHz:int): 257 | global RP2040SystemClockDefaultHz 258 | print(f"Set machine clock to {freqHz}") 259 | RP2040SystemClockDefaultHz = freqHz 260 | 261 | _inbyte = 0 262 | def write_ui_in_byte(val): 263 | global _inbyte 264 | print(f'Sim write_input_byte {val}') 265 | _inbyte = val 266 | 267 | def read_ui_in_byte(): 268 | print('Sim read_output_byte') 269 | return _inbyte 270 | 271 | 272 | _uio_byte = 0 273 | def write_uio_byte(val): 274 | global _uio_byte 275 | print(f'Sim write_bidir_byte {val}') 276 | _uio_byte = val 277 | 278 | 279 | 280 | def read_uio_byte(): 281 | print('Sim read_output_byte') 282 | return _uio_byte 283 | 284 | _outbyte = 0 285 | def write_uo_out_byte(val): 286 | global _outbyte 287 | print(f'Sim write_output_byte {val}') 288 | _outbyte = val 289 | 290 | def read_uo_out_byte(): 291 | global _outbyte 292 | v = _outbyte 293 | #_outbyte += 1 294 | print('Sim read_output_byte') 295 | return v 296 | 297 | _uio_oe_pico = 0 298 | def read_uio_outputenable(): 299 | print('Sim read_bidir_outputenable') 300 | return _uio_oe_pico 301 | 302 | def write_uio_outputenable(val): 303 | global _uio_oe_pico 304 | print(f'Sim write_bidir_outputenable {val}') 305 | _uio_oe_pico = val 306 | 307 | _clk_pin = 0 308 | def read_clock(): 309 | return _clk_pin 310 | 311 | def write_clock(val): 312 | global _clk_pin 313 | _clk_pin = val 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /src/ttboard/util/shuttle_tests.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Apr 10, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | 8 | from ttboard.demoboard import DemoBoard, Pins 9 | import ttboard.util.time as time 10 | from ttboard.mode import RPMode 11 | 12 | import ttboard.log as logging 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | 17 | def clock_and_compare_output(tt:DemoBoard, read_bidirs:bool, max_idx:int, delay_interval_ms:int): 18 | err_count = 0 19 | for i in range(max_idx): 20 | tt.clock_project_once() 21 | time.sleep_ms(delay_interval_ms) 22 | out_byte = tt.uo_out.value 23 | 24 | # give ourselves a little jitter room, in case we're a step 25 | # behind as has happened for reasons unclear 26 | max_val = i+1 27 | min_val = i-1 28 | 29 | if out_byte >= min_val and out_byte <= max_val: 30 | # close enough 31 | log.debug(f'Clock count {i}, got {out_byte}') 32 | else: 33 | log.warn(f'MISMATCH between expected count {i} and output {out_byte}') 34 | err_count += 1 35 | 36 | if read_bidirs: 37 | bidir_byte = tt.uio_in.value 38 | if bidir_byte >= min_val and bidir_byte <= max_val: 39 | # close enough 40 | log.debug(f'Clock count {i}, got bidir {bidir_byte}') 41 | else: 42 | log.warn(f'MISMATCH between expected count {i} and bidir {bidir_byte}') 43 | err_count += 1 44 | 45 | 46 | 47 | if err_count: 48 | err_msg = f'{err_count}/{max_idx} mismatches during counting test' 49 | log.error(err_msg) 50 | return err_msg 51 | 52 | log.info('RP2040 clocking acting pretty nicely') 53 | return None 54 | 55 | 56 | 57 | def factory_test_clocking(tt:DemoBoard, read_bidirs:bool, max_idx:int=128, delay_interval_ms:int=1): 58 | log.info(f'Testing manual clocking up to {max_idx} on {tt}') 59 | 60 | # select the project from the shuttle 61 | tt.shuttle.factory_test.enable() 62 | tt.mode = RPMode.ASIC_RP_CONTROL # make sure we're controlling everything 63 | 64 | 65 | tt.reset_project(True) 66 | tt.ui_in.value = 1 67 | tt.clock_project_stop() 68 | tt.reset_project(False) 69 | 70 | err = clock_and_compare_output(tt, read_bidirs, max_idx, delay_interval_ms) 71 | if err is not None: 72 | # error encountered, we're done here 73 | return err 74 | 75 | 76 | # test that reset actually resets 77 | log.info('RP2040 test project reset') 78 | 79 | # make sure we're not exactly on 80 | if tt.uo_out.value == 0: 81 | for _i in range(5): 82 | tt.clock_project_once() 83 | 84 | if tt.uo_out.value == 0: 85 | log.warn("Something is off: clocked a few times, still reporting 0") 86 | 87 | 88 | tt.reset_project(True) 89 | time.sleep_ms(10) 90 | tt.reset_project(False) 91 | err = clock_and_compare_output(tt, read_bidirs, 0xf, delay_interval_ms) 92 | if err is not None: 93 | log.error(f'Problem with clocking test post-reset: {err}') 94 | return err 95 | 96 | log.info('RP2040: reset well behaved!') 97 | return None 98 | 99 | def factory_test_clocking_04(tt:DemoBoard, max_idx:int=128, delay_interval_ms:int=1): 100 | return factory_test_clocking(tt, True, max_idx, delay_interval_ms) 101 | 102 | -------------------------------------------------------------------------------- /src/ttboard/util/time.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jan 22, 2024 3 | 4 | @author: Pat Deegan 5 | @copyright: Copyright (C) 2024 Pat Deegan, https://psychogenic.com 6 | ''' 7 | from ttboard.util.platform import IsRP2040 8 | 9 | from time import * 10 | if not IsRP2040: 11 | 12 | def sleep_ms(v): 13 | sleep(v/1000) 14 | 15 | def sleep_us(v): 16 | sleep(v/1000000) 17 | 18 | def ticks_us(): 19 | return int(time()) 20 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | parser.addoption("--shuttle", action="store", default="shuttle of interest") 3 | parser.addoption("--shuttlepath", action="store", default="directory for shuttle files") 4 | 5 | -------------------------------------------------------------------------------- /test/test_serialized_shuttle.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import os 4 | from ttboard.project_mux import DesignIndex, DangerLevel, Design 5 | 6 | Default_danger = DangerLevel.SAFE 7 | 8 | @pytest.fixture(scope="session") 9 | def shuttle(pytestconfig): 10 | return pytestconfig.getoption("shuttle") 11 | 12 | @pytest.fixture(scope="session") 13 | def shuttlepath(pytestconfig): 14 | return pytestconfig.getoption("shuttlepath") 15 | 16 | def check_design(des:Design, project:dict): 17 | 18 | project_idx = int(project['address']) 19 | clock_hz = 0 20 | if 'clock_hz' in project: 21 | clock_hz = int(project['clock_hz']) 22 | 23 | danger_level = Default_danger 24 | if 'danger_level' in project: 25 | danger_level = DangerLevel.string_to_level(project['danger_level']) 26 | 27 | assert des.project_index == project_idx 28 | assert des.clock_hz == clock_hz 29 | assert des.danger_level == danger_level 30 | 31 | 32 | def get_indices(shuttle, shuttlepath): 33 | 34 | shuttle_json_file = os.path.join(shuttlepath, f'{shuttle}.json') 35 | shuttle_bin_file = os.path.join(shuttlepath, f'{shuttle}.json.{DesignIndex.SerializedBinSuffix}') 36 | 37 | assert os.path.exists(shuttle_json_file) 38 | assert os.path.exists(shuttle_bin_file) 39 | 40 | 41 | # going to do a 3-way comparison 42 | jsonIndex = DesignIndex(None, None) 43 | jsonIndex.load_available(shuttle_json_file, force_json=True) 44 | 45 | binIndex = DesignIndex(None, None) 46 | binIndex.load_serialized(shuttle_bin_file) 47 | 48 | return (jsonIndex, binIndex) 49 | 50 | 51 | def test_check_serialization(shuttle, shuttlepath): 52 | 53 | assert len(shuttle) 54 | assert len(shuttlepath) 55 | 56 | print(f"\nRunning test for shuttle: ***{shuttle}***\n") 57 | shuttle_json_file = os.path.join(shuttlepath, f'{shuttle}.json') 58 | (jsonIndex, binIndex) = get_indices(shuttle, shuttlepath) 59 | 60 | emptyIndex = DesignIndex(None, None) 61 | 62 | with open(shuttle_json_file) as fh: 63 | index = json.load(fh) 64 | for project in index['projects']: 65 | # some munging happens, get the resulting name 66 | project_name = emptyIndex.clean_project_name(project) 67 | 68 | 69 | # both should have the same number of matches 70 | bin_found = binIndex.find(project_name) 71 | json_found = jsonIndex.find(project_name) 72 | assert len(json_found) == len(bin_found) 73 | 74 | 75 | danger_level = Default_danger 76 | if 'danger_level' in project: 77 | danger_level = DangerLevel.string_to_level(project['danger_level']) 78 | 79 | if danger_level >= DangerLevel.HIGH: 80 | print(f'{project_name}: HIGH danger') 81 | assert not jsonIndex.is_available(project_name) 82 | assert not binIndex.is_available(project_name) 83 | else: 84 | #print(f"project: {project_name} ({project['macro']})") 85 | assert jsonIndex.is_available(project_name) 86 | assert binIndex.is_available(project_name) 87 | # ok, found in both, now check values 88 | check_design(binIndex.get(project_name), project) 89 | check_design(jsonIndex.get(project_name), project) 90 | 91 | 92 | -------------------------------------------------------------------------------- /wokwi.toml: -------------------------------------------------------------------------------- 1 | # Wokwi Configuration File 2 | # Reference: https://docs.wokwi.com/vscode/project-config 3 | [wokwi] 4 | version = 1 5 | firmware = 'release.uf2' 6 | elf = 'release.uf2' 7 | --------------------------------------------------------------------------------