├── .github └── workflows │ ├── examples.yml │ ├── micropython.yml │ └── python-linting.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docs ├── dfu_mode.png ├── hardware.md ├── python-documentation.md ├── setting-up-micropython.md └── tips-and-tricks.md ├── drivers ├── aps6404 │ ├── CMakeLists.txt │ ├── aps6404.cmake │ ├── aps6404.cpp │ ├── aps6404.hpp │ ├── aps6404.pio │ └── aps6404_perf_critical.cpp └── dv_display │ ├── CMakeLists.txt │ ├── dv_display.cmake │ ├── dv_display.cpp │ ├── dv_display.hpp │ ├── pico-stick-wide.h │ ├── pico-stick.h │ ├── swd.pio │ ├── swd_load.cpp │ └── swd_load.hpp ├── examples ├── alert │ ├── alert.py │ └── future-earth.af ├── basic │ ├── buttons.py │ ├── coolvetica_rg.af │ ├── hello_world.py │ ├── hello_world_rainbow.py │ ├── hello_world_vector.py │ ├── next_sunday.af │ ├── noise.py │ ├── sdtest.py │ └── spicy_soup.af ├── bouncing_logo │ ├── bouncing_logo.py │ └── pim-logo.png ├── co2 │ ├── OpenSans-Bold.af │ └── co2.py ├── floppy_birb │ ├── OpenSans-Regular.af │ ├── birb-sprite.png │ ├── floppy_birb.py │ ├── goal.png │ ├── ground.png │ ├── pimoroni-hq.png │ ├── pipe-cap.png │ ├── pipe.png │ └── sky.png ├── magic_mirror │ ├── README.md │ ├── icons │ │ ├── cloud.png │ │ ├── no-wifi.png │ │ ├── rain.png │ │ ├── snow.png │ │ ├── storm.png │ │ ├── sun.png │ │ └── wifi.png │ ├── magic_mirror.py │ ├── magic_mirror_home_assistant.py │ └── magic_mirror_without_wifi.py ├── main.py ├── pride │ ├── coolcrab.png │ └── pride.py ├── rainbow_wheel.py ├── screenmodes.py ├── scrollgroups.py ├── seafax.py ├── sneks_and_ladders │ ├── pvgame.py │ ├── sneks_and_ladders.py │ └── tiles.png ├── starfield.py ├── starfield_rainbow.py ├── thermometer.py ├── toaster.png ├── vector_clock.py └── vector_clock_smooth.py ├── firmware ├── PIMORONI_PICOVISION │ ├── board.json │ ├── manifest.py │ ├── micropython.cmake │ ├── mpconfigboard.cmake │ ├── mpconfigboard.h │ ├── pins.csv │ └── uf2-manifest.txt ├── micropython_nano_specs.patch ├── pimoroni_pico_import.cmake └── startup_overclock.patch ├── lib ├── boot.py ├── modes.py ├── picographics.py └── pimoroni.py ├── libraries ├── pico_graphics │ ├── pico_graphics_dv.hpp │ ├── pico_graphics_pen_dv_p5.cpp │ ├── pico_graphics_pen_dv_rgb555.cpp │ └── pico_graphics_pen_dv_rgb888.cpp └── pico_synth_i2s │ ├── pico_synth_i2s.cmake │ ├── pico_synth_i2s.cpp │ ├── pico_synth_i2s.hpp │ └── pico_synth_i2s.pio ├── modules ├── picographics │ ├── README.md │ ├── micropython.cmake │ ├── picographics.c │ ├── picographics.cpp │ ├── picographics.h │ └── spritesheet-to-rgb332.py └── picosynth │ ├── micropython.cmake │ ├── picosynth.c │ ├── picosynth.cpp │ └── picosynth.h ├── picovision.cmake └── picovision_import.cmake /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: MicroPython Examples 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [created] 8 | 9 | jobs: 10 | build: 11 | name: Pack Examples 12 | runs-on: ubuntu-20.04 13 | 14 | env: 15 | RELEASE_FILE: pimoroni-picovision-examples-${{github.event.release.tag_name || github.sha}} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | path: picovision 22 | 23 | - name: Configure 24 | shell: bash 25 | working-directory: picovision 26 | run: | 27 | cmake -S . -B build -DCPACK_PACKAGE_FILE_NAME="${{env.RELEASE_FILE}}" -DCMAKE_INSTALL_PREFIX=build/install 28 | 29 | - name: Pack Examples 30 | shell: bash 31 | working-directory: picovision 32 | run: | 33 | cmake --build build --target install package 34 | 35 | - name: Store as artifact 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: ${{env.RELEASE_FILE}} 39 | path: picovision/build/install 40 | 41 | - name: Upload release zip 42 | if: github.event_name == 'release' 43 | uses: actions/upload-release-asset@v1 44 | env: 45 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | with: 47 | asset_path: picovision/build/${{env.RELEASE_FILE}}.zip 48 | upload_url: ${{github.event.release.upload_url}} 49 | asset_name: ${{env.RELEASE_FILE}}.zip 50 | asset_content_type: application/octet-stream 51 | -------------------------------------------------------------------------------- /.github/workflows/micropython.yml: -------------------------------------------------------------------------------- 1 | name: MicroPython Firmware 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [created] 8 | 9 | env: 10 | MICROPYTHON_VERSION: 856e08b1931b88271816a2f60648f6ff332235b2 11 | PIMORONI_PICO_VERSION: v1.21.0 12 | WORKFLOW_VERSION: v1 13 | 14 | jobs: 15 | deps: 16 | runs-on: ubuntu-20.04 17 | name: Dependencies 18 | steps: 19 | - name: Workspace Cache 20 | id: cache 21 | uses: actions/cache@v3 22 | with: 23 | path: ${{runner.workspace}} 24 | key: workspace-micropython-${{env.MICROPYTHON_VERSION}}-${{env.WORKFLOW_VERSION}} 25 | restore-keys: | 26 | workspace-micropython-${{env.MICROPYTHON_VERSION}}-${{env.WORKFLOW_VERSION}} 27 | 28 | # Check out MicroPython 29 | - name: Checkout MicroPython 30 | if: steps.cache.outputs.cache-hit != 'true' 31 | uses: actions/checkout@v3 32 | with: 33 | repository: micropython/micropython 34 | ref: ${{env.MICROPYTHON_VERSION}} 35 | submodules: false # MicroPython submodules are hideously broken 36 | path: micropython 37 | 38 | - name: Fetch Pico submodules 39 | if: steps.cache.outputs.cache-hit != 'true' 40 | shell: bash 41 | working-directory: micropython 42 | run: | 43 | git submodule update --init lib/pico-sdk 44 | git submodule update --init lib/cyw43-driver 45 | git submodule update --init lib/lwip 46 | git submodule update --init lib/mbedtls 47 | git submodule update --init lib/micropython-lib 48 | git submodule update --init lib/tinyusb 49 | git submodule update --init lib/btstack 50 | 51 | - name: Build mpy-cross 52 | if: steps.cache.outputs.cache-hit != 'true' 53 | shell: bash 54 | working-directory: micropython/mpy-cross 55 | run: make 56 | 57 | build: 58 | needs: deps 59 | name: Build ${{matrix.name}} 60 | runs-on: ubuntu-20.04 61 | strategy: 62 | matrix: 63 | include: 64 | - name: PicoVision 65 | shortname: picovision 66 | board: PIMORONI_PICOVISION 67 | cmake_settings: 68 | - name: PicoVision Widescreen 69 | shortname: picovision-widescreen 70 | board: PIMORONI_PICOVISION 71 | cmake_settings: -DSUPPORT_WIDE_MODES=1 72 | 73 | env: 74 | RELEASE_FILE: pimoroni-${{matrix.shortname}}-${{github.event.release.tag_name || github.sha}}-micropython.uf2 75 | FIRMWARE_DIR: "$GITHUB_WORKSPACE/picovision/firmware" 76 | BOARD_DIR: "$GITHUB_WORKSPACE/picovision/firmware/${{matrix.board}}" 77 | 78 | steps: 79 | - name: Compiler Cache 80 | uses: actions/cache@v3 81 | with: 82 | path: /home/runner/.ccache 83 | key: ccache-micropython-${{matrix.shortname}}-${{github.ref}}-${{github.sha}} 84 | restore-keys: | 85 | ccache-micropython-${{matrix.shortname}}-${{github.ref}} 86 | ccache-micropython-${{matrix.shortname}}- 87 | 88 | - name: Workspace Cache 89 | uses: actions/cache@v3 90 | with: 91 | path: ${{runner.workspace}} 92 | key: workspace-micropython-${{env.MICROPYTHON_VERSION}}-${{env.WORKFLOW_VERSION}} 93 | restore-keys: | 94 | workspace-micropython-${{env.MICROPYTHON_VERSION}}-${{env.WORKFLOW_VERSION}} 95 | 96 | - uses: actions/checkout@v3 97 | with: 98 | submodules: true 99 | path: picovision 100 | 101 | # Check out Pimoroni Pico 102 | - uses: actions/checkout@v3 103 | with: 104 | repository: pimoroni/pimoroni-pico 105 | ref: ${{env.PIMORONI_PICO_VERSION}} 106 | submodules: true 107 | path: pimoroni-pico 108 | 109 | # HACK: Patch startup overclock into Pico SDK 110 | - name: "HACK: Startup Overclock Patch" 111 | shell: bash 112 | working-directory: micropython/lib/pico-sdk 113 | run: | 114 | git apply "${{env.FIRMWARE_DIR}}/startup_overclock.patch" 115 | 116 | - name: "HACK: CMakeLists.txt Disable C++ Exceptions Patch" 117 | shell: bash 118 | working-directory: micropython 119 | run: git apply "${{env.FIRMWARE_DIR}}/micropython_nano_specs.patch" 120 | 121 | # Install apt packages 122 | - name: Install CCache & Compiler 123 | shell: bash 124 | run: 125 | sudo apt update && sudo apt install ccache gcc-arm-none-eabi 126 | 127 | # Build firmware 128 | - name: Configure MicroPython 129 | shell: bash 130 | working-directory: micropython/ports/rp2 131 | run: | 132 | cmake -S . -B build -DPICO_BUILD_DOCS=0 -DUSER_C_MODULES=${{env.BOARD_DIR}}/micropython.cmake -DMICROPY_BOARD_DIR=${{env.BOARD_DIR}} -DMICROPY_BOARD=${{env.BOARD}} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ${{matrix.cmake_settings}} 133 | 134 | - name: Build MicroPython 135 | shell: bash 136 | working-directory: micropython/ports/rp2 137 | run: | 138 | ccache --zero-stats || true 139 | cmake --build build -j 2 140 | ccache --show-stats || true 141 | 142 | - name: Rename .uf2 for artifact 143 | shell: bash 144 | working-directory: micropython/ports/rp2/build 145 | run: | 146 | cp firmware.uf2 ${{env.RELEASE_FILE}} 147 | 148 | - name: Store .uf2 as artifact 149 | uses: actions/upload-artifact@v3 150 | with: 151 | name: ${{env.RELEASE_FILE}} 152 | path: micropython/ports/rp2/build/${{env.RELEASE_FILE}} 153 | 154 | - name: Upload .uf2 155 | if: github.event_name == 'release' 156 | uses: actions/upload-release-asset@v1 157 | env: 158 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 159 | with: 160 | asset_path: micropython/ports/rp2/build/${{env.RELEASE_FILE}} 161 | upload_url: ${{github.event.release.upload_url}} 162 | asset_name: ${{env.RELEASE_FILE}} 163 | asset_content_type: application/octet-stream 164 | -------------------------------------------------------------------------------- /.github/workflows/python-linting.yml: -------------------------------------------------------------------------------- 1 | name: Python Linting 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | name: Python Linting 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Install Python Deps 15 | run: python3 -m pip install ruff 16 | 17 | - name: Lint PicoVision Examples 18 | shell: bash 19 | run: | 20 | python3 -m ruff --show-source --ignore E501 examples/ 21 | 22 | - name: Lint PicoVision Python Libraries 23 | shell: bash 24 | run: | 25 | python3 -m ruff --show-source --ignore E501,F401,F403 lib/ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.9) 2 | 3 | project(PicoVision) 4 | 5 | install(FILES 6 | ${CMAKE_CURRENT_LIST_DIR}/examples/main.py 7 | ${CMAKE_CURRENT_LIST_DIR}/examples/rainbow_wheel.py 8 | ${CMAKE_CURRENT_LIST_DIR}/examples/screenmodes.py 9 | ${CMAKE_CURRENT_LIST_DIR}/examples/scrollgroups.py 10 | ${CMAKE_CURRENT_LIST_DIR}/examples/starfield_rainbow.py 11 | ${CMAKE_CURRENT_LIST_DIR}/examples/starfield.py 12 | ${CMAKE_CURRENT_LIST_DIR}/examples/vector_clock_smooth.py 13 | ${CMAKE_CURRENT_LIST_DIR}/examples/vector_clock.py 14 | ${CMAKE_CURRENT_LIST_DIR}/examples/toaster.png 15 | DESTINATION . 16 | ) 17 | 18 | set(examples 19 | alert 20 | bouncing_logo 21 | floppy_birb 22 | pride 23 | sneks_and_ladders 24 | ) 25 | 26 | foreach(example IN LISTS examples) 27 | install(DIRECTORY 28 | ${CMAKE_CURRENT_LIST_DIR}/examples/${example}/ 29 | DESTINATION ${example} 30 | ) 31 | endforeach() 32 | 33 | # setup release packages 34 | set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) 35 | set(CPACK_GENERATOR "ZIP") 36 | include(CPack) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pimoroni Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PicoVision 2 | 3 | This repository is home to the MicroPython build and examples for Pimoroni PicoVision. 4 | 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/picovision/micropython.yml?branch=main&label=MicroPython)](https://github.com/pimoroni/picovision/actions/workflows/micropython.yml) 6 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/pimoroni/picovision)](https://github.com/pimoroni/picovision/releases/latest/) 7 | 8 | - [Introduction](#introduction) 9 | - [Download MicroPython](#download-micropython) 10 | - [Documentation](#documentation) 11 | - [C/C++ Resources](#cc-resources) 12 | - [C/C++ Community Projects](#cc-community-projects) 13 | 14 | ## Introduction 15 | 16 | PicoVision enables you to create big, bold audio visual projects using MicroPython and an HDMI display of your choice. 17 | 18 | * :link: [Buy a PicoVision here](https://shop.pimoroni.com/products/picovision) 19 | 20 | ## Download MicroPython 21 | 22 | * [PicoVision MicroPython Releases](https://github.com/pimoroni/picovision/releases) 23 | 24 | ## Documentation 25 | 26 | * :link: [Learn: Getting Started with PicoVision](https://learn.pimoroni.com/article/getting-started-with-picovision) 27 | * [Docs: Instructions for installing MicroPython](docs/setting-up-micropython.md) 28 | * [Docs: Hardware Overview](docs/hardware.md) 29 | * [Docs: Python](docs/python-documentation.md) 30 | * [Docs: Tips & Tricks](docs/tips-and-tricks.md) 31 | 32 | ## C/C++ Resources 33 | 34 | * :link: [GPU Firmware](https://github.com/MichaelBell/pico-stick/) 35 | * :link: [C++ Boilerplate](https://github.com/pimoroni/picovision-boilerplate/) 36 | 37 | ## C/C++ Community Projects 38 | 39 | * :link: [PicoVision Demos](https://github.com/technolhodgy/picovision-demos) 40 | * :link: [Mike's PicoVision Projects](https://github.com/MichaelBell/picovision-projects) 41 | * :link: [PicoVision Micro Computer](https://github.com/Gadgetoid/picovision-micro-computer) 42 | * :link: [Bad Apple for PicoVision](https://github.com/MichaelBell/badapple) 43 | * :link: [RP2040 Doom for PicoVision](https://github.com/MichaelBell/rp2040-doom) 44 | -------------------------------------------------------------------------------- /docs/dfu_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/docs/dfu_mode.png -------------------------------------------------------------------------------- /docs/setting-up-micropython.md: -------------------------------------------------------------------------------- 1 | # Installing MicroPython 2 | 3 | - [Which file to download?](#which-file-to-download) 4 | - [Entering DFU/bootloader mode](#entering-dfubootloader-mode) 5 | - [Copying the firmware to your PicoVision](#copying-the-firmware-to-your-picovision) 6 | - [Where are the examples?](#where-are-the-examples) 7 | - [Troubleshooting](#troubleshooting) 8 | 9 | We provide pre-built MicroPython images which include all the libraries and drivers you'll need to get started with PicoVision. To install MicroPython, you'll need to **copy the appropriate .uf2 file from the releases page to your device while it's in DFU/bootloader mode.** 10 | 11 | ## Which file to download? 12 | 13 | On the releases page you'll find two .uf2 files. A widescreen and a regular build of the PicoVision firmware. 14 | 15 | - [Releases page](https://github.com/pimoroni/picovision/releases) 16 | 17 | | Build | .uf2 File | 18 | |-----------------------------------------------------------|-------------------------------------------------------| 19 | | Normal resolutions, up to 80 sprites. | pimoroni-picovision-vX.X.X-micropython.uf2 | 20 | | Widescreen (max 1280x720†) resolutions, up to 32 sprites. | pimoroni-picovision-widescreen-vX.X.X-micropython.uf2 | 21 | 22 | * † @ 30Hz, limited support on displays 23 | 24 | ## Entering DFU/bootloader mode 25 | 26 | With your PicoVision plugged into your computer, just **hold down the BOOTSEL button and tap RESET**. A new drive should pop up on your computer called 'RPI_RP2'. 27 | 28 | ![Screenshot showing the RPI-RP2 drive](dfu_mode.png) 29 | 30 | ## Copying the firmware to your PicoVision 31 | 32 | **Copy the downloaded firmware image across to the 'RPI-RP2' drive**. Once it has finished uploading (which takes a few seconds) the board will restart itself. 33 | 34 | ⚠ Note that once your board restarts running MicroPython it will no longer show up as a drive. To program it and to transfer files to and from it you'll need to use an interpreter, such as Thonny or Mu. 35 | 36 | - [Download Thonny](https://thonny.org/) 37 | - [Download Mu](https://codewith.mu/) 38 | 39 | You can also transfer files to boards running MicroPython using command line tools, like `mpremote`. 40 | 41 | - https://docs.micropython.org/en/latest/reference/mpremote.html 42 | 43 | ## Where are the examples? 44 | 45 | You can find all our MicroPython examples at the link below. 46 | 47 | - [MicroPython examples](/examples) 48 | 49 | ## Troubleshooting 50 | 51 | Having trouble getting started? Check out the link below: 52 | 53 | - :link: [MicroPython FAQs](https://github.com/pimoroni/pimoroni-pico/blob/main/faqs-micropython.md) 54 | -------------------------------------------------------------------------------- /docs/tips-and-tricks.md: -------------------------------------------------------------------------------- 1 | # PicoVision: Tips & Tricks 2 | 3 | ## "Hardware" Scrolling 4 | 5 | PicoVision allows you to work with a drawing canvas that's larger than your screen and apply up to three scroll offsets to groups of scanlines. 6 | 7 | Each scanline can be assigned to a scroll group, and there's no requirement for these to be contigous. 8 | 9 | To set up scroll offsets you must use the `set_scroll_group_for_lines` method, this takes a group number (0-3, group 0 can't be scrolled) and the lines you want to assign to it. 10 | 11 | For example, slicing a 320x240 screen into three equal portions, each with their own scroll group, might look like this: 12 | 13 | ```python 14 | display.set_scroll_group_for_lines(1, 0, 80) 15 | display.set_scroll_group_for_lines(2, 80, 160) 16 | display.set_scroll_group_for_lines(3, 160, 240) 17 | ``` 18 | 19 | To scroll a portion of the screen, you must use the `set_scroll_group_offset` function. This takes the scroll group to offset and the X and Y values to scroll it: 20 | 21 | ```python 22 | display.set_scroll_group_offset(1, x, y) 23 | display.set_scroll_group_offset(2, x, y) 24 | display.set_scroll_group_offset(3, x, y) 25 | ``` 26 | 27 | If we put this together, we can scroll three lines of text across the screen without having to pay the cost of drawing them every frame: 28 | 29 | ```python 30 | import time 31 | import math 32 | 33 | from picographics import PicoGraphics, PEN_RGB555 34 | 35 | WIDTH = 320 36 | HEIGHT = 240 37 | 38 | display = PicoGraphics(pen_type=PEN_RGB555, width=WIDTH, height=HEIGHT, frame_width=WIDTH * 2, frame_height=HEIGHT) 39 | 40 | TEXT = "Hello World" 41 | TEXT_SCALE = 3 42 | 43 | BLACK = display.create_pen(0, 0, 0) 44 | WHITE = display.create_pen(255, 255, 255) 45 | 46 | # Split the display into three scrollable regions of equal size 47 | display.set_scroll_group_for_lines(1, 0, 80) 48 | display.set_scroll_group_for_lines(2, 80, 160) 49 | display.set_scroll_group_for_lines(3, 160, 240) 50 | 51 | # Calculate the position of the text 52 | text_width = display.measure_text(TEXT, scale=TEXT_SCALE) 53 | text_height = 5 * TEXT_SCALE 54 | 55 | top = int(80 / 2) - text_height 56 | 57 | left = int((WIDTH - text_width) / 2) 58 | left += int(WIDTH / 2) 59 | 60 | # Calculate how much we should move the text to swoosh it back and forth 61 | offset = (WIDTH - text_width) / 2 62 | 63 | # Draw the display, just once, and flip the buffer to the "GPU" 64 | display.set_pen(BLACK) 65 | display.clear() 66 | display.set_pen(WHITE) 67 | display.text(TEXT, left, 0 + top, scale=TEXT_SCALE) 68 | display.text(TEXT, left, 80 + top, scale=TEXT_SCALE) 69 | display.text(TEXT, left, 160 + top, scale=TEXT_SCALE) 70 | display.update() 71 | 72 | # Calculate some offsets and swish each slice of the display back and forth 73 | while True: 74 | n = math.pi / 1.5 75 | t = time.ticks_ms() / 500.0 / math.pi 76 | offset1 = WIDTH / 2 + math.sin(t) * offset 77 | offset2 = WIDTH / 2 + math.sin(t + n) * offset 78 | offset3 = WIDTH / 2 + math.sin(t + n + n) * offset 79 | display.set_scroll_group_offset(1, int(offset1), 0) 80 | display.set_scroll_group_offset(2, int(offset2), 0) 81 | display.set_scroll_group_offset(3, int(offset3), 0) 82 | ``` 83 | 84 | ## Fill Rate & Drawing Things Fast 85 | 86 | At stock clock on the CPU, PicoVision has a pixel fill rate of approximately 15 million pixels (16bit RGB555) a second. 87 | 88 | This might sound significant, but that amounts to a screen clear at 720x480 taking 22ms. 89 | 90 | That means that drawing is costly, and you should avoid uncessary drawing at all costs. While sprites are a great way to work around this, they're not always ideal. 91 | 92 | * Use `set_clip` to restrict drawing to just the regions you want to change 93 | * Use `rectangle` or `circle` instead of `clear` to clear regions for overdrawing 94 | * Use scanline scrolling to move things around the screen 95 | -------------------------------------------------------------------------------- /drivers/aps6404/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(aps6404.cmake) -------------------------------------------------------------------------------- /drivers/aps6404/aps6404.cmake: -------------------------------------------------------------------------------- 1 | set(DRIVER_NAME aps6404) 2 | add_library(${DRIVER_NAME} INTERFACE) 3 | 4 | target_sources(${DRIVER_NAME} INTERFACE 5 | ${CMAKE_CURRENT_LIST_DIR}/aps6404.cpp 6 | ${CMAKE_CURRENT_LIST_DIR}/aps6404_perf_critical.cpp 7 | ) 8 | 9 | pico_generate_pio_header(${DRIVER_NAME} ${CMAKE_CURRENT_LIST_DIR}/aps6404.pio) 10 | 11 | target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR}) 12 | 13 | target_link_libraries(${DRIVER_NAME} INTERFACE pico_stdlib hardware_pio hardware_dma hardware_irq) -------------------------------------------------------------------------------- /drivers/aps6404/aps6404.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "aps6404.hpp" 3 | #include "hardware/dma.h" 4 | #include "hardware/irq.h" 5 | #include "hardware/sync.h" 6 | #include "hardware/clocks.h" 7 | #include "pico/stdlib.h" 8 | 9 | #ifndef NO_QSTR 10 | #include "aps6404.pio.h" 11 | #endif 12 | 13 | #ifndef MICROPY_BUILD_TYPE 14 | #define mp_printf(_, ...) printf(__VA_ARGS__); 15 | #else 16 | extern "C" { 17 | #include "py/runtime.h" 18 | } 19 | #endif 20 | 21 | namespace { 22 | void aps6404_reset_program_init(PIO pio, uint sm, uint offset, uint csn, uint mosi) { 23 | uint miso = mosi + 1; 24 | pio_gpio_init(pio, csn); 25 | pio_gpio_init(pio, csn + 1); 26 | pio_gpio_init(pio, mosi); 27 | pio_sm_set_consecutive_pindirs(pio, sm, csn, 2, true); 28 | pio_sm_set_consecutive_pindirs(pio, sm, mosi, 1, true); 29 | pio_sm_set_consecutive_pindirs(pio, sm, miso, 1, false); 30 | 31 | pio_sm_config c = sram_reset_program_get_default_config(offset); 32 | sm_config_set_in_pins(&c, miso); 33 | sm_config_set_in_shift(&c, false, true, 32); 34 | sm_config_set_out_pins(&c, mosi, 1); 35 | sm_config_set_out_shift(&c, false, true, 32); 36 | sm_config_set_sideset_pins(&c, csn); 37 | sm_config_set_clkdiv(&c, 4.f); 38 | 39 | pio_sm_init(pio, sm, offset, &c); 40 | pio_sm_set_enabled(pio, sm, true); 41 | } 42 | void aps6404_program_init(PIO pio, uint sm, uint offset, uint csn, uint mosi, bool slow, bool fast, bool reset) { 43 | pio_gpio_init(pio, csn); 44 | pio_gpio_init(pio, csn + 1); 45 | pio_gpio_init(pio, mosi); 46 | pio_gpio_init(pio, mosi + 1); 47 | pio_gpio_init(pio, mosi + 2); 48 | pio_gpio_init(pio, mosi + 3); 49 | pio_sm_set_consecutive_pindirs(pio, sm, csn, 2, true); 50 | pio_sm_set_consecutive_pindirs(pio, sm, mosi, 4, false); 51 | 52 | pio_sm_config c = slow ? sram_slow_program_get_default_config(offset) : 53 | fast ? sram_fast_program_get_default_config(offset) : 54 | reset ? sram_reset_qpi_program_get_default_config(offset) : 55 | sram_program_get_default_config(offset); 56 | sm_config_set_in_pins(&c, mosi); 57 | sm_config_set_in_shift(&c, false, true, 32); 58 | sm_config_set_out_pins(&c, mosi, 4); 59 | sm_config_set_out_shift(&c, false, true, 32); 60 | sm_config_set_set_pins(&c, mosi, 4); 61 | sm_config_set_sideset_pins(&c, csn); 62 | 63 | pio_sm_init(pio, sm, offset, &c); 64 | pio_sm_set_enabled(pio, sm, true); 65 | } 66 | } 67 | 68 | static const pio_program* pio_prog[2] = {nullptr, nullptr}; 69 | static uint16_t pio_offset[2] = {0xffff, 0xffff}; 70 | 71 | void pio_remove_exclusive_program(PIO pio) { 72 | uint8_t pio_index = pio == pio0 ? 0 : 1; 73 | const pio_program* current_program = pio_prog[pio_index]; 74 | uint16_t current_offset = pio_offset[pio_index]; 75 | if(current_program) { 76 | pio_remove_program(pio, current_program, current_offset); 77 | pio_prog[pio_index] = nullptr; 78 | pio_offset[pio_index] = 0xffff; 79 | } 80 | } 81 | 82 | uint16_t pio_change_exclusive_program(PIO pio, const pio_program* prog) { 83 | pio_remove_exclusive_program(pio); 84 | uint8_t pio_index = pio == pio0 ? 0 : 1; 85 | pio_prog[pio_index] = prog; 86 | pio_offset[pio_index] = pio_add_program(pio, prog); 87 | return pio_offset[pio_index]; 88 | }; 89 | 90 | namespace pimoroni { 91 | APS6404::APS6404(uint pin_csn, uint pin_d0, PIO pio) 92 | : pin_csn(pin_csn) 93 | , pin_d0(pin_d0) 94 | , pio(pio) 95 | { 96 | // Initialize data pins 97 | for (int i = 0; i < 4; ++i) { 98 | gpio_init(pin_d0 + i); 99 | gpio_disable_pulls(pin_d0 + i); 100 | } 101 | 102 | pio_sm = pio_claim_unused_sm(pio, true); 103 | 104 | // Claim DMA channels 105 | dma_channel = dma_claim_unused_channel(true); 106 | read_cmd_dma_channel = dma_claim_unused_channel(true); 107 | setup_dma_config(); 108 | } 109 | 110 | void APS6404::init() { 111 | pio_sm_set_enabled(pio, pio_sm, false); 112 | 113 | pio_offset = pio_change_exclusive_program(pio, &sram_reset_program); 114 | aps6404_reset_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0); 115 | 116 | sleep_us(200); 117 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 118 | pio_sm_put_blocking(pio, pio_sm, 0x66000000u); 119 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 120 | pio_sm_put_blocking(pio, pio_sm, 0x99000000u); 121 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 122 | pio_sm_put_blocking(pio, pio_sm, 0x35000000u); 123 | sleep_us(500); 124 | 125 | adjust_clock(); 126 | } 127 | 128 | void APS6404::set_qpi() { 129 | pio_sm_set_enabled(pio, pio_sm, false); 130 | 131 | pio_offset = pio_change_exclusive_program(pio, &sram_reset_program); 132 | aps6404_reset_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0); 133 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 134 | pio_sm_put_blocking(pio, pio_sm, 0x35000000u); 135 | 136 | while (!pio_sm_is_tx_fifo_empty(pio, pio_sm) || pio->sm[pio_sm].addr != pio_offset); 137 | 138 | adjust_clock(); 139 | } 140 | 141 | void APS6404::set_spi() { 142 | pio_sm_set_enabled(pio, pio_sm, false); 143 | 144 | pio_offset = pio_change_exclusive_program(pio, &sram_reset_qpi_program); 145 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, false, true); 146 | pio_sm_put_blocking(pio, pio_sm, 0x00000001u); 147 | pio_sm_put_blocking(pio, pio_sm, 0xF5000000u); 148 | } 149 | 150 | void APS6404::adjust_clock() { 151 | pio_sm_set_enabled(pio, pio_sm, false); 152 | uint32_t clock_hz = clock_get_hz(clk_sys); 153 | 154 | if (clock_hz > 296000000) { 155 | pio_offset = pio_change_exclusive_program(pio, &sram_fast_program); 156 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, true, false); 157 | } 158 | else if (clock_hz < 130000000) { 159 | pio_offset = pio_change_exclusive_program(pio, &sram_slow_program); 160 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, true, false, false); 161 | } 162 | else { 163 | pio_offset = pio_change_exclusive_program(pio, &sram_program); 164 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, false, false); 165 | } 166 | 167 | last_cmd_was_write = false; 168 | page_smashing_ok = clock_hz <= 168000000; 169 | } 170 | 171 | 172 | 173 | void APS6404::setup_dma_config() { 174 | dma_channel_config c = dma_channel_get_default_config(read_cmd_dma_channel); 175 | channel_config_set_read_increment(&c, true); 176 | channel_config_set_write_increment(&c, false); 177 | channel_config_set_dreq(&c, pio_get_dreq(pio, pio_sm, true)); 178 | channel_config_set_transfer_data_size(&c, DMA_SIZE_32); 179 | 180 | dma_channel_configure( 181 | read_cmd_dma_channel, &c, 182 | &pio->txf[pio_sm], 183 | multi_read_cmd_buffer, 184 | 0, 185 | false 186 | ); 187 | 188 | write_config = dma_channel_get_default_config(dma_channel); 189 | channel_config_set_read_increment(&write_config, true); 190 | channel_config_set_write_increment(&write_config, false); 191 | channel_config_set_dreq(&write_config, pio_get_dreq(pio, pio_sm, true)); 192 | channel_config_set_transfer_data_size(&write_config, DMA_SIZE_32); 193 | channel_config_set_bswap(&write_config, true); 194 | 195 | read_config = dma_channel_get_default_config(dma_channel); 196 | channel_config_set_read_increment(&read_config, false); 197 | channel_config_set_write_increment(&read_config, true); 198 | channel_config_set_dreq(&read_config, pio_get_dreq(pio, pio_sm, false)); 199 | channel_config_set_transfer_data_size(&read_config, DMA_SIZE_32); 200 | channel_config_set_bswap(&read_config, true); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /drivers/aps6404/aps6404.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "hardware/pio.h" 5 | #include "hardware/dma.h" 6 | 7 | namespace pimoroni { 8 | class APS6404 { 9 | public: 10 | static constexpr int RAM_SIZE = 8 * 1024 * 1024; 11 | static constexpr int PAGE_SIZE = 1024; 12 | 13 | APS6404(uint pin_csn = 17, uint pin_d0 = 19, PIO pio = pio1); 14 | 15 | void init(); 16 | 17 | void set_qpi(); 18 | void set_spi(); 19 | 20 | // Must be called if the system clock rate is changed after init(). 21 | void adjust_clock(); 22 | 23 | // Start a write, this completes asynchronously, this function blocks if another 24 | // transfer is already in progress 25 | // Writes should always be <= 1KB. 26 | void write(uint32_t addr, uint32_t* data, uint32_t len_in_bytes); 27 | void write_repeat(uint32_t addr, uint32_t data, uint32_t len_in_bytes); 28 | 29 | // Start a read, this completes asynchronously, this function only blocks if another 30 | // transfer is already in progress 31 | void read(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words); 32 | 33 | // Start multiple reads to the same buffer. They completes asynchronously, 34 | // this function only blocks if another transfer is already in progress 35 | void multi_read(uint32_t* addresses, uint32_t* lengths, uint32_t num_addresses, uint32_t* read_buf, int chain_channel = -1); 36 | 37 | // Read and block until completion 38 | void read_blocking(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words) { 39 | read(addr, read_buf, len_in_words); 40 | wait_for_finish_blocking(); 41 | } 42 | 43 | // Block until any outstanding read or write completes 44 | void wait_for_finish_blocking() { 45 | dma_channel_wait_for_finish_blocking(dma_channel); 46 | } 47 | 48 | private: 49 | void write_no_page_crossing(uint32_t addr, uint32_t* data, uint32_t len_in_bytes); 50 | void start_read(uint32_t* read_buf, uint32_t total_len_in_words, int chain_channel = -1); 51 | void setup_dma_config(); 52 | uint32_t* add_read_to_cmd_buffer(uint32_t* cmd_buf, uint32_t addr, uint32_t len_in_words); 53 | 54 | uint pin_csn; // CSn, SCK must be next pin after CSn 55 | uint pin_d0; // D0, D1, D2, D3 must be consecutive 56 | 57 | PIO pio; 58 | uint16_t pio_sm; 59 | uint16_t pio_offset; 60 | const pio_program* pio_prog; 61 | 62 | uint dma_channel; 63 | uint read_cmd_dma_channel; 64 | bool last_cmd_was_write = false; 65 | bool page_smashing_ok = true; 66 | 67 | dma_channel_config write_config; 68 | dma_channel_config read_config; 69 | 70 | static constexpr int MULTI_READ_MAX_PAGES = 128; 71 | uint32_t multi_read_cmd_buffer[3 * MULTI_READ_MAX_PAGES]; 72 | uint32_t repeat_data; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /drivers/aps6404/aps6404.pio: -------------------------------------------------------------------------------- 1 | ; Pins: 2 | ; - CSn is side-set 0 3 | ; - SCK is side-set 1 4 | ; - D0 is IN/OUT/SET pin 0 5 | ; 6 | ; DMA is 32 bit. Stream takes form: 7 | ; 32 bits For read: length of read in nibbles, minus 4 8 | ; For write: length of write in nibbles, minus 1 9 | ; Original data length in bits must be a multiple of 32 10 | ; 8 bits AP6404 cmd (0xeb = read, 0x38 = write) 11 | ; 24 bits address 12 | ; 32 bits sram_offset_do_read or sram_offset_do_write 13 | ; For write, data 14 | 15 | ; Program for running at high frequencies (130MHz-290MHz RP2040 system clock) 16 | ; Waits an extra half cycle on read as the data back from the PSRAM appears a fixed 17 | ; amount of *time* after the CLK 18 | .program sram 19 | .side_set 2 20 | .wrap_target 21 | top: 22 | set x, 6 side 0b01 23 | out y, 32 side 0b01 24 | set pindirs, 0xf side 0b01 25 | 26 | ; Write command, followed by data 27 | addr_lp: 28 | out pins, 4 side 0b00 29 | jmp x--, addr_lp side 0b10 30 | out pins, 4 side 0b00 31 | out pc, 32 side 0b10 32 | PUBLIC do_write: 33 | out pins, 4 side 0b00 34 | jmp y--, do_write side 0b10 35 | set pindirs, 0 side 0b00 36 | out null, 32 side 0b01 37 | jmp top side 0b01 38 | 39 | ; Special command to read one byte 40 | PUBLIC do_read_one: 41 | ; Wait for 8.5 clocks (6 + 2.5 for latencies) 42 | set x, 6 side 0b00 43 | rwt_lp2: 44 | set pindirs, 0 side 0b10 45 | jmp x-- rwt_lp2 side 0b00 46 | set x, 1 side 0b10 47 | jmp rd_rem side 0b01 [1] 48 | 49 | ; Read command 50 | PUBLIC do_read: 51 | ; Wait for 8.5 clocks (6 + 2.5 for latencies) 52 | set x, 7 side 0b00 53 | rwt_lp: 54 | set pindirs, 0 side 0b10 55 | jmp x-- rwt_lp side 0b00 56 | 57 | set x, 2 side 0b10 58 | rd_lp: 59 | in pins, 4 side 0b00 60 | jmp y--, rd_lp side 0b10 61 | 62 | ; Read the remaining 3 nibbles, but no more clocks so we don't read beyond 63 | ; the requested length from the SRAM. 64 | rd_rem: 65 | in pins, 4 side 0b01 66 | jmp x--, rd_rem side 0b01 67 | .wrap 68 | 69 | ; Program for running at low frequencies (<130MHz RP2040 system clock) 70 | .program sram_slow 71 | .side_set 2 72 | .wrap_target 73 | top: 74 | set x, 6 side 0b01 75 | out y, 32 side 0b01 76 | set pindirs, 0xf side 0b01 77 | 78 | ; Write command, followed by data 79 | addr_lp: 80 | out pins, 4 side 0b00 81 | jmp x--, addr_lp side 0b10 82 | out pins, 4 side 0b00 83 | out pc, 32 side 0b10 84 | PUBLIC do_write: 85 | out pins, 4 side 0b00 86 | jmp y--, do_write side 0b10 87 | set pindirs, 0 side 0b00 88 | out null, 32 side 0b01 89 | jmp top side 0b01 90 | 91 | ; Special command to read one byte 92 | PUBLIC do_read_one: 93 | ; Wait for 8 clocks (6 + 2 for latencies) 94 | set x, 6 side 0b00 95 | rwt_lp2: 96 | set pindirs, 0 side 0b10 97 | jmp x-- rwt_lp2 side 0b00 98 | set x, 1 side 0b10 99 | jmp rd_rem side 0b01 100 | 101 | ; Read command 102 | PUBLIC do_read: 103 | set pindirs, 0 side 0b00 104 | ; Wait for 8 clocks (6 + 2 for latencies) 105 | set x, 6 side 0b10 106 | rwt_lp: 107 | nop side 0b00 108 | jmp x-- rwt_lp side 0b10 109 | 110 | set x, 2 side 0b00 111 | rd_lp: 112 | in pins, 4 side 0b10 113 | jmp y--, rd_lp side 0b00 114 | 115 | ; Read the remaining 3 nibbles, but no more clocks so we don't read beyond 116 | ; the requested length from the SRAM. 117 | rd_rem: 118 | in pins, 4 side 0b01 119 | jmp x--, rd_rem side 0b01 120 | .wrap 121 | 122 | ; Program for running at extreme frequencies (>290MHz RP2040 system clock) 123 | ; Runs clock at 1/3rd RP2040 clock with 66% duty cycle. Out of spec but 124 | ; then everything we are doing here is! 125 | .program sram_fast 126 | .side_set 2 127 | .wrap_target 128 | top: 129 | set x, 6 side 0b01 130 | out y, 32 side 0b01 131 | set pindirs, 0xf side 0b01 132 | 133 | ; Write command, followed by data 134 | addr_lp: 135 | out pins, 4 side 0b00 136 | jmp x--, addr_lp [1] side 0b10 137 | out pins, 4 side 0b00 138 | out pc, 32 [1] side 0b10 139 | PUBLIC do_write: 140 | out pins, 4 side 0b00 141 | jmp y--, do_write [1] side 0b10 142 | set pindirs, 0 side 0b00 143 | out null, 32 side 0b01 144 | jmp top side 0b01 145 | 146 | ; Special command to read one byte 147 | PUBLIC do_read_one: 148 | ; Wait for 8 clocks (6 + 2 for latencies) 149 | set x, 6 side 0b00 150 | rwt_lp2: 151 | set pindirs, 0 side 0b10 152 | jmp x-- rwt_lp2 side 0b00 153 | set x, 1 side 0b10 154 | jmp rd_rem side 0b01 155 | 156 | ; Read command 157 | PUBLIC do_read: 158 | set pindirs, 0 side 0b00 159 | ; Wait for 8 clocks (6 + 2 for latencies) 160 | set x, 6 [1] side 0b10 161 | rwt_lp: 162 | nop side 0b00 163 | jmp x-- rwt_lp [1] side 0b10 164 | 165 | set x, 2 side 0b00 166 | rd_lp: 167 | nop side 0b10 168 | in pins, 4 side 0b10 169 | jmp y--, rd_lp side 0b00 170 | 171 | ; Read the remaining 2 nibbles, but no more clocks so we don't read beyond 172 | ; the requested length from the SRAM. 173 | rd_rem: 174 | in pins, 4 [1] side 0b01 175 | jmp x--, rd_rem side 0b01 176 | .wrap 177 | 178 | 179 | ; Write only program that writes a short (<32 bit) command, 180 | ; ignoring the rest of the 32 bit input word. 181 | ; This is used to write the reset command and enter QPI mode. 182 | .program sram_reset 183 | .side_set 2 184 | .wrap_target 185 | out y, 32 side 0b01 186 | pull side 0b01 187 | wr_lp: 188 | out pins, 1 side 0b00 189 | jmp y--, wr_lp side 0b10 190 | out null, 32 [7] side 0b01 ; Ensure large enough gap between commands for reset time 191 | nop [7] side 0b01 192 | .wrap 193 | 194 | .program sram_reset_qpi 195 | .side_set 2 196 | .wrap_target 197 | out y, 32 side 0b01 198 | pull side 0b01 199 | set pindirs, 0xF side 0b01 200 | wr_lp: 201 | out pins, 4 side 0b00 202 | jmp y--, wr_lp side 0b10 203 | out null, 32 [7] side 0b01 ; Ensure large enough gap between commands for reset time 204 | set pindirs, 0 [7] side 0b01 205 | .wrap 206 | -------------------------------------------------------------------------------- /drivers/aps6404/aps6404_perf_critical.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "aps6404.hpp" 3 | #include "hardware/dma.h" 4 | 5 | #ifndef NO_QSTR 6 | #include "aps6404.pio.h" 7 | #endif 8 | 9 | namespace pimoroni { 10 | void APS6404::write_no_page_crossing(uint32_t addr, uint32_t* data, uint32_t len_in_bytes) { 11 | int len = len_in_bytes; 12 | int page_len = PAGE_SIZE - (addr & (PAGE_SIZE - 1)); 13 | 14 | if ((page_len & 3) != 0) { 15 | while (len > page_len) { 16 | wait_for_finish_blocking(); 17 | hw_set_bits(&dma_hw->ch[dma_channel].al1_ctrl, DMA_CH0_CTRL_TRIG_INCR_READ_BITS); 18 | 19 | pio_sm_put_blocking(pio, pio_sm, (page_len << 1) - 1); 20 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 21 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 22 | 23 | dma_channel_transfer_from_buffer_now(dma_channel, data, (page_len >> 2) + 1); 24 | 25 | len -= page_len; 26 | addr += page_len; 27 | data += page_len >> 2; 28 | int bytes_sent_last_word = page_len & 3; 29 | page_len = std::min(4 - bytes_sent_last_word, len); 30 | 31 | dma_channel_wait_for_finish_blocking(dma_channel); 32 | pio_sm_put_blocking(pio, pio_sm, (page_len << 1) - 1); 33 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 34 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 35 | pio_sm_put_blocking(pio, pio_sm, __builtin_bswap32(*data >> (8 * bytes_sent_last_word))); 36 | 37 | addr += page_len; 38 | len -= page_len; 39 | ++data; 40 | page_len = PAGE_SIZE - page_len; 41 | } 42 | } 43 | 44 | for (page_len = std::min(page_len, len); 45 | len > 0; 46 | addr += page_len, data += page_len >> 2, len -= page_len, page_len = std::min(PAGE_SIZE, len)) 47 | { 48 | wait_for_finish_blocking(); 49 | hw_set_bits(&dma_hw->ch[dma_channel].al1_ctrl, DMA_CH0_CTRL_TRIG_INCR_READ_BITS); 50 | 51 | pio_sm_put_blocking(pio, pio_sm, (page_len << 1) - 1); 52 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 53 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 54 | 55 | dma_channel_transfer_from_buffer_now(dma_channel, data, (page_len >> 2) + 1); 56 | } 57 | } 58 | 59 | void APS6404::write(uint32_t addr, uint32_t* data, uint32_t len_in_bytes) { 60 | if (!last_cmd_was_write) { 61 | last_cmd_was_write = true; 62 | wait_for_finish_blocking(); 63 | dma_channel_set_write_addr(dma_channel, &pio->txf[pio_sm], false); 64 | dma_hw->ch[dma_channel].al1_ctrl = write_config.ctrl; 65 | } 66 | 67 | if (!page_smashing_ok) { 68 | write_no_page_crossing(addr, data, len_in_bytes); 69 | return; 70 | } 71 | 72 | for (int len = len_in_bytes, page_len = std::min(PAGE_SIZE, len); 73 | len > 0; 74 | addr += page_len, data += page_len >> 2, len -= page_len, page_len = std::min(PAGE_SIZE, len)) 75 | { 76 | wait_for_finish_blocking(); 77 | hw_set_bits(&dma_hw->ch[dma_channel].al1_ctrl, DMA_CH0_CTRL_TRIG_INCR_READ_BITS); 78 | 79 | pio_sm_put_blocking(pio, pio_sm, (page_len << 1) - 1); 80 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 81 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 82 | 83 | dma_channel_transfer_from_buffer_now(dma_channel, data, (page_len >> 2) + 1); 84 | } 85 | } 86 | 87 | void APS6404::write_repeat(uint32_t addr, uint32_t data, uint32_t len_in_bytes) { 88 | if (!last_cmd_was_write) { 89 | last_cmd_was_write = true; 90 | dma_channel_set_write_addr(dma_channel, &pio->txf[pio_sm], false); 91 | dma_hw->ch[dma_channel].al1_ctrl = write_config.ctrl; 92 | } 93 | 94 | int first_page_len = PAGE_SIZE; 95 | if (!page_smashing_ok) { 96 | first_page_len -= (addr & (PAGE_SIZE - 1)); 97 | if ((first_page_len & 3) != 0 && (int)len_in_bytes > first_page_len) { 98 | wait_for_finish_blocking(); 99 | hw_clear_bits(&dma_hw->ch[dma_channel].al1_ctrl, DMA_CH0_CTRL_TRIG_INCR_READ_BITS); 100 | 101 | pio_sm_put_blocking(pio, pio_sm, (first_page_len << 1) - 1); 102 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 103 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 104 | 105 | dma_channel_transfer_from_buffer_now(dma_channel, &data, (first_page_len >> 2) + 1); 106 | 107 | len_in_bytes -= first_page_len; 108 | addr += first_page_len; 109 | int bytes_sent_last_word = first_page_len & 3; 110 | first_page_len = PAGE_SIZE; 111 | data = (data >> (8 * bytes_sent_last_word)) | (data << (32 - (8 * bytes_sent_last_word))); 112 | } 113 | } 114 | 115 | for (int len = len_in_bytes, page_len = std::min(first_page_len, len); 116 | len > 0; 117 | addr += page_len, len -= page_len, page_len = std::min(PAGE_SIZE, len)) 118 | { 119 | wait_for_finish_blocking(); 120 | hw_clear_bits(&dma_hw->ch[dma_channel].al1_ctrl, DMA_CH0_CTRL_TRIG_INCR_READ_BITS); 121 | repeat_data = data; 122 | 123 | pio_sm_put_blocking(pio, pio_sm, (page_len << 1) - 1); 124 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 125 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 126 | 127 | dma_channel_transfer_from_buffer_now(dma_channel, &repeat_data, (page_len >> 2) + 1); 128 | } 129 | } 130 | 131 | void APS6404::read(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words) { 132 | start_read(read_buf, len_in_words); 133 | 134 | uint32_t first_page_len = PAGE_SIZE; 135 | if (!page_smashing_ok) { 136 | first_page_len -= (addr & (PAGE_SIZE - 1)); 137 | } 138 | 139 | if (first_page_len >= len_in_words << 2) { 140 | pio_sm_put_blocking(pio, pio_sm, (len_in_words * 8) - 4); 141 | pio_sm_put_blocking(pio, pio_sm, 0xeb000000u | addr); 142 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_read); 143 | } 144 | else { 145 | uint32_t* cmd_buf = add_read_to_cmd_buffer(multi_read_cmd_buffer, addr, len_in_words); 146 | dma_channel_transfer_from_buffer_now(read_cmd_dma_channel, multi_read_cmd_buffer, cmd_buf - multi_read_cmd_buffer); 147 | } 148 | } 149 | 150 | void APS6404::multi_read(uint32_t* addresses, uint32_t* lengths, uint32_t num_reads, uint32_t* read_buf, int chain_channel) { 151 | uint32_t total_len = 0; 152 | uint32_t* cmd_buf = multi_read_cmd_buffer; 153 | for (uint32_t i = 0; i < num_reads; ++i) { 154 | total_len += lengths[i]; 155 | cmd_buf = add_read_to_cmd_buffer(cmd_buf, addresses[i], lengths[i]); 156 | } 157 | 158 | start_read(read_buf, total_len, chain_channel); 159 | 160 | dma_channel_transfer_from_buffer_now(read_cmd_dma_channel, multi_read_cmd_buffer, cmd_buf - multi_read_cmd_buffer); 161 | } 162 | 163 | void APS6404::start_read(uint32_t* read_buf, uint32_t total_len_in_words, int chain_channel) { 164 | dma_channel_config c = read_config; 165 | if (chain_channel >= 0) { 166 | channel_config_set_chain_to(&c, chain_channel); 167 | } 168 | 169 | last_cmd_was_write = false; 170 | wait_for_finish_blocking(); 171 | 172 | dma_channel_configure( 173 | dma_channel, &c, 174 | read_buf, 175 | &pio->rxf[pio_sm], 176 | total_len_in_words, 177 | true 178 | ); 179 | } 180 | 181 | uint32_t* APS6404::add_read_to_cmd_buffer(uint32_t* cmd_buf, uint32_t addr, uint32_t len_in_words) { 182 | int32_t len_remaining = len_in_words << 2; 183 | uint32_t align = page_smashing_ok ? 0 : (addr & (PAGE_SIZE - 1)); 184 | uint32_t len = std::min((PAGE_SIZE - align), (uint32_t)len_remaining); 185 | 186 | while (true) { 187 | if (len < 2) { 188 | *cmd_buf++ = 0; 189 | *cmd_buf++ = 0xeb000000u | addr; 190 | *cmd_buf++ = pio_offset + sram_offset_do_read_one; 191 | } 192 | else { 193 | *cmd_buf++ = (len * 2) - 4; 194 | *cmd_buf++ = 0xeb000000u | addr; 195 | *cmd_buf++ = pio_offset + sram_offset_do_read; 196 | } 197 | len_remaining -= len; 198 | addr += len; 199 | 200 | if (len_remaining <= 0) break; 201 | 202 | len = len_remaining; 203 | if (len > PAGE_SIZE) len = PAGE_SIZE; 204 | } 205 | 206 | return cmd_buf; 207 | } 208 | } -------------------------------------------------------------------------------- /drivers/dv_display/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(dv_display.cmake) -------------------------------------------------------------------------------- /drivers/dv_display/dv_display.cmake: -------------------------------------------------------------------------------- 1 | set(DRIVER_NAME dv_display) 2 | 3 | # SWD loader 4 | add_library(swd_load INTERFACE) 5 | 6 | target_sources(swd_load INTERFACE 7 | ${CMAKE_CURRENT_LIST_DIR}/swd_load.cpp) 8 | 9 | target_include_directories(swd_load INTERFACE ${CMAKE_CURRENT_LIST_DIR}) 10 | 11 | pico_generate_pio_header(swd_load ${CMAKE_CURRENT_LIST_DIR}/swd.pio) 12 | 13 | target_link_libraries(swd_load INTERFACE pico_stdlib) 14 | 15 | # main DV display driver 16 | add_library(${DRIVER_NAME} INTERFACE) 17 | 18 | target_sources(${DRIVER_NAME} INTERFACE 19 | ${CMAKE_CURRENT_LIST_DIR}/${DRIVER_NAME}.cpp) 20 | 21 | target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR}) 22 | 23 | # Pull in pico libraries that we need 24 | target_link_libraries(${DRIVER_NAME} INTERFACE swd_load pico_stdlib aps6404 pimoroni_i2c) 25 | -------------------------------------------------------------------------------- /drivers/dv_display/swd.pio: -------------------------------------------------------------------------------- 1 | ; Pins: 2 | ; - SWC is side-set 0 3 | ; - SWD is IN/OUT/SET pin 0 4 | ; 5 | ; SWD write: 6 | ; 7 bits request (request parity set in input) 7 | ; 1 bit park 8 | ; 1 bit turn 9 | ; 3 bits response - fail if not 001 - sent LSB first 10 | ; 1 bit turn 11 | ; 32 bits data 12 | ; 1 bit parity (computed and sent from PIO) 13 | ; 14 | .program swd_write 15 | .side_set 1 opt 16 | error: 17 | irq 0 side 1 18 | 19 | top: 20 | ; Wait driven 21 | set pins, 0 side 1 22 | set pindirs, 1 side 1 23 | mov isr, null side 1 24 | 25 | set y, 6 side 1 26 | pull side 1 27 | set pindirs, 1 side 1 28 | 29 | ; Send 7 bits 30 | send_cmd: 31 | out pins, 1 [1] side 0 32 | jmp y--, send_cmd [1] side 1 33 | 34 | set pindirs, 0 side 0 35 | set y, 3 side 0 36 | 37 | ; Read 5 bits: park, turn, rsp 38 | read_rsp: 39 | in pins, 1 [1] side 1 40 | jmp y--, read_rsp [1] side 0 41 | in pins, 1 side 1 42 | 43 | set x, 0x1C side 0 44 | mov y, isr side 0 45 | pull side 1 46 | jmp x!=y, error side 1 ; Not OK 47 | 48 | set pindirs, 1 side 1 49 | set x, 0 side 1 50 | 51 | send_data: 52 | out y, 1 side 0 53 | mov pins, y side 0 54 | jmp y--, negate side 1 55 | jmp !osre, send_data side 1 56 | 57 | .wrap_target 58 | mov pins, x [1] side 0 59 | jmp top side 1 60 | 61 | negate: 62 | mov x, ~x side 1 63 | jmp !osre, send_data side 1 64 | 65 | ; Must be identical to above, except ignoring error. 66 | .program swd_write_ignore_error 67 | .side_set 1 opt 68 | error: 69 | irq 0 side 1 70 | 71 | top: 72 | ; Wait driven 73 | set pins, 0 side 1 74 | set pindirs, 1 side 1 75 | mov isr, null side 1 76 | 77 | set y, 6 side 1 78 | pull side 1 79 | set pindirs, 1 side 1 80 | 81 | ; Send 7 bits 82 | send_cmd: 83 | out pins, 1 [1] side 0 84 | jmp y--, send_cmd [1] side 1 85 | 86 | set pindirs, 0 side 0 87 | set y, 3 side 0 88 | 89 | ; Read 5 bits: park, turn, rsp 90 | read_rsp: 91 | in pins, 1 [1] side 1 92 | jmp y--, read_rsp [1] side 0 93 | in pins, 1 side 1 94 | 95 | set x, 0x1C side 0 96 | mov y, isr side 0 97 | pull side 1 98 | nop side 1 ; Ignore 99 | 100 | set pindirs, 1 side 1 101 | set x, 0 side 1 102 | 103 | send_data: 104 | out y, 1 side 0 105 | mov pins, y side 0 106 | jmp y--, negate side 1 107 | jmp !osre, send_data side 1 108 | 109 | .wrap_target 110 | mov pins, x [1] side 0 111 | jmp top side 1 112 | 113 | negate: 114 | mov x, ~x side 1 115 | jmp !osre, send_data side 1 116 | 117 | .program swd_read 118 | .side_set 1 opt 119 | error: 120 | irq 0 side 1 121 | 122 | .wrap_target 123 | top: 124 | ; Wait driven 125 | set pins, 0 side 1 126 | set pindirs, 1 side 1 127 | mov isr, null side 1 128 | 129 | set y, 6 side 1 130 | pull side 1 131 | set pindirs, 1 side 1 132 | 133 | ; Send 7 bits 134 | send_cmd: 135 | out pins, 1 [1] side 0 136 | jmp y--, send_cmd [1] side 1 137 | 138 | set pindirs, 0 side 0 139 | set y, 3 side 0 140 | 141 | ; Read 5 bits: park, turn, rsp 142 | read_rsp: 143 | in pins, 1 [1] side 1 144 | jmp y--, read_rsp [1] side 0 145 | in pins, 1 side 1 146 | 147 | set x, 0x1C side 1 148 | mov y, isr side 1 149 | jmp x!=y, error side 1 ; Not OK 150 | 151 | ; Read 32 bits 152 | set y, 31 [1] side 0 153 | read_data: 154 | in pins, 1 [1] side 1 155 | jmp y--, read_data [1] side 0 156 | 157 | ; Ignore parity and turn 158 | mov isr, ::isr [1] side 1 159 | push [1] side 0 160 | 161 | .program swd_raw_write 162 | .side_set 1 opt 163 | ; Wait undriven 164 | set pins, 0 side 1 165 | set pindirs, 1 side 1 166 | out y, 32 side 1 167 | 168 | send: 169 | out pins, 1 [1] side 0 170 | jmp y--, send [1] side 1 171 | 172 | out null, 32 side 1 173 | 174 | .program swd_raw_read 175 | .side_set 1 opt 176 | out y, 32 side 1 177 | 178 | read: 179 | in pins, 1 [1] side 1 180 | jmp y--, read [1] side 0 181 | 182 | push side 1 183 | 184 | % c-sdk { 185 | void swd_initial_init(PIO pio, uint sm, uint swc, uint swd) { 186 | pio_gpio_init(pio, swc); 187 | pio_gpio_init(pio, swd); 188 | pio_sm_set_consecutive_pindirs(pio, sm, swc, 1, true); 189 | pio_sm_set_consecutive_pindirs(pio, sm, swd, 1, false); 190 | } 191 | 192 | void swd_program_init(PIO pio, uint sm, uint offset, uint swc, uint swd, bool read, float clkdiv) { 193 | pio_sm_config c = read ? swd_read_program_get_default_config(offset) : swd_write_program_get_default_config(offset); 194 | sm_config_set_in_pins(&c, swd); 195 | sm_config_set_in_shift(&c, false, false, 32); 196 | sm_config_set_set_pins(&c, swd, 1); 197 | sm_config_set_out_pins(&c, swd, 1); 198 | sm_config_set_out_shift(&c, true, false, 32); 199 | sm_config_set_sideset_pins(&c, swc); 200 | sm_config_set_clkdiv(&c, clkdiv); 201 | 202 | pio_sm_init(pio, sm, offset, &c); 203 | pio_sm_set_enabled(pio, sm, true); 204 | } 205 | 206 | void swd_raw_program_init(PIO pio, uint sm, uint offset, uint swc, uint swd, bool read, float clkdiv) { 207 | pio_sm_config c = read ? swd_raw_read_program_get_default_config(offset) : swd_raw_write_program_get_default_config(offset); 208 | sm_config_set_in_pins(&c, swd); 209 | sm_config_set_in_shift(&c, false, false, 32); 210 | sm_config_set_set_pins(&c, swd, 1); 211 | sm_config_set_out_pins(&c, swd, 1); 212 | sm_config_set_out_shift(&c, true, true, 32); 213 | sm_config_set_sideset_pins(&c, swc); 214 | sm_config_set_clkdiv(&c, clkdiv); 215 | 216 | pio_sm_init(pio, sm, offset, &c); 217 | pio_sm_set_enabled(pio, sm, true); 218 | } 219 | %} -------------------------------------------------------------------------------- /drivers/dv_display/swd_load.hpp: -------------------------------------------------------------------------------- 1 | bool swd_load_program(const uint* addresses, const uint** data, const uint* data_len_in_bytes, uint num_sections, uint pc = 0x20000001, uint sp = 0x20042000, bool use_xip_as_ram = false); 2 | bool swd_reset(); -------------------------------------------------------------------------------- /examples/alert/alert.py: -------------------------------------------------------------------------------- 1 | """ 2 | A spaceship alert panel, with spurious graphs, 'radar' and noise. Used with permission of Pimoroni Mining Corporation. 3 | 4 | Press the Y button to cycle through the alert modes. 5 | 6 | You will need the future_earth.af font saved to your PicoVision alongside this script. 7 | Font originally from https://www.dafont.com/future-earth.font 8 | """ 9 | 10 | from modes import VGA 11 | from picovector import PicoVector, ANTIALIAS_X16, Polygon 12 | from pimoroni import Button 13 | import random 14 | from picosynth import PicoSynth, Channel 15 | 16 | # list of alert modes 17 | MODES = ["NOT ALERT", "RED ALERT", "YELLOW ALERT", "BLUE ALERT", "RAINBOW ALERT"] 18 | # ships you might encounter in your voyages 19 | SHIPS = ["GELF_SHIP", "BORG_CUBE", "STAR_DEST", "VORLONS", "BASE_STAR", "TARDIS", "THARGOID", "PLANET_X", "JWST", "ELONS_TESLA", "PLUTO", "THE_REVENGE", "BLACK_PEARL"] 20 | PULSE_SPEED = 0.2 21 | RAINBOW_SPEED = 0.04 22 | SIREN_URGENCY = 8 23 | VOLUME = 0.5 24 | 25 | # set up hardware 26 | graphics = VGA() 27 | 28 | vector = PicoVector(graphics) 29 | vector.set_font("/alert/future-earth.af", 120) 30 | 31 | WHITE = graphics.create_pen(255, 255, 255) 32 | BLACK = graphics.create_pen(0, 0, 0) 33 | RED = graphics.create_pen(255, 0, 0) 34 | GREEN = graphics.create_pen(32, 84, 36) 35 | 36 | WIDTH, HEIGHT = graphics.get_bounds() 37 | 38 | synth = PicoSynth() 39 | 40 | siren = synth.channel(0) 41 | siren.configure( 42 | waveforms=Channel.SQUARE | Channel.TRIANGLE, 43 | attack=0.1, 44 | decay=0.1, 45 | sustain=0.0, 46 | release=0.5, 47 | volume=VOLUME 48 | ) 49 | 50 | synth.play() 51 | 52 | button_y = Button(9) 53 | 54 | 55 | def make_starfield(): 56 | # populates the starfield with random stars and ships 57 | global stars, pmc, ship 58 | stars = [] 59 | pmc = (random.randint(0, WIDTH - 50), random.randint(HEIGHT - 160, HEIGHT - 120)) 60 | for i in range(90): 61 | stars.append((random.randint(8, WIDTH - 8), random.randint(HEIGHT - 180, HEIGHT - 100))) 62 | ship = (random.randint(0, WIDTH - 50), random.randint(HEIGHT - 160, HEIGHT - 120), random.choice(SHIPS)) 63 | 64 | 65 | def draw_starfield(): 66 | # draws the starfield and ships 67 | global stars, pmc 68 | graphics.set_pen(BLACK) 69 | graphics.rectangle(0, HEIGHT - 185, WIDTH, HEIGHT - 95) 70 | graphics.set_pen(GREEN) 71 | for s in stars: 72 | graphics.circle(s[0], s[1], 1) 73 | graphics.set_pen(WHITE) 74 | graphics.triangle(pmc[0], pmc[1], pmc[0] - 12, pmc[1] + 8, pmc[0] - 12, pmc[1] - 8) 75 | graphics.text("PMC_ship", pmc[0] + 4, pmc[1] + 4, scale=1) 76 | graphics.set_pen(RED) 77 | graphics.circle(ship[0], ship[1], 3) 78 | graphics.text(ship[2], ship[0] + 4, ship[1] + 4, scale=1) 79 | 80 | 81 | def full_refresh(): 82 | global pulse, frequency 83 | # redraws the border and starfield 84 | # drawing big polygons is resource intensive, so we only want to do it when we switch modes 85 | # first refresh the starfield 86 | make_starfield() 87 | pulse = 0.0 88 | frequency = 440 89 | # draw the elements that we don't need to redraw every time 90 | # we're drawing them twice, so they end up in both buffers 91 | for i in range(2): 92 | # clear the screen 93 | graphics.set_pen(BLACK) 94 | graphics.clear() 95 | graphics.set_pen(WHITE) 96 | # draw a border, with vectors 97 | vector.draw(OUTER_RECTANGLE, INNER_RECTANGLE) 98 | graphics.set_pen(BLACK) 99 | vector.set_font_size(30) 100 | vector.text("ALERT STATUS", 50, -15) 101 | draw_starfield() 102 | graphics.update() 103 | 104 | 105 | def rectangle(x, y, w, h, cx=0, cy=0): 106 | # returns a polygon representing a rectangle, with optional cutoff corners 107 | points = [] 108 | 109 | if cx > 0 and cy > 0: 110 | points.append((x, y + cy)) 111 | points.append((x + cx, y)) 112 | 113 | points.append((x + w - cx, y)) 114 | points.append((x + w, y + cy)) 115 | 116 | points.append((x + w, y + h - cy)) 117 | points.append((x + w - cx, y + h)) 118 | 119 | points.append((x + cx, y + h)) 120 | points.append((x, y + h - cy)) 121 | else: 122 | points.append((x, y)) 123 | points.append((x + w, y)) 124 | points.append((x + w, y + h)) 125 | points.append((x, y + h)) 126 | 127 | return Polygon(*points) 128 | 129 | 130 | # the shapes we'll use for the border 131 | BORDER_THICKNESS = 18 132 | OUTER_RECTANGLE = rectangle(0, 0, WIDTH, int(HEIGHT * 0.6), 40, 40) 133 | INNER_RECTANGLE = rectangle(BORDER_THICKNESS, BORDER_THICKNESS, WIDTH - (BORDER_THICKNESS * 2), int(HEIGHT * 0.6) - (BORDER_THICKNESS * 2), 30, 30) 134 | 135 | # variables 136 | stars = [] 137 | pmc = (0, 0) # for the Pimoroni Mining Corporation ship 138 | ship = (0, 0, "") 139 | text_hue = 0.0 140 | graph_hue = 0.0 141 | pulse = 0.0 142 | pulse_direction = PULSE_SPEED 143 | graph_data = [] 144 | frequency = 440 145 | 146 | # setup 147 | current_mode = MODES[0] 148 | # populate the graph data list with 12 random values between 1 and 100 149 | for i in range(12): 150 | graph_data.append(random.randint(1, 100)) 151 | vector.set_antialiasing(ANTIALIAS_X16) 152 | full_refresh() 153 | 154 | while True: 155 | if button_y.is_pressed: 156 | # cycle through the modes 157 | current_mode = MODES[(MODES.index(current_mode) + 1) % len(MODES)] 158 | full_refresh() 159 | 160 | # just clear the areas with text / graphs, for faster drawing 161 | graphics.set_pen(BLACK) 162 | graphics.rectangle(75, 60, 500, 55) 163 | graphics.rectangle(75, 180, 340, 55) 164 | graphics.rectangle(0, HEIGHT - 100, WIDTH, 100) 165 | 166 | # draw the text 167 | vector.set_font_size(120) 168 | # static 169 | if current_mode == "NOT ALERT": 170 | graphics.set_pen(WHITE) 171 | vector.text(current_mode, 80, -10) 172 | # increment the text hue (for rainbow mode) 173 | elif current_mode == "RAINBOW ALERT": 174 | text_hue += RAINBOW_SPEED 175 | graphics.set_pen(graphics.create_pen_hsv(text_hue, 1.0, 1.0)) 176 | vector.text(current_mode, 80, -10) 177 | else: 178 | # pulse the text 179 | pulse += pulse_direction 180 | if pulse >= 1.0: 181 | pulse = 1.0 182 | pulse_direction = -PULSE_SPEED 183 | elif pulse <= 0.0: 184 | pulse = 0.0 185 | pulse_direction = PULSE_SPEED 186 | 187 | if current_mode == "RED ALERT": 188 | text_hue = 0 / 360 189 | elif current_mode == "YELLOW ALERT": 190 | text_hue = 60 / 360 191 | elif current_mode == "BLUE ALERT": 192 | text_hue = 230 / 360 193 | graphics.set_pen(graphics.create_pen_hsv(text_hue, 1.0, pulse)) 194 | vector.text(current_mode, 80, -10) 195 | 196 | # draw spurious graphs 197 | # default colours for the graph 198 | graph_hue_start = 120 / 360 # green 199 | graph_hue_end = 0 / 360 # red 200 | # override the default colours 201 | if current_mode == "BLUE ALERT": 202 | graph_hue_start = 180 / 360 203 | graph_hue_end = 230 / 360 204 | # adjust the graph data 205 | for i in range(len(graph_data)): 206 | # adjust value up or down 207 | graph_data[i] += random.randint(-1, 1) 208 | if current_mode == "NOT ALERT": 209 | # everything is ok, keep values between 0 and 30 210 | graph_data[i] = max(0, min(graph_data[i], 20)) 211 | elif current_mode == "RED ALERT": 212 | # everything is bad, keep values between 80 and 100 213 | graph_data[i] = max(80, min(graph_data[i], 100)) 214 | elif current_mode == "YELLOW ALERT": 215 | # everything is yellow, keep values between 40 and 70 216 | graph_data[i] = max(40, min(graph_data[i], 70)) 217 | else: 218 | # keep value between 0 and 100 219 | graph_data[i] = max(0, min(graph_data[i], 100)) 220 | 221 | if current_mode == "RAINBOW ALERT": 222 | # use the text colour for the graphs 223 | graphics.set_pen(graphics.create_pen_hsv(text_hue, 1.0, 1.0)) 224 | else: 225 | # calculate a hue based on the value 226 | graph_hue = graph_hue_start + ((graph_data[i]) * (graph_hue_end - graph_hue_start) / 100) 227 | # make sure hue is between 0 and 1 228 | graph_hue = max(0.0, min(graph_hue, 1.0)) 229 | graphics.set_pen(graphics.create_pen_hsv(graph_hue, 1.0, 1.0)) 230 | # draw the bars at the bottom of the screen 231 | graphics.rectangle(int(WIDTH / 12 * i), HEIGHT - graph_data[i], 50, graph_data[i]) 232 | 233 | # make some noise 234 | if current_mode == "RED ALERT": 235 | # make a sireny noise 236 | siren.frequency(frequency) 237 | siren.trigger_attack() 238 | frequency += SIREN_URGENCY 239 | if frequency > 540: 240 | frequency = 440 241 | else: 242 | # beepety boopety 243 | # pick a random number from the graph data 244 | frequency = random.choice(graph_data) * 8 245 | # check frequency is not below 1 246 | frequency = max(1, frequency) 247 | siren.frequency(frequency) 248 | siren.trigger_attack() 249 | 250 | graphics.update() 251 | -------------------------------------------------------------------------------- /examples/alert/future-earth.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/alert/future-earth.af -------------------------------------------------------------------------------- /examples/basic/buttons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple demo of how to read the buttons on PicoVision. This example doesn't use the display. 3 | 4 | Buttons A and X are wired to the GPU, whilst Y is wired to the CPU, so you need to read them a little differently. 5 | """ 6 | 7 | from picovision import PicoVision, PEN_RGB555 8 | import machine 9 | import time 10 | 11 | display = PicoVision(PEN_RGB555, 640, 480) 12 | 13 | button_y = machine.Pin(9, machine.Pin.IN, machine.Pin.PULL_UP) 14 | 15 | print("Press a button!") 16 | 17 | while True: 18 | if button_y.value() == 0: 19 | print("Y is pressed") 20 | time.sleep(0.5) 21 | if display.is_button_x_pressed(): 22 | print("X is pressed") 23 | time.sleep(0.5) 24 | if display.is_button_a_pressed(): 25 | print("A is pressed") 26 | time.sleep(0.5) 27 | -------------------------------------------------------------------------------- /examples/basic/coolvetica_rg.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/basic/coolvetica_rg.af -------------------------------------------------------------------------------- /examples/basic/hello_world.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple demo of how to display text on PicoVision. 3 | """ 4 | 5 | from picovision import PicoVision, PEN_RGB555 6 | 7 | display = PicoVision(PEN_RGB555, 640, 480) 8 | 9 | WHITE = display.create_pen(255, 255, 255) 10 | 11 | display.set_pen(WHITE) 12 | display.text("Hello PicoVision!", 0, 0, scale=4) 13 | display.update() 14 | -------------------------------------------------------------------------------- /examples/basic/hello_world_rainbow.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bitmap font demo! 3 | 4 | Bitmap fonts are fast but blocky. They are best used for small text. 5 | """ 6 | 7 | from picovision import PicoVision, PEN_RGB555 8 | 9 | display = PicoVision(PEN_RGB555, 640, 480) 10 | 11 | # variable to keep track of pen hue 12 | hue = 0.0 13 | 14 | display.set_font("bitmap8") 15 | 16 | for i in range(14): 17 | # create a pen and set the drawing color 18 | PEN_COLOUR = display.create_pen_hsv(hue, 1.0, 1.0) 19 | display.set_pen(PEN_COLOUR) 20 | # draw text 21 | display.text("Hello PicoVision!", i * 30, i * 40, scale=4) 22 | # increment hue 23 | hue += 1.0 / 14 24 | 25 | display.update() 26 | -------------------------------------------------------------------------------- /examples/basic/hello_world_vector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vector font demo! Vector fonts are slower but smoother. They are best used for large text. 3 | 4 | You will need to copy the .af font files to your PicoVision 5 | (this script assumes they are in the basic/ directory). 6 | 7 | Spicy Soup font originally from https://www.dafont.com/spicy-soup.font 8 | Next Sunday font originally from https://www.dafont.com/next-sunday.font 9 | Coolvetica font originally from https://www.dafont.com/coolvetica.font 10 | 11 | Find out how to convert your own fonts to .af here: https://github.com/lowfatcode/alright-fonts 12 | """ 13 | 14 | from picovision import PicoVision, PEN_RGB555 15 | from picovector import PicoVector, ANTIALIAS_X16 16 | import time 17 | 18 | display = PicoVision(PEN_RGB555, 640, 480) 19 | vector = PicoVector(display) 20 | 21 | WIDTH, HEIGHT = display.get_bounds() 22 | BLACK = display.create_pen(0, 0, 0) 23 | 24 | # variable to keep track of hue 25 | hue = 0.0 26 | 27 | # antialiasing draws the vector multiple times for a smoother look 28 | vector.set_antialiasing(ANTIALIAS_X16) 29 | 30 | while True: 31 | # reset the hue 32 | hue = 0.0 33 | 34 | # clear to black 35 | display.set_pen(BLACK) 36 | display.clear() 37 | # set the vector font and size 38 | vector.set_font("basic/spicy_soup.af", 72) 39 | # draw the text seven times 40 | for i in range(7): 41 | # create a HSV (hue, value, saturation) pen and set the drawing color 42 | PEN_COLOUR = display.create_pen_hsv(hue, 1.0, 1.0) 43 | display.set_pen(PEN_COLOUR) 44 | # draw text 45 | vector.text("Hello PicoVision!", 10, i * 65) 46 | # increment hue 47 | hue += 1.0 / 7 48 | display.update() 49 | time.sleep(5) 50 | 51 | display.set_pen(BLACK) 52 | display.clear() 53 | vector.set_font("basic/next_sunday.af", 55) 54 | # draw the text ten times, sideways 55 | for i in range(10): 56 | PEN_COLOUR = display.create_pen_hsv(hue, 1.0, 1.0) 57 | display.set_pen(PEN_COLOUR) 58 | vector.text("Hello PicoVision!", i * WIDTH // 10, HEIGHT, 270) 59 | hue += 1.0 / 10 60 | display.update() 61 | time.sleep(5) 62 | 63 | display.set_pen(BLACK) 64 | display.clear() 65 | vector.set_font("basic/coolvetica_rg.af", 60) 66 | # draw the text many times 67 | for i in range(30): 68 | PEN_COLOUR = display.create_pen_hsv(hue, 1.0, 1.0) 69 | display.set_pen(PEN_COLOUR) 70 | vector.text("Hello PicoVision!", WIDTH // 2, HEIGHT // 2, i * 12) 71 | hue += 1.0 / 30 72 | display.update() 73 | time.sleep(5) 74 | 75 | display.set_pen(BLACK) 76 | display.clear() 77 | vector.set_font("basic/coolvetica_rg.af", 100) 78 | # draw the text many times 79 | for i in range(50): 80 | PEN_COLOUR = display.create_pen_hsv(hue, 1.0, 1.0) 81 | display.set_pen(PEN_COLOUR) 82 | vector.text("Hello PicoVision!", 10, (i * 8) - 30) 83 | hue += 1.0 / 25 84 | display.update() 85 | time.sleep(5) 86 | -------------------------------------------------------------------------------- /examples/basic/next_sunday.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/basic/next_sunday.af -------------------------------------------------------------------------------- /examples/basic/noise.py: -------------------------------------------------------------------------------- 1 | # This basic example shows how you can use PicoSynth to play simple tones. 2 | # It doesn't do anything with the display. 3 | 4 | from picosynth import PicoSynth, Channel 5 | import time 6 | 7 | VOLUME = 0.5 8 | 9 | # this handy list converts notes into frequencies 10 | TONES = { 11 | "B0": 31, 12 | "C1": 33, 13 | "CS1": 35, 14 | "D1": 37, 15 | "DS1": 39, 16 | "E1": 41, 17 | "F1": 44, 18 | "FS1": 46, 19 | "G1": 49, 20 | "GS1": 52, 21 | "A1": 55, 22 | "AS1": 58, 23 | "B1": 62, 24 | "C2": 65, 25 | "CS2": 69, 26 | "D2": 73, 27 | "DS2": 78, 28 | "E2": 82, 29 | "F2": 87, 30 | "FS2": 93, 31 | "G2": 98, 32 | "GS2": 104, 33 | "A2": 110, 34 | "AS2": 117, 35 | "B2": 123, 36 | "C3": 131, 37 | "CS3": 139, 38 | "D3": 147, 39 | "DS3": 156, 40 | "E3": 165, 41 | "F3": 175, 42 | "FS3": 185, 43 | "G3": 196, 44 | "GS3": 208, 45 | "A3": 220, 46 | "AS3": 233, 47 | "B3": 247, 48 | "C4": 262, 49 | "CS4": 277, 50 | "D4": 294, 51 | "DS4": 311, 52 | "E4": 330, 53 | "F4": 349, 54 | "FS4": 370, 55 | "G4": 392, 56 | "GS4": 415, 57 | "A4": 440, 58 | "AS4": 466, 59 | "B4": 494, 60 | "C5": 523, 61 | "CS5": 554, 62 | "D5": 587, 63 | "DS5": 622, 64 | "E5": 659, 65 | "F5": 698, 66 | "FS5": 740, 67 | "G5": 784, 68 | "GS5": 831, 69 | "A5": 880, 70 | "AS5": 932, 71 | "B5": 988, 72 | "C6": 1047, 73 | "CS6": 1109, 74 | "D6": 1175, 75 | "DS6": 1245, 76 | "E6": 1319, 77 | "F6": 1397, 78 | "FS6": 1480, 79 | "G6": 1568, 80 | "GS6": 1661, 81 | "A6": 1760, 82 | "AS6": 1865, 83 | "B6": 1976, 84 | "C7": 2093, 85 | "CS7": 2217, 86 | "D7": 2349, 87 | "DS7": 2489, 88 | "E7": 2637, 89 | "F7": 2794, 90 | "FS7": 2960, 91 | "G7": 3136, 92 | "GS7": 3322, 93 | "A7": 3520, 94 | "AS7": 3729, 95 | "B7": 3951, 96 | "C8": 4186, 97 | "CS8": 4435, 98 | "D8": 4699, 99 | "DS8": 4978 100 | } 101 | 102 | synth = PicoSynth() 103 | 104 | # create a new noise channel 105 | noise = synth.channel(0) 106 | # change these details to modify the sound 107 | # waveforms you can use are NOISE, SQUARE, SAW, TRIANGLE, SINE, or WAVE 108 | # you can combine more than one, like this: waveforms=Channel.SQUARE | Channel.SAW, 109 | noise.configure( 110 | waveforms=Channel.SINE, 111 | attack=0.1, 112 | decay=0.1, 113 | sustain=0.5, 114 | release=0.1, 115 | volume=VOLUME 116 | ) 117 | # for more details on what attack, decay, sustain and release mean, see: 118 | # https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope 119 | 120 | synth.play() 121 | 122 | while True: 123 | # play a scale 124 | for note in ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]: 125 | noise.frequency(TONES[note]) 126 | noise.trigger_attack() 127 | time.sleep(0.5) 128 | 129 | # and down again 130 | for note in ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]: 131 | noise.frequency(TONES[note]) 132 | noise.trigger_attack() 133 | time.sleep(0.5) 134 | -------------------------------------------------------------------------------- /examples/basic/sdtest.py: -------------------------------------------------------------------------------- 1 | # This simple example shows how to read and write from the SD card on PicoVision. 2 | # You will need sdcard.py saved to your Pico. 3 | # Find it in https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/examples/common 4 | from machine import Pin, SPI 5 | import sdcard 6 | import os 7 | 8 | # set up the SD card 9 | sd_spi = SPI(1, sck=Pin(10, Pin.OUT), mosi=Pin(11, Pin.OUT), miso=Pin(12, Pin.OUT)) 10 | sd = sdcard.SDCard(sd_spi, Pin(15)) 11 | os.mount(sd, "/sd") 12 | 13 | # create a file and add some text 14 | # if this file already exists it will be overwritten 15 | with open("/sd/sdtest.txt", "w") as f: 16 | f.write("Hello PicoVision!\r\n") 17 | f.close() 18 | 19 | # open the file and append some text (useful for logging!) 20 | with open("/sd/sdtest.txt", "a") as f: 21 | f.write("We're appending some text\r\n") 22 | f.close() 23 | 24 | # read the file and print the contents to the console 25 | with open("/sd/sdtest.txt", "r") as f: 26 | data = f.read() 27 | print(data) 28 | f.close() 29 | 30 | # unmount the SD card 31 | os.umount("/sd") 32 | -------------------------------------------------------------------------------- /examples/basic/spicy_soup.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/basic/spicy_soup.af -------------------------------------------------------------------------------- /examples/bouncing_logo/bouncing_logo.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import errno 4 | from picographics import PicoGraphics, PEN_RGB555 5 | from machine import Pin 6 | 7 | """ 8 | Pimoroni logo bouncing around on the screen 9 | Please upload the pim-logo.png to the pico 10 | Multiple logos (maximum of 4) can be added to the screen 11 | Press Y to add logo images to the screen 12 | """ 13 | 14 | # set up the display and drawing variables 15 | display = PicoGraphics(PEN_RGB555, 640, 480) 16 | 17 | WIDTH, HEIGHT = display.get_bounds() 18 | BLACK = display.create_pen(0, 0, 0) 19 | WHITE = display.create_pen(255, 255, 255) 20 | NAME = "PIMORONI" 21 | DROP_SHADOW_OFFSET = 5 22 | TEXT_SCALE = 10 23 | 24 | # image indices 25 | LOGOSUB = [] 26 | SUB_IMAGE_SIZE = 32 27 | 28 | 29 | MAX_SPRITE_SLOTS = 16 30 | MAX_LOGOS = MAX_SPRITE_SLOTS // 4 31 | 32 | y_btn = Pin(9, Pin.IN, Pin.PULL_UP) 33 | 34 | 35 | has_sprite = False # set if the sprite file exists in the pico 36 | 37 | try: 38 | # load the sprites 39 | # Given that the png image is 64 by 64 pixels it is split to 4 portions of 32 by 32 pixels 40 | # This is done by providing a 2 tuple as third argument as (portion_width, portion_height) 41 | for _ in range(2): 42 | LOGOSUB = display.load_animation(0, "/bouncing_logo/pim-logo.png", (SUB_IMAGE_SIZE, SUB_IMAGE_SIZE)) 43 | display.update() 44 | 45 | has_sprite = True 46 | except OSError as ioe: 47 | if ioe.errno != errno.ENOENT: 48 | raise 49 | has_sprite = False 50 | 51 | logos = [] 52 | 53 | # specify image dimensions and randomise starting position 54 | IMAGE_WIDTH = 64 55 | IMAGE_HEIGHT = 64 56 | DEFAULT_VELOCITY = 20 # set the default velocity of the image 57 | 58 | 59 | class Logo: 60 | def __init__(self): 61 | self.x_start = random.randint(50, WIDTH - IMAGE_WIDTH) 62 | self.y_start = random.randint(50, HEIGHT - IMAGE_HEIGHT) 63 | self.x_end = self.x_start + IMAGE_WIDTH 64 | self.y_end = self.y_start + IMAGE_HEIGHT 65 | self.x_vel = ( 66 | DEFAULT_VELOCITY if random.randint(0, 1) == 1 else -DEFAULT_VELOCITY 67 | ) 68 | self.y_vel = ( 69 | DEFAULT_VELOCITY if random.randint(0, 1) == 1 else -DEFAULT_VELOCITY 70 | ) 71 | 72 | def draw(self, logo_id): 73 | """Draws the logo at its x and y positions 74 | In order to display logo in 64 by 64 pixels, 4 sub images are displayed 75 | Each subimage uses up a sprite slot 76 | If any sub logo image is not present in the pico a rectangle is displayed instead 77 | """ 78 | logo_num = logo_id + 1 79 | if has_sprite: 80 | display.display_sprite( 81 | ((logo_num * 4) - 3), # Sprite slot 82 | LOGOSUB[0], # Image index 83 | self.x_start, # X position 84 | self.y_start # Y position 85 | ) 86 | display.display_sprite( 87 | ((logo_num * 4) - 2), 88 | LOGOSUB[1], 89 | self.x_start + SUB_IMAGE_SIZE, 90 | self.y_start, 91 | ) 92 | display.display_sprite( 93 | ((logo_num * 4) - 1), 94 | LOGOSUB[2], 95 | self.x_start, 96 | self.y_start + SUB_IMAGE_SIZE, 97 | ) 98 | display.display_sprite( 99 | (logo_num * 4), 100 | LOGOSUB[3], 101 | self.x_start + SUB_IMAGE_SIZE, 102 | self.y_start + SUB_IMAGE_SIZE, 103 | ) 104 | else: 105 | display.set_pen(WHITE) 106 | display.rectangle(self.x_start, self.y_start, 100, 100) 107 | display.set_pen(BLACK) 108 | display.text( 109 | "File not found", 110 | self.x_start, 111 | (self.y_start + (self.y_end - self.y_start) // 2), 112 | scale=1, 113 | ) 114 | 115 | 116 | def edge_collision(logo): 117 | """Checks if logo hits the walls and reverse its velocity""" 118 | # Horizontal walls 119 | if (logo.x_start + IMAGE_WIDTH) >= WIDTH or logo.x_start < 0: 120 | logo.x_vel = -logo.x_vel 121 | if logo.x_start < 0: 122 | logo.x_start = 0 123 | elif (logo.x_start + IMAGE_WIDTH) > WIDTH: 124 | logo.x_start = WIDTH - IMAGE_WIDTH 125 | 126 | # Vertical walls 127 | if (logo.y_start + IMAGE_HEIGHT) >= HEIGHT or logo.y_start <= 0: 128 | logo.y_vel = -logo.y_vel 129 | if logo.y_start < 0: 130 | logo.y_start = 0 131 | elif (logo.y_start + IMAGE_HEIGHT) > HEIGHT: 132 | logo.y_start = HEIGHT - IMAGE_HEIGHT 133 | 134 | 135 | offset = DEFAULT_VELOCITY 136 | 137 | 138 | def object_collision(j): 139 | """Checks if a given logo collides with other logos on the screen""" 140 | for i in range(j + 1, len(logos)): # compares the logo with succeeding logos in the list 141 | 142 | # right collision 143 | if abs(logos[i].x_start - logos[j].x_end) < offset and abs(logos[i].y_start - logos[j].y_start) <= IMAGE_HEIGHT: 144 | if logos[i].x_vel < 0: 145 | logos[i].x_vel *= -1 146 | if logos[j].x_vel > 0: 147 | logos[j].x_vel *= -1 148 | 149 | # left collision 150 | elif abs(logos[j].x_start - logos[i].x_end) < offset and abs(logos[i].y_start - logos[j].y_start) <= IMAGE_HEIGHT: 151 | if logos[i].x_vel > 0: 152 | logos[i].x_vel *= -1 153 | if logos[j].x_vel < 0: 154 | logos[j].x_vel *= -1 155 | 156 | # up collision 157 | elif abs(logos[i].y_start - logos[j].y_end) < offset and abs(logos[i].x_start - logos[j].x_start) <= IMAGE_WIDTH: 158 | if logos[i].y_vel < 0: 159 | logos[i].y_vel *= -1 160 | if logos[j].y_vel > 0: 161 | logos[j].y_vel *= -1 162 | 163 | # down collision 164 | elif abs(logos[j].y_start - logos[i].y_end) < offset and abs(logos[i].x_start - logos[j].x_start) <= IMAGE_WIDTH: 165 | if logos[i].y_vel > 0: 166 | logos[i].y_vel *= -1 167 | if logos[j].y_vel < 0: 168 | logos[j].y_vel *= -1 169 | 170 | 171 | def add_logo(): 172 | if len(logos) < MAX_LOGOS: 173 | new_logo = Logo() 174 | logos.append(new_logo) 175 | else: 176 | print("Unable to add more logos as the maximum amount has been reached") 177 | 178 | 179 | def draw_background(): 180 | """Draws a gradient background""" 181 | t = time.ticks_ms() / 1000.0 182 | grid_size = 40 183 | for y in range(0, HEIGHT // grid_size): 184 | for x in range(0, WIDTH // grid_size): 185 | h = x + y + int(t * 5) 186 | h = h / 50.0 187 | 188 | display.set_pen(display.create_pen_hsv(h, 0.5, 1)) 189 | display.rectangle(x * grid_size, y * grid_size, grid_size, grid_size) 190 | 191 | 192 | add_logo() # add initial logo 193 | 194 | 195 | last_time = time.ticks_ms() 196 | 197 | while True: 198 | current_time = time.ticks_ms() 199 | 200 | # add logo when Y button is pressed 201 | if current_time - last_time > 500: 202 | if not y_btn.value(): 203 | add_logo() 204 | 205 | last_time = current_time 206 | 207 | draw_background() 208 | 209 | # display name with offset 210 | text_width = display.measure_text(NAME, scale=TEXT_SCALE) 211 | text_height = int(7.5 * TEXT_SCALE) 212 | display.set_pen(BLACK) 213 | display.text( 214 | NAME, 215 | ((WIDTH - text_width) // 2) - DROP_SHADOW_OFFSET, 216 | (HEIGHT // 2 - text_height // 2) + DROP_SHADOW_OFFSET, 217 | scale=10, 218 | ) 219 | 220 | display.set_pen(WHITE) 221 | display.text( 222 | NAME, ((WIDTH - text_width) // 2), HEIGHT // 2 - text_height // 2, scale=10 223 | ) 224 | 225 | # Check for edge and object collisions which updates the velocity accordingly 226 | for num in range(len(logos)): 227 | logo = logos[num] 228 | edge_collision(logo) 229 | object_collision(num) 230 | 231 | # update positions for each logo and display it 232 | for num in range(len(logos)): 233 | logo = logos[num] 234 | logo.x_start += logo.x_vel 235 | logo.y_start += logo.y_vel 236 | logo.x_end = logo.x_start + IMAGE_WIDTH 237 | logo.y_end = logo.y_start + IMAGE_HEIGHT 238 | logo.draw(num) 239 | 240 | display.update() 241 | -------------------------------------------------------------------------------- /examples/bouncing_logo/pim-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/bouncing_logo/pim-logo.png -------------------------------------------------------------------------------- /examples/co2/OpenSans-Bold.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/co2/OpenSans-Bold.af -------------------------------------------------------------------------------- /examples/co2/co2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add a SCD41 sensor breakout to PicoVision to make a handy CO2 detector! 3 | https://shop.pimoroni.com/products/scd41-co2-sensor-breakout 4 | Press A to reset the high/low values. 5 | 6 | You will need the OpenSans-Bold.af font saved to your PicoVision alongside this script. 7 | """ 8 | 9 | from picovision import PicoVision, PEN_RGB555 10 | from pimoroni_i2c import PimoroniI2C 11 | import breakout_scd41 12 | from picovector import Polygon, PicoVector, ANTIALIAS_X16 13 | 14 | i2c = PimoroniI2C(sda=6, scl=7) 15 | 16 | display = PicoVision(PEN_RGB555, 640, 480) 17 | button_a = display.is_button_a_pressed 18 | vector = PicoVector(display) 19 | 20 | WIDTH, HEIGHT = display.get_bounds() 21 | 22 | BLACK = display.create_pen(0, 0, 0) 23 | WHITE = display.create_pen(255, 255, 255) 24 | RED = display.create_pen(127, 0, 0) 25 | GREEN = display.create_pen(0, 127, 0) 26 | 27 | # pick what bits of the colour wheel to use (from 0-360°) 28 | # https://www.cssscript.com/demo/hsv-hsl-color-wheel-picker-reinvented/ 29 | CO2_HUE_START = 100 # green 30 | CO2_HUE_END = 0 # red 31 | TEMPERATURE_HUE_START = 240 32 | TEMPERATURE_HUE_END = 359 33 | HUMIDITY_SATURATION_START = 0 34 | HUMIDITY_SATURATION_END = 360 35 | 36 | # the range of readings to map to colours (and scale our graphs to) 37 | # https://www.kane.co.uk/knowledge-centre/what-are-safe-levels-of-co-and-co2-in-rooms 38 | CO2_MIN = 300 39 | CO2_MAX = 2000 40 | TEMPERATURE_MIN = 10 41 | TEMPERATURE_MAX = 40 42 | HUMIDITY_MIN = 0 43 | HUMIDITY_MAX = 100 44 | # use these to draw indicator lines on the CO2 graph 45 | OK_CO2_LEVEL = 800 46 | BAD_CO2_LEVEL = 1500 47 | 48 | MAX_READINGS = 120 # how many readings to store (and draw on the graph) 49 | 50 | # the areas of the screen we want to draw our graphs into 51 | CO2_GRAPH_TOP = HEIGHT * 0.2 52 | CO2_GRAPH_BOTTOM = HEIGHT * 0.75 53 | TEMPERATURE_GRAPH_TOP = HEIGHT * 0.75 54 | TEMPERATURE_GRAPH_BOTTOM = HEIGHT 55 | HUMIDITY_GRAPH_TOP = HEIGHT * 0.75 56 | HUMIDITY_GRAPH_BOTTOM = HEIGHT 57 | PADDING = 5 58 | 59 | LINE_THICKNESS = 3 # thickness of indicator and graph lines 60 | 61 | 62 | def graph_polygon(top, bottom, min, max, readings, y_offset=0): 63 | # function to generate a filled graph polygon and scale it to the available space 64 | points = [] 65 | for r in range(len(readings)): 66 | x = round(r * (WIDTH / len(readings))) 67 | y = round(bottom + ((readings[r] - min) * (top - bottom) / (max - min)) + y_offset) 68 | points.append((x, y)) 69 | points.append((x, round(bottom))) 70 | points.append((0, round(bottom))) 71 | return Polygon(*points) 72 | 73 | 74 | def line_polygon(top, bottom, min, max, value): 75 | # function to generate a straight line on our graph at a specific value 76 | points = [] 77 | points.append((0, round(bottom + ((value - min) * (top - bottom) / (max - min))))) 78 | points.append((WIDTH, round(bottom + ((value - min) * (top - bottom) / (max - min))))) 79 | points.append((WIDTH, round(bottom + ((value - min) * (top - bottom) / (max - min))) + LINE_THICKNESS)) 80 | points.append((0, round(bottom + ((value - min) * (top - bottom) / (max - min))) + LINE_THICKNESS)) 81 | return Polygon(*points) 82 | 83 | 84 | highest_co2 = 0.0 85 | lowest_co2 = 4000.0 86 | co2_readings = [] 87 | temperature_readings = [] 88 | humidity_readings = [] 89 | 90 | # set up 91 | vector.set_antialiasing(ANTIALIAS_X16) 92 | vector.set_font("/co2/OpenSans-Bold.af", 50) 93 | display.set_pen(WHITE) 94 | 95 | try: 96 | breakout_scd41.init(i2c) 97 | breakout_scd41.start() 98 | vector.text("Waiting for sensor to be ready", 0, 0) 99 | display.update() 100 | except RuntimeError as e: 101 | print(e) 102 | vector.text("SCD41 breakout not detected :(", 0, 0) 103 | vector.text("but you could buy one at pimoroni.com ;)", 0, HEIGHT - 120) 104 | display.update() 105 | 106 | while True: 107 | if button_a(): 108 | # reset recorded high / low values 109 | highest_co2 = 0.0 110 | lowest_co2 = 4000.0 111 | 112 | if breakout_scd41.ready(): 113 | # read the sensor 114 | co2, temperature, humidity = breakout_scd41.measure() 115 | 116 | # if lists are empty, populate the list with the current readings 117 | if len(co2_readings) == 0: 118 | for i in range(MAX_READINGS): 119 | co2_readings.append(co2) 120 | temperature_readings.append(temperature) 121 | humidity_readings.append(humidity) 122 | 123 | # update highest / lowest values 124 | if co2 < lowest_co2: 125 | lowest_co2 = co2 126 | if co2 > highest_co2: 127 | highest_co2 = co2 128 | 129 | # calculates some colours from the sensor readings 130 | co2_hue = max(0, CO2_HUE_START + ((co2 - CO2_MIN) * (CO2_HUE_END - CO2_HUE_START) / (CO2_MAX - CO2_MIN))) 131 | temperature_hue = max(0, TEMPERATURE_HUE_START + ((temperature - TEMPERATURE_MIN) * (TEMPERATURE_HUE_END - TEMPERATURE_HUE_START) / (TEMPERATURE_MAX - TEMPERATURE_MIN))) 132 | humidity_hue = max(0, HUMIDITY_SATURATION_START + ((humidity - HUMIDITY_MIN) * (HUMIDITY_SATURATION_END - HUMIDITY_SATURATION_START) / (HUMIDITY_MAX - HUMIDITY_MIN))) 133 | 134 | # keep track of readings in a list (so we can draw the graph) 135 | co2_readings.append(co2) 136 | temperature_readings.append(temperature) 137 | humidity_readings.append(humidity) 138 | 139 | # we only need to save a screen's worth of readings, so delete the oldest 140 | if len(co2_readings) > MAX_READINGS: 141 | co2_readings.pop(0) 142 | if len(temperature_readings) > MAX_READINGS: 143 | temperature_readings.pop(0) 144 | if len(humidity_readings) > MAX_READINGS: 145 | humidity_readings.pop(0) 146 | 147 | # clear to black 148 | display.set_pen(BLACK) 149 | display.clear() 150 | 151 | # draw the graphs and text 152 | # draw the CO2 indicator lines 153 | display.set_pen(RED) 154 | vector.draw(line_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, BAD_CO2_LEVEL)) 155 | display.set_pen(GREEN) 156 | vector.draw(line_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, OK_CO2_LEVEL)) 157 | # draw the CO2 graph 158 | display.set_pen(display.create_pen_hsv(co2_hue / 360, 1.0, 1.0)) 159 | co2_graph = graph_polygon(CO2_GRAPH_TOP, CO2_GRAPH_BOTTOM, CO2_MIN, CO2_MAX, co2_readings) 160 | vector.draw(co2_graph) 161 | # draw the CO2 text 162 | vector.set_font_size(80) 163 | vector.text("CO2", PADDING, -30) 164 | vector.text(f"{co2:.0f}ppm", WIDTH // 2, -30) 165 | display.set_pen(BLACK) 166 | vector.set_font_size(30) 167 | vector.text(f"Low {lowest_co2:.0f}ppm", PADDING, round(TEMPERATURE_GRAPH_TOP) - 40) 168 | vector.text(f"High {highest_co2:.0f}ppm", WIDTH // 2, round(TEMPERATURE_GRAPH_TOP) - 40) 169 | 170 | # draw the humidity graph 171 | # here we're using the 'hue' value to affect the saturation (so light blue to dark blue) 172 | display.set_pen(display.create_pen_hsv(240 / 360, humidity_hue / 360, 1.0)) 173 | # draw this polygon twice, applying an offset to make a line rather than a filled shape 174 | humidity_graph_upper = graph_polygon(HUMIDITY_GRAPH_TOP, HUMIDITY_GRAPH_BOTTOM, HUMIDITY_MIN, HUMIDITY_MAX, humidity_readings) 175 | humidity_graph_lower = graph_polygon(HUMIDITY_GRAPH_TOP, HUMIDITY_GRAPH_BOTTOM, HUMIDITY_MIN, HUMIDITY_MAX, humidity_readings, LINE_THICKNESS) 176 | vector.draw(humidity_graph_lower, humidity_graph_upper) 177 | vector.text(f"Humidity: {humidity:.0f}%", WIDTH // 2, int(HUMIDITY_GRAPH_BOTTOM) - 40) 178 | 179 | # draw the temperature graph 180 | display.set_pen(display.create_pen_hsv(temperature_hue / 360, 1.0, 1.0)) 181 | # draw this polygon twice, applying an offset to make a line rather than a filled shape 182 | temperature_graph_upper = graph_polygon(TEMPERATURE_GRAPH_TOP, TEMPERATURE_GRAPH_BOTTOM, TEMPERATURE_MIN, TEMPERATURE_MAX, temperature_readings) 183 | temperature_graph_lower = graph_polygon(TEMPERATURE_GRAPH_TOP, TEMPERATURE_GRAPH_BOTTOM, TEMPERATURE_MIN, TEMPERATURE_MAX, temperature_readings, LINE_THICKNESS) 184 | vector.draw(temperature_graph_lower, temperature_graph_upper) 185 | vector.text(f"Temperature: {temperature:.0f}°c", PADDING, int(TEMPERATURE_GRAPH_BOTTOM) - 40) 186 | 187 | display.update() 188 | -------------------------------------------------------------------------------- /examples/floppy_birb/OpenSans-Regular.af: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/OpenSans-Regular.af -------------------------------------------------------------------------------- /examples/floppy_birb/birb-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/birb-sprite.png -------------------------------------------------------------------------------- /examples/floppy_birb/goal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/goal.png -------------------------------------------------------------------------------- /examples/floppy_birb/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/ground.png -------------------------------------------------------------------------------- /examples/floppy_birb/pimoroni-hq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/pimoroni-hq.png -------------------------------------------------------------------------------- /examples/floppy_birb/pipe-cap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/pipe-cap.png -------------------------------------------------------------------------------- /examples/floppy_birb/pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/pipe.png -------------------------------------------------------------------------------- /examples/floppy_birb/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/floppy_birb/sky.png -------------------------------------------------------------------------------- /examples/magic_mirror/README.md: -------------------------------------------------------------------------------- 1 | - [Micro Magic Mirror](#micro-magic-mirror) 2 | - [About](#about) 3 | - [Icons](#icons) 4 | - [Variants](#variants) 5 | - [Magic Mirror](#magic-mirror) 6 | - [Magic Mirror Without Wifi](#magic-mirror-without-wifi) 7 | - [Magic Mirror (Home Assistant)](#magic-mirror-home-assistant) 8 | 9 | # Micro Magic Mirror 10 | 11 | ## About 12 | 13 | Micro Magic Mirror is a home dashboard for showing you useful information. We're working on an associated learn guide to show you how we built our Magic Mirror. 14 | 15 | To get online data, create a file called secrets.py containing your WiFi SSID and password, like this: 16 | ``` 17 | WIFI_SSID = "ssid_goes_here" 18 | WIFI_PASSWORD = "password_goes_here" 19 | ``` 20 | Press the Y button to update the online data. 21 | 22 | ## Icons 23 | 24 | Weather/wifi icons are from [Icons8](https://icons8.com). 25 | 26 | ## Variants 27 | 28 | There are three variants of this example in this directory, showing various different functions. For an example that will do something out of the box without any configuring, go for `magic_mirror.py`. 29 | 30 | ### Magic Mirror 31 | 32 | [magic_mirror.py](magic_mirror.py) 33 | 34 | This example connects to wifi and shows you the time (from an NTP server), the current weather conditions (from the OpenMeteo API) and an inspirational quote (from quotable.io). 35 | 36 | ### Magic Mirror Without Wifi 37 | 38 | [magic_mirror_without_wifi.py](magic_mirror_without_wifi.py) 39 | 40 | This stripped down version shows how we're drawing the basic framework of our Magic Mirror display, before we start adding in the wifi functions. It has a graph showing the temperature from the internal sensor on the Pico W. 41 | 42 | ### Magic Mirror (Home Assistant) 43 | 44 | [magic_mirror_home_assistant.py](magic_mirror_home_assistant.py) 45 | 46 | Do you automate your home with [Home Assistant](https://www.home-assistant.io/)? (if not you should try it, it's great fun). This version of Magic Mirror displays sunrise/sunset data it gets from a local Home Assistant server. 47 | 48 | To connect to your Home Assistant, you'll need to create a token at http://homeassistant.local:8123/profile > Long Lived Access Tokens and add it to your `secrets.py` like this: 49 | ``` 50 | HOME_ASSISTANT_TOKEN = "token_goes_here" 51 | ``` 52 | Solar data is provided by the Home Assistant Sun integration, which should be installed by default. The example assumes your Pico W is connected to the same network as your Home Assistant server, and that your Home Assistant server is located at http://homeassistant.local:8123 -------------------------------------------------------------------------------- /examples/magic_mirror/icons/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/cloud.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/no-wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/no-wifi.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/rain.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/snow.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/storm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/storm.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/sun.png -------------------------------------------------------------------------------- /examples/magic_mirror/icons/wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/magic_mirror/icons/wifi.png -------------------------------------------------------------------------------- /examples/magic_mirror/magic_mirror_without_wifi.py: -------------------------------------------------------------------------------- 1 | """ 2 | MicroMagicMirror - a basic home dashboard that shows the time and a graph from the Pico's internal temperature sensor. 3 | """ 4 | 5 | from modes import VGA 6 | import time 7 | import machine 8 | 9 | # Constants for drawing the display 10 | TITLE = "MicroMagicMirror" 11 | TITLE_POSITION = 20 12 | ROW_1_POSITION = 72 13 | COLUMN_2_POSITION = 80 14 | CLOCK_POSITION = 430 15 | PADDING = 10 16 | GRAPH_TOP = 120 17 | GRAPH_BOTTOM = 380 18 | 19 | # set up our display and some useful constants 20 | graphics = VGA() 21 | 22 | WIDTH, HEIGHT = graphics.get_bounds() 23 | 24 | BLACK = graphics.create_pen(0, 0, 0) 25 | RED = graphics.create_pen(255, 0, 0) 26 | ORANGE = graphics.create_pen(246, 138, 30) 27 | YELLOW = graphics.create_pen(255, 216, 0) 28 | GREEN = graphics.create_pen(0, 121, 64) 29 | BLUE = graphics.create_pen(0, 0, 255) 30 | INDIGO = graphics.create_pen(36, 64, 142) 31 | VIOLET = graphics.create_pen(115, 41, 130) 32 | WHITE = graphics.create_pen(255, 255, 255) 33 | 34 | # create the rtc object 35 | rtc = machine.RTC() 36 | 37 | sensor_temp = machine.ADC(4) 38 | conversion_factor = 3.3 / (65535) # used for calculating a temperature from the raw sensor reading 39 | 40 | # some other variables for keeping track of stuff 41 | timer = 0 42 | second = 0 43 | last_second = 0 44 | last_x = 0 45 | last_y = 0 46 | 47 | temperatures = [] 48 | 49 | 50 | def redraw_display_if_reqd(): 51 | global year, month, day, wd, hour, minute, second, last_second, last_x, last_y 52 | if second != last_second: 53 | # clear the screen 54 | graphics.set_pen(BLACK) 55 | graphics.clear() 56 | 57 | # draw a lovely rainbow 58 | graphics.set_pen(VIOLET) 59 | graphics.circle(0, 0, 140) 60 | graphics.set_pen(BLUE) 61 | graphics.circle(0, 0, 120) 62 | graphics.set_pen(GREEN) 63 | graphics.circle(0, 0, 100) 64 | graphics.set_pen(YELLOW) 65 | graphics.circle(0, 0, 80) 66 | graphics.set_pen(ORANGE) 67 | graphics.circle(0, 0, 60) 68 | graphics.set_pen(RED) 69 | graphics.circle(0, 0, 40) 70 | 71 | # draw the title 72 | graphics.set_pen(WHITE) 73 | graphics.text(TITLE, COLUMN_2_POSITION, TITLE_POSITION, scale=7) 74 | 75 | # show system temperature 76 | # the following two lines do some maths to convert the number from the temp sensor into celsius 77 | reading = sensor_temp.read_u16() * conversion_factor 78 | internal_temperature = 27 - (reading - 0.706) / 0.001721 79 | graphics.text(f"uMM is running at {internal_temperature:.1f}°C.", COLUMN_2_POSITION, ROW_1_POSITION, WIDTH - COLUMN_2_POSITION, scale=2) 80 | 81 | # draw a graph of the last 60 temperature readings 82 | temperatures.append(internal_temperature) 83 | if len(temperatures) > 61: 84 | temperatures.pop(0) 85 | 86 | # draw a box for the graph 87 | graphics.set_pen(WHITE) 88 | graphics.line(PADDING, GRAPH_TOP, WIDTH - PADDING, GRAPH_TOP) 89 | graphics.line(PADDING, GRAPH_BOTTOM, WIDTH - PADDING, GRAPH_BOTTOM) 90 | graphics.line(PADDING, GRAPH_TOP, PADDING, GRAPH_BOTTOM) 91 | graphics.line(WIDTH - PADDING, GRAPH_TOP, WIDTH - PADDING, GRAPH_BOTTOM) 92 | 93 | # scale the graph to fit the screen 94 | max_temp = max(temperatures) + 2 95 | min_temp = min(temperatures) - 2 96 | 97 | # draw the scale 98 | graphics.text(f"{max_temp:.1f}°C", PADDING, GRAPH_TOP - 15, scale=2) 99 | graphics.text(f"{min_temp:.1f}°C", PADDING, GRAPH_BOTTOM + 3, scale=2) 100 | 101 | # draw dots and join them with a line 102 | graphics.set_pen(RED) 103 | for i, temp in enumerate(temperatures): 104 | x = PADDING + i * (WIDTH - PADDING * 2) // 60 105 | y = GRAPH_BOTTOM - (temp - min_temp) / (max_temp - min_temp) * (GRAPH_BOTTOM - GRAPH_TOP) 106 | if i > 0: 107 | graphics.line(int(x), int(y), int(last_x), int(last_y), 3) 108 | last_x = x 109 | last_y = y 110 | 111 | # draw clock 112 | graphics.set_pen(WHITE) 113 | graphics.text(f"{hour:02}:{minute:02}:{second:02}", PADDING, CLOCK_POSITION, scale=7) 114 | text_width = graphics.measure_text(f"{day}/{month}/{year}", scale=7) 115 | graphics.text(f"{day}/{month}/{year}", WIDTH - text_width, CLOCK_POSITION, scale=7) 116 | 117 | last_second = second 118 | graphics.update() 119 | 120 | 121 | graphics.set_font("bitmap8") 122 | 123 | redraw_display_if_reqd() 124 | 125 | while True: 126 | ticks_now = time.ticks_ms() 127 | 128 | year, month, day, wd, hour, minute, second, _ = rtc.datetime() 129 | 130 | redraw_display_if_reqd() 131 | -------------------------------------------------------------------------------- /examples/pride/coolcrab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/pride/coolcrab.png -------------------------------------------------------------------------------- /examples/pride/pride.py: -------------------------------------------------------------------------------- 1 | # A customisable Pride flag. If you want to be a cool crab like @Foone you can add an image on top of the flag. 2 | # Need to generate a pride flag in MS-DOS? Check out https://github.com/foone/VGAPride ! 3 | 4 | from modes import VGA 5 | import math 6 | import pngdec 7 | 8 | display = VGA() 9 | 10 | # Create a new PNG decoder for our PicoGraphics 11 | png = pngdec.PNG(display) 12 | 13 | WIDTH, HEIGHT = display.get_bounds() 14 | 15 | # If you want to add a PNG image, add the filename here 16 | IMAGE = "/pride/coolcrab.png" 17 | 18 | # List of available pen colours, add more if necessary 19 | RED = display.create_pen(209, 34, 41) 20 | ORANGE = display.create_pen(246, 138, 30) 21 | YELLOW = display.create_pen(255, 216, 0) 22 | GREEN = display.create_pen(0, 121, 64) 23 | INDIGO = display.create_pen(36, 64, 142) 24 | VIOLET = display.create_pen(115, 41, 130) 25 | WHITE = display.create_pen(255, 255, 255) 26 | PINK = display.create_pen(255, 175, 200) 27 | BLUE = display.create_pen(116, 215, 238) 28 | BROWN = display.create_pen(97, 57, 21) 29 | BLACK = display.create_pen(0, 0, 0) 30 | MAGENTA = display.create_pen(255, 33, 140) 31 | CYAN = display.create_pen(33, 177, 255) 32 | AMETHYST = display.create_pen(156, 89, 209) 33 | 34 | # Uncomment one of these to change flag 35 | # If adding your own, colour order is left to right (or top to bottom) 36 | COLOUR_ORDER = [RED, ORANGE, YELLOW, GREEN, INDIGO, VIOLET] # traditional pride flag 37 | # COLOUR_ORDER = [BLACK, BROWN, RED, ORANGE, YELLOW, GREEN, INDIGO, VIOLET] # Philadelphia pride flag 38 | # COLOUR_ORDER = [BLUE, PINK, WHITE, PINK, BLUE] # trans flag 39 | # COLOUR_ORDER = [MAGENTA, YELLOW, CYAN] # pan flag 40 | # COLOUR_ORDER = [MAGENTA, VIOLET, INDIGO] # bi flag 41 | # COLOUR_ORDER = [YELLOW, WHITE, AMETHYST, BLACK] # non-binary flag 42 | 43 | # Add chevrons to the left 44 | # CHEVRONS = [] # No chevrons 45 | CHEVRONS = [WHITE, PINK, BLUE, BROWN, BLACK] # Progress Pride Flag 46 | # Initial chevron height compared to screen height 47 | FIRST_CHEVRON_HEIGHT = 0.4 48 | 49 | # Change this for vertical stripes 50 | STRIPES_DIRECTION = "horizontal" 51 | 52 | # Draw the flag 53 | if STRIPES_DIRECTION == "horizontal": 54 | stripe_width = round(HEIGHT / len(COLOUR_ORDER)) 55 | for x in range(len(COLOUR_ORDER)): 56 | display.set_pen(COLOUR_ORDER[x]) 57 | display.rectangle(0, stripe_width * x, WIDTH, stripe_width) 58 | 59 | if STRIPES_DIRECTION == "vertical": 60 | stripe_width = round(WIDTH / len(COLOUR_ORDER)) 61 | for x in range(len(COLOUR_ORDER)): 62 | display.set_pen(COLOUR_ORDER[x]) 63 | display.rectangle(stripe_width * x, 0, stripe_width, HEIGHT) 64 | 65 | if len(CHEVRONS) > 0: 66 | stripe_width = round((HEIGHT * (1 - FIRST_CHEVRON_HEIGHT)) / len(CHEVRONS)) 67 | offset = -stripe_width * math.floor((len(CHEVRONS) + 1) / 2) 68 | middle = round(HEIGHT / 2) 69 | for x in range(len(CHEVRONS) - 1, -1, -1): 70 | display.set_pen(CHEVRONS[x]) 71 | display.triangle( 72 | x * stripe_width + offset, -stripe_width, 73 | (x + 1) * stripe_width + offset + middle, middle, 74 | x * stripe_width + offset, HEIGHT + stripe_width) 75 | 76 | try: 77 | png.open_file(IMAGE) 78 | # cool crab is 500 x 255px - if your image is a different size you might need to tweak the position 79 | png.decode(WIDTH // 2 - (500 // 2), HEIGHT // 2 - (255 // 2)) 80 | except OSError as e: 81 | print(f"{e} - image not found.") 82 | 83 | # Once all the drawing is done, update the display. 84 | display.update() 85 | -------------------------------------------------------------------------------- /examples/rainbow_wheel.py: -------------------------------------------------------------------------------- 1 | # A spinny rainbow wheel. Change up some of the constants below to see what happens. 2 | 3 | import math 4 | from picographics import PicoGraphics, PEN_RGB555 5 | 6 | # Constants for drawing 7 | INNER_RADIUS = 30 8 | OUTER_RADIUS = 200 9 | NUMBER_OF_LINES = 24 10 | HUE_SHIFT = 0.02 11 | ROTATION_SPEED = 2 12 | LINE_THICKNESS = 2 13 | 14 | # Set up the display 15 | graphics = PicoGraphics(pen_type=PEN_RGB555, width=640, height=480) 16 | 17 | WIDTH, HEIGHT = graphics.get_bounds() 18 | 19 | BLACK = graphics.create_pen(0, 0, 0) 20 | 21 | # Variables to keep track of rotation and hue positions 22 | r = 0 23 | t = 0 24 | 25 | while True: 26 | graphics.set_pen(BLACK) 27 | graphics.clear() 28 | for i in range(0, 360, 360 // NUMBER_OF_LINES): 29 | graphics.set_pen(graphics.create_pen_hsv((i / 360) + t, 1.0, 1.0)) 30 | # Draw some lines, offset by the rotation variable 31 | graphics.line(int(WIDTH / 2 + math.cos(math.radians(i + r)) * INNER_RADIUS), 32 | int(HEIGHT / 2 + math.sin(math.radians(i + r)) * INNER_RADIUS), 33 | int(WIDTH / 2 + math.cos(math.radians(i + 90 + r)) * OUTER_RADIUS), 34 | int(HEIGHT / 2 + math.sin(math.radians(i + 90 + r)) * OUTER_RADIUS), 35 | LINE_THICKNESS) 36 | graphics.update() 37 | r += ROTATION_SPEED 38 | t += HUE_SHIFT 39 | -------------------------------------------------------------------------------- /examples/screenmodes.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import time 3 | import machine 4 | from picographics import PicoGraphics, PEN_RGB555, WIDESCREEN 5 | 6 | DISPLAY_WIDTH = 320 7 | DISPLAY_HEIGHT = 240 8 | 9 | FRAME_WIDTH = 1280 10 | FRAME_HEIGHT = 720 11 | 12 | # number of modes listed in array 13 | numModes = 16 14 | # current mode being dispalyed 15 | currentMode = 0 16 | # mode which will be selected with the Y button 17 | modeSelect = 0 18 | modes = [ 19 | (320, 240, 0), 20 | (360, 240, 0), 21 | (360, 288, 0), 22 | (400, 225, 1), 23 | (400, 240, 1), 24 | (400, 300, 1), 25 | (480, 270, 1), 26 | (960, 270, 1), 27 | (640, 480, 0), 28 | (720, 480, 0), 29 | (720, 576, 0), 30 | (800, 450, 1), 31 | (800, 480, 1), 32 | (800, 600, 1), 33 | (960, 540, 1), 34 | (1280, 720, 1), 35 | ] 36 | 37 | display = PicoGraphics( 38 | PEN_RGB555, DISPLAY_WIDTH, DISPLAY_HEIGHT, FRAME_WIDTH, FRAME_HEIGHT 39 | ) 40 | 41 | modeChange = 0 42 | button_y = machine.Pin(9, machine.Pin.IN, machine.Pin.PULL_UP) 43 | 44 | BLACK = display.create_pen(0, 0, 0) 45 | WHITE = display.create_pen(250, 250, 250) 46 | DARKGREY = display.create_pen(32, 32, 32) 47 | GREY = display.create_pen(96, 96, 96) 48 | SKYBLUE = display.create_pen(32, 32, 192) 49 | RED = display.create_pen(192, 32, 32) 50 | GREEN = display.create_pen(32, 192, 32) 51 | 52 | display.set_font("bitmap8") 53 | 54 | modechange = 0 55 | 56 | while True: 57 | for p in range(2): 58 | if modeChange == 0: 59 | display.set_pen(0) 60 | display.clear() 61 | 62 | for f in range(720): 63 | display.set_pen(display.create_pen(0, 0, int(f / 3))) 64 | display.line(0, f, FRAME_WIDTH, f) 65 | 66 | display.set_pen(WHITE) 67 | display.text("Pimoroni Screen Mode Selector", 0, 0, 300, 1) 68 | 69 | if modeChange == 0 or modeChange > 2: 70 | display.update() 71 | if modeChange > 4: 72 | modeChange = 0 73 | 74 | elif modeChange > 2: 75 | modeChange -= 2 76 | 77 | while True: 78 | if modeChange > 2: 79 | break 80 | if display.is_button_a_pressed(): 81 | print("X is pressed") 82 | modeSelect -= 1 83 | if modeSelect < 0: 84 | modeSelect = numModes - 1 85 | while display.is_button_a_pressed(): 86 | m = 0 87 | if display.is_button_x_pressed(): 88 | print("A is pressed") 89 | modeSelect += 1 90 | if modeSelect > numModes - 1: 91 | modeSelect = 0 92 | while display.is_button_x_pressed(): 93 | m = 0 94 | 95 | for p in range(2): 96 | display.set_pen(DARKGREY) 97 | display.rectangle(14, 22, 137, numModes * 10 + 14) 98 | for f in range(numModes): 99 | display.set_pen(GREY) 100 | if f == modeSelect: 101 | display.set_pen(SKYBLUE) 102 | if button_y.value() == 0: 103 | if modeSelect != currentMode: 104 | if modes[f][2] and WIDESCREEN == 0: 105 | display.set_pen(RED) 106 | modeChange = -1 107 | else: 108 | modeChange = 3 109 | display.set_pen(GREEN) 110 | 111 | display.rectangle(20, f * 10 + 29, 125, 10) 112 | display.set_pen(WHITE) 113 | modeText = "{width:} x {height:}".format( 114 | width=modes[f][0], height=modes[f][1] 115 | ) 116 | display.text(modeText, 30, f * 10 + 30, 300, 1) 117 | 118 | if currentMode == f: 119 | display.text(">", 22, f * 10 + 30, 300, 1) 120 | display.text("<", 137, f * 10 + 30, 300, 1) 121 | 122 | display.update() 123 | 124 | if modeChange == 2: 125 | for f in range(100): 126 | display.set_pen(GREEN) 127 | display.rectangle(20, modeSelect * 10 + 29, 125, 10) 128 | display.set_pen(WHITE) 129 | display.text( 130 | "OK - Hit Y {timeout:}".format(timeout=int((99 - f) / 10)), 131 | 30, 132 | modeSelect * 10 + 30, 133 | 300, 134 | 1, 135 | ) 136 | display.update() 137 | time.sleep(0.15) 138 | 139 | if button_y.value() == 0: 140 | break 141 | modechange = 0 142 | if f > 98: 143 | del display 144 | gc.collect() 145 | display = PicoGraphics( 146 | PEN_RGB555, 147 | modes[currentMode][0], 148 | modes[currentMode][1], 149 | FRAME_WIDTH, 150 | FRAME_HEIGHT, 151 | ) 152 | else: 153 | currentMode = modeSelect 154 | modeChange = 5 155 | 156 | if modeChange == -1: 157 | display.set_pen(RED) 158 | display.rectangle(20, modeSelect * 10 + 29, 125, 10) 159 | display.set_pen(WHITE) 160 | display.text("Unsupported - Wide Only", 30, modeSelect * 10 + 30, 300, 1) 161 | display.update() 162 | modeChange = 0 163 | time.sleep(0.25) 164 | 165 | if modeChange == 1: 166 | display.set_pen(GREEN) 167 | display.rectangle(20, modeSelect * 10 + 29, 125, 10) 168 | display.set_pen(WHITE) 169 | display.text("Mode Change", 30, modeSelect * 10 + 30, 300, 1) 170 | display.update() 171 | time.sleep(0.9) 172 | 173 | del display 174 | gc.collect() 175 | display = PicoGraphics( 176 | PEN_RGB555, 177 | modes[modeSelect][0], 178 | modes[modeSelect][1], 179 | FRAME_WIDTH, 180 | FRAME_HEIGHT, 181 | ) 182 | 183 | modeChange = 4 184 | -------------------------------------------------------------------------------- /examples/seafax.py: -------------------------------------------------------------------------------- 1 | # News headlines, Pirate style! 2 | 3 | # You will need to create or update the file secrets.py with your network credentials using Thonny 4 | # in order for the example to connect to WiFi. 5 | 6 | # secrets.py should contain: 7 | # WIFI_SSID = "" 8 | # WIFI_PASSWORD = "" 9 | 10 | from picographics import PicoGraphics, PEN_RGB555 11 | from pimoroni import Button 12 | import time 13 | import network 14 | import ntptime 15 | import machine 16 | from urllib import urequest 17 | import gc 18 | try: 19 | from secrets import WIFI_SSID, WIFI_PASSWORD 20 | except ImportError: 21 | print("Create secrets.py with your WiFi credentials") 22 | 23 | # Uncomment one URL to use (Top Stories, World News and Technology) 24 | # URL = "https://feeds.bbci.co.uk/news/rss.xml" 25 | # URL = "https://feeds.bbci.co.uk/news/world/rss.xml" 26 | URL = "https://feeds.bbci.co.uk/news/technology/rss.xml" 27 | 28 | display = PicoGraphics(pen_type=PEN_RGB555, width=640, height=480) 29 | 30 | WIDTH, HEIGHT = display.get_bounds() 31 | 32 | BG = display.create_pen(0, 0, 0) 33 | BLUE = display.create_pen(0, 0, 255) 34 | YELLOW = display.create_pen(255, 255, 0) 35 | WHITE = display.create_pen(255, 255, 255) 36 | RED = display.create_pen(255, 0, 0) 37 | GREEN = display.create_pen(0, 255, 0) 38 | CYAN = display.create_pen(0, 255, 255) 39 | 40 | rtc = machine.RTC() 41 | 42 | button_y = Button(9) 43 | 44 | # Enable the Wireless 45 | wlan = network.WLAN(network.STA_IF) 46 | wlan.active(True) 47 | 48 | 49 | def network_connect(ssid, psk): 50 | # Number of attempts to make before timeout 51 | max_wait = 5 52 | 53 | print("connecting...") 54 | wlan.config(pm=0xa11140) # Turn WiFi power saving off for some slow APs 55 | wlan.connect(ssid, psk) 56 | 57 | while max_wait > 0: 58 | if wlan.status() < 0 or wlan.status() >= 3: 59 | break 60 | max_wait -= 1 61 | print('waiting for connection...') 62 | time.sleep(1) 63 | 64 | # Handle connection error. 65 | if wlan.status() != 3: 66 | print("Unable to connect.") 67 | 68 | 69 | # Function to sync the Pico RTC using NTP 70 | def sync_time(): 71 | try: 72 | network_connect(WIFI_SSID, WIFI_PASSWORD) 73 | except NameError: 74 | print("Create secrets.py with your WiFi credentials") 75 | try: 76 | ntptime.settime() 77 | except OSError: 78 | print("Unable to sync with NTP server. Check network and try again.") 79 | 80 | 81 | def read_until(stream, char): 82 | result = b"" 83 | while True: 84 | c = stream.read(1) 85 | if c == char: 86 | return result 87 | result += c 88 | 89 | 90 | def discard_until(stream, c): 91 | while stream.read(1) != c: 92 | pass 93 | 94 | 95 | def parse_xml_stream(s, accept_tags, group_by, max_items=7): 96 | tag = [] 97 | text = b"" 98 | count = 0 99 | current = {} 100 | while True: 101 | char = s.read(1) 102 | if len(char) == 0: 103 | break 104 | 105 | if char == b"<": 106 | next_char = s.read(1) 107 | 108 | # Discard stuff like ") 111 | continue 112 | 113 | # Detect ") # Discard ]> 119 | gc.collect() 120 | 121 | elif next_char == b"/": 122 | current_tag = read_until(s, b">") 123 | top_tag = tag[-1] 124 | 125 | # Populate our result dict 126 | if top_tag in accept_tags: 127 | current[top_tag.decode("utf-8")] = text.decode("utf-8") 128 | 129 | # If we've found a group of items, yield the dict 130 | elif top_tag == group_by: 131 | yield current 132 | current = {} 133 | count += 1 134 | if count == max_items: 135 | return 136 | tag.pop() 137 | text = b"" 138 | gc.collect() 139 | continue 140 | 141 | else: 142 | current_tag = read_until(s, b">") 143 | tag += [next_char + current_tag.split(b" ")[0]] 144 | text = b"" 145 | gc.collect() 146 | 147 | else: 148 | text += char 149 | 150 | 151 | def get_rss(): 152 | try: 153 | stream = urequest.urlopen(URL) 154 | output = list(parse_xml_stream(stream, [b"title", b"description", b"guid", b"pubDate"], b"item")) 155 | return output 156 | 157 | except OSError as e: 158 | print(e) 159 | return False 160 | 161 | 162 | def update(): 163 | global feed 164 | # Gets Feed Data 165 | feed = get_rss() 166 | 167 | 168 | def sea_header(): 169 | display.set_pen(WHITE) 170 | display.set_font("bitmap8") 171 | 172 | current_t = rtc.datetime() 173 | header_string = "P101 SEAFAX {day:02d}/{month:02d}/{year}".format(day=current_t[2], month=current_t[1], year=current_t[0]) 174 | time_string = "{hour:02d}:{minute:02d}/{seconds:02d}".format(hour=current_t[4], minute=current_t[5], seconds=current_t[6]) 175 | 176 | display.text(header_string, 10, 5, WIDTH, 1) 177 | display.set_pen(YELLOW) 178 | display.text(time_string, WIDTH - 40, 5, WIDTH, 1) 179 | 180 | display.set_pen(WHITE) 181 | display.text("PIM", 10, 20, WIDTH, 7) 182 | w = display.measure_text("PIM", 7) 183 | 184 | display.set_pen(BLUE) 185 | display.rectangle(w + 10, 20, 550, 50) 186 | 187 | display.set_pen(BG) 188 | display.text("HEADLINES", w + 15, 25, WIDTH, 6) 189 | display.set_pen(YELLOW) 190 | display.text("HEADLINES", w + 15, 23, WIDTH, 6) 191 | 192 | 193 | feed = None 194 | 195 | sync_time() 196 | update() 197 | 198 | while True: 199 | # get new data when button Y is pressed 200 | if button_y.is_pressed: 201 | sync_time() 202 | update() 203 | 204 | display.set_pen(BG) 205 | display.clear() 206 | 207 | sea_header() 208 | 209 | if feed: 210 | display.set_pen(WHITE) 211 | display.text(feed[0]["title"], 10, 100, WIDTH - 50, 2) 212 | display.text(feed[1]["title"], 10, 150, WIDTH - 50, 2) 213 | display.text(feed[2]["title"], 10, 200, WIDTH - 50, 2) 214 | display.text(feed[3]["title"], 10, 250, WIDTH - 50, 2) 215 | display.text(feed[4]["title"], 10, 300, WIDTH - 50, 2) 216 | display.text(feed[5]["title"], 10, 350, WIDTH - 50, 2) 217 | display.text(feed[6]["title"], 10, 400, WIDTH - 50, 2) 218 | else: 219 | display.set_pen(BLUE) 220 | display.rectangle(0, (HEIGHT // 2) - 20, WIDTH, 40) 221 | display.set_pen(YELLOW) 222 | display.text("Unable to display news feed!", 5, (HEIGHT // 2) - 15, WIDTH, 2) 223 | display.text("Check your network settings in secrets.py", 5, (HEIGHT // 2) + 2, WIDTH, 2) 224 | 225 | display.set_pen(BLUE) 226 | display.rectangle(0, HEIGHT - 30, WIDTH, 10) 227 | display.set_pen(YELLOW) 228 | display.text("PicoVision by Pimoroni", 5, HEIGHT - 29, WIDTH, 1) 229 | display.set_pen(RED) 230 | display.text("PIRATE", 5, HEIGHT - 15, WIDTH, 1) 231 | display.set_pen(GREEN) 232 | display.text("MONKEY", 200, HEIGHT - 15, WIDTH, 1) 233 | display.set_pen(YELLOW) 234 | display.text("ROBOT", 400, HEIGHT - 15, WIDTH, 1) 235 | display.set_pen(CYAN) 236 | display.text("NINJA", WIDTH - 25, HEIGHT - 15, WIDTH, 1) 237 | 238 | display.update() 239 | time.sleep(0.01) 240 | -------------------------------------------------------------------------------- /examples/sneks_and_ladders/pvgame.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | TILE_W = 16 4 | TILE_H = 16 5 | TILE_SIZE = (TILE_W, TILE_H) 6 | 7 | 8 | # A simple wrapper around `load_sprite` to cache decoded PNG images onto the filesystem 9 | def cached_png(display, filename, source=None): 10 | cache = filename[:-4] # strip .png 11 | if source is not None: 12 | x, y, w, h = source 13 | cache = f"{cache}-{x}-{y}-{w}-{h}" 14 | cache += ".bin" 15 | 16 | try: 17 | with open(cache, "rb") as f: 18 | width, height = struct.unpack("HH", f.read(4)) 19 | data = f.read() 20 | except OSError: 21 | width, height, data = display.load_sprite(filename, source=source) 22 | 23 | with open(cache, "wb") as f: 24 | f.write(struct.pack("HH", width, height)) 25 | f.write(data) 26 | return width, height, data 27 | 28 | 29 | # A class for managing sprites 30 | class SpriteList: 31 | def __init__(self, display, max_sprites=32): 32 | self._gfx = display 33 | self.max_sprites = max_sprites 34 | self.items = [] 35 | 36 | def add(self, image, x, y, v_scale=1, force=False): 37 | if len(self.items) == self.max_sprites: 38 | if force: 39 | self.items.pop(0) 40 | else: 41 | return 42 | self.items.append((image, x, y, v_scale)) 43 | 44 | def clear(self): 45 | self.items = [] 46 | 47 | def display(self): 48 | for i in range(self.max_sprites): 49 | try: 50 | image, x, y, v_scale = self.items[i] 51 | self._gfx.display_sprite(i, image, x, y, v_scale=v_scale) 52 | except IndexError: 53 | self._gfx.clear_sprite(i) 54 | 55 | 56 | # A class for managing rectangle to rectangle collisions 57 | class CollisionList: 58 | def __init__(self): 59 | self.zones = [] 60 | 61 | def add(self, zone, action, **args): 62 | self.zones.append((zone, action, args)) 63 | 64 | def clear(self): 65 | self.zones = [] 66 | 67 | def test(self, x, y, w, h): 68 | for zone, action, args in self.zones: 69 | zx, zy, zw, zh = zone 70 | if not (x > zx + zw or x + w < zx or y > zy + zh or y + h < zy): 71 | action(**args) 72 | 73 | 74 | # A class for managing a basic sprite actor within a tile grid array 75 | class Actor: 76 | LEFT = True 77 | RIGHT = False 78 | 79 | def __init__(self, spritelist, x, y, frames_left, frames_right, ping_pong=False): 80 | self.spritelist = spritelist 81 | self.x = x 82 | self.y = y 83 | self.v_x = -1 84 | self.v_y = 0 85 | self.frames_left = frames_left 86 | self.frames_right = frames_right 87 | 88 | if ping_pong: 89 | self.frames_left += reversed(frames_left[1:][:-1]) 90 | self.frames_right += reversed(frames_right[1:][:-1]) 91 | 92 | self.l_count = len(self.frames_left) 93 | self.r_count = len(self.frames_right) 94 | 95 | self.facing = self.LEFT 96 | 97 | def bounds(self): 98 | return (self.x, self.y, TILE_W, TILE_H) 99 | 100 | def update(self, level_data): 101 | level_width = len(level_data[0]) * TILE_W 102 | 103 | self.x += self.v_x 104 | self.y += self.v_y 105 | 106 | tile_x = int((self.x + 8) / TILE_W) 107 | tile_y = int(self.y / TILE_H) 108 | 109 | tile_below = level_data[tile_y + 1][tile_x] 110 | 111 | if tile_below == 0 or self.x < 0 or (self.x + TILE_W) > level_width: 112 | self.v_x *= -1 113 | self.x += self.v_x 114 | self.facing = self.LEFT if self.v_x < 0 else self.RIGHT 115 | 116 | def draw(self, t, offset_x, offset_y): 117 | if self.facing == self.LEFT: 118 | frame = self.frames_left[t % self.l_count] 119 | else: 120 | frame = self.frames_right[t % self.r_count] 121 | self.spritelist.add(frame, self.x + offset_x, self.y + offset_y) 122 | -------------------------------------------------------------------------------- /examples/sneks_and_ladders/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/sneks_and_ladders/tiles.png -------------------------------------------------------------------------------- /examples/starfield.py: -------------------------------------------------------------------------------- 1 | # Travel through a Windows 3.1-esque starfield, with stars growing as they get 'closer'. 2 | 3 | from picographics import PicoGraphics, PEN_RGB555 4 | import random 5 | 6 | # Constants to play with 7 | NUMBER_OF_STARS = 200 8 | TRAVEL_SPEED = 1.2 9 | STAR_GROWTH = 0.12 10 | 11 | # Set up our display 12 | graphics = PicoGraphics(pen_type=PEN_RGB555, width=640, height=480) 13 | 14 | WIDTH, HEIGHT = graphics.get_bounds() 15 | 16 | BLACK = graphics.create_pen(0, 0, 0) 17 | WHITE = graphics.create_pen(255, 255, 255) 18 | 19 | stars = [] 20 | 21 | 22 | def new_star(): 23 | # Create a new star, with initial x, y, and size 24 | # Initial x will fall between -WIDTH / 2 and +WIDTH / 2 and y between -HEIGHT/2 and +HEIGHT/2 25 | # These are relative values for now, treating (0, 0) as the centre of the screen. 26 | star = [random.randint(0, WIDTH) - WIDTH // 2, random.randint(0, HEIGHT) - HEIGHT // 2, 0.5] 27 | return star 28 | 29 | 30 | for i in range(0, NUMBER_OF_STARS): 31 | stars.append(new_star()) 32 | 33 | while True: 34 | graphics.set_pen(BLACK) 35 | graphics.clear() 36 | graphics.set_pen(WHITE) 37 | for i in range(0, NUMBER_OF_STARS): 38 | # Load a star from the stars list 39 | s = stars[i] 40 | 41 | # Update x 42 | s[0] = s[0] * TRAVEL_SPEED 43 | 44 | # Update y 45 | s[1] = s[1] * TRAVEL_SPEED 46 | 47 | if s[0] <= - WIDTH // 2 or s[0] >= WIDTH // 2 or s[1] <= - HEIGHT // 2 or s[1] >= HEIGHT // 2 or s[2] >= 5: 48 | # This star has fallen off the screen (or rolled dead centre and grown too big!) 49 | # Replace it with a new one 50 | s = new_star() 51 | 52 | # Grow the star as it travels outward 53 | s[2] += STAR_GROWTH 54 | 55 | # Save the updated star to the list 56 | stars[i] = s 57 | 58 | # Draw star, adding offsets to our relative coordinates to allow for (0, 0) being in the top left corner. 59 | graphics.circle(int(s[0]) + WIDTH // 2, int(s[1]) + HEIGHT // 2, int(s[2])) 60 | graphics.update() 61 | -------------------------------------------------------------------------------- /examples/starfield_rainbow.py: -------------------------------------------------------------------------------- 1 | # Travel through a Windows 3.1-esque starfield, with stars growing as they get 'closer'. 2 | # This version is rainbow (we didn't get that in 1992!) 3 | 4 | from picographics import PicoGraphics, PEN_RGB555 5 | import random 6 | 7 | # Constants to play with 8 | NUMBER_OF_STARS = 150 9 | TRAVEL_SPEED = 1.2 10 | STAR_GROWTH = 0.20 11 | HUE_SHIFT = 0.05 12 | 13 | # Set up our display 14 | graphics = PicoGraphics(pen_type=PEN_RGB555, width=640, height=480) 15 | 16 | WIDTH, HEIGHT = graphics.get_bounds() 17 | 18 | BLACK = graphics.create_pen(0, 0, 0) 19 | WHITE = graphics.create_pen(255, 255, 255) 20 | 21 | stars = [] 22 | 23 | 24 | def new_star(): 25 | # Create a new star, with initial x, y, size and hue 26 | # Initial x will fall between -WIDTH / 2 and +WIDTH / 2 and y between -HEIGHT/2 and +HEIGHT/2 27 | # These are relative values for now, treating (0, 0) as the centre of the screen. 28 | star = [random.randint(0, WIDTH) - WIDTH // 2, random.randint(0, HEIGHT) - HEIGHT // 2, 1.0, random.random()] 29 | return star 30 | 31 | 32 | for i in range(0, NUMBER_OF_STARS): 33 | stars.append(new_star()) 34 | 35 | while True: 36 | graphics.set_pen(BLACK) 37 | graphics.clear() 38 | for i in range(0, NUMBER_OF_STARS): 39 | # Load a star from the stars list 40 | s = stars[i] 41 | 42 | # Update x 43 | s[0] = s[0] * TRAVEL_SPEED 44 | 45 | # Update y 46 | s[1] = s[1] * TRAVEL_SPEED 47 | 48 | if s[0] <= - WIDTH // 2 or s[0] >= WIDTH // 2 or s[1] <= - HEIGHT // 2 or s[1] >= HEIGHT // 2 or s[2] >= 10: 49 | # This star has fallen off the screen (or rolled dead centre and grown too big!) 50 | # Replace it with a new one 51 | s = new_star() 52 | 53 | # Grow the star as it travels outward 54 | s[2] += STAR_GROWTH 55 | 56 | # Adjust the hue 57 | s[3] += HUE_SHIFT 58 | 59 | # Save the updated star to the list 60 | stars[i] = s 61 | 62 | # Set the pen colour based on the star's hue 63 | graphics.set_pen(graphics.create_pen_hsv(s[3], 1.0, 1.0)) 64 | 65 | # Draw star, adding offsets to our relative coordinates to allow for (0, 0) being in the top left corner. 66 | graphics.circle(int(s[0]) + WIDTH // 2, int(s[1]) + HEIGHT // 2, int(s[2])) 67 | graphics.update() 68 | -------------------------------------------------------------------------------- /examples/thermometer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import machine 3 | from picographics import PicoGraphics, PEN_RGB555 4 | 5 | display = PicoGraphics(pen_type=PEN_RGB555, width=640, height=480) 6 | 7 | WIDTH, HEIGHT = display.get_bounds() 8 | 9 | sensor_temp = machine.ADC(4) 10 | 11 | BLACK = display.create_pen(0, 0, 0) 12 | WHITE = display.create_pen(255, 255, 255) 13 | 14 | conversion_factor = 3.3 / (65535) # used for calculating a temperature from the raw sensor reading 15 | 16 | temp_min = 10 17 | temp_max = 30 18 | bar_width = 4 19 | 20 | temperatures = [] 21 | 22 | colors = [(0, 0, 255), (0, 255, 0), (255, 255, 0), (255, 0, 0)] 23 | 24 | 25 | def temperature_to_color(temp): 26 | temp = min(temp, temp_max) 27 | temp = max(temp, temp_min) 28 | 29 | f_index = float(temp - temp_min) / float(temp_max - temp_min) 30 | f_index *= len(colors) - 1 31 | index = int(f_index) 32 | 33 | if index == len(colors) - 1: 34 | return colors[index] 35 | 36 | blend_b = f_index - index 37 | blend_a = 1.0 - blend_b 38 | 39 | a = colors[index] 40 | b = colors[index + 1] 41 | 42 | return [int((a[i] * blend_a) + (b[i] * blend_b)) for i in range(3)] 43 | 44 | 45 | while True: 46 | # fills the screen with black 47 | display.set_pen(BLACK) 48 | display.clear() 49 | 50 | # the following two lines do some maths to convert the number from the temp sensor into celsius 51 | reading = sensor_temp.read_u16() * conversion_factor 52 | temperature = 27 - (reading - 0.706) / 0.001721 53 | 54 | temperatures.append(temperature) 55 | 56 | # shifts the temperatures history to the left by one sample 57 | if len(temperatures) > WIDTH // bar_width: 58 | temperatures.pop(0) 59 | 60 | i = 0 61 | 62 | for t in temperatures: 63 | # chooses a pen colour based on the temperature 64 | TEMPERATURE_COLOUR = display.create_pen(*temperature_to_color(t)) 65 | display.set_pen(TEMPERATURE_COLOUR) 66 | 67 | # draws the reading as a tall, thin rectangle 68 | display.rectangle(i, HEIGHT - (round(t) * 8), bar_width, HEIGHT) 69 | 70 | # the next tall thin rectangle needs to be drawn 71 | # "bar_width" (default: 5) pixels to the right of the last one 72 | i += bar_width 73 | 74 | # draws a white background for the text 75 | display.set_pen(WHITE) 76 | display.rectangle(1, 1, 120, 30) 77 | 78 | # writes the reading as text in the white rectangle 79 | display.set_pen(BLACK) 80 | display.text("{:.2f}".format(temperature) + "c", 3, 3, 0, 4) 81 | 82 | # time to update the display 83 | display.update() 84 | 85 | # waits for 5 seconds 86 | time.sleep(5) 87 | -------------------------------------------------------------------------------- /examples/toaster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/picovision/0d9c413128341ab784a8df9852929c4011265737/examples/toaster.png -------------------------------------------------------------------------------- /examples/vector_clock.py: -------------------------------------------------------------------------------- 1 | import time 2 | import gc 3 | 4 | from picographics import PicoGraphics, PEN_RGB555 5 | from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_X16 6 | 7 | 8 | display = PicoGraphics(PEN_RGB555, 720, 480) 9 | 10 | vector = PicoVector(display) 11 | vector.set_antialiasing(ANTIALIAS_X16) 12 | 13 | RED = display.create_pen(200, 0, 0) 14 | BLACK = display.create_pen(0, 0, 0) 15 | GREY = display.create_pen(200, 200, 200) 16 | WHITE = display.create_pen(255, 255, 255) 17 | 18 | """ 19 | # Redefine colours for a Blue clock 20 | RED = display.create_pen(200, 0, 0) 21 | BLACK = display.create_pen(135, 159, 169) 22 | GREY = display.create_pen(10, 40, 50) 23 | WHITE = display.create_pen(14, 60, 76) 24 | """ 25 | 26 | WIDTH, HEIGHT = display.get_bounds() 27 | 28 | hub = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 24, 5) 29 | 30 | face = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 48, int(HEIGHT / 2)) 31 | 32 | print(time.localtime()) 33 | 34 | last_second = None 35 | 36 | while True: 37 | t_start = time.ticks_ms() 38 | year, month, day, hour, minute, second, _, _ = time.localtime() 39 | 40 | if last_second == second: 41 | continue 42 | 43 | last_second = second 44 | 45 | display.set_pen(0) 46 | display.clear() 47 | 48 | display.set_pen(BLACK) 49 | display.circle(int(WIDTH / 2), int(HEIGHT / 2), int(HEIGHT / 2)) 50 | display.set_pen(WHITE) 51 | display.circle(int(WIDTH / 2), int(HEIGHT / 2), int(HEIGHT / 2) - 4) 52 | 53 | display.set_pen(GREY) 54 | 55 | for a in range(60): 56 | tick_mark = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) 57 | vector.rotate(tick_mark, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 58 | vector.translate(tick_mark, 0, 2) 59 | vector.draw(tick_mark) 60 | 61 | for a in range(12): 62 | hour_mark = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) 63 | vector.rotate(hour_mark, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 64 | vector.translate(hour_mark, 0, 2) 65 | vector.draw(hour_mark) 66 | 67 | angle_second = second * 6 68 | second_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) 69 | second_hand = Polygon((-2, -second_hand_length), (-2, int(HEIGHT / 8)), (2, int(HEIGHT / 8)), (2, -second_hand_length)) 70 | vector.rotate(second_hand, angle_second, 0, 0) 71 | vector.translate(second_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 72 | 73 | angle_minute = minute * 6 74 | angle_minute += second / 10.0 75 | minute_hand_length = int(HEIGHT / 2) - int(HEIGHT / 24) 76 | minute_hand = Polygon((-5, -minute_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -minute_hand_length)) 77 | vector.rotate(minute_hand, angle_minute, 0, 0) 78 | vector.translate(minute_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 79 | 80 | angle_hour = (hour % 12) * 30 81 | angle_hour += minute / 2 82 | hour_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) 83 | hour_hand = Polygon((-5, -hour_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -hour_hand_length)) 84 | vector.rotate(hour_hand, angle_hour, 0, 0) 85 | vector.translate(hour_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 86 | 87 | display.set_pen(GREY) 88 | 89 | vector.draw(minute_hand) 90 | vector.draw(hour_hand) 91 | vector.draw(second_hand) 92 | 93 | display.set_pen(BLACK) 94 | 95 | for a in range(60): 96 | tick_mark = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) 97 | vector.rotate(tick_mark, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 98 | vector.draw(tick_mark) 99 | 100 | for a in range(12): 101 | hour_mark = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) 102 | vector.rotate(hour_mark, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 103 | vector.draw(hour_mark) 104 | 105 | vector.translate(minute_hand, 0, -5) 106 | vector.translate(hour_hand, 0, -5) 107 | vector.draw(minute_hand) 108 | vector.draw(hour_hand) 109 | 110 | display.set_pen(RED) 111 | vector.translate(second_hand, 0, -5) 112 | vector.draw(second_hand) 113 | vector.draw(hub) 114 | 115 | display.update() 116 | gc.collect() 117 | 118 | t_end = time.ticks_ms() 119 | print(f"Took {t_end - t_start}ms") 120 | -------------------------------------------------------------------------------- /examples/vector_clock_smooth.py: -------------------------------------------------------------------------------- 1 | import time 2 | import gc 3 | 4 | from picographics import PicoGraphics, PEN_RGB555 5 | from picovector import PicoVector, Polygon, RegularPolygon, Rectangle, ANTIALIAS_X16 6 | 7 | # machine.freq(168_000_000) 8 | 9 | display = PicoGraphics(PEN_RGB555, 720, 480) 10 | 11 | vector = PicoVector(display) 12 | vector.set_antialiasing(ANTIALIAS_X16) 13 | 14 | RED = display.create_pen(200, 0, 0) 15 | BLACK = display.create_pen(0, 0, 0) 16 | GREY = display.create_pen(200, 200, 200) 17 | WHITE = display.create_pen(255, 255, 255) 18 | 19 | """ 20 | # Redefine colours for a Blue clock 21 | RED = display.create_pen(200, 0, 0) 22 | BLACK = display.create_pen(135, 159, 169) 23 | GREY = display.create_pen(10, 40, 50) 24 | WHITE = display.create_pen(14, 60, 76) 25 | """ 26 | 27 | WIDTH, HEIGHT = display.get_bounds() 28 | 29 | hub = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 24, 5) 30 | 31 | face = RegularPolygon(int(WIDTH / 2), int(HEIGHT / 2), 48 * 2, int(HEIGHT / 2), (360.0 / 48)) 32 | 33 | print(time.localtime()) 34 | 35 | # Pre-calculate all the polygons for the clock face tick marks 36 | ticks = [] 37 | tick_shadows = [] 38 | 39 | for a in range(60): 40 | if a % 5 == 0: 41 | continue 42 | 43 | tick_mark = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) 44 | vector.rotate(tick_mark, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 45 | 46 | tick_shadow = Rectangle(int(WIDTH / 2) - 3, 10, 6, int(HEIGHT / 48)) 47 | vector.rotate(tick_shadow, 360 / 60.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 48 | vector.translate(tick_shadow, 0, 2) 49 | 50 | ticks.append(tick_mark) 51 | tick_shadows.append(tick_shadow) 52 | 53 | for a in range(12): 54 | hour_mark = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) 55 | vector.rotate(hour_mark, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 56 | 57 | hour_shadow = Rectangle(int(WIDTH / 2) - 5, 10, 10, int(HEIGHT / 10)) 58 | vector.rotate(hour_shadow, 360 / 12.0 * a, int(WIDTH / 2), int(HEIGHT / 2)) 59 | vector.translate(hour_shadow, 0, 2) 60 | 61 | ticks.append(hour_mark) 62 | tick_shadows.append(hour_shadow) 63 | 64 | 65 | # Set the background colour and draw the clock face and tick marks into both PSRAM buffers 66 | for _ in range(2): 67 | display.set_pen(BLACK) 68 | display.clear() 69 | display.set_pen(WHITE) 70 | vector.draw(face) 71 | 72 | display.set_pen(GREY) 73 | 74 | for shadow in tick_shadows: 75 | vector.draw(shadow) 76 | 77 | display.set_pen(BLACK) 78 | 79 | for tick in ticks: 80 | vector.draw(tick) 81 | 82 | display.update() 83 | 84 | 85 | t_frames = 0 86 | t_total = 0 87 | last_second = None 88 | 89 | last_clip = [(0, 0, 0, 0), (0, 0, 0, 0)] 90 | 91 | while True: 92 | year, month, day, hour, minute, second, _, _ = time.localtime() 93 | 94 | if last_second != second: 95 | t_sec = time.ticks_ms() 96 | last_second = second 97 | 98 | t_start = time.ticks_ms() 99 | 100 | second_frac = (t_start - t_sec) / 1000.0 101 | angle_second = (second + second_frac) * 6 102 | second_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) 103 | second_hand = Polygon((-2, -second_hand_length), (-2, int(HEIGHT / 8)), (2, int(HEIGHT / 8)), (2, -second_hand_length)) 104 | vector.rotate(second_hand, angle_second, 0, 0) 105 | vector.translate(second_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 106 | 107 | angle_minute = minute * 6 108 | angle_minute += (second + second_frac) / 10.0 109 | minute_hand_length = int(HEIGHT / 2) - int(HEIGHT / 24) 110 | minute_hand = Polygon((-5, -minute_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -minute_hand_length)) 111 | vector.rotate(minute_hand, angle_minute, 0, 0) 112 | vector.translate(minute_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 113 | 114 | hour_frac = (second + second_frac) / 60 115 | angle_hour = (hour % 12) * 30 116 | angle_hour += (minute + hour_frac) / 2 117 | hour_hand_length = int(HEIGHT / 2) - int(HEIGHT / 8) 118 | hour_hand = Polygon((-5, -hour_hand_length), (-10, int(HEIGHT / 16)), (10, int(HEIGHT / 16)), (5, -hour_hand_length)) 119 | vector.rotate(hour_hand, angle_hour, 0, 0) 120 | vector.translate(hour_hand, int(WIDTH / 2), int(HEIGHT / 2) + 5) 121 | 122 | second_hand_bounds = second_hand.bounds() 123 | minute_hand_bounds = minute_hand.bounds() 124 | hour_hand_bounds = hour_hand.bounds() 125 | 126 | # Create a clipping region which includes all of the hands 127 | clip_x = min(second_hand_bounds[0], minute_hand_bounds[0], hour_hand_bounds[0]) 128 | clip_y = min(second_hand_bounds[1], minute_hand_bounds[1], hour_hand_bounds[1]) - 5 129 | clip_max_x = max(second_hand_bounds[0] + second_hand_bounds[2], minute_hand_bounds[0] + minute_hand_bounds[2], hour_hand_bounds[0] + hour_hand_bounds[2]) 130 | clip_max_y = max(second_hand_bounds[1] + second_hand_bounds[3], minute_hand_bounds[1] + minute_hand_bounds[3], hour_hand_bounds[1] + hour_hand_bounds[3]) + 5 131 | 132 | # Remember this clipping window for 2 frames time 133 | last_clip.append((clip_x, clip_y, clip_max_x, clip_max_y),) 134 | 135 | # Expand to also cover the clipping window from 2 frames ago 136 | clip_x = min(clip_x, last_clip[0][0]) 137 | clip_y = min(clip_y, last_clip[0][1]) 138 | clip_max_x = max(clip_max_x, last_clip[0][2]) 139 | clip_max_y = max(clip_max_y, last_clip[0][3]) 140 | 141 | # Apply the clipping region so we only redraw the parts of the clock which have changed 142 | display.set_clip(clip_x, clip_y, clip_max_x - clip_x + 1, clip_max_y - clip_y + 1) 143 | 144 | # Drop the clipping window from 2 frames ago, we no longer need it 145 | last_clip = last_clip[1:] 146 | 147 | # Clear to white just inside our anti-aliased outer circle and clipping region 148 | display.set_pen(WHITE) 149 | display.circle(int(WIDTH / 2), int(HEIGHT / 2), int(HEIGHT / 2) - 4) 150 | 151 | # Draw the shadows 152 | display.set_pen(GREY) 153 | 154 | for shadow in tick_shadows: 155 | vector.draw(shadow) 156 | 157 | vector.draw(minute_hand) 158 | vector.draw(hour_hand) 159 | vector.draw(second_hand) 160 | 161 | # Draw the face 162 | display.set_pen(BLACK) 163 | 164 | for tick in ticks: 165 | vector.draw(tick) 166 | 167 | vector.translate(minute_hand, 0, -5) 168 | vector.translate(hour_hand, 0, -5) 169 | vector.draw(minute_hand) 170 | vector.draw(hour_hand) 171 | 172 | display.set_pen(RED) 173 | vector.translate(second_hand, 0, -5) 174 | vector.draw(second_hand) 175 | vector.draw(hub) 176 | 177 | t_end = time.ticks_ms() 178 | t_total += t_end - t_start 179 | t_frames += 1 180 | 181 | if t_frames == 60: 182 | f_avg = t_total / 60.0 183 | f_fps = 1000.0 / f_avg 184 | print(f"Took {t_total}ms for 60 frames, {f_avg:.02f}ms avg. {f_fps:.02f}FPS avg.") 185 | t_frames = 0 186 | t_total = 0 187 | 188 | display.update() 189 | gc.collect() 190 | -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/board.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploy": [ 3 | "../deploy.md" 4 | ], 5 | "docs": "", 6 | "features": [ 7 | "HDMI", 8 | "Buttons" 9 | ], 10 | "images": [ 11 | ], 12 | "mcu": "rp2040", 13 | "product": "Pimoroni PicoVision (2MiB)", 14 | "thumbnail": "", 15 | "url": "https://shop.pimoroni.com/products/picovision", 16 | "vendor": "Pimoroni" 17 | } 18 | -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/manifest.py: -------------------------------------------------------------------------------- 1 | include("$(PORT_DIR)/boards/manifest.py") 2 | 3 | freeze("$(BOARD_DIR)/../../lib/") 4 | 5 | # mip, ntptime, urequests, webrepl etc - see: 6 | # https://github.com/micropython/micropython-lib/blob/master/micropython/bundles/bundle-networking/manifest.py 7 | require("bundle-networking") 8 | 9 | # Bluetooth 10 | require("aioble") 11 | 12 | require("urllib.urequest") 13 | require("umqtt.simple") 14 | 15 | require("sdcard") -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/micropython.cmake: -------------------------------------------------------------------------------- 1 | set(PIMORONI_PICO_PATH ../../../../pimoroni-pico) 2 | include(${CMAKE_CURRENT_LIST_DIR}/../pimoroni_pico_import.cmake) 3 | 4 | include_directories(${PIMORONI_PICO_PATH}/micropython) 5 | 6 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../") 7 | list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython") 8 | list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython/modules") 9 | 10 | # Enable support for string_view (for PicoGraphics) 11 | set(CMAKE_C_STANDARD 11) 12 | set(CMAKE_CXX_STANDARD 17) 13 | 14 | # Essential 15 | include(pimoroni_i2c/micropython) 16 | include(pimoroni_bus/micropython) 17 | 18 | # Pico Graphics Essential 19 | include(hershey_fonts/micropython) 20 | include(bitmap_fonts/micropython) 21 | include(picovector/micropython) 22 | include(modules/picographics/micropython) 23 | 24 | # Pico Graphics Extra 25 | include(jpegdec/micropython) 26 | include(pngdec/micropython) 27 | include(qrcode/micropython/micropython) 28 | 29 | # Sensors & Breakouts 30 | include(micropython-common-breakouts) 31 | include(pcf85063a/micropython) 32 | 33 | # Pico Synth 34 | include(modules/picosynth/micropython) 35 | 36 | # Utility 37 | include(adcfft/micropython) 38 | 39 | # Note: cppmem is *required* for C++ code to function on MicroPython 40 | # it redirects `malloc` and `free` calls to MicroPython's heap 41 | include(cppmem/micropython) 42 | 43 | # version.py, pimoroni.py and boot.py 44 | include(modules_py/modules_py) 45 | -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/mpconfigboard.cmake: -------------------------------------------------------------------------------- 1 | # cmake file for Pimoroni Badger 2040W 2 | set(MICROPY_BOARD PICO_W) 3 | 4 | set(MICROPY_C_HEAP_SIZE 4096) 5 | 6 | set(MICROPY_PY_LWIP ON) 7 | set(MICROPY_PY_NETWORK_CYW43 ON) 8 | 9 | # Bluetooth 10 | set(MICROPY_PY_BLUETOOTH ON) 11 | set(MICROPY_BLUETOOTH_BTSTACK ON) 12 | set(MICROPY_PY_BLUETOOTH_CYW43 ON) 13 | 14 | # Board specific version of the frozen manifest 15 | set(MICROPY_FROZEN_MANIFEST ${CMAKE_CURRENT_LIST_DIR}/manifest.py) -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/mpconfigboard.h: -------------------------------------------------------------------------------- 1 | // This is a hack! Need to replace with upstream board definition. 2 | #define MICROPY_HW_BOARD_NAME "Pimoroni PicoVision 2MB" 3 | #define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024) 4 | 5 | // Enable networking. 6 | #define MICROPY_PY_NETWORK 1 7 | #define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoVision" 8 | 9 | // CYW43 driver configuration. 10 | #define CYW43_USE_SPI (1) 11 | #define CYW43_LWIP (1) 12 | #define CYW43_GPIO (1) 13 | #define CYW43_SPI_PIO (1) 14 | 15 | // For debugging mbedtls - also set 16 | // Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose 17 | // #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1 18 | 19 | #define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT 20 | 21 | #define MICROPY_HW_PIN_RESERVED(i) ((i) == CYW43_PIN_WL_HOST_WAKE || (i) == CYW43_PIN_WL_REG_ON) -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/pins.csv: -------------------------------------------------------------------------------- 1 | GP0,GPIO0 2 | GP1,GPIO1 3 | GP2,GPIO2 4 | GP3,GPIO3 5 | GP4,GPIO4 6 | GP5,GPIO5 7 | GP6,GPIO6 8 | GP7,GPIO7 9 | GP8,GPIO8 10 | GP9,GPIO9 11 | GP10,GPIO10 12 | GP11,GPIO11 13 | GP12,GPIO12 14 | GP13,GPIO13 15 | GP14,GPIO14 16 | GP15,GPIO15 17 | GP16,GPIO16 18 | GP17,GPIO17 19 | GP18,GPIO18 20 | GP19,GPIO19 21 | GP20,GPIO20 22 | GP21,GPIO21 23 | GP22,GPIO22 24 | GP26,GPIO26 25 | GP27,GPIO27 26 | GP28,GPIO28 27 | WL_GPIO0,EXT_GPIO0 28 | WL_GPIO1,EXT_GPIO1 29 | WL_GPIO2,EXT_GPIO2 30 | LED,EXT_GPIO0 -------------------------------------------------------------------------------- /firmware/PIMORONI_PICOVISION/uf2-manifest.txt: -------------------------------------------------------------------------------- 1 | *.py 2 | examples/*.jpg 3 | examples/*.py 4 | images/*.jpg 5 | images/*.txt 6 | badges/*.txt 7 | badges/*.jpg 8 | books/*.txt 9 | icons/*.jpg -------------------------------------------------------------------------------- /firmware/micropython_nano_specs.patch: -------------------------------------------------------------------------------- 1 | diff --git a/ports/rp2/CMakeLists.txt b/ports/rp2/CMakeLists.txt 2 | index 4334a0a..0b010fe 100644 3 | --- a/ports/rp2/CMakeLists.txt 4 | +++ b/ports/rp2/CMakeLists.txt 5 | @@ -411,6 +411,15 @@ target_compile_options(${MICROPY_TARGET} PRIVATE 6 | target_link_options(${MICROPY_TARGET} PRIVATE 7 | -Wl,--defsym=__micropy_c_heap_size__=${MICROPY_C_HEAP_SIZE} 8 | ) 9 | +# Do not include stack unwinding & exception handling for C++ user modules 10 | +target_compile_definitions(usermod INTERFACE PICO_CXX_ENABLE_EXCEPTIONS=0 PICO_MAX_SHARED_IRQ_HANDLERS=8) 11 | +target_compile_options(usermod INTERFACE $<$: 12 | + -fno-exceptions 13 | + -fno-unwind-tables 14 | + -fno-rtti 15 | + -fno-use-cxa-atexit 16 | +>) 17 | +target_link_options(usermod INTERFACE -specs=nano.specs) 18 | 19 | set_source_files_properties( 20 | ${PICO_SDK_PATH}/src/rp2_common/pico_double/double_math.c 21 | -------------------------------------------------------------------------------- /firmware/pimoroni_pico_import.cmake: -------------------------------------------------------------------------------- 1 | # This file can be dropped into a project to help locate the Pimoroni Pico libraries 2 | # It will also set up the required include and module search paths. 3 | 4 | if (DEFINED ENV{PIMORONI_PICO_FETCH_FROM_GIT} AND (NOT PIMORONI_PICO_FETCH_FROM_GIT)) 5 | set(PIMORONI_PICO_FETCH_FROM_GIT $ENV{PIMORONI_PICO_FETCH_FROM_GIT}) 6 | message("Using PIMORONI_PICO_FETCH_FROM_GIT from environment ('${PIMORONI_PICO_FETCH_FROM_GIT}')") 7 | endif () 8 | 9 | if (DEFINED ENV{PIMORONI_PICO_FETCH_FROM_GIT_PATH} AND (NOT PIMORONI_PICO_FETCH_FROM_GIT_PATH)) 10 | set(PIMORONI_PICO_FETCH_FROM_GIT_PATH $ENV{PIMORONI_PICO_FETCH_FROM_GIT_PATH}) 11 | message("Using PIMORONI_PICO_FETCH_FROM_GIT_PATH from environment ('${PIMORONI_PICO_FETCH_FROM_GIT_PATH}')") 12 | endif () 13 | 14 | if (NOT PIMORONI_PICO_PATH) 15 | if (PIMORONI_PICO_FETCH_FROM_GIT) 16 | include(FetchContent) 17 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 18 | if (PIMORONI_PICO_FETCH_FROM_GIT_PATH) 19 | get_filename_component(FETCHCONTENT_BASE_DIR "${PIMORONI_PICO_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 20 | endif () 21 | FetchContent_Declare( 22 | pimoroni_pico 23 | GIT_REPOSITORY https://github.com/pimoroni/pimoroni-pico 24 | GIT_TAG main 25 | ) 26 | if (NOT pimoroni_pico) 27 | message("Downloading PIMORONI_PICO SDK") 28 | FetchContent_Populate(pimoroni_pico) 29 | set(PIMORONI_PICO_PATH ${pimoroni_pico_SOURCE_DIR}) 30 | endif () 31 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 32 | elseif(PICO_SDK_PATH AND EXISTS "${PICO_SDK_PATH}/../pimoroni-pico") 33 | set(PIMORONI_PICO_PATH ${PICO_SDK_PATH}/../pimoroni-pico) 34 | message("Defaulting PIMORONI_PICO_PATH as sibling of PICO_SDK_PATH: ${PIMORONI_PICO_PATH}") 35 | elseif(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/../../pimoroni-pico/") 36 | set(PIMORONI_PICO_PATH ${CMAKE_CURRENT_BINARY_DIR}/../../pimoroni-pico/) 37 | else() 38 | message(FATAL_ERROR "Pimoroni Pico location was not specified. Please set PIMORONI_PICO_PATH.") 39 | endif() 40 | endif() 41 | 42 | get_filename_component(PIMORONI_PICO_PATH "${PIMORONI_PICO_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 43 | if (NOT EXISTS ${PIMORONI_PICO_PATH}) 44 | message(FATAL_ERROR "Directory '${PIMORONI_PICO_PATH}' not found") 45 | endif () 46 | 47 | if (NOT EXISTS ${PIMORONI_PICO_PATH}/pimoroni_pico_import.cmake) 48 | message(FATAL_ERROR "Directory '${PIMORONI_PICO_PATH}' does not appear to contain the Pimoroni Pico libraries") 49 | endif () 50 | 51 | message("PIMORONI_PICO_PATH is ${PIMORONI_PICO_PATH}") 52 | 53 | set(PIMORONI_PICO_PATH ${PIMORONI_PICO_PATH} CACHE PATH "Path to the Pimoroni Pico libraries" FORCE) 54 | 55 | include_directories(${PIMORONI_PICO_PATH}) 56 | list(APPEND CMAKE_MODULE_PATH ${PIMORONI_PICO_PATH}) 57 | -------------------------------------------------------------------------------- /firmware/startup_overclock.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/rp2_common/pico_standard_link/crt0.S b/src/rp2_common/pico_standard_link/crt0.S 2 | index 7d29f76..3baa997 100644 3 | --- a/src/rp2_common/pico_standard_link/crt0.S 4 | +++ b/src/rp2_common/pico_standard_link/crt0.S 5 | @@ -11,6 +11,8 @@ 6 | #include "hardware/regs/addressmap.h" 7 | #include "hardware/regs/sio.h" 8 | #include "pico/binary_info/defs.h" 9 | +#include "hardware/regs/resets.h" 10 | +#include "hardware/regs/rosc.h" 11 | 12 | #ifdef NDEBUG 13 | #ifndef COLLAPSE_IRQS 14 | @@ -225,6 +227,12 @@ _reset_handler: 15 | cmp r0, #0 16 | bne hold_non_core0_in_bootrom 17 | 18 | + // Increase ROSC frequency to ~48MHz (range 14.4 - 96) 19 | + // Speeds up memory zero init and preinit phases. 20 | + ldr r0, =(ROSC_BASE + ROSC_DIV_OFFSET) 21 | + ldr r1, =0xaa2 22 | + str r1, [r0] 23 | + 24 | // In a NO_FLASH binary, don't perform .data copy, since it's loaded 25 | // in-place by the SRAM load. Still need to clear .bss 26 | #if !PICO_NO_FLASH 27 | -------------------------------------------------------------------------------- /lib/boot.py: -------------------------------------------------------------------------------- 1 | import cppmem 2 | # Switch C++ memory allocations to use MicroPython's heap 3 | cppmem.set_mode(cppmem.MICROPYTHON) -------------------------------------------------------------------------------- /lib/modes.py: -------------------------------------------------------------------------------- 1 | from picovision import PicoVision, PEN_RGB555, PEN_RGB888 2 | 3 | 4 | def HVGA(): 5 | return PicoVision(PEN_RGB888, 320, 240) 6 | 7 | 8 | def VGA(): 9 | return PicoVision(PEN_RGB555, 640, 480) 10 | 11 | 12 | def Mode7Plus(): 13 | return PicoVision(PEN_RGB555, 720, 400) 14 | 15 | 16 | def PAL(): 17 | return PicoVision(PEN_RGB555, 720, 576) 18 | 19 | 20 | def NTSC(): 21 | return PicoVision(PEN_RGB555, 720, 480) 22 | 23 | 24 | def WVGA(): 25 | return PicoVision(PEN_RGB555, 800, 480) 26 | -------------------------------------------------------------------------------- /lib/picographics.py: -------------------------------------------------------------------------------- 1 | from picovision import * 2 | from picovision import PicoVision as PicoGraphics -------------------------------------------------------------------------------- /lib/pimoroni.py: -------------------------------------------------------------------------------- 1 | import time 2 | from machine import Pin, PWM, ADC 3 | 4 | 5 | BREAKOUT_GARDEN_I2C_PINS = {"sda": 4, "scl": 5} 6 | PICO_EXPLORER_I2C_PINS = {"sda": 20, "scl": 21} 7 | HEADER_I2C_PINS = {"sda": 20, "scl": 21} 8 | 9 | # Motor and encoder directions 10 | NORMAL_DIR = 0x00 11 | REVERSED_DIR = 0x01 12 | 13 | BREAKOUT_GARDEN_SPI_SLOT_FRONT = 0 14 | BREAKOUT_GARDEN_SPI_SLOT_BACK = 1 15 | PICO_EXPLORER_SPI_ONBOARD = 2 16 | 17 | 18 | class Analog: 19 | def __init__(self, pin, amplifier_gain=1, resistor=0, offset=0): 20 | self.gain = amplifier_gain 21 | self.resistor = resistor 22 | self.offset = offset 23 | self.pin = ADC(pin) 24 | 25 | def read_voltage(self): 26 | return max((((self.pin.read_u16() * 3.3) / 65535) + self.offset) / self.gain, 0.0) 27 | 28 | def read_current(self): 29 | if self.resistor > 0: 30 | return self.read_voltage() / self.resistor 31 | else: 32 | return self.read_voltage() 33 | 34 | 35 | class AnalogMux: 36 | def __init__(self, addr0, addr1=None, addr2=None, en=None, muxed_pin=None): 37 | self.addr0_pin = Pin(addr0, Pin.OUT) 38 | self.addr1_pin = Pin(addr1, Pin.OUT) if addr1 is not None else None 39 | self.addr2_pin = Pin(addr2, Pin.OUT) if addr2 is not None else None 40 | self.en_pin = Pin(en, Pin.OUT) if en is not None else None 41 | self.max_address = 0b001 42 | if addr1 is not None: 43 | self.max_address = 0b011 44 | if addr2 is not None: 45 | self.max_address = 0b111 46 | self.pulls = [None] * (self.max_address + 1) 47 | self.muxed_pin = muxed_pin 48 | 49 | def select(self, address): 50 | if address < 0: 51 | raise ValueError("address is less than zero") 52 | elif address > self.max_address: 53 | raise ValueError("address is greater than number of available addresses") 54 | else: 55 | if self.muxed_pin and self.pulls[address] is None: 56 | self.muxed_pin.init(Pin.IN, None) 57 | 58 | self.addr0_pin.value(address & 0b001) 59 | 60 | if self.addr1_pin is not None: 61 | self.addr1_pin.value(address & 0b010) 62 | 63 | if self.addr2_pin is not None: 64 | self.addr2_pin.value(address & 0b100) 65 | 66 | if self.en_pin is not None: 67 | self.en_pin.value(1) 68 | 69 | if self.muxed_pin and self.pulls[address] is not None: 70 | self.muxed_pin.init(Pin.IN, self.pulls[address]) 71 | 72 | def disable(self): 73 | if self.en_pin is not None: 74 | self.en_pin.value(0) 75 | else: 76 | raise RuntimeError("there is no enable pin assigned to this mux") 77 | 78 | def configure_pull(self, address, pull=None): 79 | if address < 0: 80 | raise ValueError("address is less than zero") 81 | elif address > self.max_address: 82 | raise ValueError("address is greater than number of available addresses") 83 | else: 84 | self.pulls[address] = pull 85 | 86 | def read(self): 87 | if self.muxed_pin is not None: 88 | return self.muxed_pin.value() 89 | else: 90 | raise RuntimeError("there is no muxed pin assigned to this mux") 91 | 92 | 93 | class Button: 94 | def __init__(self, button, invert=True, repeat_time=200, hold_time=1000): 95 | self.invert = invert 96 | self.repeat_time = repeat_time 97 | self.hold_time = hold_time 98 | self.pin = Pin(button, pull=Pin.PULL_UP if invert else Pin.PULL_DOWN) 99 | self.last_state = False 100 | self.pressed = False 101 | self.pressed_time = 0 102 | 103 | def read(self): 104 | current_time = time.ticks_ms() 105 | state = self.raw() 106 | changed = state != self.last_state 107 | self.last_state = state 108 | 109 | if changed: 110 | if state: 111 | self.pressed_time = current_time 112 | self.pressed = True 113 | self.last_time = current_time 114 | return True 115 | else: 116 | self.pressed_time = 0 117 | self.pressed = False 118 | self.last_time = 0 119 | 120 | if self.repeat_time == 0: 121 | return False 122 | 123 | if self.pressed: 124 | repeat_rate = self.repeat_time 125 | if self.hold_time > 0 and current_time - self.pressed_time > self.hold_time: 126 | repeat_rate /= 3 127 | if current_time - self.last_time > repeat_rate: 128 | self.last_time = current_time 129 | return True 130 | 131 | return False 132 | 133 | def raw(self): 134 | if self.invert: 135 | return not self.pin.value() 136 | else: 137 | return self.pin.value() 138 | 139 | @property 140 | def is_pressed(self): 141 | return self.raw() 142 | 143 | 144 | class RGBLED: 145 | def __init__(self, r, g, b, invert=True): 146 | self.invert = invert 147 | self.led_r = PWM(Pin(r)) 148 | self.led_r.freq(1000) 149 | self.led_g = PWM(Pin(g)) 150 | self.led_g.freq(1000) 151 | self.led_b = PWM(Pin(b)) 152 | self.led_b.freq(1000) 153 | 154 | def set_rgb(self, r, g, b): 155 | if self.invert: 156 | r = 255 - r 157 | g = 255 - g 158 | b = 255 - b 159 | self.led_r.duty_u16(int((r * 65535) / 255)) 160 | self.led_g.duty_u16(int((g * 65535) / 255)) 161 | self.led_b.duty_u16(int((b * 65535) / 255)) 162 | 163 | 164 | # A simple class for handling Proportional, Integral & Derivative (PID) control calculations 165 | class PID: 166 | def __init__(self, kp, ki, kd, sample_rate): 167 | self.kp = kp 168 | self.ki = ki 169 | self.kd = kd 170 | self.setpoint = 0 171 | self._error_sum = 0 172 | self._last_value = 0 173 | self._sample_rate = sample_rate 174 | 175 | def calculate(self, value, value_change=None): 176 | error = self.setpoint - value 177 | self._error_sum += error * self._sample_rate 178 | if value_change is None: 179 | rate_error = (value - self._last_value) / self._sample_rate 180 | else: 181 | rate_error = value_change 182 | self._last_value = value 183 | 184 | return (error * self.kp) + (self._error_sum * self.ki) - (rate_error * self.kd) 185 | 186 | 187 | class Buzzer: 188 | def __init__(self, pin): 189 | self.pwm = PWM(Pin(pin)) 190 | 191 | def set_tone(self, freq, duty=0.5): 192 | if freq < 50.0: # uh... https://github.com/micropython/micropython/blob/af64c2ddbd758ab6bac0fcca94c66d89046663be/ports/rp2/machine_pwm.c#L105-L119 193 | self.pwm.duty_u16(0) 194 | return False 195 | 196 | self.pwm.freq(freq) 197 | self.pwm.duty_u16(int(65535 * duty)) 198 | return True 199 | 200 | 201 | class ShiftRegister: 202 | def __init__(self, clk, lat, dat): 203 | self.clk = Pin(clk, Pin.OUT) 204 | self.lat = Pin(lat, Pin.OUT) 205 | self.dat = Pin(dat, Pin.IN) 206 | 207 | def __iter__(self): 208 | self.lat.off() 209 | self.lat.on() 210 | for _ in range(8): 211 | yield self.dat.value() 212 | self.clk.on() 213 | self.clk.off() 214 | 215 | def __getitem__(self, k): 216 | return list(self)[k] 217 | 218 | def read(self): 219 | out = 0 220 | for bit in self: 221 | out <<= 1 222 | out += bit 223 | return out 224 | 225 | def is_set(self, mask): 226 | return self.read() & mask == mask 227 | 228 | 229 | # A basic wrapper for PWM with regular on/off and toggle functions from Pin 230 | # Intended to be used for driving LEDs with brightness control & compatibility with Pin 231 | class PWMLED: 232 | def __init__(self, pin, invert=False): 233 | self._invert = invert 234 | self._led = PWM(Pin(pin, Pin.OUT)) 235 | self._led.freq(1000) 236 | self._brightness = 0 237 | self.brightness(0) 238 | 239 | def brightness(self, brightness): 240 | brightness = min(1.0, max(0.0, brightness)) 241 | self._brightness = brightness 242 | if self._invert: 243 | brightness = 1.0 - brightness 244 | self._led.duty_u16(int(65535 * brightness)) 245 | 246 | def on(self): 247 | self.brightness(1) 248 | 249 | def off(self): 250 | self.brightness(0) 251 | 252 | def toggle(self): 253 | self.brightness(1 - self._brightness) -------------------------------------------------------------------------------- /libraries/pico_graphics/pico_graphics_dv.hpp: -------------------------------------------------------------------------------- 1 | #include "pico_graphics.hpp" 2 | #include "dv_display.hpp" 3 | 4 | namespace pimoroni { 5 | enum BlendMode { 6 | TARGET = 0, 7 | FIXED = 1, 8 | }; 9 | 10 | class PicoGraphicsDV : public PicoGraphics { 11 | public: 12 | DVDisplay &driver; 13 | BlendMode blend_mode = BlendMode::TARGET; 14 | 15 | void set_blend_mode(BlendMode mode) { 16 | blend_mode = mode; 17 | } 18 | 19 | virtual void set_depth(uint8_t new_depth) {} 20 | virtual void set_bg(uint c) {}; 21 | 22 | PicoGraphicsDV(uint16_t width, uint16_t height, DVDisplay &dv_display) 23 | : PicoGraphics(width, height, nullptr), 24 | driver(dv_display) 25 | { 26 | this->pen_type = PEN_DV_RGB555; 27 | } 28 | }; 29 | 30 | class PicoGraphics_PenDV_RGB555 : public PicoGraphicsDV { 31 | public: 32 | RGB555 color; 33 | RGB555 background; 34 | uint16_t depth = 0; 35 | 36 | PicoGraphics_PenDV_RGB555(uint16_t width, uint16_t height, DVDisplay &dv_display); 37 | void set_pen(uint c) override; 38 | void set_bg(uint c) override; 39 | void set_depth(uint8_t new_depth) override; 40 | void set_pen(uint8_t r, uint8_t g, uint8_t b) override; 41 | int create_pen(uint8_t r, uint8_t g, uint8_t b) override; 42 | int create_pen_hsv(float h, float s, float v) override; 43 | void set_pixel(const Point &p) override; 44 | void set_pixel_span(const Point &p, uint l) override; 45 | void set_pixel_alpha(const Point &p, const uint8_t a) override; 46 | 47 | bool supports_alpha_blend() override {return true;} 48 | 49 | bool render_pico_vector_tile(const Rect &bounds, uint8_t* alpha_data, uint32_t stride, uint8_t alpha_type) override; 50 | 51 | static size_t buffer_size(uint w, uint h) { 52 | return w * h * sizeof(RGB555); 53 | } 54 | }; 55 | 56 | class PicoGraphics_PenDV_RGB888 : public PicoGraphicsDV { 57 | public: 58 | RGB888 color; 59 | RGB888 background; 60 | 61 | PicoGraphics_PenDV_RGB888(uint16_t width, uint16_t height, DVDisplay &dv_display); 62 | void set_pen(uint c) override; 63 | void set_bg(uint c) override; 64 | void set_pen(uint8_t r, uint8_t g, uint8_t b) override; 65 | int create_pen(uint8_t r, uint8_t g, uint8_t b) override; 66 | int create_pen_hsv(float h, float s, float v) override; 67 | void set_pixel(const Point &p) override; 68 | void set_pixel_span(const Point &p, uint l) override; 69 | void set_pixel_alpha(const Point &p, const uint8_t a) override; 70 | 71 | bool supports_alpha_blend() override {return true;} 72 | 73 | bool render_pico_vector_tile(const Rect &bounds, uint8_t* alpha_data, uint32_t stride, uint8_t alpha_type) override; 74 | 75 | static size_t buffer_size(uint w, uint h) { 76 | return w * h * 3; 77 | } 78 | }; 79 | 80 | class PicoGraphics_PenDV_P5 : public PicoGraphicsDV { 81 | public: 82 | static const uint16_t palette_size = 32; 83 | uint8_t color; 84 | uint8_t depth = 0; 85 | bool used[2][palette_size]; 86 | 87 | std::array, 512> candidate_cache; 88 | std::array candidates; 89 | bool cache_built = false; 90 | uint8_t cached_palette = 0; 91 | 92 | PicoGraphics_PenDV_P5(uint16_t width, uint16_t height, DVDisplay &dv_display); 93 | void set_pen(uint c) override; 94 | void set_pen(uint8_t r, uint8_t g, uint8_t b) override; 95 | void set_depth(uint8_t new_depth) override; 96 | int update_pen(uint8_t i, uint8_t r, uint8_t g, uint8_t b) override; 97 | int create_pen(uint8_t r, uint8_t g, uint8_t b) override; 98 | int create_pen_hsv(float h, float s, float v) override; 99 | int reset_pen(uint8_t i) override; 100 | 101 | int get_palette_size() override {return 0;}; 102 | RGB* get_palette() override {return nullptr;}; 103 | 104 | void set_pixel(const Point &p) override; 105 | void set_pixel_span(const Point &p, uint l) override; 106 | void get_dither_candidates(const RGB &col, const RGB *palette, size_t len, std::array &candidates); 107 | void set_pixel_dither(const Point &p, const RGB &c) override; 108 | 109 | static size_t buffer_size(uint w, uint h) { 110 | return w * h; 111 | } 112 | }; 113 | } -------------------------------------------------------------------------------- /libraries/pico_graphics/pico_graphics_pen_dv_p5.cpp: -------------------------------------------------------------------------------- 1 | #include "pico_graphics_dv.hpp" 2 | 3 | namespace pimoroni { 4 | 5 | inline constexpr uint32_t RGB_to_RGB888(const uint8_t r, const uint8_t g, const uint8_t b) { 6 | return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; 7 | } 8 | 9 | PicoGraphics_PenDV_P5::PicoGraphics_PenDV_P5(uint16_t width, uint16_t height, DVDisplay &dv_display) 10 | : PicoGraphicsDV(width, height, dv_display) 11 | { 12 | this->pen_type = PEN_DV_P5; 13 | for(auto i = 0u; i < palette_size; i++) { 14 | driver.set_palette_colour(i, RGB_to_RGB888(i, i, i) << 3, 0); 15 | used[0][i] = false; 16 | int n = palette_size - i; 17 | driver.set_palette_colour(i, RGB_to_RGB888(n, n, n) << 3, 1); 18 | used[1][i] = false; 19 | } 20 | cache_built = false; 21 | } 22 | void PicoGraphics_PenDV_P5::set_pen(uint c) { 23 | color = c & 0x1f; 24 | } 25 | void PicoGraphics_PenDV_P5::set_depth(uint8_t new_depth) { 26 | depth = new_depth > 0 ? 1 : 0; 27 | } 28 | void PicoGraphics_PenDV_P5::set_pen(uint8_t r, uint8_t g, uint8_t b) { 29 | uint8_t current_palette = driver.get_local_palette_index(); 30 | RGB888 *driver_palette = driver.get_palette(current_palette); 31 | RGB palette[palette_size]; 32 | for(auto i = 0u; i < palette_size; i++) { 33 | palette[i] = RGB((uint)driver_palette[i]); 34 | } 35 | 36 | int pen = RGB(r, g, b).closest(palette, palette_size); 37 | if(pen != -1) color = pen; 38 | } 39 | int PicoGraphics_PenDV_P5::update_pen(uint8_t i, uint8_t r, uint8_t g, uint8_t b) { 40 | uint8_t current_palette = driver.get_local_palette_index(); 41 | i &= 0x1f; 42 | used[current_palette][i] = true; 43 | cache_built = false; 44 | driver.set_palette_colour(i, RGB_to_RGB888(r, g, b)); 45 | return i; 46 | } 47 | int PicoGraphics_PenDV_P5::create_pen(uint8_t r, uint8_t g, uint8_t b) { 48 | uint8_t current_palette = driver.get_local_palette_index(); 49 | // Create a colour and place it in the palette if there's space 50 | for(auto i = 0u; i < palette_size; i++) { 51 | if(!used[current_palette][i]) { 52 | used[current_palette][i] = true; 53 | cache_built = false; 54 | driver.set_palette_colour(i, RGB_to_RGB888(r, g, b)); 55 | return i; 56 | } 57 | } 58 | return -1; 59 | } 60 | int PicoGraphics_PenDV_P5::create_pen_hsv(float h, float s, float v) { 61 | RGB p = RGB::from_hsv(h, s, v); 62 | return create_pen(p.r, p.g, p.b); 63 | } 64 | int PicoGraphics_PenDV_P5::reset_pen(uint8_t i) { 65 | uint8_t current_palette = driver.get_local_palette_index(); 66 | driver.set_palette_colour(i, 0); 67 | used[current_palette][i] = false; 68 | cache_built = false; 69 | return i; 70 | } 71 | void PicoGraphics_PenDV_P5::set_pixel(const Point &p) { 72 | driver.write_palette_pixel(p, (color << 2) | depth); 73 | } 74 | 75 | void PicoGraphics_PenDV_P5::set_pixel_span(const Point &p, uint l) { 76 | driver.write_palette_pixel_span(p, l, (color << 2) | depth); 77 | } 78 | 79 | void PicoGraphics_PenDV_P5::get_dither_candidates(const RGB &col, const RGB *palette, size_t len, std::array &candidates) { 80 | RGB error; 81 | for(size_t i = 0; i < candidates.size(); i++) { 82 | candidates[i] = (col + error).closest(palette, len); 83 | error += (col - palette[candidates[i]]); 84 | } 85 | 86 | // sort by a rough approximation of luminance, this ensures that neighbouring 87 | // pixels in the dither matrix are at extreme opposites of luminence 88 | // giving a more balanced output 89 | std::sort(candidates.begin(), candidates.end(), [palette](int a, int b) { 90 | return palette[a].luminance() > palette[b].luminance(); 91 | }); 92 | } 93 | 94 | void PicoGraphics_PenDV_P5::set_pixel_dither(const Point &p, const RGB &c) { 95 | if(!bounds.contains(p)) return; 96 | 97 | uint8_t current_palette = driver.get_local_palette_index(); 98 | 99 | if(!cache_built || current_palette != cached_palette) { 100 | RGB888 *driver_palette = driver.get_palette(current_palette); 101 | RGB palette[palette_size]; 102 | for(auto i = 0u; i < palette_size; i++) { 103 | palette[i] = RGB((uint)driver_palette[i]); 104 | } 105 | 106 | for(uint i = 0; i < 512; i++) { 107 | RGB cache_col((i & 0x1C0) >> 1, (i & 0x38) << 2, (i & 0x7) << 5); 108 | get_dither_candidates(cache_col, palette, palette_size, candidate_cache[i]); 109 | } 110 | cache_built = true; 111 | cached_palette = current_palette; 112 | } 113 | 114 | uint cache_key = ((c.r & 0xE0) << 1) | ((c.g & 0xE0) >> 2) | ((c.b & 0xE0) >> 5); 115 | 116 | // find the pattern coordinate offset 117 | uint pattern_index = (p.x & 0b11) | ((p.y & 0b11) << 2); 118 | 119 | // set the pixel 120 | color = candidate_cache[cache_key][dither16_pattern[pattern_index]]; 121 | set_pixel(p); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /libraries/pico_graphics/pico_graphics_pen_dv_rgb555.cpp: -------------------------------------------------------------------------------- 1 | #include "pico_graphics_dv.hpp" 2 | 3 | #ifndef MICROPY_BUILD_TYPE 4 | #define mp_printf(_, ...) printf(__VA_ARGS__); 5 | #else 6 | extern "C" { 7 | #include "py/runtime.h" 8 | } 9 | #endif 10 | 11 | namespace pimoroni { 12 | PicoGraphics_PenDV_RGB555::PicoGraphics_PenDV_RGB555(uint16_t width, uint16_t height, DVDisplay &dv_display) 13 | : PicoGraphicsDV(width, height, dv_display) 14 | { 15 | this->pen_type = PEN_DV_RGB555; 16 | } 17 | void PicoGraphics_PenDV_RGB555::set_pen(uint c) { 18 | color = c; 19 | } 20 | void PicoGraphics_PenDV_RGB555::set_bg(uint c) { 21 | background = c; 22 | } 23 | void PicoGraphics_PenDV_RGB555::set_depth(uint8_t new_depth) { 24 | depth = new_depth > 0 ? 0x8000 : 0; 25 | } 26 | void PicoGraphics_PenDV_RGB555::set_pen(uint8_t r, uint8_t g, uint8_t b) { 27 | RGB src_color{r, g, b}; 28 | color = src_color.to_rgb555(); 29 | } 30 | int PicoGraphics_PenDV_RGB555::create_pen(uint8_t r, uint8_t g, uint8_t b) { 31 | return RGB(r, g, b).to_rgb555(); 32 | } 33 | int PicoGraphics_PenDV_RGB555::create_pen_hsv(float h, float s, float v) { 34 | return RGB::from_hsv(h, s, v).to_rgb555(); 35 | } 36 | void PicoGraphics_PenDV_RGB555::set_pixel(const Point &p) { 37 | driver.write_pixel(p, (uint16_t)(color | depth)); 38 | } 39 | void PicoGraphics_PenDV_RGB555::set_pixel_span(const Point &p, uint l) { 40 | driver.write_pixel_span(p, l, (uint16_t)(color | depth)); 41 | } 42 | void PicoGraphics_PenDV_RGB555::set_pixel_alpha(const Point &p, const uint8_t a) { 43 | uint16_t src = background; 44 | if (blend_mode == BlendMode::TARGET) { 45 | driver.read_pixel_span(p, 1, &src); 46 | } 47 | 48 | uint8_t src_r = (src >> 7) & 0b11111000; 49 | uint8_t src_g = (src >> 2) & 0b11111000; 50 | uint8_t src_b = (src << 3) & 0b11111000; 51 | 52 | uint8_t dst_r = (color >> 7) & 0b11111000; 53 | uint8_t dst_g = (color >> 2) & 0b11111000; 54 | uint8_t dst_b = (color << 3) & 0b11111000; 55 | 56 | RGB555 blended = RGB(src_r, src_g, src_b).blend(RGB(dst_r, dst_g, dst_b), a).to_rgb555(); 57 | 58 | driver.write_pixel(p, blended); 59 | }; 60 | 61 | bool PicoGraphics_PenDV_RGB555::render_pico_vector_tile(const Rect &src_bounds, uint8_t* alpha_data, uint32_t stride, uint8_t alpha_type) { 62 | 63 | // Alpha configuration 64 | const uint8_t alpha_map4[4] = { 0, 8, 12, 15 }; 65 | const uint8_t alpha_map16[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; 66 | const uint8_t* alpha_map = (alpha_type == 2) ? alpha_map16 : alpha_map4; 67 | const uint8_t alpha_max = (alpha_type == 2) ? 16 : 4; 68 | 69 | const Rect bounds = src_bounds.intersection(clip); 70 | if (bounds.w <= 0 || bounds.h <= 0) return true; 71 | alpha_data += bounds.x - src_bounds.x + stride * (bounds.y - src_bounds.y); 72 | 73 | // Double buffering - in the main loop one buffer is being written to the PSRAM or read into 74 | // in the background while the other is processed 75 | uint16_t buf0[128] alignas(4); 76 | uint16_t buf1[128] alignas(4); 77 | if (bounds.w > 128 || bounds.h > 128) { 78 | return false; 79 | } 80 | uint16_t* rbuf = buf0; 81 | uint16_t* wbuf = buf1; 82 | 83 | // Start reading the first row 84 | uint32_t address = driver.point_to_address({bounds.x, bounds.y}); 85 | uint32_t row_len_in_words = (bounds.w + 1) >> 1; 86 | if (blend_mode == BlendMode::TARGET) { 87 | driver.raw_read_async(address, (uint32_t*)rbuf, row_len_in_words); 88 | } 89 | 90 | const uint32_t colour_expanded = (uint32_t(color & 0x7C00) << 10) | (uint32_t(color & 0x3E0) << 5) | (color & 0x1F); 91 | const uint32_t background_expanded = (uint32_t(background & 0x7C00) << 10) | (uint32_t(background & 0x3E0) << 5) | (background & 0x1F); 92 | uint32_t address_stride = driver.frame_row_stride(); 93 | 94 | if (blend_mode == BlendMode::TARGET) { 95 | driver.raw_wait_for_finish_blocking(); 96 | } 97 | 98 | for (int32_t y = 0; y < bounds.h; ++y) { 99 | std::swap(wbuf, rbuf); 100 | 101 | uint32_t prev_address = address; 102 | 103 | address += address_stride; 104 | 105 | // Process this row 106 | uint8_t* alpha_ptr = &alpha_data[stride * y]; 107 | if (blend_mode == BlendMode::TARGET) { 108 | for (int32_t x = 0; x < bounds.w; ++x) { 109 | uint8_t alpha = *alpha_ptr++; 110 | if (alpha >= alpha_max) { 111 | wbuf[x] = (uint16_t)(color | depth); 112 | } else if (alpha > 0) { 113 | alpha = alpha_map[alpha]; 114 | 115 | uint16_t src = wbuf[x]; 116 | 117 | // Who said Cortex-M0 can't do SIMD? 118 | const uint32_t src_expanded = (uint32_t(src & 0x7C00) << 10) | (uint32_t(src & 0x3E0) << 5) | (src & 0x1F); 119 | const uint32_t blended = src_expanded * (16 - alpha) + colour_expanded * alpha; 120 | wbuf[x] = ((blended >> 14) & 0x7C00) | ((blended >> 9) & 0x3E0) | ((blended >> 4) & 0x1F) | (alpha > 7 ? depth : (src & 0x8000)); 121 | } 122 | 123 | // Halfway through processing this row switch from writing previous row to reading the next 124 | if (x+1 == (int32_t)row_len_in_words && y+1 < bounds.h) { 125 | driver.raw_read_async(address, (uint32_t*)rbuf, row_len_in_words); 126 | } 127 | } 128 | } else { 129 | for (int32_t x = 0; x < bounds.w; ++x) { 130 | uint8_t alpha = *alpha_ptr++; 131 | if (alpha >= alpha_max) { 132 | wbuf[x] = (uint16_t)(color | depth); 133 | } else if (alpha > 0) { 134 | alpha = alpha_map[alpha]; 135 | 136 | const uint32_t blended = background_expanded * (16 - alpha) + colour_expanded * alpha; 137 | wbuf[x] = ((blended >> 14) & 0x7C00) | ((blended >> 9) & 0x3E0) | ((blended >> 4) & 0x1F) | (alpha > 7 ? depth : 0); 138 | } else { 139 | wbuf[x] = background; 140 | } 141 | } 142 | } 143 | 144 | // Write the row out while we loop on to the next 145 | driver.raw_write_async_bytes(prev_address, (uint32_t*)wbuf, bounds.w << 1); 146 | } 147 | 148 | // Wait for the last write to finish as we are writing from stack 149 | driver.raw_wait_for_finish_blocking(); 150 | 151 | return true; 152 | } 153 | } -------------------------------------------------------------------------------- /libraries/pico_graphics/pico_graphics_pen_dv_rgb888.cpp: -------------------------------------------------------------------------------- 1 | #include "pico_graphics_dv.hpp" 2 | 3 | namespace pimoroni { 4 | PicoGraphics_PenDV_RGB888::PicoGraphics_PenDV_RGB888(uint16_t width, uint16_t height, DVDisplay &dv_display) 5 | : PicoGraphicsDV(width, height, dv_display) 6 | { 7 | this->pen_type = PEN_DV_RGB888; 8 | } 9 | void PicoGraphics_PenDV_RGB888::set_pen(uint c) { 10 | color = c; 11 | } 12 | void PicoGraphics_PenDV_RGB888::set_bg(uint c) { 13 | background = c; 14 | } 15 | void PicoGraphics_PenDV_RGB888::set_pen(uint8_t r, uint8_t g, uint8_t b) { 16 | RGB src_color{r, g, b}; 17 | color = src_color.to_rgb888(); 18 | } 19 | int PicoGraphics_PenDV_RGB888::create_pen(uint8_t r, uint8_t g, uint8_t b) { 20 | return RGB(r, g, b).to_rgb888(); 21 | } 22 | int PicoGraphics_PenDV_RGB888::create_pen_hsv(float h, float s, float v) { 23 | return RGB::from_hsv(h, s, v).to_rgb888(); 24 | } 25 | void PicoGraphics_PenDV_RGB888::set_pixel(const Point &p) { 26 | driver.write_pixel(p, color); 27 | } 28 | void PicoGraphics_PenDV_RGB888::set_pixel_span(const Point &p, uint l) { 29 | driver.write_pixel_span(p, l, color); 30 | } 31 | void PicoGraphics_PenDV_RGB888::set_pixel_alpha(const Point &p, const uint8_t a) { 32 | uint32_t src = background; 33 | if (blend_mode == BlendMode::TARGET) { 34 | driver.read_24bpp_pixel_span(p, 1, (uint8_t*)&src); 35 | } 36 | 37 | // TODO: "uint" should be RGB888 but there's curently a mismatch between "uint32_t" and "unsigned int" 38 | RGB888 blended = RGB((uint)src).blend(RGB((uint)color), a).to_rgb888(); 39 | 40 | driver.write_pixel(p, blended); 41 | }; 42 | 43 | bool PicoGraphics_PenDV_RGB888::render_pico_vector_tile(const Rect &src_bounds, uint8_t* alpha_data, uint32_t stride, uint8_t alpha_type) { 44 | 45 | // Alpha configuration 46 | const uint8_t alpha_map4[4] = { 0, 8, 12, 15 }; 47 | const uint8_t alpha_map16[16] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; 48 | const uint8_t* alpha_map = (alpha_type == 2) ? alpha_map16 : alpha_map4; 49 | const uint8_t alpha_max = (alpha_type == 2) ? 16 : 4; 50 | 51 | const Rect bounds = src_bounds.intersection(clip); 52 | if (bounds.w <= 0 || bounds.h <= 0) return true; 53 | alpha_data += bounds.x - src_bounds.x + stride * (bounds.y - src_bounds.y); 54 | 55 | // Double buffering - in the main loop one buffer is being written to the PSRAM or read into 56 | // in the background while the other is processed 57 | uint8_t buf0[128*3] alignas(4); 58 | uint8_t buf1[128*3] alignas(4); 59 | if (bounds.w > 128 || bounds.h > 128) { 60 | return false; 61 | } 62 | uint8_t* rbuf = buf0; 63 | uint8_t* wbuf = buf1; 64 | 65 | // Start reading the first row 66 | uint32_t address = driver.point_to_address({bounds.x, bounds.y}); 67 | uint32_t row_len_in_words = (bounds.w * 3 + 3) >> 2; 68 | if (blend_mode == BlendMode::TARGET) { 69 | driver.raw_read_async(address, (uint32_t*)rbuf, row_len_in_words); 70 | } 71 | 72 | const uint8_t colour_expanded[3] = { (uint8_t)color, (uint8_t)(color >> 8), (uint8_t)(color >> 16) }; 73 | const uint8_t background_expanded[3] = { (uint8_t)background, (uint8_t)(background >> 8), (uint8_t)(background >> 16) }; 74 | uint32_t address_stride = driver.frame_row_stride(); 75 | 76 | if (blend_mode == BlendMode::TARGET) { 77 | driver.raw_wait_for_finish_blocking(); 78 | } 79 | 80 | for (int32_t y = 0; y < bounds.h; ++y) { 81 | std::swap(wbuf, rbuf); 82 | 83 | uint32_t prev_address = address; 84 | 85 | address += address_stride; 86 | 87 | // Process this row 88 | uint8_t* alpha_ptr = &alpha_data[stride * y]; 89 | if (blend_mode == BlendMode::TARGET) { 90 | for (int32_t x = 0; x < bounds.w; ++x) { 91 | uint8_t alpha = *alpha_ptr++; 92 | if (alpha >= alpha_max) { 93 | wbuf[x*3] = colour_expanded[0]; 94 | wbuf[x*3+1] = colour_expanded[1]; 95 | wbuf[x*3+2] = colour_expanded[2]; 96 | } else if (alpha > 0) { 97 | alpha = alpha_map[alpha]; 98 | for (int32_t i = 0; i < 3; ++i) { 99 | uint8_t src = wbuf[x*3+i]; 100 | wbuf[x*3+i] = (src * (16 - alpha) + colour_expanded[i] * alpha) >> 4; 101 | } 102 | } 103 | 104 | // Halfway through processing this row switch from writing previous row to reading the next 105 | if (x == bounds.w >> 1 && y+1 < bounds.h) { 106 | driver.raw_read_async(address, (uint32_t*)rbuf, row_len_in_words); 107 | } 108 | } 109 | } else { 110 | for (int32_t x = 0; x < bounds.w; ++x) { 111 | uint8_t alpha = *alpha_ptr++; 112 | if (alpha >= alpha_max) { 113 | wbuf[x*3] = colour_expanded[0]; 114 | wbuf[x*3+1] = colour_expanded[1]; 115 | wbuf[x*3+2] = colour_expanded[2]; 116 | } else if (alpha > 0) { 117 | alpha = alpha_map[alpha]; 118 | for (int32_t i = 0; i < 3; ++i) { 119 | wbuf[x*3+i] = (background_expanded[i] * (16 - alpha) + colour_expanded[i] * alpha) >> 4; 120 | } 121 | } else { 122 | wbuf[x*3] = background_expanded[0]; 123 | wbuf[x*3+1] = background_expanded[1]; 124 | wbuf[x*3+2] = background_expanded[2]; 125 | } 126 | } 127 | } 128 | 129 | // Write the row out while we loop on to the next 130 | driver.raw_write_async_bytes(prev_address, (uint32_t*)wbuf, bounds.w * 3); 131 | } 132 | 133 | // Wait for the last write to finish as we are writing from stack 134 | driver.raw_wait_for_finish_blocking(); 135 | 136 | return true; 137 | } 138 | } -------------------------------------------------------------------------------- /libraries/pico_synth_i2s/pico_synth_i2s.cmake: -------------------------------------------------------------------------------- 1 | add_library(pico_synth_i2s INTERFACE) 2 | 3 | pico_generate_pio_header(pico_synth_i2s ${CMAKE_CURRENT_LIST_DIR}/pico_synth_i2s.pio) 4 | 5 | 6 | target_sources(pico_synth_i2s INTERFACE 7 | ${CMAKE_CURRENT_LIST_DIR}/pico_synth_i2s.cpp 8 | ${CMAKE_CURRENT_LIST_DIR}/../pico_synth/pico_synth.cpp 9 | ) 10 | 11 | target_include_directories(pico_synth_i2s INTERFACE ${CMAKE_CURRENT_LIST_DIR}) 12 | 13 | # Pull in pico libraries that we need 14 | target_link_libraries(pico_synth_i2s INTERFACE pico_stdlib hardware_pio hardware_dma) -------------------------------------------------------------------------------- /libraries/pico_synth_i2s/pico_synth_i2s.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "hardware/dma.h" 4 | #include "hardware/irq.h" 5 | #include "hardware/clocks.h" 6 | 7 | 8 | #ifndef NO_QSTR 9 | #include "pico_synth_i2s.pio.h" 10 | #endif 11 | 12 | #include "pico_synth_i2s.hpp" 13 | 14 | 15 | namespace pimoroni { 16 | 17 | PicoSynth_I2S* PicoSynth_I2S::picosynth = nullptr; 18 | 19 | // once the dma transfer of the scanline is complete we move to the 20 | // next scanline (or quit if we're finished) 21 | void __isr PicoSynth_I2S::dma_complete() { 22 | if(picosynth != nullptr && dma_channel_get_irq0_status(picosynth->audio_dma_channel)) { 23 | picosynth->next_audio_sequence(); 24 | } 25 | } 26 | 27 | PicoSynth_I2S::~PicoSynth_I2S() { 28 | if(picosynth == this) { 29 | partial_teardown(); 30 | 31 | dma_channel_unclaim(picosynth->audio_dma_channel); // This works now the teardown behaves correctly 32 | pio_sm_unclaim(picosynth->audio_pio, picosynth->audio_sm); 33 | pio_remove_program(picosynth->audio_pio, &audio_i2s_program, picosynth->audio_sm_offset); 34 | irq_set_enabled(DMA_IRQ_0, false); 35 | irq_remove_handler(DMA_IRQ_0, dma_complete); 36 | 37 | picosynth = nullptr; 38 | } 39 | } 40 | 41 | void PicoSynth_I2S::partial_teardown() { 42 | // Stop the audio SM 43 | pio_sm_set_enabled(picosynth->audio_pio, picosynth->audio_sm, false); 44 | 45 | // Reset the I2S pins to avoid popping when audio is suddenly stopped 46 | const uint pins_to_clear = 1 << picosynth->i2s_data | 1 << picosynth->i2s_bclk | 1 << picosynth->i2s_lrclk; 47 | pio_sm_set_pins_with_mask(picosynth->audio_pio, picosynth->audio_sm, 0, pins_to_clear); 48 | 49 | // Abort any in-progress DMA transfer 50 | dma_safe_abort(picosynth->audio_dma_channel); 51 | } 52 | 53 | void PicoSynth_I2S::init() { 54 | 55 | if(picosynth != nullptr) { 56 | // Tear down the old PicoSynth instance's hardware resources 57 | partial_teardown(); 58 | } 59 | 60 | // setup audio pio program 61 | if(picosynth == nullptr) { 62 | audio_sm_offset = pio_add_program(audio_pio, &audio_i2s_program); 63 | } 64 | 65 | pio_gpio_init(audio_pio, i2s_data); 66 | pio_gpio_init(audio_pio, i2s_bclk); 67 | pio_gpio_init(audio_pio, i2s_lrclk); 68 | 69 | audio_i2s_program_init(audio_pio, audio_sm, audio_sm_offset, i2s_data, i2s_bclk); 70 | uint32_t system_clock_frequency = clock_get_hz(clk_sys); 71 | uint32_t divider = system_clock_frequency * 4 / SYSTEM_FREQ; // avoid arithmetic overflow 72 | pio_sm_set_clkdiv_int_frac(audio_pio, audio_sm, divider >> 8u, divider & 0xffu); 73 | 74 | audio_dma_channel = dma_claim_unused_channel(true); 75 | 76 | dma_channel_config audio_config = dma_channel_get_default_config(audio_dma_channel); 77 | channel_config_set_transfer_data_size(&audio_config, DMA_SIZE_16); 78 | //channel_config_set_bswap(&audio_config, false); // byte swap to reverse little endian 79 | channel_config_set_dreq(&audio_config, pio_get_dreq(audio_pio, audio_sm, true)); 80 | dma_channel_configure(audio_dma_channel, &audio_config, &audio_pio->txf[audio_sm], NULL, 0, false); 81 | 82 | dma_channel_set_irq0_enabled(audio_dma_channel, true); 83 | 84 | if(picosynth == nullptr) { 85 | irq_add_shared_handler(DMA_IRQ_0, dma_complete, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); 86 | irq_set_enabled(DMA_IRQ_0, true); 87 | } 88 | 89 | picosynth = this; 90 | } 91 | 92 | void PicoSynth_I2S::dma_safe_abort(uint channel) { 93 | // Tear down the DMA channel. 94 | // This is copied from: https://github.com/raspberrypi/pico-sdk/pull/744/commits/5e0e8004dd790f0155426e6689a66e08a83cd9fc 95 | uint32_t irq0_save = dma_hw->inte0 & (1u << channel); 96 | hw_clear_bits(&dma_hw->inte0, irq0_save); 97 | 98 | dma_hw->abort = 1u << channel; 99 | 100 | // To fence off on in-flight transfers, the BUSY bit should be polled 101 | // rather than the ABORT bit, because the ABORT bit can clear prematurely. 102 | while (dma_hw->ch[channel].ctrl_trig & DMA_CH0_CTRL_TRIG_BUSY_BITS) tight_loop_contents(); 103 | 104 | // Clear the interrupt (if any) and restore the interrupt masks. 105 | dma_hw->ints0 = 1u << channel; 106 | hw_set_bits(&dma_hw->inte0, irq0_save); 107 | } 108 | 109 | void PicoSynth_I2S::play_sample(uint8_t *data, uint32_t length) { 110 | stop_playing(); 111 | 112 | if(picosynth == this) { 113 | // Restart the audio SM and start a new DMA transfer 114 | pio_sm_set_enabled(audio_pio, audio_sm, true); 115 | dma_channel_transfer_from_buffer_now(audio_dma_channel, data, length / 2); 116 | play_mode = PLAYING_BUFFER; 117 | } 118 | } 119 | 120 | void PicoSynth_I2S::play_synth() { 121 | if(play_mode != PLAYING_SYNTH) { 122 | stop_playing(); 123 | } 124 | 125 | if(picosynth == this && play_mode == NOT_PLAYING) { 126 | // Nothing is playing, so we can set up the first buffer straight away 127 | current_buffer = 0; 128 | 129 | populate_next_synth(); 130 | 131 | // Restart the audio SM and start a new DMA transfer 132 | pio_sm_set_enabled(audio_pio, audio_sm, true); 133 | 134 | play_mode = PLAYING_SYNTH; 135 | 136 | next_audio_sequence(); 137 | } 138 | } 139 | 140 | void PicoSynth_I2S::next_audio_sequence() { 141 | // Clear any interrupt request caused by our channel 142 | //dma_channel_acknowledge_irq0(audio_dma_channel); 143 | // NOTE Temporary replacement of the above until this reaches pico-sdk main: 144 | // https://github.com/raspberrypi/pico-sdk/issues/974 145 | dma_hw->ints0 = 1u << audio_dma_channel; 146 | 147 | if(play_mode == PLAYING_SYNTH) { 148 | 149 | dma_channel_transfer_from_buffer_now(audio_dma_channel, tone_buffers[current_buffer], TONE_BUFFER_SIZE); 150 | current_buffer = (current_buffer + 1) % NUM_TONE_BUFFERS; 151 | 152 | populate_next_synth(); 153 | } 154 | else { 155 | play_mode = NOT_PLAYING; 156 | } 157 | } 158 | 159 | void PicoSynth_I2S::populate_next_synth() { 160 | int16_t *samples = tone_buffers[current_buffer]; 161 | for(uint i = 0; i < TONE_BUFFER_SIZE; i++) { 162 | samples[i] = synth.get_audio_frame(); 163 | } 164 | } 165 | 166 | void PicoSynth_I2S::stop_playing() { 167 | if(picosynth == this) { 168 | // Stop the audio SM 169 | pio_sm_set_enabled(audio_pio, audio_sm, false); 170 | 171 | // Reset the I2S pins to avoid popping when audio is suddenly stopped 172 | const uint pins_to_clear = 1 << i2s_data | 1 << i2s_bclk | 1 << i2s_lrclk; 173 | pio_sm_set_pins_with_mask(audio_pio, audio_sm, 0, pins_to_clear); 174 | 175 | // Abort any in-progress DMA transfer 176 | dma_safe_abort(audio_dma_channel); 177 | 178 | play_mode = NOT_PLAYING; 179 | } 180 | } 181 | 182 | AudioChannel& PicoSynth_I2S::synth_channel(uint channel) { 183 | assert(channel < PicoSynth::CHANNEL_COUNT); 184 | return synth.channels[channel]; 185 | } 186 | 187 | void PicoSynth_I2S::set_volume(float value) { 188 | value = value < 0.0f ? 0.0f : value; 189 | value = value > 1.0f ? 1.0f : value; 190 | this->volume = floor(value * 255.0f); 191 | this->synth.volume = this->volume * 255.0f; 192 | } 193 | 194 | float PicoSynth_I2S::get_volume() { 195 | return this->volume / 255.0f; 196 | } 197 | 198 | void PicoSynth_I2S::adjust_volume(float delta) { 199 | this->set_volume(this->get_volume() + delta); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /libraries/pico_synth_i2s/pico_synth_i2s.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "hardware/pio.h" 4 | #include "common/pimoroni_common.hpp" 5 | #include "../pico_synth/pico_synth.hpp" 6 | 7 | namespace pimoroni { 8 | 9 | class PicoSynth_I2S { 10 | public: 11 | uint i2s_data = 9; 12 | uint i2s_bclk = 10; 13 | uint i2s_lrclk = 11; 14 | 15 | PIO audio_pio; 16 | uint audio_sm; 17 | uint audio_sm_offset = 0; 18 | uint audio_dma_channel; 19 | 20 | private: 21 | static const uint SYSTEM_FREQ = 22050; 22 | 23 | uint16_t volume = 127; 24 | 25 | static void dma_complete(); 26 | 27 | static const uint NUM_TONE_BUFFERS = 2; 28 | static const uint TONE_BUFFER_SIZE = 4; 29 | int16_t tone_buffers[NUM_TONE_BUFFERS][TONE_BUFFER_SIZE] = {0}; 30 | uint current_buffer = 0; 31 | 32 | PicoSynth synth; 33 | 34 | enum PlayMode { 35 | PLAYING_BUFFER, 36 | //PLAYING_TONE, 37 | PLAYING_SYNTH, 38 | NOT_PLAYING 39 | }; 40 | 41 | PlayMode play_mode = NOT_PLAYING; 42 | 43 | public: 44 | PicoSynth_I2S(uint i2s_data, uint i2s_bclk, uint i2s_lrclk, uint audio_pio, uint audio_sm) : i2s_data(i2s_data), i2s_bclk(i2s_bclk), i2s_lrclk(i2s_lrclk), audio_pio(audio_pio == 0 ? pio0 : pio1), audio_sm(audio_sm) { 45 | // Nothing to see here... 46 | } 47 | 48 | PicoSynth_I2S(uint i2s_data, uint i2s_bclk, uint i2s_lrclk) : PicoSynth_I2S(i2s_data, i2s_bclk, i2s_lrclk, 0, 0) {}; 49 | 50 | ~PicoSynth_I2S(); 51 | 52 | void init(); 53 | static inline void pio_program_init(PIO pio, uint sm, uint offset); 54 | 55 | void set_volume(float value); 56 | float get_volume(); 57 | void adjust_volume(float delta); 58 | 59 | void play_sample(uint8_t *data, uint32_t length); 60 | void play_synth(); 61 | void stop_playing(); 62 | AudioChannel& synth_channel(uint channel); 63 | 64 | static PicoSynth_I2S* picosynth; 65 | 66 | private: 67 | void partial_teardown(); 68 | void dma_safe_abort(uint channel); 69 | void next_audio_sequence(); 70 | void populate_next_synth(); 71 | }; 72 | 73 | } -------------------------------------------------------------------------------- /libraries/pico_synth_i2s/pico_synth_i2s.pio: -------------------------------------------------------------------------------- 1 | ; 2 | ; Copyright (c) 2020 Raspberry Pi (Trading) Ltd. 3 | ; 4 | ; SPDX-License-Identifier: BSD-3-Clause 5 | ; 6 | 7 | ; Transmit a mono or stereo I2S audio stream as stereo 8 | ; This is 16 bits per sample; can be altered by modifying the "set" params, 9 | ; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register. 10 | ; 11 | ; Autopull must be enabled, with threshold set to 32. 12 | ; Since I2S is MSB-first, shift direction should be to left. 13 | ; Hence the format of the FIFO word is: 14 | ; 15 | ; | 31 : 16 | 15 : 0 | 16 | ; | sample ws=0 | sample ws=1 | 17 | ; 18 | ; Data is output at 1 bit per clock. Use clock divider to adjust frequency. 19 | ; Fractional divider will probably be needed to get correct bit clock period, 20 | ; but for common syslck freqs this should still give a constant word select period. 21 | ; 22 | ; One output pin is used for the data output. 23 | ; Two side-set pins are used. Bit 0 is clock, bit 1 is word select. 24 | 25 | ; Send 16 bit words to the PIO for mono, 32 bit words for stereo 26 | 27 | .program audio_i2s 28 | .side_set 2 29 | 30 | ; /--- LRCLK 31 | ; |/-- BCLK 32 | bitloop1: ; || 33 | out pins, 1 side 0b10 34 | jmp x-- bitloop1 side 0b11 35 | out pins, 1 side 0b00 36 | set x, 14 side 0b01 37 | 38 | bitloop0: 39 | out pins, 1 side 0b00 40 | jmp x-- bitloop0 side 0b01 41 | out pins, 1 side 0b10 42 | public entry_point: 43 | set x, 14 side 0b11 44 | 45 | % c-sdk { 46 | 47 | static inline void audio_i2s_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) { 48 | pio_sm_config sm_config = audio_i2s_program_get_default_config(offset); 49 | 50 | sm_config_set_out_pins(&sm_config, data_pin, 1); 51 | sm_config_set_sideset_pins(&sm_config, clock_pin_base); 52 | sm_config_set_out_shift(&sm_config, false, true, 32); 53 | 54 | pio_sm_init(pio, sm, offset, &sm_config); 55 | 56 | uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); 57 | pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); 58 | pio_sm_set_pins_with_mask(pio, sm, 0, pin_mask); // clear pins 59 | 60 | pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_offset_entry_point)); 61 | } 62 | 63 | %} -------------------------------------------------------------------------------- /modules/picographics/micropython.cmake: -------------------------------------------------------------------------------- 1 | set(MOD_NAME picographics) 2 | string(TOUPPER ${MOD_NAME} MOD_NAME_UPPER) 3 | add_library(usermod_${MOD_NAME} INTERFACE) 4 | 5 | option(SUPPORT_WIDE_MODES "Build with widescreen support." OFF) 6 | 7 | get_filename_component(PICOVISION_PATH ${CMAKE_CURRENT_LIST_DIR}/../.. ABSOLUTE) 8 | 9 | target_sources(usermod_${MOD_NAME} INTERFACE 10 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c 11 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.cpp 12 | ${PICOVISION_PATH}/drivers/dv_display/dv_display.cpp 13 | ${PICOVISION_PATH}/drivers/dv_display/swd_load.cpp 14 | ${PICOVISION_PATH}/drivers/aps6404/aps6404.cpp 15 | ${PICOVISION_PATH}/drivers/aps6404/aps6404_perf_critical.cpp 16 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics/pico_graphics.cpp 17 | ${PICOVISION_PATH}/libraries/pico_graphics/pico_graphics_pen_dv_rgb888.cpp 18 | ${PICOVISION_PATH}/libraries/pico_graphics/pico_graphics_pen_dv_rgb555.cpp 19 | ${PICOVISION_PATH}/libraries/pico_graphics/pico_graphics_pen_dv_p5.cpp 20 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics/types.cpp 21 | ) 22 | 23 | pico_generate_pio_header(usermod_${MOD_NAME} ${PICOVISION_PATH}/drivers/dv_display/swd.pio) 24 | pico_generate_pio_header(usermod_${MOD_NAME} ${PICOVISION_PATH}/drivers/aps6404/aps6404.pio) 25 | 26 | # MicroPython compiles with -Os by default, these functions are critical path enough that -O2 is worth it (note -O3 is slower in this case) 27 | set_source_files_properties(${PICOVISION_PATH}/drivers/aps6404/aps6404_perf_critical.cpp PROPERTIES COMPILE_OPTIONS "-O2") 28 | 29 | 30 | target_include_directories(usermod_${MOD_NAME} INTERFACE 31 | ${CMAKE_CURRENT_LIST_DIR} 32 | ${PICOVISION_PATH} 33 | ${PICOVISION_PATH}/drivers/aps6404 34 | ${PICOVISION_PATH}/drivers/dv_display 35 | ${PICOVISION_PATH}/libraries/pico_graphics # for pico_graphics_dv.hpp 36 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics # for pico_graphics.hpp 37 | ${PIMORONI_PICO_PATH}/libraries/pngdec 38 | ) 39 | 40 | target_compile_definitions(usermod_${MOD_NAME} INTERFACE 41 | -DMODULE_${MOD_NAME_UPPER}_ENABLED=1 42 | ) 43 | 44 | if (SUPPORT_WIDE_MODES) 45 | target_compile_definitions(usermod_${MOD_NAME} INTERFACE 46 | -DSUPPORT_WIDE_MODES=1 47 | ) 48 | endif() 49 | 50 | target_link_libraries(usermod INTERFACE usermod_${MOD_NAME}) 51 | 52 | set_source_files_properties( 53 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c 54 | PROPERTIES COMPILE_FLAGS 55 | "-Wno-discarded-qualifiers" 56 | ) 57 | -------------------------------------------------------------------------------- /modules/picographics/picographics.h: -------------------------------------------------------------------------------- 1 | #include "py/runtime.h" 2 | #include "py/objstr.h" 3 | 4 | enum PicoGraphicsPenType { 5 | PEN_1BIT = 0, 6 | PEN_3BIT, 7 | PEN_P2, 8 | PEN_P4, 9 | PEN_P8, 10 | PEN_RGB332, 11 | PEN_RGB565, 12 | PEN_RGB888, 13 | PEN_INKY7, 14 | PEN_DV_RGB888, 15 | PEN_DV_RGB555, 16 | PEN_DV_P5 17 | }; 18 | 19 | enum PicoGraphicsBusType { 20 | BUS_I2C, 21 | BUS_SPI, 22 | BUS_PARALLEL, 23 | BUS_PIO 24 | }; 25 | 26 | // Type 27 | extern const mp_obj_type_t ModPicoGraphics_type; 28 | 29 | // Module functions 30 | extern mp_obj_t ModPicoGraphics_get_required_buffer_size(mp_obj_t display_in, mp_obj_t pen_type_in); 31 | 32 | // DV Display specific functions 33 | extern mp_obj_t ModPicoGraphics_set_scroll_group_offset(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 34 | extern mp_obj_t ModPicoGraphics_set_scroll_group_for_lines(size_t n_args, const mp_obj_t *args); 35 | extern mp_obj_t ModPicoGraphics_tilemap(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 36 | extern mp_obj_t ModPicoGraphics_load_animation(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 37 | 38 | // Class methods 39 | extern mp_obj_t ModPicoGraphics_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args); 40 | 41 | extern mp_obj_t ModPicoGraphics_update(mp_obj_t self_in); 42 | 43 | // Palette management 44 | extern mp_obj_t ModPicoGraphics_update_pen(size_t n_args, const mp_obj_t *args); 45 | extern mp_obj_t ModPicoGraphics_reset_pen(mp_obj_t self_in, mp_obj_t pen); 46 | extern mp_obj_t ModPicoGraphics_set_palette(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 47 | extern mp_obj_t ModPicoGraphics_hsv_to_rgb(size_t n_args, const mp_obj_t *args); 48 | 49 | extern mp_obj_t ModPicoGraphics_set_remote_palette(mp_obj_t self_in, mp_obj_t index); 50 | extern mp_obj_t ModPicoGraphics_set_local_palette(mp_obj_t self_in, mp_obj_t index); 51 | 52 | // Pen 53 | extern mp_obj_t ModPicoGraphics_set_pen(mp_obj_t self_in, mp_obj_t pen); 54 | extern mp_obj_t ModPicoGraphics_create_pen(size_t n_args, const mp_obj_t *args); 55 | extern mp_obj_t ModPicoGraphics_create_pen_hsv(size_t n_args, const mp_obj_t *args); 56 | extern mp_obj_t ModPicoGraphics_set_thickness(mp_obj_t self_in, mp_obj_t thickness); 57 | 58 | // Alpha Blending 59 | extern mp_obj_t ModPicoGraphics_set_bg(mp_obj_t self_in, mp_obj_t pen); 60 | extern mp_obj_t ModPicoGraphics_set_blend_mode(mp_obj_t self_in, mp_obj_t pen); 61 | 62 | // Depth 63 | extern mp_obj_t ModPicoGraphics_set_depth(mp_obj_t self_in, mp_obj_t depth); 64 | 65 | // Primitives 66 | extern mp_obj_t ModPicoGraphics_set_clip(size_t n_args, const mp_obj_t *args); 67 | extern mp_obj_t ModPicoGraphics_remove_clip(mp_obj_t self_in); 68 | extern mp_obj_t ModPicoGraphics_clear(mp_obj_t self_in); 69 | extern mp_obj_t ModPicoGraphics_pixel(mp_obj_t self_in, mp_obj_t x, mp_obj_t y); 70 | extern mp_obj_t ModPicoGraphics_pixel_span(size_t n_args, const mp_obj_t *args); 71 | extern mp_obj_t ModPicoGraphics_rectangle(size_t n_args, const mp_obj_t *args); 72 | extern mp_obj_t ModPicoGraphics_circle(size_t n_args, const mp_obj_t *args); 73 | extern mp_obj_t ModPicoGraphics_character(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 74 | extern mp_obj_t ModPicoGraphics_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 75 | extern mp_obj_t ModPicoGraphics_measure_text(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 76 | extern mp_obj_t ModPicoGraphics_polygon(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 77 | extern mp_obj_t ModPicoGraphics_triangle(size_t n_args, const mp_obj_t *args); 78 | extern mp_obj_t ModPicoGraphics_line(size_t n_args, const mp_obj_t *args); 79 | 80 | // Sprites 81 | extern mp_obj_t ModPicoGraphics_load_sprite(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 82 | extern mp_obj_t ModPicoGraphics_display_sprite(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 83 | extern mp_obj_t ModPicoGraphics_clear_sprite(mp_obj_t self_in, mp_obj_t slot); 84 | 85 | // Utility 86 | extern mp_obj_t ModPicoGraphics_set_font(mp_obj_t self_in, mp_obj_t font); 87 | extern mp_obj_t ModPicoGraphics_get_bounds(mp_obj_t self_in); 88 | 89 | extern mp_obj_t ModPicoGraphics_get_i2c(mp_obj_t self_in); 90 | 91 | extern mp_obj_t ModPicoGraphics__del__(mp_obj_t self_in); 92 | 93 | // IO IO 94 | extern mp_obj_t ModPicoGraphics_is_button_x_pressed(mp_obj_t self_in); 95 | extern mp_obj_t ModPicoGraphics_is_button_a_pressed(mp_obj_t self_in); 96 | 97 | extern mp_obj_t ModPicoGraphics_get_gpu_io_value(mp_obj_t self_in, mp_obj_t pin); 98 | extern mp_obj_t ModPicoGraphics_set_gpu_io_value(mp_obj_t self_in, mp_obj_t pin, mp_obj_t value); 99 | extern mp_obj_t ModPicoGraphics_set_gpu_io_output_enable(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); 100 | extern mp_obj_t ModPicoGraphics_set_gpu_io_pull_up(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); 101 | extern mp_obj_t ModPicoGraphics_set_gpu_io_pull_down(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); 102 | 103 | extern mp_obj_t ModPicoGraphics_set_gpu_io_adc_enable(mp_obj_t self_in, mp_obj_t pin, mp_obj_t enable); 104 | extern mp_obj_t ModPicoGraphics_get_gpu_io_adc_voltage(mp_obj_t self_in, mp_obj_t pin); 105 | 106 | extern mp_obj_t ModPicoGraphics_get_gpu_temp(mp_obj_t self_in); 107 | 108 | // Loop 109 | extern mp_obj_t ModPicoGraphics_loop(mp_obj_t self_in, mp_obj_t update, mp_obj_t render); 110 | -------------------------------------------------------------------------------- /modules/picographics/spritesheet-to-rgb332.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python3 2 | from PIL import Image 3 | import numpy 4 | import sys 5 | import pathlib 6 | 7 | # Run with `./filename.py source-image.jpg` 8 | IMAGE_PATH = pathlib.Path(sys.argv[1]) 9 | OUTPUT_PATH = IMAGE_PATH.with_suffix(".rgb332") 10 | 11 | 12 | def image_to_data(image): 13 | """Generator function to convert a PIL image to 16-bit 565 RGB bytes.""" 14 | # NumPy is much faster at doing this. NumPy code provided by: 15 | # Keith (https://www.blogger.com/profile/02555547344016007163) 16 | pb = numpy.array(image.convert('RGBA')).astype('uint16') 17 | 18 | r = (pb[:, :, 0] & 0b11100000) >> 0 19 | g = (pb[:, :, 1] & 0b11100000) >> 3 20 | b = (pb[:, :, 2] & 0b11000000) >> 6 21 | a = pb[:, :, 3] # Discard 22 | 23 | # AAAA RRRR GGGG BBBB 24 | color = r | g | b 25 | return color.astype("uint8").flatten().tobytes() 26 | 27 | 28 | img = Image.open(IMAGE_PATH) 29 | w, h = img.size 30 | data = image_to_data(img) 31 | 32 | print(f"Converted: {w}x{h} {len(data)} bytes") 33 | 34 | with open(OUTPUT_PATH, "wb") as f: 35 | f.write(data) 36 | 37 | print(f"Written to: {OUTPUT_PATH}") -------------------------------------------------------------------------------- /modules/picosynth/micropython.cmake: -------------------------------------------------------------------------------- 1 | set(MOD_NAME picosynth) 2 | string(TOUPPER ${MOD_NAME} MOD_NAME_UPPER) 3 | add_library(usermod_${MOD_NAME} INTERFACE) 4 | 5 | get_filename_component(PICOVISION_PATH ${CMAKE_CURRENT_LIST_DIR}/../.. ABSOLUTE) 6 | 7 | target_sources(usermod_${MOD_NAME} INTERFACE 8 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c 9 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.cpp 10 | ${PIMORONI_PICO_PATH}/libraries/pico_synth/pico_synth.cpp 11 | ${PICOVISION_PATH}/libraries/pico_synth_i2s/pico_synth_i2s.cpp 12 | ) 13 | 14 | pico_generate_pio_header(usermod_${MOD_NAME} ${PICOVISION_PATH}/libraries/pico_synth_i2s/pico_synth_i2s.pio) 15 | 16 | 17 | target_include_directories(usermod_${MOD_NAME} INTERFACE 18 | ${CMAKE_CURRENT_LIST_DIR} 19 | ) 20 | 21 | target_compile_definitions(usermod_${MOD_NAME} INTERFACE 22 | -DMODULE_${MOD_NAME_UPPER}_ENABLED=1 23 | ) 24 | 25 | target_link_libraries(usermod INTERFACE usermod_${MOD_NAME}) 26 | 27 | set_source_files_properties( 28 | ${CMAKE_CURRENT_LIST_DIR}/${MOD_NAME}.c 29 | PROPERTIES COMPILE_FLAGS 30 | "-Wno-discarded-qualifiers" 31 | ) 32 | -------------------------------------------------------------------------------- /modules/picosynth/picosynth.c: -------------------------------------------------------------------------------- 1 | #include "picosynth.h" 2 | 3 | 4 | /***** Methods *****/ 5 | MP_DEFINE_CONST_FUN_OBJ_1(Channel___del___obj, Channel___del__); 6 | MP_DEFINE_CONST_FUN_OBJ_KW(Channel_configure_obj, 1, Channel_configure); 7 | MP_DEFINE_CONST_FUN_OBJ_1(Channel_restore_obj, Channel_restore); 8 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_waveforms_obj, 1, 2, Channel_waveforms); 9 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_frequency_obj, 1, 2, Channel_frequency); 10 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_volume_obj, 1, 2, Channel_volume); 11 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_attack_duration_obj, 1, 2, Channel_attack_duration); 12 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_decay_duration_obj, 1, 2, Channel_decay_duration); 13 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_sustain_level_obj, 1, 2, Channel_sustain_level); 14 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_release_duration_obj, 1, 2, Channel_release_duration); 15 | MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(Channel_pulse_width_obj, 1, 2, Channel_pulse_width); 16 | MP_DEFINE_CONST_FUN_OBJ_1(Channel_trigger_attack_obj, Channel_trigger_attack); 17 | MP_DEFINE_CONST_FUN_OBJ_1(Channel_trigger_release_obj, Channel_trigger_release); 18 | MP_DEFINE_CONST_FUN_OBJ_KW(Channel_play_tone_obj, 2, Channel_play_tone); 19 | 20 | MP_DEFINE_CONST_FUN_OBJ_1(PicoSynth___del___obj, PicoSynth___del__); 21 | MP_DEFINE_CONST_FUN_OBJ_2(PicoSynth_set_volume_obj, PicoSynth_set_volume); 22 | MP_DEFINE_CONST_FUN_OBJ_1(PicoSynth_get_volume_obj, PicoSynth_get_volume); 23 | MP_DEFINE_CONST_FUN_OBJ_2(PicoSynth_adjust_volume_obj, PicoSynth_adjust_volume); 24 | MP_DEFINE_CONST_FUN_OBJ_2(PicoSynth_play_sample_obj, PicoSynth_play_sample); 25 | MP_DEFINE_CONST_FUN_OBJ_1(PicoSynth_play_synth_obj, PicoSynth_play_synth); 26 | MP_DEFINE_CONST_FUN_OBJ_1(PicoSynth_stop_playing_obj, PicoSynth_stop_playing); 27 | MP_DEFINE_CONST_FUN_OBJ_2(PicoSynth_synth_channel_obj, PicoSynth_synth_channel); 28 | 29 | /***** Binding of Methods *****/ 30 | STATIC const mp_rom_map_elem_t Channel_locals_dict_table[] = { 31 | { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&Channel___del___obj) }, 32 | { MP_ROM_QSTR(MP_QSTR_configure), MP_ROM_PTR(&Channel_configure_obj) }, 33 | { MP_ROM_QSTR(MP_QSTR_restore), MP_ROM_PTR(&Channel_restore_obj) }, 34 | { MP_ROM_QSTR(MP_QSTR_waveforms), MP_ROM_PTR(&Channel_waveforms_obj) }, 35 | { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_PTR(&Channel_frequency_obj) }, 36 | { MP_ROM_QSTR(MP_QSTR_volume), MP_ROM_PTR(&Channel_volume_obj) }, 37 | { MP_ROM_QSTR(MP_QSTR_attack_duration), MP_ROM_PTR(&Channel_attack_duration_obj) }, 38 | { MP_ROM_QSTR(MP_QSTR_decay_duration), MP_ROM_PTR(&Channel_decay_duration_obj) }, 39 | { MP_ROM_QSTR(MP_QSTR_sustain_level), MP_ROM_PTR(&Channel_sustain_level_obj) }, 40 | { MP_ROM_QSTR(MP_QSTR_release_duration), MP_ROM_PTR(&Channel_release_duration_obj) }, 41 | { MP_ROM_QSTR(MP_QSTR_pulse_width), MP_ROM_PTR(&Channel_pulse_width_obj) }, 42 | { MP_ROM_QSTR(MP_QSTR_trigger_attack), MP_ROM_PTR(&Channel_trigger_attack_obj) }, 43 | { MP_ROM_QSTR(MP_QSTR_trigger_release), MP_ROM_PTR(&Channel_trigger_release_obj) }, 44 | { MP_ROM_QSTR(MP_QSTR_play_tone), MP_ROM_PTR(&Channel_play_tone_obj) }, 45 | 46 | { MP_ROM_QSTR(MP_QSTR_NOISE), MP_ROM_INT(128) }, 47 | { MP_ROM_QSTR(MP_QSTR_SQUARE), MP_ROM_INT(64) }, 48 | { MP_ROM_QSTR(MP_QSTR_SAW), MP_ROM_INT(32) }, 49 | { MP_ROM_QSTR(MP_QSTR_TRIANGLE), MP_ROM_INT(16) }, 50 | { MP_ROM_QSTR(MP_QSTR_SINE), MP_ROM_INT(8) }, 51 | { MP_ROM_QSTR(MP_QSTR_WAVE), MP_ROM_INT(1) }, 52 | }; 53 | 54 | STATIC const mp_rom_map_elem_t PicoSynth_locals_dict_table[] = { 55 | { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&PicoSynth___del___obj) }, 56 | { MP_ROM_QSTR(MP_QSTR_set_volume), MP_ROM_PTR(&PicoSynth_set_volume_obj) }, 57 | { MP_ROM_QSTR(MP_QSTR_get_volume), MP_ROM_PTR(&PicoSynth_get_volume_obj) }, 58 | { MP_ROM_QSTR(MP_QSTR_adjust_volume), MP_ROM_PTR(&PicoSynth_adjust_volume_obj) }, 59 | { MP_ROM_QSTR(MP_QSTR_play_sample), MP_ROM_PTR(&PicoSynth_play_sample_obj) }, 60 | { MP_ROM_QSTR(MP_QSTR_play), MP_ROM_PTR(&PicoSynth_play_synth_obj) }, 61 | { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&PicoSynth_stop_playing_obj) }, 62 | { MP_ROM_QSTR(MP_QSTR_channel), MP_ROM_PTR(&PicoSynth_synth_channel_obj) }, 63 | 64 | }; 65 | 66 | STATIC MP_DEFINE_CONST_DICT(Channel_locals_dict, Channel_locals_dict_table); 67 | STATIC MP_DEFINE_CONST_DICT(PicoSynth_locals_dict, PicoSynth_locals_dict_table); 68 | 69 | /***** Class Definition *****/ 70 | #ifdef MP_DEFINE_CONST_OBJ_TYPE 71 | MP_DEFINE_CONST_OBJ_TYPE( 72 | Channel_type, 73 | MP_QSTR_Channel, 74 | MP_TYPE_FLAG_NONE, 75 | make_new, Channel_make_new, 76 | print, Channel_print, 77 | locals_dict, (mp_obj_dict_t*)&Channel_locals_dict 78 | ); 79 | 80 | MP_DEFINE_CONST_OBJ_TYPE( 81 | PicoSynth_type, 82 | MP_QSTR_PicoSynth, 83 | MP_TYPE_FLAG_NONE, 84 | make_new, PicoSynth_make_new, 85 | print, PicoSynth_print, 86 | locals_dict, (mp_obj_dict_t*)&PicoSynth_locals_dict 87 | ); 88 | #else 89 | const mp_obj_type_t Channel_type = { 90 | { &mp_type_type }, 91 | .name = MP_QSTR_Channel, 92 | .print = Channel_print, 93 | .make_new = Channel_make_new, 94 | .locals_dict = (mp_obj_dict_t*)&Channel_locals_dict, 95 | }; 96 | 97 | const mp_obj_type_t PicoSynth_type = { 98 | { &mp_type_type }, 99 | .name = MP_QSTR_PicoSynth, 100 | .print = PicoSynth_print, 101 | .make_new = PicoSynth_make_new, 102 | .locals_dict = (mp_obj_dict_t*)&PicoSynth_locals_dict, 103 | }; 104 | #endif 105 | 106 | /***** Globals Table *****/ 107 | STATIC const mp_map_elem_t picosynth_globals_table[] = { 108 | { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_picosynth) }, 109 | { MP_OBJ_NEW_QSTR(MP_QSTR_Channel), (mp_obj_t)&Channel_type }, 110 | { MP_OBJ_NEW_QSTR(MP_QSTR_PicoSynth), (mp_obj_t)&PicoSynth_type }, 111 | }; 112 | STATIC MP_DEFINE_CONST_DICT(mp_module_picosynth_globals, picosynth_globals_table); 113 | 114 | /***** Module Definition *****/ 115 | const mp_obj_module_t picosynth_user_cmodule = { 116 | .base = { &mp_type_module }, 117 | .globals = (mp_obj_dict_t*)&mp_module_picosynth_globals, 118 | }; 119 | #if MICROPY_VERSION <= 70144 120 | MP_REGISTER_MODULE(MP_QSTR_picosynth, picosynth_user_cmodule, MODULE_PICOSYNTH_ENABLED); 121 | #else 122 | MP_REGISTER_MODULE(MP_QSTR_picosynth, picosynth_user_cmodule); 123 | #endif -------------------------------------------------------------------------------- /modules/picosynth/picosynth.h: -------------------------------------------------------------------------------- 1 | // Include MicroPython API. 2 | #include "py/runtime.h" 3 | 4 | /***** Extern of Class Definition *****/ 5 | extern const mp_obj_type_t Channel_type; 6 | extern const mp_obj_type_t PicoSynth_type; 7 | 8 | /***** Extern of Class Methods *****/ 9 | extern void Channel_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind); 10 | extern mp_obj_t Channel_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args); 11 | extern mp_obj_t Channel___del__(mp_obj_t self_in); 12 | extern mp_obj_t Channel_configure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 13 | extern mp_obj_t Channel_restore(mp_obj_t self_in); 14 | extern mp_obj_t Channel_waveforms(size_t n_args, const mp_obj_t *args); 15 | extern mp_obj_t Channel_frequency(size_t n_args, const mp_obj_t *args); 16 | extern mp_obj_t Channel_volume(size_t n_args, const mp_obj_t *args); 17 | extern mp_obj_t Channel_attack_duration(size_t n_args, const mp_obj_t *args); 18 | extern mp_obj_t Channel_decay_duration(size_t n_args, const mp_obj_t *args); 19 | extern mp_obj_t Channel_sustain_level(size_t n_args, const mp_obj_t *args); 20 | extern mp_obj_t Channel_release_duration(size_t n_args, const mp_obj_t *args); 21 | extern mp_obj_t Channel_pulse_width(size_t n_args, const mp_obj_t *args); 22 | extern mp_obj_t Channel_trigger_attack(mp_obj_t self_in); 23 | extern mp_obj_t Channel_trigger_release(mp_obj_t self_in); 24 | extern mp_obj_t Channel_play_tone(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args); 25 | extern mp_obj_t Channel_stop_playing(mp_obj_t self_in); 26 | 27 | extern void PicoSynth_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind); 28 | extern mp_obj_t PicoSynth_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args); 29 | extern mp_obj_t PicoSynth___del__(mp_obj_t self_in); 30 | 31 | extern mp_obj_t PicoSynth_set_volume(mp_obj_t self_in, mp_obj_t value); 32 | extern mp_obj_t PicoSynth_get_volume(mp_obj_t self_in); 33 | extern mp_obj_t PicoSynth_adjust_volume(mp_obj_t self_in, mp_obj_t delta); 34 | 35 | extern mp_obj_t PicoSynth_play_sample(mp_obj_t self_in, mp_obj_t data); 36 | extern mp_obj_t PicoSynth_play_synth(mp_obj_t self_in); 37 | extern mp_obj_t PicoSynth_stop_playing(mp_obj_t self_in); 38 | 39 | extern mp_obj_t PicoSynth_synth_channel(mp_obj_t self_in, mp_obj_t channel_in); -------------------------------------------------------------------------------- /picovision.cmake: -------------------------------------------------------------------------------- 1 | 2 | include(drivers/aps6404/aps6404) 3 | include(drivers/dv_display/dv_display) 4 | 5 | set(LIB_NAME picovision) 6 | add_library(${LIB_NAME} INTERFACE) 7 | 8 | target_sources(${LIB_NAME} INTERFACE 9 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics/pico_graphics.cpp 10 | ${CMAKE_CURRENT_LIST_DIR}/libraries/pico_graphics/pico_graphics_pen_dv_rgb888.cpp 11 | ${CMAKE_CURRENT_LIST_DIR}/libraries/pico_graphics/pico_graphics_pen_dv_rgb555.cpp 12 | ${CMAKE_CURRENT_LIST_DIR}/libraries/pico_graphics/pico_graphics_pen_dv_p5.cpp 13 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics/types.cpp 14 | ) 15 | 16 | target_include_directories(${LIB_NAME} INTERFACE 17 | ${CMAKE_CURRENT_LIST_DIR} 18 | ${CMAKE_CURRENT_LIST_DIR}/libraries/pico_graphics # for pico_graphics_dv.hpp 19 | ${PIMORONI_PICO_PATH}/libraries/pico_graphics # for pico_graphics.hpp 20 | ${PIMORONI_PICO_PATH}/libraries/pngdec 21 | ) 22 | 23 | target_link_libraries(${LIB_NAME} INTERFACE aps6404 dv_display pico_stdlib) -------------------------------------------------------------------------------- /picovision_import.cmake: -------------------------------------------------------------------------------- 1 | # This file can be dropped into a project to help locate the Picovision library 2 | # It will also set up the required include and module search paths. 3 | 4 | if (NOT PIMORONI_PICOVISION_PATH) 5 | set(PIMORONI_PICOVISION_PATH "../../picovision/") 6 | endif() 7 | 8 | if(NOT IS_ABSOLUTE ${PIMORONI_PICOVISION_PATH}) 9 | get_filename_component( 10 | PIMORONI_PICOVISION_PATH 11 | "${CMAKE_CURRENT_BINARY_DIR}/${PIMORONI_PICOVISION_PATH}" 12 | ABSOLUTE) 13 | endif() 14 | 15 | if (NOT EXISTS ${PIMORONI_PICOVISION_PATH}) 16 | message(FATAL_ERROR "Directory '${PIMORONI_PICOVISION_PATH}' not found") 17 | endif () 18 | 19 | if (NOT EXISTS ${PIMORONI_PICOVISION_PATH}/picovision_import.cmake) 20 | message(FATAL_ERROR "Directory '${PIMORONI_PICOVISION_PATH}' does not appear to contain the Picovision library") 21 | endif () 22 | 23 | message("PIMORONI_PICOVISION_PATH is ${PIMORONI_PICOVISION_PATH}") 24 | 25 | set(PIMORONI_PICOVISION_PATH ${PIMORONI_PICOVISION_PATH} CACHE PATH "Path to the Picovision libraries" FORCE) 26 | 27 | include_directories(${PIMORONI_PICOVISION_PATH}) 28 | list(APPEND CMAKE_MODULE_PATH ${PIMORONI_PICOVISION_PATH}) 29 | 30 | include(picovision) 31 | include_directories(${PIMORONI_PICO_PATH}/libraries/pico_graphics) 32 | --------------------------------------------------------------------------------