├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── .cortex-debug.peripherals.state.json ├── .cortex-debug.registers.state.json ├── c_cpp_properties.json ├── launch.json └── settings.json ├── CMakeLists.txt ├── FrameFormat.txt ├── LICENSE ├── README.md ├── aps6404.cpp ├── aps6404.hpp ├── aps6404.pio ├── constants.hpp ├── convert_elf.py ├── display.cpp ├── display.hpp ├── edid.cpp ├── edid.hpp ├── frame_decode.cpp ├── frame_decode.hpp ├── i2c_interface.cpp ├── i2c_interface.hpp ├── i2c_slave ├── CMakeLists.txt ├── i2c_slave.c └── include │ ├── i2c_fifo.h │ └── i2c_slave.h ├── main.cpp ├── memmap.ld ├── micropython ├── ram.py └── test.py ├── openocd └── target │ └── rp2040-no-flash.cfg ├── pico_sdk_import.cmake ├── pico_stick_frame.hpp ├── pimoroni_pico_import.cmake ├── pins.hpp ├── sprite.cpp └── sprite.hpp /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: [created] 8 | 9 | env: 10 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 11 | BUILD_TYPE: Release 12 | 13 | jobs: 14 | build: 15 | name: ${{matrix.name}} 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-20.04 20 | name: Linux 21 | cache-key: linux 22 | cmake-args: '-DPIMORONI_PICO_PATH=$GITHUB_WORKSPACE/pimoroni-pico -DPICO_SDK_PATH=$GITHUB_WORKSPACE/pico-sdk -DCMAKE_INSTALL_PREFIX=$GITHUB_WORKSPACE/install' 23 | apt-packages: clang-tidy gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib 24 | 25 | runs-on: ${{matrix.os}} 26 | 27 | env: 28 | PICO_SDK_PATH: $GITHUB_WORKSPACE/pico-sdk 29 | PIMORONI_PICO_LIBS: $GITHUB_WORKSPACE/pimoroni-pico 30 | RELEASE_FILE: ${{github.event.repository.name}}-${{github.event.release.tag_name || github.sha}} 31 | 32 | steps: 33 | - name: Checkout Code 34 | uses: actions/checkout@v2 35 | with: 36 | path: project 37 | submodules: true 38 | 39 | # Checkout the Pimoroni Pico Libraries 40 | - name: Checkout Pimoroni Pico Libraries 41 | uses: actions/checkout@v2 42 | with: 43 | repository: pimoroni/pimoroni-pico 44 | path: pimoroni-pico 45 | 46 | # Checkout the Pico SDK 47 | - name: Checkout Pico SDK 48 | uses: actions/checkout@v2 49 | with: 50 | repository: raspberrypi/pico-sdk 51 | path: pico-sdk 52 | submodules: true 53 | 54 | # Linux deps 55 | - name: Install deps 56 | if: runner.os == 'Linux' 57 | run: | 58 | sudo apt update && sudo apt install ${{matrix.apt-packages}} 59 | 60 | - name: Create Build Environment 61 | run: cmake -E make_directory ${{runner.workspace}}/build 62 | 63 | - name: Configure CMake 64 | shell: bash 65 | working-directory: ${{runner.workspace}}/build 66 | run: cmake $GITHUB_WORKSPACE/project -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCPACK_PACKAGE_FILE_NAME=${{env.RELEASE_FILE}} ${{matrix.cmake-args}} 67 | 68 | - name: Build 69 | working-directory: ${{runner.workspace}}/build 70 | shell: bash 71 | run: | 72 | cmake --build . --config $BUILD_TYPE -j 2 73 | 74 | - name: Upload normal build 75 | if: success() || failure() 76 | uses: actions/upload-artifact@v3 77 | with: 78 | name: pico-stick-build 79 | path: ${{runner.workspace}}/build/pico-stick.* 80 | 81 | - name: Upload widescreen build 82 | if: success() || failure() 83 | uses: actions/upload-artifact@v3 84 | with: 85 | name: pico-stick-wide-build 86 | path: ${{runner.workspace}}/build/pico-stick-wide.* 87 | 88 | - name: Build release package 89 | if: success() 90 | working-directory: ${{runner.workspace}}/build 91 | shell: bash 92 | run: | 93 | cmake --build . --config $BUILD_TYPE --target package -j 2 94 | 95 | - name: Upload release package 96 | if: success() 97 | uses: actions/upload-artifact@v3 98 | with: 99 | name: ${{env.RELEASE_FILE}}.zip 100 | path: ${{runner.workspace}}/build/${{env.RELEASE_FILE}}.zip 101 | 102 | - name: Upload .zip 103 | if: github.event_name == 'release' 104 | uses: actions/upload-release-asset@v1 105 | env: 106 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 107 | with: 108 | asset_path: ${{runner.workspace}}/build/${{env.RELEASE_FILE}}.zip 109 | upload_url: ${{github.event.release.upload_url}} 110 | asset_name: ${{env.RELEASE_FILE}}.zip 111 | asset_content_type: application/zip 112 | 113 | - name: Upload .tar.gz 114 | if: github.event_name == 'release' 115 | uses: actions/upload-release-asset@v1 116 | env: 117 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 118 | with: 119 | asset_path: ${{runner.workspace}}/build/${{env.RELEASE_FILE}}.tar.gz 120 | upload_url: ${{github.event.release.upload_url}} 121 | asset_name: ${{env.RELEASE_FILE}}.tar.gz 122 | asset_content_type: application/octet-stream 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "PicoDVI"] 2 | path = PicoDVI 3 | url = https://github.com/MichaelBell/PicoDVI.git 4 | -------------------------------------------------------------------------------- /.vscode/.cortex-debug.peripherals.state.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.vscode/.cortex-debug.registers.state.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/../pico-sdk/src/**" 8 | ], 9 | "defines": [], 10 | "compilerPath": "/usr/bin/gcc", 11 | "cStandard": "c17", 12 | "cppStandard": "gnu++14", 13 | "intelliSenseMode": "linux-gcc-x64", 14 | "configurationProvider": "ms-vscode.cmake-tools" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Pico Debug", 9 | "cwd": "${workspaceRoot}", 10 | "executable": "${command:cmake.launchTargetPath}", 11 | "request": "launch", 12 | "type": "cortex-debug", 13 | "servertype": "external", 14 | "gdbPath": "/usr/bin/gdb-multiarch", 15 | "gdbTarget": "127.0.0.1:3333", 16 | "device": "RP2040", 17 | "configFiles": [ 18 | "interface/picoprobe.cfg", 19 | "target/rp2040.cfg" 20 | ], 21 | "svdFile": "${env:PICO_SDK_PATH}/src/rp2040/hardware_regs/rp2040.svd", 22 | //"runToMain": true, 23 | // Work around for stopping at main on restart 24 | "overrideLaunchCommands": [ 25 | "monitor reset init", 26 | "load", 27 | "monitor reset init" 28 | ], 29 | "postRestartCommands": [ 30 | // "break main", 31 | // "continue" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureSettings": { 3 | // "PICO_SDK_PATH": "../../pico-sdk" // <-- Set this if you've not set ${env:PICO_SDK_PATH} 4 | }, 5 | "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", 6 | "files.associations": { 7 | "array": "cpp", 8 | "*.tcc": "cpp", 9 | "string": "cpp", 10 | "vector": "cpp", 11 | "string_view": "cpp", 12 | "functional": "cpp", 13 | "iomanip": "cpp", 14 | "istream": "cpp", 15 | "limits": "cpp", 16 | "ostream": "cpp", 17 | "ratio": "cpp", 18 | "sstream": "cpp", 19 | "streambuf": "cpp", 20 | "memory_resource": "cpp", 21 | "memory": "cpp", 22 | "fstream": "cpp", 23 | "cmath": "cpp", 24 | "vreg.h": "c", 25 | "tuple": "cpp", 26 | "cstdlib": "c", 27 | "unordered_map": "cpp", 28 | "numeric": "cpp" 29 | }, 30 | "cortex-debug.variableUseNaturalFormat": false 31 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | # Change your executable name to something creative! 4 | set(NAME pico-stick) # <-- Name your project/executable here! 5 | set(NAME_WIDE pico-stick-wide) # <-- Name your project/executable here! 6 | 7 | #include(pimoroni_pico_import.cmake) 8 | include(pico_sdk_import.cmake) 9 | 10 | # Gooey boilerplate 11 | project(${NAME} C CXX ASM) 12 | set(CMAKE_C_STANDARD 11) 13 | set(CMAKE_CXX_STANDARD 17) 14 | 15 | set(PICO_NO_FLASH 1) 16 | #set(PICO_COPY_TO_RAM 1) 17 | 18 | # Initialize the SDK 19 | pico_sdk_init() 20 | 21 | # Enforce consistent compile options. 22 | # For the moment, don't use -O3 options that increase code size significantly 23 | add_compile_options(-Wall -Werror -O2 -fgcse-after-reload -floop-interchange -fpeel-loops -fpredictive-commoning -fsplit-paths -ftree-loop-distribute-patterns -ftree-loop-distribution -ftree-vectorize -ftree-partial-pre -funswitch-loops) 24 | 25 | 26 | add_subdirectory(i2c_slave) 27 | add_subdirectory(PicoDVI/software/libdvi) 28 | include_directories(PicoDVI/software/include PicoDVI/software/assets) 29 | 30 | # Add your source files 31 | add_executable(${NAME} 32 | main.cpp # <-- Add source files here! 33 | aps6404.cpp 34 | display.cpp 35 | frame_decode.cpp 36 | sprite.cpp 37 | i2c_interface.cpp 38 | edid.cpp 39 | ) 40 | 41 | add_executable(${NAME_WIDE} 42 | main.cpp # <-- Add source files here! 43 | aps6404.cpp 44 | display.cpp 45 | frame_decode.cpp 46 | sprite.cpp 47 | i2c_interface.cpp 48 | edid.cpp 49 | ) 50 | 51 | target_compile_definitions(${NAME} PRIVATE 52 | DVI_VERTICAL_REPEAT=1 53 | DVI_DEFAULT_SERIAL_CONFIG=pico_sock_cfg 54 | PICO_STACK_SIZE=0x300 55 | PICO_CORE1_STACK_SIZE=0x200 56 | DVI_N_TMDS_BUFFERS=0 57 | DVI_SYMBOLS_PER_WORD=2 58 | DVI_DEFAULT_PIO_INST=pio1 59 | TMDS_FULLRES_NO_INTERP_SAVE=1 60 | PICO_HEAP_SIZE=2048 61 | ) 62 | 63 | target_compile_definitions(${NAME_WIDE} PRIVATE 64 | DVI_VERTICAL_REPEAT=1 65 | DVI_DEFAULT_SERIAL_CONFIG=pico_sock_cfg 66 | PICO_STACK_SIZE=0x300 67 | PICO_CORE1_STACK_SIZE=0x200 68 | DVI_N_TMDS_BUFFERS=0 69 | DVI_SYMBOLS_PER_WORD=2 70 | DVI_DEFAULT_PIO_INST=pio1 71 | TMDS_FULLRES_NO_INTERP_SAVE=1 72 | PICO_HEAP_SIZE=2048 73 | SUPPORT_WIDE_MODES=1 74 | ) 75 | 76 | set_target_properties(${NAME} PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/memmap.ld) 77 | pico_add_link_depend(${NAME} ${CMAKE_CURRENT_LIST_DIR}/memmap.ld) 78 | pico_generate_pio_header(${NAME} ${CMAKE_CURRENT_LIST_DIR}/aps6404.pio) 79 | 80 | set_target_properties(${NAME_WIDE} PROPERTIES PICO_TARGET_LINKER_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/memmap.ld) 81 | pico_add_link_depend(${NAME_WIDE} ${CMAKE_CURRENT_LIST_DIR}/memmap.ld) 82 | pico_generate_pio_header(${NAME_WIDE} ${CMAKE_CURRENT_LIST_DIR}/aps6404.pio) 83 | 84 | # Don't forget to link the libraries you need! 85 | target_link_libraries(${NAME} pico_stdlib pico_multicore i2c_slave libdvi hardware_watchdog hardware_pwm hardware_adc) 86 | target_link_libraries(${NAME_WIDE} pico_stdlib pico_multicore i2c_slave libdvi hardware_watchdog hardware_pwm hardware_adc) 87 | 88 | # create map/bin/hex file etc. 89 | #pico_add_extra_outputs(${NAME}) 90 | 91 | pico_enable_stdio_usb(${NAME} 0) 92 | pico_enable_stdio_uart(${NAME} 1) 93 | pico_enable_stdio_usb(${NAME_WIDE} 0) 94 | pico_enable_stdio_uart(${NAME_WIDE} 1) 95 | 96 | find_package(PythonInterp 3.6 REQUIRED) 97 | 98 | add_custom_command(TARGET ${NAME} POST_BUILD 99 | COMMAND ${CMAKE_OBJDUMP} -s $ >$>,$,$>.dmp 100 | COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/convert_elf.py $>,$,$>.dmp >$>,$,$>.h 101 | ) 102 | add_custom_command(TARGET ${NAME_WIDE} POST_BUILD 103 | COMMAND ${CMAKE_OBJDUMP} -s $ >$>,$,$>.dmp 104 | COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/convert_elf.py $>,$,$>.dmp >$>,$,$>.h 105 | ) 106 | 107 | # Set up files for the release packages 108 | install(FILES 109 | ${CMAKE_CURRENT_BINARY_DIR}/${NAME}.elf 110 | ${CMAKE_CURRENT_BINARY_DIR}/${NAME}.h 111 | ${CMAKE_CURRENT_BINARY_DIR}/${NAME_WIDE}.elf 112 | ${CMAKE_CURRENT_BINARY_DIR}/${NAME_WIDE}.h 113 | ${CMAKE_CURRENT_LIST_DIR}/README.md 114 | DESTINATION . 115 | ) 116 | 117 | set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) 118 | set(CPACK_GENERATOR "ZIP" "TGZ") 119 | include(CPack) 120 | -------------------------------------------------------------------------------- /FrameFormat.txt: -------------------------------------------------------------------------------- 1 | Pico Stick RAM transfer format 2 | 3 | 4 bytes: Magic word: PICO (0x50, 0x49, 0x43, 0x4F) - if wrong then no further data is read. 4 | 5 | DVI setup: 6 | 4 bytes: 7 | Res select: (Off, 640x480, 720x480, 720x576, 800x480, 800x600) - if doesn't match the boot mode specified over I2C then DVI timing is stopped (not implemented) 8 | Unused 9 | Vertical repeat - number of times to repeat each scanline vertically 10 | Output Enable: (On, Off) - if off then DVI timing but display is black (not implemented) 11 | 2 bytes: Horizontal offset (e.g. 0) - To allow part of the screen to be used, can specify an offset. This is in pixels (the configured repeat is not taken into account), must be a multiple of 2. (Not implemented - must be 0) 12 | 2 bytes: Horizontal length (e.g. 640) - Width of the part of the screen to fill. This is in pixels (the configured repeat is not taken into account, because it can be configured per line), must be a multiple of 2. (Not implemented - must be full width) 13 | 2 bytes: Vertical offset (e.g. 0) - To allow part of the screen to be used, can specify an offset. This is in repeated lines (the configured repeat *is* taken into account). (Not implemented - must be 0) 14 | 2 bytes: Vertical length (e.g. 480) - Height of the part of the screen to fill. This is in repeated lines (the configured repeat *is* taken into account). (Not implemented - must be full height) 15 | 16 | Frame table header: 17 | 2 bytes: Number of frames - Number of frame descriptions that follow. Display will wrap through these frames allowing animations or transitions without flipping the RAMs 18 | 2 bytes: First frame number - The first frame to display ater the RAMs are flipped. 19 | 2 bytes: Frame table length - Length of each frame table. Normally same as vertical length. 20 | 1 byte: Frame rate divider - The frame counter is updated at the DVI frame rate divided by this divider. (If 0 the frame number is not advanced but can be updated over I2C) 21 | 1 byte: Bank number - Indication of which RAM bank this is. When driver sees this value is changed it resets the output frame to the configured first frame number. 22 | 1 byte: Number of palettes - For 256 colour palette mode, use a multiple of 8 palettes 23 | 1 byte: Palette advance - 0 or 1 to indicate whether the palette tables should be indexed by the frame counter. 24 | 2 bytes: Number of sprites in sprite table 25 | 26 | Frame tables: 27 | Number of frames times: 28 | Frame table length times: 29 | 3 bits: Scroll offset index - Which scroll offset from the I2C register to apply to the line address, or 0 for none. 30 | 2 bits: Line mode (ARGB1555, RGB888, 32 colour palette 0CCCCC0A, 256 colour palette) 31 | 3 bits: Horizontal repeat, must be 1 or 2 32 | 3 bytes: Line address 33 | 34 | Palette tables: 35 | Number of palettes times: 36 | 32 times: 37 | 1 byte: Red 38 | 1 byte: Blue 39 | 1 byte: Green 40 | 41 | Sprite table: 42 | Number of sprites times: 43 | 4 bits: Sprite mode (ARGB1555, RGB888, 32 colour palette, 256 colour palette) 44 | 4 bits: Sprite submode (Unused, was originally thinking palette index) 45 | 3 bytes: Sprite entry address (must be a multiple of 4) 46 | 47 | Sprite entry: 48 | 1 byte: Width 49 | 1 byte: Height 50 | Height times: 51 | 1 byte x offset of first pixel on the line (allows transparent pixels at the start of the line to be skipped) 52 | 1 byte line width from the offset (allows transparent pixels at the end of the line to be skipped) 53 | 2 bytes padding if height is even 54 | Height times: 55 | Line width times: 56 | Sprite pixel data 57 | 58 | Line and sprite data can be arranged in any way in the rest of the RAM, addressed by the tables above. 59 | The rest of RAM can also store arbitrary data for use by the application. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 (c) Michael Bell 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 16 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 20 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 21 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DV Stick Driver 2 | 3 | The DV Stick is a system with 2 RP2040s, one driving the digital video (the GPU) and the other running application code (the CPU). 4 | 5 | This is the repo for the GPU side of the DV stick, which uses PicoDVI to drive the display. 6 | 7 | ## Building 8 | 9 | Note that currently this firmware only builds using gcc 9. 10 | 11 | ## Getting it running 12 | 13 | The driver RP2040 has no flash and is designed to be programmed over SWD either from the debugging port, or direct from the application CPU Pico. 14 | 15 | ## Loading from the CPU 16 | 17 | The build produces a `pico-stick.h` in the `build/` directory that should be copied to `drivers/dv_display` in the [picovision repo](https://github.com/pimoroni/picovision). This is also saved in the github action artifact. 18 | 19 | When the DVDisplay driver is initialized it will upload the image from `pico-stick.h` to the driver RP2040. 20 | 21 | ## Writing your own CPU side firmware 22 | 23 | If you want to bypass the `dv_display` and write your own CPU side firmware, this describes the [format for the data read from RAM](https://github.com/MichaelBell/pico-stick/blob/main/FrameFormat.txt) by the GPU. I would recommend grabbing the SWD programmer from the dv_display driver to get the GPU firmware loaded. 24 | 25 | Between RAM bank switches the CPU interacts with the GPU over I2C, the interface is [documented in a spreadsheet](https://docs.google.com/spreadsheets/d/1PKt1zPrB67C1ntRw4sIHiO5FZF0tHdjhlcEdujFQAuE/edit#gid=0). 26 | 27 | ## Loading over SWD for debugging 28 | 29 | You will need an SWD connection to the debugging port on the DV stick - this is connected to the driver RP2040. If you're on Windows the easiest way is with a RPi Debug Probe, or if you're using a Raspberry Pi you can wire it up to the SWD as normal. 30 | 31 | You'll need to use a customised version of the OpenOCD rp2040.cfg file, because there is no flash attached, and that is assumed by the default config. Copy the file from `openocd/target/` in this repo to scripts/target in your OpenOCD install. 32 | 33 | This is the command for launching OpenOCD on my laptop, you should be able to adapt accordingly: 34 | 35 | C:\Users\mike\pico\openocd-x64-standalone\scripts> ..\openocd.exe -f interface/cmsis-dap.cfg -f target/rp2040-no-flash.cfg -c "adapter speed 5000;bindto 0.0.0.0" 36 | 37 | Once you have OpenOCD running you should be able to load the project in VS code and launch it in the debugger - after setting an appropriate IP for OpenOCD in `launch.json` (if required). 38 | 39 | Alternatively, you should be able to grab the elf from the github build action and program with a command along the lines of: 40 | 41 | gdb-multiarch -ex "target remote localhost:3333" -ex "monitor reset init" -ex "load" -ex "continue" pico-stick.elf 42 | 43 | If you're using an application that normally loads the driver itself,don't forget to comment out `swd_load_program` in `dv_display.cpp`. 44 | 45 | ## Credits 46 | 47 | This driver would not be possible without [PicoDVI](https://github.com/Wren6991/PicoDVI) by Luke Wren - this repo is just a wrapper around PicoDVI. The version used here has several modifications to increase speed at the cost of using more RAM - which for this system is a better tradeoff as the application is running on a different processor. 48 | 49 | The [i2c slave library](https://github.com/vmilea/pico_i2c_slave/) is by Valentin Milea. -------------------------------------------------------------------------------- /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 | #include "aps6404.pio.h" 9 | 10 | namespace pimoroni { 11 | APS6404::APS6404(uint pin_csn, uint pin_d0, PIO pio) 12 | : pin_csn(pin_csn) 13 | , pin_d0(pin_d0) 14 | , pio(pio) 15 | { 16 | // Initialize data pins 17 | for (int i = 0; i < 4; ++i) { 18 | gpio_init(pin_d0 + i); 19 | gpio_disable_pulls(pin_d0 + i); 20 | } 21 | 22 | pio_prog = &sram_reset_program; 23 | pio_offset = pio_add_program(pio, &sram_reset_program); 24 | pio_sm = pio_claim_unused_sm(pio, true); 25 | 26 | // Claim DMA channels 27 | dma_channel = dma_claim_unused_channel(true); 28 | read_cmd_dma_channel = dma_claim_unused_channel(true); 29 | setup_dma_config(); 30 | } 31 | 32 | void APS6404::init() { 33 | aps6404_reset_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0); 34 | 35 | sleep_us(200); 36 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 37 | pio_sm_put_blocking(pio, pio_sm, 0x66000000u); 38 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 39 | pio_sm_put_blocking(pio, pio_sm, 0x99000000u); 40 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 41 | pio_sm_put_blocking(pio, pio_sm, 0x35000000u); 42 | sleep_us(500); 43 | 44 | adjust_clock(); 45 | } 46 | 47 | void APS6404::set_qpi() { 48 | pio_sm_set_enabled(pio, pio_sm, false); 49 | pio_remove_program(pio, pio_prog, pio_offset); 50 | 51 | pio_prog = &sram_reset_program; 52 | pio_offset = pio_add_program(pio, &sram_reset_program); 53 | aps6404_reset_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0); 54 | pio_sm_put_blocking(pio, pio_sm, 0x00000007u); 55 | pio_sm_put_blocking(pio, pio_sm, 0x35000000u); 56 | 57 | while (!pio_sm_is_tx_fifo_empty(pio, pio_sm) || pio->sm[pio_sm].addr != pio_offset); 58 | 59 | adjust_clock(); 60 | } 61 | 62 | void APS6404::set_spi() { 63 | pio_sm_set_enabled(pio, pio_sm, false); 64 | pio_remove_program(pio, pio_prog, pio_offset); 65 | 66 | pio_prog = &sram_reset_qpi_program; 67 | pio_offset = pio_add_program(pio, &sram_reset_qpi_program); 68 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, false, true); 69 | pio_sm_put_blocking(pio, pio_sm, 0x00000001u); 70 | pio_sm_put_blocking(pio, pio_sm, 0xF5000000u); 71 | } 72 | 73 | void APS6404::adjust_clock() { 74 | pio_sm_set_enabled(pio, pio_sm, false); 75 | pio_remove_program(pio, pio_prog, pio_offset); 76 | 77 | if (clock_get_hz(clk_sys) > 285000000) { 78 | pio_prog = &sram_fast_program; 79 | pio_offset = pio_add_program(pio, &sram_fast_program); 80 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, true, false); 81 | } 82 | else if (clock_get_hz(clk_sys) < 130000000) { 83 | pio_prog = &sram_slow_program; 84 | pio_offset = pio_add_program(pio, &sram_slow_program); 85 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, true, false, false); 86 | } 87 | else { 88 | pio_prog = &sram_program; 89 | pio_offset = pio_add_program(pio, &sram_program); 90 | aps6404_program_init(pio, pio_sm, pio_offset, pin_csn, pin_d0, false, false, false); 91 | } 92 | } 93 | 94 | void APS6404::write(uint32_t addr, uint32_t* data, uint32_t len_in_words) { 95 | wait_for_finish_blocking(); 96 | 97 | for (int len = len_in_words, page_len = std::min((PAGE_SIZE - (addr & (PAGE_SIZE - 1))) >> 2, len_in_words); 98 | len > 0; 99 | addr += page_len << 2, data += page_len, len -= page_len, page_len = std::min(PAGE_SIZE >> 2, len)) 100 | { 101 | wait_for_finish_blocking(); 102 | 103 | pio_sm_put_blocking(pio, pio_sm, (page_len * 8) - 1); 104 | pio_sm_put_blocking(pio, pio_sm, 0x38000000u | addr); 105 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_write); 106 | 107 | dma_channel_configure( 108 | dma_channel, &write_config, 109 | &pio->txf[pio_sm], 110 | data, 111 | page_len, 112 | true 113 | ); 114 | } 115 | } 116 | 117 | void APS6404::read(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words) { 118 | start_read(read_buf, len_in_words); 119 | 120 | uint32_t first_page_len = (PAGE_SIZE - (addr & (PAGE_SIZE - 1))); 121 | 122 | if (first_page_len >= len_in_words << 2) { 123 | pio_sm_put_blocking(pio, pio_sm, (len_in_words * 8) - 4); 124 | pio_sm_put_blocking(pio, pio_sm, 0xeb000000u | addr); 125 | pio_sm_put_blocking(pio, pio_sm, pio_offset + sram_offset_do_read); 126 | } 127 | else { 128 | uint32_t* cmd_buf = add_read_to_cmd_buffer(multi_read_cmd_buffer, addr, len_in_words << 2); 129 | dma_channel_transfer_from_buffer_now(read_cmd_dma_channel, multi_read_cmd_buffer, cmd_buf - multi_read_cmd_buffer); 130 | } 131 | } 132 | 133 | void APS6404::multi_read(uint32_t* addresses, uint32_t* lengths, uint32_t num_reads, uint32_t* read_buf, int chain_channel) { 134 | uint32_t total_len = 0; 135 | uint32_t* cmd_buf = multi_read_cmd_buffer; 136 | for (uint32_t i = 0; i < num_reads; ++i) { 137 | total_len += lengths[i]; 138 | cmd_buf = add_read_to_cmd_buffer(cmd_buf, addresses[i], lengths[i]); 139 | } 140 | 141 | start_read(read_buf, total_len >> 2, chain_channel); 142 | 143 | dma_channel_transfer_from_buffer_now(read_cmd_dma_channel, multi_read_cmd_buffer, cmd_buf - multi_read_cmd_buffer); 144 | } 145 | 146 | void APS6404::start_read(uint32_t* read_buf, uint32_t total_len_in_words, int chain_channel) { 147 | wait_for_finish_blocking(); 148 | 149 | dma_channel_config c = read_config; 150 | if (chain_channel >= 0) { 151 | channel_config_set_chain_to(&c, chain_channel); 152 | } 153 | 154 | dma_channel_configure( 155 | dma_channel, &c, 156 | read_buf, 157 | &pio->rxf[pio_sm], 158 | total_len_in_words, 159 | true 160 | ); 161 | } 162 | 163 | void APS6404::setup_dma_config() { 164 | dma_channel_config c = dma_channel_get_default_config(read_cmd_dma_channel); 165 | channel_config_set_read_increment(&c, true); 166 | channel_config_set_write_increment(&c, false); 167 | channel_config_set_dreq(&c, pio_get_dreq(pio, pio_sm, true)); 168 | channel_config_set_transfer_data_size(&c, DMA_SIZE_32); 169 | 170 | dma_channel_configure( 171 | read_cmd_dma_channel, &c, 172 | &pio->txf[pio_sm], 173 | multi_read_cmd_buffer, 174 | 0, 175 | false 176 | ); 177 | 178 | write_config = dma_channel_get_default_config(dma_channel); 179 | channel_config_set_read_increment(&write_config, true); 180 | channel_config_set_write_increment(&write_config, false); 181 | channel_config_set_dreq(&write_config, pio_get_dreq(pio, pio_sm, true)); 182 | channel_config_set_transfer_data_size(&write_config, DMA_SIZE_32); 183 | channel_config_set_bswap(&write_config, true); 184 | 185 | read_config = dma_channel_get_default_config(dma_channel); 186 | channel_config_set_read_increment(&read_config, false); 187 | channel_config_set_write_increment(&read_config, true); 188 | channel_config_set_dreq(&read_config, pio_get_dreq(pio, pio_sm, false)); 189 | channel_config_set_transfer_data_size(&read_config, DMA_SIZE_32); 190 | channel_config_set_bswap(&read_config, true); 191 | } 192 | 193 | uint32_t* APS6404::add_read_to_cmd_buffer(uint32_t* cmd_buf, uint32_t addr, uint32_t len_in_bytes) { 194 | int32_t len_remaining = len_in_bytes; 195 | uint32_t len = std::min((PAGE_SIZE - (addr & (PAGE_SIZE - 1))), (uint32_t)len_remaining); 196 | 197 | while (true) { 198 | if (len < 2) { 199 | *cmd_buf++ = 0; 200 | *cmd_buf++ = 0xeb000000u | addr; 201 | *cmd_buf++ = pio_offset + sram_offset_do_read_one; 202 | } 203 | else { 204 | *cmd_buf++ = (len * 2) - 4; 205 | *cmd_buf++ = 0xeb000000u | addr; 206 | *cmd_buf++ = pio_offset + sram_offset_do_read; 207 | } 208 | len_remaining -= len; 209 | addr += len; 210 | 211 | if (len_remaining <= 0) break; 212 | 213 | len = len_remaining; 214 | if (len > PAGE_SIZE) len = PAGE_SIZE; 215 | } 216 | 217 | return cmd_buf; 218 | } 219 | 220 | void APS6404::wait_for_finish_blocking() { 221 | dma_channel_wait_for_finish_blocking(dma_channel); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /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 = pio0); 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_words); 27 | 28 | // Start a read, this completes asynchronously, this function only blocks if another 29 | // transfer is already in progress 30 | void read(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words); 31 | 32 | // Start multiple reads to the same buffer. They completes asynchronously, 33 | // this function only blocks if another transfer is already in progress 34 | void multi_read(uint32_t* addresses, uint32_t* lengths, uint32_t num_addresses, uint32_t* read_buf, int chain_channel = -1); 35 | 36 | // Read and block until completion 37 | void read_blocking(uint32_t addr, uint32_t* read_buf, uint32_t len_in_words) { 38 | read(addr, read_buf, len_in_words); 39 | wait_for_finish_blocking(); 40 | } 41 | 42 | // Block until any outstanding read or write completes 43 | void wait_for_finish_blocking(); 44 | 45 | private: 46 | void start_read(uint32_t* read_buf, uint32_t total_len_in_words, int chain_channel = -1); 47 | void setup_dma_config(); 48 | uint32_t* add_read_to_cmd_buffer(uint32_t* cmd_buf, uint32_t addr, uint32_t len_in_words); 49 | 50 | uint pin_csn; // CSn, SCK must be next pin after CSn 51 | uint pin_d0; // D0, D1, D2, D3 must be consecutive 52 | 53 | PIO pio; 54 | uint16_t pio_sm; 55 | uint16_t pio_offset; 56 | const pio_program* pio_prog; 57 | 58 | uint dma_channel; 59 | uint read_cmd_dma_channel; 60 | 61 | dma_channel_config write_config; 62 | dma_channel_config read_config; 63 | 64 | static constexpr int MULTI_READ_MAX_PAGES = 128; 65 | uint32_t multi_read_cmd_buffer[3 * MULTI_READ_MAX_PAGES]; 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /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 | jmp top side 0b01 37 | 38 | ; Special command to read one byte 39 | PUBLIC do_read_one: 40 | ; Wait for 8.5 clocks (6 + 2.5 for latencies) 41 | set x, 6 side 0b00 42 | rwt_lp2: 43 | set pindirs, 0 side 0b10 44 | jmp x-- rwt_lp2 side 0b00 45 | set x, 1 side 0b10 46 | jmp rd_rem side 0b01 [1] 47 | 48 | ; Read command 49 | PUBLIC do_read: 50 | ; Wait for 8.5 clocks (6 + 2.5 for latencies) 51 | set x, 7 side 0b00 52 | rwt_lp: 53 | set pindirs, 0 side 0b10 54 | jmp x-- rwt_lp side 0b00 55 | 56 | set x, 2 side 0b10 57 | rd_lp: 58 | in pins, 4 side 0b00 59 | jmp y--, rd_lp side 0b10 60 | 61 | ; Read the remaining 3 nibbles, but no more clocks so we don't read beyond 62 | ; the requested length from the SRAM. 63 | rd_rem: 64 | in pins, 4 side 0b01 65 | jmp x--, rd_rem side 0b01 66 | .wrap 67 | 68 | ; Program for running at low frequencies (<130MHz RP2040 system clock) 69 | .program sram_slow 70 | .side_set 2 71 | .wrap_target 72 | top: 73 | set x, 6 side 0b01 74 | out y, 32 side 0b01 75 | set pindirs, 0xf side 0b01 76 | 77 | ; Write command, followed by data 78 | addr_lp: 79 | out pins, 4 side 0b00 80 | jmp x--, addr_lp side 0b10 81 | out pins, 4 side 0b00 82 | out pc, 32 side 0b10 83 | PUBLIC do_write: 84 | out pins, 4 side 0b00 85 | jmp y--, do_write side 0b10 86 | set pindirs, 0 side 0b00 87 | jmp top side 0b01 88 | 89 | ; Special command to read one byte 90 | PUBLIC do_read_one: 91 | ; Wait for 8 clocks (6 + 2 for latencies) 92 | set x, 6 side 0b00 93 | rwt_lp2: 94 | set pindirs, 0 side 0b10 95 | jmp x-- rwt_lp2 side 0b00 96 | set x, 1 side 0b10 97 | jmp rd_rem side 0b01 98 | 99 | ; Read command 100 | PUBLIC do_read: 101 | set pindirs, 0 side 0b00 102 | ; Wait for 8 clocks (6 + 2 for latencies) 103 | set x, 6 side 0b10 104 | rwt_lp: 105 | nop side 0b00 106 | jmp x-- rwt_lp side 0b10 107 | 108 | set x, 2 side 0b00 109 | rd_lp: 110 | in pins, 4 side 0b10 111 | jmp y--, rd_lp side 0b00 112 | 113 | ; Read the remaining 3 nibbles, but no more clocks so we don't read beyond 114 | ; the requested length from the SRAM. 115 | rd_rem: 116 | in pins, 4 side 0b01 117 | jmp x--, rd_rem side 0b01 118 | .wrap 119 | 120 | ; Program for running at extreme frequencies (>290MHz RP2040 system clock) 121 | ; Runs clock at 1/3rd RP2040 clock with 66% duty cycle. Out of spec but 122 | ; then everything we are doing here is! 123 | .program sram_fast 124 | .side_set 2 125 | .wrap_target 126 | top: 127 | set x, 6 side 0b01 128 | out y, 32 side 0b01 129 | set pindirs, 0xf side 0b01 130 | 131 | ; Write command, followed by data 132 | addr_lp: 133 | out pins, 4 side 0b00 134 | jmp x--, addr_lp [1] side 0b10 135 | out pins, 4 side 0b00 136 | out pc, 32 [1] side 0b10 137 | PUBLIC do_write: 138 | out pins, 4 side 0b00 139 | jmp y--, do_write [1] side 0b10 140 | set pindirs, 0 side 0b00 141 | jmp top side 0b01 142 | 143 | ; Special command to read one byte 144 | PUBLIC do_read_one: 145 | ; Wait for 8 clocks (6 + 2 for latencies) 146 | set x, 6 side 0b00 147 | rwt_lp2: 148 | set pindirs, 0 side 0b10 149 | jmp x-- rwt_lp2 side 0b00 150 | set x, 1 side 0b10 151 | jmp rd_rem side 0b01 152 | 153 | ; Read command 154 | PUBLIC do_read: 155 | set pindirs, 0 side 0b00 156 | ; Wait for 8 clocks (6 + 2 for latencies) 157 | set x, 6 [1] side 0b10 158 | rwt_lp: 159 | nop side 0b00 160 | jmp x-- rwt_lp [1] side 0b10 161 | 162 | set x, 2 side 0b00 163 | rd_lp: 164 | nop side 0b10 165 | in pins, 4 side 0b10 166 | jmp y--, rd_lp side 0b00 167 | 168 | ; Read the remaining 2 nibbles, but no more clocks so we don't read beyond 169 | ; the requested length from the SRAM. 170 | rd_rem: 171 | in pins, 4 [1] side 0b01 172 | jmp x--, rd_rem side 0b01 173 | .wrap 174 | 175 | 176 | ; Write only program that writes a short (<32 bit) command, 177 | ; ignoring the rest of the 32 bit input word. 178 | ; This is used to write the reset command and enter QPI mode. 179 | .program sram_reset 180 | .side_set 2 181 | .wrap_target 182 | out y, 32 side 0b01 183 | pull side 0b01 184 | wr_lp: 185 | out pins, 1 side 0b00 186 | jmp y--, wr_lp side 0b10 187 | out null, 32 [7] side 0b01 ; Ensure large enough gap between commands for reset time 188 | nop [7] side 0b01 189 | .wrap 190 | 191 | .program sram_reset_qpi 192 | .side_set 2 193 | .wrap_target 194 | out y, 32 side 0b01 195 | pull side 0b01 196 | set pindirs, 0xF side 0b01 197 | wr_lp: 198 | out pins, 4 side 0b00 199 | jmp y--, wr_lp side 0b10 200 | out null, 32 [7] side 0b01 ; Ensure large enough gap between commands for reset time 201 | set pindirs, 0 [7] side 0b01 202 | .wrap 203 | 204 | 205 | % c-sdk { 206 | void aps6404_reset_program_init(PIO pio, uint sm, uint offset, uint csn, uint mosi) { 207 | uint miso = mosi + 1; 208 | pio_gpio_init(pio, csn); 209 | pio_gpio_init(pio, csn + 1); 210 | pio_gpio_init(pio, mosi); 211 | pio_sm_set_consecutive_pindirs(pio, sm, csn, 2, true); 212 | pio_sm_set_consecutive_pindirs(pio, sm, mosi, 1, true); 213 | pio_sm_set_consecutive_pindirs(pio, sm, miso, 1, false); 214 | 215 | pio_sm_config c = sram_reset_program_get_default_config(offset); 216 | sm_config_set_in_pins(&c, miso); 217 | sm_config_set_in_shift(&c, false, true, 32); 218 | sm_config_set_out_pins(&c, mosi, 1); 219 | sm_config_set_out_shift(&c, false, true, 32); 220 | sm_config_set_sideset_pins(&c, csn); 221 | sm_config_set_clkdiv(&c, 4.f); 222 | 223 | pio_sm_init(pio, sm, offset, &c); 224 | pio_sm_set_enabled(pio, sm, true); 225 | } 226 | void aps6404_program_init(PIO pio, uint sm, uint offset, uint csn, uint mosi, bool slow, bool fast, bool reset) { 227 | pio_gpio_init(pio, csn); 228 | pio_gpio_init(pio, csn + 1); 229 | pio_gpio_init(pio, mosi); 230 | pio_gpio_init(pio, mosi + 1); 231 | pio_gpio_init(pio, mosi + 2); 232 | pio_gpio_init(pio, mosi + 3); 233 | pio_sm_set_consecutive_pindirs(pio, sm, csn, 2, true); 234 | pio_sm_set_consecutive_pindirs(pio, sm, mosi, 4, false); 235 | 236 | pio_sm_config c = slow ? sram_slow_program_get_default_config(offset) : 237 | fast ? sram_fast_program_get_default_config(offset) : 238 | reset ? sram_reset_qpi_program_get_default_config(offset) : 239 | sram_program_get_default_config(offset); 240 | sm_config_set_in_pins(&c, mosi); 241 | sm_config_set_in_shift(&c, false, true, 32); 242 | sm_config_set_out_pins(&c, mosi, 4); 243 | sm_config_set_out_shift(&c, false, true, 32); 244 | sm_config_set_set_pins(&c, mosi, 4); 245 | sm_config_set_sideset_pins(&c, csn); 246 | 247 | pio_sm_init(pio, sm, offset, &c); 248 | pio_sm_set_enabled(pio, sm, true); 249 | } 250 | %} 251 | -------------------------------------------------------------------------------- /constants.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef SUPPORT_WIDE_MODES 4 | #define SUPPORT_WIDE_MODES 0 5 | #endif 6 | 7 | constexpr int PALETTE_SIZE = 32; 8 | constexpr int NUM_SCROLL_GROUPS = 8; 9 | 10 | #if !SUPPORT_WIDE_MODES 11 | // Support for normal modes, require <300MHz overclock, 12 | // work on pretty much any screen, up to 80 sprites (if they are small) 13 | constexpr int MAX_SPRITES = 80; 14 | constexpr int MAX_FRAME_WIDTH = 720; 15 | constexpr int MAX_FRAME_HEIGHT = 576; 16 | constexpr int MAX_SPRITE_DATA_BYTES = 0xD800; 17 | constexpr int MAX_SPRITE_WIDTH = 64; 18 | constexpr int MAX_SPRITE_HEIGHT = 32; 19 | constexpr int MAX_PATCHES_PER_LINE = 10; 20 | constexpr int NUM_LINE_BUFFERS = 4; 21 | constexpr int NUM_TMDS_BUFFERS = 8; 22 | #else 23 | // Support for modes up to 720p30, require extreme overclocks 24 | // doesn't work on all screens. Only 32 sprites and 20kB active sprite data 25 | constexpr int MAX_SPRITES = 32; 26 | constexpr int MAX_FRAME_WIDTH = 1280; 27 | constexpr int MAX_FRAME_HEIGHT = 720; 28 | constexpr int MAX_SPRITE_DATA_BYTES = 20480; 29 | constexpr int MAX_SPRITE_WIDTH = 64; 30 | constexpr int MAX_SPRITE_HEIGHT = 32; 31 | constexpr int MAX_PATCHES_PER_LINE = 10; 32 | constexpr int NUM_LINE_BUFFERS = 4; 33 | constexpr int NUM_TMDS_BUFFERS = 7; 34 | #endif -------------------------------------------------------------------------------- /convert_elf.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | if len(sys.argv) < 2: 5 | print("Usage: python convert_elf.py filename.elf.txt") 6 | sys.exit(-1) 7 | 8 | dumped_elf = open(sys.argv[1], "r") 9 | 10 | in_section = False 11 | in_array = False 12 | array_num = 0 13 | print_sections = (".text", ".rodata", ".binary_info", ".data", ".scratch_x", ".scratch_y", ".usb_ram") 14 | address = 0 15 | for line in dumped_elf: 16 | if line.startswith("Contents of section"): 17 | in_section = False 18 | for valid_section in print_sections: 19 | if valid_section in line: 20 | in_section = True 21 | break 22 | continue 23 | 24 | if in_section and line.startswith(" "): 25 | line_address = int(line[1:9], 16) 26 | if address != line_address: 27 | if in_array: 28 | print("};") 29 | address = line_address 30 | print("constexpr uint elf_data{}_addr = 0x{:08x};".format(array_num, address)) 31 | print("const uint elf_data{}[] = {{".format(array_num)) 32 | in_array = True 33 | array_num += 1 34 | try: 35 | for i in range(10,45,9): 36 | data_swapped = int(line[i:i+8], 16) 37 | data = ((data_swapped & 0xFF) << 24) | ((data_swapped & 0xFF00) << 8) | ((data_swapped & 0xFF0000) >> 8) | ((data_swapped & 0xFF000000) >> 24) 38 | print("0x{:08x},".format(data)) 39 | address += 4 40 | except: 41 | pass 42 | 43 | if in_array: 44 | print("};") 45 | 46 | print("const uint section_addresses[] = {") 47 | for i in range(array_num): 48 | print("elf_data{}_addr,".format(i)) 49 | print("};") 50 | print("const uint* section_data[] = {") 51 | for i in range(array_num): 52 | print("elf_data{},".format(i)) 53 | print("};") 54 | print("const uint section_data_len[] = {") 55 | for i in range(array_num): 56 | print("sizeof(elf_data{}),".format(i)) 57 | print("};") 58 | -------------------------------------------------------------------------------- /display.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "pico/stdlib.h" 4 | #include "pico/multicore.h" 5 | #include "display.hpp" 6 | #include "hardware/sync.h" 7 | #include "hardware/structs/bus_ctrl.h" 8 | #include "hardware/pwm.h" 9 | #include "pico/multicore.h" 10 | 11 | #include "pins.hpp" 12 | 13 | extern "C" { 14 | #include "dvi_serialiser.h" 15 | #include "common_dvi_pin_configs.h" 16 | 17 | #include "tmds_double_encode.h" 18 | #include "tmds_encode.h" 19 | } 20 | 21 | using namespace pico_stick; 22 | 23 | #define PROFILE_SCANLINE 0 24 | #define PROFILE_SCANLINE_MAX 0 25 | #define PROFILE_VSYNC 0 26 | 27 | #define TEST_SPRITES 0 28 | 29 | static pico_stick::FrameTableEntry __attribute__((section(".usb_ram.frame_table"))) the_frame_table[MAX_FRAME_HEIGHT]; 30 | 31 | DisplayDriver::DisplayDriver(PIO pio) 32 | : frame_data(ram) 33 | , current_res(RESOLUTION_720x480) 34 | , ram(PIN_RAM_CS, PIN_RAM_D0) 35 | , dvi0{ 36 | .timing{&dvi_timing_720x480p_60hz}, 37 | .ser_cfg{ 38 | .pio = pio, 39 | .sm_tmds = {0, 1, 2}, 40 | .pins_tmds = {PIN_HDMI_D0, PIN_HDMI_D1, PIN_HDMI_D2}, 41 | .pins_clk = PIN_HDMI_CLK, 42 | .invert_diffpairs = true}} 43 | { 44 | frame_table = the_frame_table; 45 | } 46 | 47 | bool DisplayDriver::set_res(pico_stick::Resolution res) 48 | { 49 | const dvi_timing* normal_modes[] = { 50 | &dvi_timing_640x480p_60hz, 51 | &dvi_timing_720x480p_60hz, 52 | &dvi_timing_720x400p_70hz, 53 | &dvi_timing_720x576p_50hz, 54 | }; 55 | 56 | if (res <= RESOLUTION_720x576) { 57 | dvi0.timing = normal_modes[(int)res]; 58 | current_res = res; 59 | printf("Set res %d\n", res); 60 | return true; 61 | } 62 | 63 | #if SUPPORT_WIDE_MODES 64 | const dvi_timing* wide_modes[] = { 65 | &dvi_timing_800x600p_60hz, 66 | &dvi_timing_800x480p_60hz, 67 | &dvi_timing_800x450p_60hz, 68 | &dvi_timing_960x540p_60hz, 69 | &dvi_timing_960x540p_50hz, 70 | &dvi_timing_1280x720p_30hz, 71 | }; 72 | 73 | if (res >= RESOLUTION_800x600 && res <= RESOLUTION_1280x720) { 74 | dvi0.timing = wide_modes[(int)res - (int)RESOLUTION_800x600]; 75 | current_res = res; 76 | return true; 77 | } 78 | #endif 79 | 80 | return false; 81 | } 82 | 83 | namespace { 84 | void core1_main() { 85 | DisplayDriver* driver = (DisplayDriver*)multicore_fifo_pop_blocking(); 86 | driver->run_core1(); 87 | } 88 | } 89 | 90 | void DisplayDriver::run_core1() { 91 | dvi_register_irqs_this_core(&dvi0, DMA_IRQ_0); 92 | while (true) { 93 | sem_acquire_blocking(&dvi_start_sem); 94 | printf("Core 1 up\n"); 95 | dvi_start(&dvi0); 96 | while (true) { 97 | uint32_t line_counter = multicore_fifo_pop_blocking(); 98 | const int8_t lmode = (line_counter & 0xFF000000u) >> 24; 99 | line_counter &= 0xFFFFFFu; 100 | uint32_t *colourbuf = (uint32_t*)multicore_fifo_pop_blocking(); 101 | if (!colourbuf) break; 102 | 103 | uint32_t *tmdsbuf = (uint32_t*)multicore_fifo_pop_blocking(); 104 | prepare_scanline_core1(line_counter, colourbuf, tmdsbuf, lmode); 105 | multicore_fifo_push_blocking(0); 106 | } 107 | } 108 | __builtin_unreachable(); 109 | } 110 | 111 | void DisplayDriver::init() { 112 | //ram.init(); 113 | 114 | if (!ever_inited) { 115 | gpio_init(PIN_VSYNC); 116 | gpio_put(PIN_VSYNC, 0); 117 | gpio_set_dir(PIN_VSYNC, GPIO_OUT); 118 | 119 | dvi_init(&dvi0, next_striped_spin_lock_num(), next_striped_spin_lock_num()); 120 | for (int i = 0; i < NUM_TMDS_BUFFERS; ++i) { 121 | void* bufptr = (void*)&tmds_buffers[i * 3 * MAX_FRAME_WIDTH / DVI_SYMBOLS_PER_WORD]; 122 | queue_add_blocking_u32(&dvi0.q_tmds_free, &bufptr); 123 | } 124 | sem_init(&dvi_start_sem, 0, 1); 125 | hw_set_bits(&bus_ctrl_hw->priority, (BUSCTRL_BUS_PRIORITY_PROC1_BITS | BUSCTRL_BUS_PRIORITY_DMA_R_BITS | BUSCTRL_BUS_PRIORITY_DMA_W_BITS)); 126 | 127 | Sprite::init(); 128 | 129 | for (int i = 0; i < MAX_FRAME_HEIGHT; ++i) { 130 | for (int j = 0; j < MAX_PATCHES_PER_LINE; ++j) { 131 | patches[i][j].data = nullptr; 132 | } 133 | } 134 | 135 | ever_inited = true; 136 | } 137 | else { 138 | dvi_setup(&dvi0); 139 | } 140 | 141 | if (!luts_inited) { 142 | // Setup TMDS symbol LUTs 143 | tmds_double_encode_setup_default_lut(tmds_15bpp_lut, balanced_symbol_luts); 144 | 145 | memcpy(tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 6), tmds_15bpp_lut, PALETTE_SIZE * PALETTE_SIZE * 2); 146 | memcpy(tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 10), tmds_15bpp_lut, PALETTE_SIZE * PALETTE_SIZE * 2); 147 | 148 | luts_inited = true; 149 | } 150 | 151 | // This calculation shouldn't overflow for any resolution we could plausibly support. 152 | const uint32_t pixel_clk_khz = dvi0.timing->bit_clk_khz / 10; 153 | const uint32_t scanline_pixels = dvi0.timing->h_front_porch + dvi0.timing->h_sync_width + dvi0.timing->h_back_porch + dvi0.timing->h_active_pixels; 154 | diags.available_vsync_time = (1000u * (dvi0.timing->v_front_porch + dvi0.timing->v_sync_width + dvi0.timing->v_back_porch) * 155 | scanline_pixels) / pixel_clk_khz; 156 | diags.available_total_scanline_time = (1000u * dvi0.timing->v_active_lines * scanline_pixels) / pixel_clk_khz; 157 | diags.available_time_per_scanline = (1000u * scanline_pixels) / pixel_clk_khz; 158 | printf("%dx%d active area\n", dvi0.timing->h_active_pixels, dvi0.timing->v_active_lines); 159 | printf("Available VSYNC time: %luus\n", diags.available_vsync_time); 160 | printf("Available time for all active scanlines: %luus\n", diags.available_total_scanline_time); 161 | printf("Available time per scanline: %luus\n", diags.available_time_per_scanline); 162 | } 163 | 164 | void DisplayDriver::run() { 165 | if (!frame_data.read_headers()) { 166 | printf("Failed to read header\n"); 167 | return; 168 | } 169 | printf("Configured display size %dx%d, v rep=%d\n", frame_data.config.h_length, frame_data.config.v_length, frame_data.config.v_repeat); 170 | dvi0.vertical_repeat = frame_data.config.v_repeat; 171 | 172 | multicore_launch_core1(core1_main); 173 | multicore_fifo_push_blocking(uint32_t(this)); 174 | 175 | printf("DVI Initialized\n"); 176 | sem_release(&dvi_start_sem); 177 | 178 | #if TEST_SPRITES 179 | constexpr int num_sprites = MAX_SPRITES; 180 | int32_t x[num_sprites]; 181 | int32_t y[num_sprites]; 182 | int32_t xdir[num_sprites]; 183 | int32_t ydir[num_sprites]; 184 | constexpr int sprite_move_shift = 7; 185 | 186 | for (int i = 0; i < num_sprites; ++i) { 187 | #if 1 188 | x[i] = (rand() % 720) << sprite_move_shift; 189 | y[i] = (rand() % 480) << sprite_move_shift; 190 | xdir[i] = (rand() % 61) - 30; 191 | ydir[i] = (rand() % 61) - 30; 192 | #else 193 | x[i] = (i*18) << sprite_move_shift; //(rand() % 640) << sprite_move_shift; 194 | y[i] = (36 * (1 + (i >> 3))) << sprite_move_shift; //(120 + i) << sprite_move_shift; 195 | xdir[i] = 0; 196 | ydir[i] = 0; 197 | #endif 198 | } 199 | #endif 200 | 201 | bool first_frame = true; 202 | uint heartbeat = 9; 203 | //int frame_address_dir = 4; 204 | while (!stop_display) { 205 | if (heartbet_led) { 206 | uint val; 207 | if (heartbeat < 32) val = heartbeat << 3; 208 | else if (heartbeat == 32) val = 255; 209 | else val = (64 - heartbeat) << 3; 210 | if (++heartbeat == 64) heartbeat = 0; 211 | pwm_set_gpio_level(PIN_LED, val * val); 212 | } 213 | 214 | uint32_t vsync_start_time = time_us_32(); 215 | 216 | if (spi_mode) { 217 | ram.set_qpi(); 218 | } 219 | 220 | if (!frame_data.read_headers()) { 221 | // TODO! 222 | return; 223 | } 224 | //printf("%hdx%hd\n", frame_data.config.h_length, frame_data.config.v_length); 225 | 226 | // Update frame counter 227 | if (frame_data.frame_table_header.bank_number != last_bank) { 228 | frame_counter = frame_data.frame_table_header.first_frame; 229 | last_bank = frame_data.frame_table_header.bank_number; 230 | frames_to_next_count = frame_data.frame_table_header.frame_rate_divider; 231 | 232 | if (frame_data.frame_table_header.palette_advance || palette_idx >= frame_data.frame_table_header.num_palettes) { 233 | palette_idx = 0; 234 | } 235 | } 236 | else if (frame_data.frame_table_header.frame_rate_divider != 0) 237 | { 238 | if (--frames_to_next_count <= 0) { 239 | if (++frame_counter >= frame_data.frame_table_header.num_frames) { 240 | frame_counter = 0; 241 | } 242 | if (frame_data.frame_table_header.palette_advance && ++palette_idx >= frame_data.frame_table_header.num_palettes) { 243 | palette_idx = 0; 244 | } 245 | frames_to_next_count = frame_data.frame_table_header.frame_rate_divider; 246 | } 247 | } 248 | 249 | //pwm_set_gpio_level(PIN_LED, frame_data.frame_table_header.bank_number ? 255*255 : 0); 250 | 251 | frame_data.get_frame_table(frame_counter, frame_table); 252 | 253 | if (frame_data.config.v_repeat != dvi0.vertical_repeat) { 254 | printf("Changing v repeat to %d\n", frame_data.config.v_repeat); 255 | // Wait until it is safe to change the vertical repeat 256 | while (dvi0.timing_state.v_state == DVI_STATE_ACTIVE) 257 | __compiler_memory_barrier(); 258 | dvi0.vertical_repeat = frame_data.config.v_repeat; 259 | } 260 | 261 | setup_palette(); 262 | 263 | update_sprites(); 264 | 265 | // Update offsets 266 | for (int i = 1; i < NUM_SCROLL_GROUPS; ++i) { 267 | frame_scroll[i] = next_frame_scroll[i]; 268 | } 269 | 270 | // Read first 2 lines 271 | line_counter = 0; 272 | read_two_lines(0); 273 | ram.wait_for_finish_blocking(); 274 | line_counter = 2; 275 | 276 | diags.peak_scanline_time = std::max(diags.peak_scanline_time, std::max(diags.scanline_max_prep_time[0], diags.scanline_max_prep_time[1])); 277 | diags.vsync_time = time_us_32() - vsync_start_time; 278 | #if PROFILE_SCANLINE 279 | #if PROFILE_SCANLINE_MAX 280 | printf("Ln %luus, lt: %d\n", diags.scanline_max_prep_time[0] + diags.scanline_max_prep_time[1], dvi0.total_late_scanlines); 281 | #else 282 | printf("Ln %luus, lt: %d\n", diags.scanline_total_prep_time[0] + diags.scanline_total_prep_time[1], dvi0.total_late_scanlines); 283 | #endif 284 | #endif 285 | #if PROFILE_VSYNC 286 | printf("VSYNC %luus, late: %d\n", diags.vsync_time, dvi0.total_late_scanlines); 287 | #endif 288 | 289 | if (diags_callback) { 290 | diags.total_late_scanlines = dvi0.total_late_scanlines; 291 | diags_callback(diags); 292 | } 293 | 294 | // Clear per frame diags 295 | diags.scanline_total_prep_time[0] = 0; 296 | diags.scanline_total_prep_time[1] = 0; 297 | diags.scanline_max_prep_time[0] = 0; 298 | diags.scanline_max_prep_time[1] = 0; 299 | diags.scanline_max_sprites[0] = 0; 300 | diags.scanline_max_sprites[1] = 0; 301 | 302 | main_loop(); 303 | 304 | gpio_put(PIN_VSYNC, 0); 305 | 306 | // Grace period for slow RAM bank switch 307 | sleep_us(10); 308 | 309 | #if TEST_SPRITES 310 | // Temp: Move our sprites around 311 | for (int i = 0; i < num_sprites; ++i) { 312 | x[i] += xdir[i]; 313 | y[i] += ydir[i]; 314 | if (x[i] < (-20 << sprite_move_shift) && xdir[i] < 0) xdir[i] = -xdir[i]; 315 | if (x[i] > (720 << sprite_move_shift) && xdir[i] > 0) xdir[i] = -xdir[i]; 316 | if (y[i] < (-20 << sprite_move_shift) && ydir[i] < 0) ydir[i] = -ydir[i]; 317 | if (y[i] > (480 << sprite_move_shift) && ydir[i] > 0) ydir[i] = -ydir[i]; 318 | #if 1 319 | BlendMode blend_mode = BLEND_NONE; 320 | #if 1 321 | if (i & 1) { 322 | if (i & 2) { 323 | blend_mode = BLEND_BLEND; 324 | } 325 | else { 326 | blend_mode = BLEND_DEPTH; 327 | } 328 | } else { 329 | if (i & 2) { 330 | blend_mode = BLEND_BLEND2; 331 | } 332 | else { 333 | blend_mode = BLEND_DEPTH2; 334 | } 335 | } 336 | #else 337 | blend_mode = BLEND_BLEND; 338 | #endif 339 | if (i & 1) 340 | set_sprite(i, 4, blend_mode, x[i] >> sprite_move_shift, y[i] >> sprite_move_shift); 341 | else 342 | set_sprite(i, ((i + heartbeat) >> 3) & 3, blend_mode, x[i] >> sprite_move_shift, y[i] >> sprite_move_shift); 343 | #endif 344 | } 345 | #endif 346 | 347 | #if 0 348 | if (heartbeat == 0) { 349 | frame_data_address_offset += frame_address_dir; 350 | if (frame_data_address_offset >= 255 * 4) { 351 | frame_address_dir = -4; 352 | } 353 | else if (frame_data_address_offset <= 0) { 354 | frame_address_dir = 4; 355 | } 356 | } 357 | #endif 358 | 359 | if (first_frame) { 360 | dvi0.total_late_scanlines = 0; 361 | first_frame = false; 362 | } 363 | } 364 | 365 | dvi_stop(&dvi0); 366 | sleep_ms(1); 367 | 368 | memset(frame_scroll, 0, sizeof(frame_scroll)); 369 | memset(next_frame_scroll, 0, sizeof(next_frame_scroll)); 370 | 371 | for (int i = 0; i < MAX_SPRITES; ++i) { 372 | clear_sprite(i); 373 | } 374 | 375 | if (heartbet_led) { 376 | pwm_set_gpio_level(PIN_LED, 0); 377 | } 378 | 379 | stop_display = false; 380 | } 381 | 382 | void DisplayDriver::main_loop() { 383 | uint pixel_data_read_idx = 1; 384 | while (line_counter < frame_data.config.v_length + 2) { 385 | if (line_counter < frame_data.config.v_length) { 386 | // Read two lines into the buffers we just output 387 | read_two_lines(pixel_data_read_idx); 388 | } 389 | else { 390 | // We are done reading RAM, indicate RAM bank can be switched 391 | if (spi_mode) { 392 | ram.set_spi(); 393 | } 394 | 395 | gpio_put(PIN_VSYNC, 1); 396 | } 397 | 398 | // Flip the buffer index to the one read last time, which is now ready to output 399 | pixel_data_read_idx ^= 1; 400 | 401 | uint32_t *core0_tmds_buf = nullptr, *core1_tmds_buf; 402 | queue_remove_blocking_u32(&dvi0.q_tmds_free, &core1_tmds_buf); 403 | sio_hw->fifo_wr = (line_counter - 2) | (line_mode[pixel_data_read_idx * 2] << 24); 404 | sio_hw->fifo_wr = uint32_t(pixel_ptr[pixel_data_read_idx * 2]); 405 | sio_hw->fifo_wr = uint32_t(core1_tmds_buf); 406 | __sev(); 407 | 408 | if (line_counter < frame_data.config.v_length + 1) { 409 | uint32_t* core0_colour_buf = pixel_ptr[pixel_data_read_idx * 2 + 1]; 410 | 411 | queue_remove_blocking_u32(&dvi0.q_tmds_free, &core0_tmds_buf); 412 | prepare_scanline_core0(line_counter - 1, core0_colour_buf, core0_tmds_buf, line_mode[pixel_data_read_idx * 2 + 1]); 413 | } 414 | 415 | multicore_fifo_pop_blocking(); 416 | queue_add_blocking_u32(&dvi0.q_tmds_valid, &core1_tmds_buf); 417 | if (line_counter < frame_data.config.v_length + 1) { 418 | queue_add_blocking_u32(&dvi0.q_tmds_valid, &core0_tmds_buf); 419 | } 420 | 421 | line_counter += 2; 422 | } 423 | } 424 | 425 | void DisplayDriver::set_sprite(int8_t i, int16_t idx, BlendMode mode, int16_t x, int16_t y, uint8_t v_scale) { 426 | if (i < MAX_SPRITES) { 427 | sprites[i].set_sprite_table_idx(idx); 428 | sprites[i].set_blend_mode(mode); 429 | sprites[i].set_sprite_pos(x, y); 430 | sprites[i].set_sprite_v_scale(v_scale); 431 | } 432 | } 433 | 434 | void DisplayDriver::move_sprite(int8_t i, int16_t x, int16_t y) { 435 | sprites[i].set_sprite_pos(x, y); 436 | } 437 | 438 | void DisplayDriver::clear_sprite(int8_t i) { 439 | sprites[i].set_sprite_table_idx(-1); 440 | } 441 | 442 | void DisplayDriver::clear_late_scanlines() { 443 | dvi0.total_late_scanlines = 0; 444 | } 445 | 446 | void DisplayDriver::prepare_scanline_core0(int line_number, uint32_t* pixel_data, uint32_t* tmds_buf, int scanline_mode) { 447 | uint32_t start = time_us_32(); 448 | 449 | int i; 450 | for (i = 0; i < MAX_PATCHES_PER_LINE; ++i) { 451 | if (patches[line_number][i].data) { 452 | if (scanline_mode & (RGB888 | PALETTE)) Sprite::apply_blend_patch_byte_x(patches[line_number][i], (uint8_t*)pixel_data); 453 | else Sprite::apply_blend_patch_555_y(patches[line_number][i], (uint8_t*)pixel_data); 454 | patches[line_number][i].data = nullptr; 455 | } 456 | else { 457 | break; 458 | } 459 | } 460 | if (scanline_mode & DOUBLE_PIXELS) { 461 | if ((scanline_mode & (PALETTE | RGB888)) == (PALETTE | RGB888)) tmds_encode_palette_data(pixel_data, tmds_doubled_palette256_lut, tmds_buf, frame_data.config.h_length >> 1, 0, 8); 462 | else if (scanline_mode & RGB888) tmds_encode_24bpp(pixel_data, tmds_buf, frame_data.config.h_length >> 1); 463 | else if (scanline_mode & PALETTE) tmds_encode_palette_data(pixel_data, tmds_doubled_palette_lut, tmds_buf, frame_data.config.h_length >> 1, 2, 5); 464 | else tmds_encode_15bpp(pixel_data, tmds_buf, frame_data.config.h_length >> 1); 465 | } 466 | else if (scanline_mode & PALETTE) tmds_encode_fullres_palette(pixel_data, tmds_palette_luts, tmds_buf, frame_data.config.h_length); 467 | else tmds_encode_fullres_15bpp(pixel_data, tmds_15bpp_lut, tmds_buf, frame_data.config.h_length); 468 | 469 | const uint32_t scanline_time = time_us_32() - start; 470 | diags.scanline_max_prep_time[0] = std::max(scanline_time, diags.scanline_max_prep_time[0]); 471 | diags.scanline_max_sprites[0] = std::max(uint32_t(i), diags.scanline_max_sprites[0]); 472 | diags.scanline_total_prep_time[0] += scanline_time; 473 | } 474 | 475 | void DisplayDriver::prepare_scanline_core1(int line_number, uint32_t* pixel_data, uint32_t* tmds_buf, int scanline_mode) { 476 | uint32_t start = time_us_32(); 477 | 478 | int i; 479 | for (i = 0; i < MAX_PATCHES_PER_LINE; ++i) { 480 | if (patches[line_number][i].data) { 481 | if (scanline_mode & (RGB888 | PALETTE)) Sprite::apply_blend_patch_byte_x(patches[line_number][i], (uint8_t*)pixel_data); 482 | else Sprite::apply_blend_patch_555_x(patches[line_number][i], (uint8_t*)pixel_data); 483 | patches[line_number][i].data = nullptr; 484 | } 485 | else { 486 | break; 487 | } 488 | } 489 | if (scanline_mode & DOUBLE_PIXELS) { 490 | if ((scanline_mode & (PALETTE | RGB888)) == (PALETTE | RGB888)) tmds_encode_palette_data(pixel_data, tmds_doubled_palette256_lut, tmds_buf, frame_data.config.h_length >> 1, 0, 8); 491 | else if (scanline_mode & RGB888) tmds_encode_24bpp(pixel_data, tmds_buf, frame_data.config.h_length >> 1); 492 | else if (scanline_mode & PALETTE) tmds_encode_palette_data(pixel_data, tmds_doubled_palette_lut, tmds_buf, frame_data.config.h_length >> 1, 2, 5); 493 | else tmds_encode_15bpp(pixel_data, tmds_buf, frame_data.config.h_length >> 1); 494 | } 495 | else if (scanline_mode & PALETTE) tmds_encode_fullres_palette(pixel_data, tmds_palette_luts, tmds_buf, frame_data.config.h_length); 496 | else tmds_encode_fullres_15bpp(pixel_data, tmds_15bpp_lut, tmds_buf, frame_data.config.h_length); 497 | 498 | const uint32_t scanline_time = time_us_32() - start; 499 | diags.scanline_max_prep_time[1] = std::max(scanline_time, diags.scanline_max_prep_time[1]); 500 | diags.scanline_max_sprites[1] = std::max(uint32_t(i), diags.scanline_max_sprites[1]); 501 | diags.scanline_total_prep_time[1] += scanline_time; 502 | } 503 | 504 | void DisplayDriver::read_two_lines(uint idx) { 505 | uint32_t addresses[4]; 506 | uint32_t read_lengths[4]; 507 | uint32_t* ptr = pixel_data[idx]; 508 | int address_idx = 0; 509 | 510 | for (int i = 0; i < 2; ++i) { 511 | const FrameTableEntry& entry = frame_table[line_counter + i]; 512 | const ScrollConfig& scroll_config = frame_scroll[entry.frame_offset_idx()]; 513 | 514 | uint32_t addr = entry.line_address() + scroll_config.start_address_offset; 515 | if (scroll_config.max_start_address > 0 && addr >= scroll_config.max_start_address) { 516 | addr = entry.line_address() + scroll_config.start_address_offset2; 517 | } 518 | 519 | addresses[address_idx] = addr; 520 | pixel_ptr[idx * 2 + i] = ptr; 521 | 522 | const bool double_pixels = (entry.h_repeat() == 2); 523 | uint32_t line_length = frame_data.config.h_length * get_pixel_data_len(entry.line_mode()); 524 | if (double_pixels) line_length >>= 1; 525 | ptr += line_length >> 2; 526 | 527 | if (scroll_config.wrap_position > 0 && (uint32_t)scroll_config.wrap_position < line_length) { 528 | read_lengths[address_idx++] = scroll_config.wrap_position; 529 | addresses[address_idx] = addr + scroll_config.wrap_offset; 530 | read_lengths[address_idx++] = line_length - scroll_config.wrap_position; 531 | } 532 | else { 533 | read_lengths[address_idx++] = line_length; 534 | } 535 | 536 | int8_t lmode = 0; 537 | if (double_pixels) lmode |= DOUBLE_PIXELS; 538 | if (entry.line_mode() == MODE_PALETTE256) lmode |= PALETTE | RGB888; 539 | else if (entry.line_mode() == MODE_PALETTE) lmode |= PALETTE; 540 | else if (entry.line_mode() == MODE_RGB888) lmode |= RGB888; 541 | line_mode[idx * 2 + i] = lmode; 542 | } 543 | 544 | ram.multi_read(addresses, read_lengths, address_idx, pixel_data[idx]); 545 | } 546 | 547 | void DisplayDriver::setup_palette() { 548 | if (frame_data.frame_table_header.num_palettes == 0) return; 549 | 550 | uint8_t palette[PALETTE_SIZE * 3]; 551 | frame_data.get_palette(palette_idx, frame_counter, palette); 552 | ram.wait_for_finish_blocking(); 553 | 554 | if (balanced_symbol_luts) { 555 | tmds_double_encode_setup_balanced_lut(palette, tmds_palette_luts, 3); 556 | tmds_double_encode_setup_balanced_lut(palette + 1, tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 4), 3); 557 | tmds_double_encode_setup_balanced_lut(palette + 2, tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 8), 3); 558 | } 559 | else { 560 | tmds_double_encode_setup_lut(palette, tmds_palette_luts, 3); 561 | tmds_double_encode_setup_lut(palette + 1, tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 4), 3); 562 | tmds_double_encode_setup_lut(palette + 2, tmds_palette_luts + (PALETTE_SIZE * PALETTE_SIZE * 8), 3); 563 | } 564 | tmds_setup_palette_symbols(palette, tmds_doubled_palette_lut, PALETTE_SIZE, 32); 565 | 566 | if (frame_data.frame_table_header.num_palettes >= palette_idx + 8) { 567 | // 256 colour palette (pixel doubled only) 568 | tmds_setup_palette_symbols(palette, tmds_doubled_palette256_lut, 32, 256); 569 | for (int i = 1; i < 8; ++i) { 570 | frame_data.get_palette(palette_idx + i, frame_counter, palette); 571 | ram.wait_for_finish_blocking(); 572 | tmds_setup_palette_symbols(palette, tmds_doubled_palette256_lut + 32 * i, 32, 256); 573 | } 574 | } 575 | } 576 | 577 | void DisplayDriver::update_sprites() { 578 | Sprite::clear_sprite_data(); 579 | for (int i = 0; i < MAX_SPRITES; ++i) { 580 | int16_t sprite_table_idx = sprites[i].get_sprite_table_idx(); 581 | if (sprite_table_idx < 0) continue; 582 | 583 | bool copied = false; 584 | for (int j = 0; j < i; ++j) { 585 | if (sprites[j].get_sprite_table_idx() == sprite_table_idx) { 586 | sprites[i].copy_sprite(sprites[j]); 587 | copied = true; 588 | break; 589 | } 590 | } 591 | 592 | if (!copied) sprites[i].update_sprite(frame_data); 593 | sprites[i].setup_patches(*this); 594 | } 595 | } -------------------------------------------------------------------------------- /display.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "pico/sem.h" 6 | #include "aps6404.hpp" 7 | extern "C" 8 | { 9 | #include "dvi.h" 10 | #include "dvi_timing.h" 11 | } 12 | #include "constants.hpp" 13 | #include "frame_decode.hpp" 14 | #include "sprite.hpp" 15 | 16 | class DisplayDriver 17 | { 18 | public: 19 | DisplayDriver(PIO pio = pio1); 20 | 21 | // May only be called before init 22 | bool set_res(pico_stick::Resolution res); 23 | pico_stick::Resolution get_res() const { return current_res; } 24 | 25 | void init(); 26 | 27 | // Runs the display. This never returns and also starts processing on core1. 28 | // The resolution and hence timing are specified in the PSRAM 29 | // so this sets up/resets the DVI appropriately as they change. 30 | void run(); 31 | 32 | // Setup a sprite with data and position 33 | void set_sprite(int8_t i, int16_t table_idx, pico_stick::BlendMode mode, int16_t x, int16_t y, uint8_t v_scale=1); 34 | 35 | // Move an existing sprite 36 | void move_sprite(int8_t i, int16_t x, int16_t y); 37 | 38 | // Disbale a sprite 39 | void clear_sprite(int8_t i); 40 | 41 | void set_frame_data_address_offset(int idx, int offset, uint32_t max_addr, int offset2) { 42 | next_frame_scroll[idx].start_address_offset = offset; 43 | next_frame_scroll[idx].max_start_address = max_addr; 44 | next_frame_scroll[idx].start_address_offset2 = offset2; 45 | } 46 | 47 | void set_scroll_wrap(int idx, int16_t position, int16_t offset) { 48 | next_frame_scroll[idx].wrap_position = position; 49 | next_frame_scroll[idx].wrap_offset = offset + position; 50 | } 51 | 52 | // Override the value of frame_counter, used to switch frames if the frame divider is 0 53 | void set_frame_counter(int val) { 54 | frame_counter = val; 55 | } 56 | 57 | // Set the value of palette index 58 | void set_palette_idx(int val) { 59 | palette_idx = val; 60 | } 61 | 62 | // Called internally by run(). 63 | void run_core1(); 64 | 65 | pimoroni::APS6404 &get_ram() { return ram; } 66 | uint32_t get_clock_khz() { return dvi0.timing->bit_clk_khz; } 67 | 68 | // Diagnostic data - all times in us. 69 | struct Diags { 70 | uint32_t scanline_total_prep_time[2] = {0, 0}; 71 | uint32_t scanline_max_prep_time[2] = {0, 0}; 72 | uint32_t scanline_max_sprites[2] = {0, 0}; 73 | uint32_t vsync_time = 0; 74 | uint32_t peak_scanline_time = 0; 75 | uint32_t total_late_scanlines = 0; 76 | uint32_t available_total_scanline_time = 0; 77 | uint32_t available_time_per_scanline = 0; 78 | uint32_t available_vsync_time = 0; 79 | }; 80 | const Diags& get_diags() const { return diags; } 81 | void clear_peak_scanline_time() { diags.peak_scanline_time = 0; } 82 | void clear_late_scanlines(); 83 | 84 | // Set this callback to get diags info each frame before it is cleared 85 | void (*diags_callback)(const Diags&) = nullptr; 86 | 87 | // Defaults to QPI. If use_spi true then RAM set back to SPI mode for VSYNC. 88 | void set_spi_mode(bool use_spi) { spi_mode = use_spi; } 89 | 90 | void enable_heartbeat(bool enable) { heartbet_led = enable; } 91 | 92 | void enable_balanced_luts(bool enable) { 93 | balanced_symbol_luts = enable; 94 | luts_inited = false; 95 | } 96 | 97 | void stop() { stop_display = true; } 98 | 99 | private: 100 | friend class Sprite; 101 | 102 | enum ScanlineMode { 103 | DOUBLE_PIXELS = 1, 104 | PALETTE = 2, 105 | RGB888 = 4, 106 | }; 107 | 108 | void main_loop(); 109 | void prepare_scanline_core0(int line_number, uint32_t *pixel_data, uint32_t *tmds_buf, int scanline_mode); 110 | void prepare_scanline_core1(int line_number, uint32_t *pixel_data, uint32_t *tmds_buf, int scanline_mode); 111 | void read_two_lines(uint idx); 112 | void setup_palette(); 113 | void clear_patches(); 114 | void update_sprites(); 115 | 116 | FrameDecode frame_data; 117 | pico_stick::Resolution current_res; 118 | 119 | pimoroni::APS6404 ram; 120 | struct dvi_inst dvi0; 121 | struct semaphore dvi_start_sem; 122 | 123 | uint8_t last_bank = 2; 124 | int frames_to_next_count = 0; 125 | int frame_counter = 0; 126 | int line_counter = 0; 127 | int palette_idx = 0; 128 | 129 | struct ScrollConfig { 130 | // Everything here is in bytes 131 | int start_address_offset = 0; 132 | uint32_t max_start_address = 0; // If non-zero, use start_address_offset2 if addr + start_address_offset >= max_start_address_offset 133 | int start_address_offset2 = 0; 134 | int16_t wrap_position = 0; 135 | int16_t wrap_offset = 0; // This is the offset from the start of the line. 136 | }; 137 | 138 | ScrollConfig frame_scroll[NUM_SCROLL_GROUPS] = {0}; 139 | ScrollConfig next_frame_scroll[NUM_SCROLL_GROUPS] = {0}; 140 | 141 | // Must be as long as the greatest supported frame height. 142 | pico_stick::FrameTableEntry* frame_table; 143 | 144 | // Patches that require blending, done by CPU 145 | Sprite::BlendPatch patches[MAX_FRAME_HEIGHT][MAX_PATCHES_PER_LINE]; 146 | 147 | // Must be long enough to accept two lines plus one padding word at maximum data length and maximum width 148 | uint32_t pixel_data[NUM_LINE_BUFFERS / 2][((MAX_FRAME_WIDTH + 1) * 3) / 2]; 149 | uint32_t* pixel_ptr[NUM_LINE_BUFFERS]; 150 | int8_t line_mode[NUM_LINE_BUFFERS]; 151 | 152 | Sprite sprites[MAX_SPRITES]; 153 | 154 | // Palette TMDS symbol look up tables 155 | uint32_t tmds_palette_luts[PALETTE_SIZE * PALETTE_SIZE * 12]; 156 | uint32_t* tmds_15bpp_lut = &tmds_palette_luts[PALETTE_SIZE * PALETTE_SIZE * 2]; 157 | 158 | // Pixel doubling TMDS LUTs 159 | uint32_t tmds_doubled_palette_lut[PALETTE_SIZE * 3]; 160 | uint32_t tmds_doubled_palette256_lut[256 * 3]; 161 | 162 | // TMDS buffers. Better to have them here than rely on dynamic allocation 163 | uint32_t tmds_buffers[NUM_TMDS_BUFFERS * 3 * MAX_FRAME_WIDTH / DVI_SYMBOLS_PER_WORD]; 164 | 165 | Diags diags; 166 | 167 | // Whether the RAM should be in SPI mode for the app processor 168 | bool spi_mode = false; 169 | 170 | // Whether to output heartbeat LED 171 | bool heartbet_led = true; 172 | 173 | // Whether to stop 174 | volatile bool stop_display = false; 175 | 176 | // Whether we have been initialized 177 | bool ever_inited = false; 178 | bool luts_inited = false; 179 | 180 | // Whether to use balanced symbols for extra device compatability 181 | bool balanced_symbol_luts = false; 182 | }; -------------------------------------------------------------------------------- /edid.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "hardware/i2c.h" 4 | #include "hardware/gpio.h" 5 | 6 | #include "pins.hpp" 7 | 8 | static uint8_t __attribute__((section(".usb_ram.frame_table"))) edid_data[128]; 9 | 10 | uint8_t* read_edid() { 11 | i2c_init(i2c0, 100 * 1000); 12 | gpio_set_function(PIN_HDMI_I2C_SDA, GPIO_FUNC_I2C); 13 | gpio_set_function(PIN_HDMI_I2C_SCL, GPIO_FUNC_I2C); 14 | gpio_pull_up(PIN_HDMI_I2C_SDA); 15 | gpio_pull_up(PIN_HDMI_I2C_SCL); 16 | 17 | printf("Read EDID\n"); 18 | int ret = i2c_read_blocking(i2c0, 0x50, edid_data, 1, false); 19 | if (ret < 0) { 20 | printf("No EDID found\n"); 21 | memset(edid_data, 0, 128); 22 | return edid_data; 23 | } 24 | 25 | edid_data[0] = 0; 26 | i2c_write_blocking(i2c0, 0x50, edid_data, 1, true); 27 | i2c_read_blocking(i2c0, 0x50, edid_data, 128, false); 28 | 29 | #if 0 30 | for (int i = 0; i < 8; ++i) { 31 | for (int j = 0; j < 16; ++j) { 32 | printf("%02x ", edid_data[i*16 + j]); 33 | } 34 | printf("\n"); 35 | } 36 | #endif 37 | 38 | return edid_data; 39 | } 40 | 41 | uint8_t* get_edid_data() { 42 | return edid_data; 43 | } -------------------------------------------------------------------------------- /edid.hpp: -------------------------------------------------------------------------------- 1 | uint8_t* read_edid(); 2 | uint8_t* get_edid_data(); -------------------------------------------------------------------------------- /frame_decode.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "frame_decode.hpp" 4 | 5 | using namespace pico_stick; 6 | 7 | namespace { 8 | constexpr int headers_len_in_bytes = (4 + sizeof(Config) + sizeof(FrameTableHeader)); 9 | constexpr int headers_len_in_words = headers_len_in_bytes / 4; 10 | 11 | } 12 | 13 | bool FrameDecode::read_headers() { 14 | uint32_t buffer[headers_len_in_words]; 15 | 16 | ram.read_blocking(0, buffer, headers_len_in_words); 17 | 18 | if (buffer[0] != 0x4F434950) { 19 | // Magic word wrong. 20 | printf("Magic word should be 0x4F434950, got %08lx\n", buffer[0]); 21 | return false; 22 | } 23 | 24 | memcpy(&config, buffer + 1, sizeof(Config)); 25 | memcpy(&frame_table_header, buffer + 1 + sizeof(Config) / 4, sizeof(FrameTableHeader)); 26 | 27 | return true; 28 | } 29 | 30 | void FrameDecode::get_frame_table(int frame_counter, FrameTableEntry* frame_table) { 31 | uint32_t address = get_frame_table_address() + frame_counter * frame_table_header.frame_table_length; 32 | 33 | ram.read_blocking(address, (uint32_t*)frame_table, frame_table_header.frame_table_length); 34 | } 35 | 36 | void FrameDecode::get_palette(int idx, int frame_counter, uint8_t palette[PALETTE_SIZE * 3]) { 37 | uint32_t address = get_palette_table_address() + idx * PALETTE_SIZE * 3; 38 | 39 | ram.read(address, (uint32_t*)palette, (PALETTE_SIZE * 3) / 4); 40 | } 41 | 42 | void FrameDecode::get_sprite_header(int idx, pico_stick::SpriteHeader* sprite_header) { 43 | uint32_t address = get_sprite_table_address() + idx * 4; 44 | 45 | ram.read_blocking(address, (uint32_t*)sprite_header, 1); 46 | 47 | uint32_t header_data; 48 | ram.read_blocking(sprite_header->sprite_address(), &header_data, 1); 49 | uint8_t* header_ptr = (uint8_t*)&header_data; 50 | sprite_header->width = header_ptr[0]; 51 | sprite_header->height = header_ptr[1]; 52 | } 53 | 54 | uint32_t FrameDecode::get_sprite(int idx, const pico_stick::SpriteHeader& sprite_header, pico_stick::SpriteLine* sprite_line_table, uint32_t* sprite_data, uint32_t buffer_len) { 55 | uint32_t address = sprite_header.sprite_address(); 56 | 57 | assert(sprite_header.height <= MAX_SPRITE_HEIGHT); 58 | ram.read_blocking(address, buffer, (sprite_header.height >> 1) + 1); 59 | 60 | uint32_t total_length = 0; 61 | uint8_t* ptr = (uint8_t*)buffer + 2; 62 | for (uint8_t y = 0; y < sprite_header.height; ++y) { 63 | sprite_line_table[y].data_start = total_length; 64 | sprite_line_table[y].offset = *ptr++; 65 | sprite_line_table[y].width = *ptr++; 66 | total_length += sprite_line_table[y].width * get_pixel_data_len(sprite_header.sprite_mode()); 67 | } 68 | 69 | if (total_length > buffer_len) return 0; 70 | 71 | address += 4 + 4 * (sprite_header.height >> 1); 72 | uint32_t length_in_words = (total_length + 3) >> 2; 73 | ram.read(address, sprite_data, length_in_words); 74 | 75 | return length_in_words << 2; 76 | } 77 | 78 | uint32_t FrameDecode::get_frame_table_address() { 79 | return headers_len_in_bytes; 80 | } 81 | 82 | uint32_t FrameDecode::get_palette_table_address() { 83 | return headers_len_in_bytes + frame_table_header.num_frames * frame_table_header.frame_table_length * 4; 84 | } 85 | 86 | uint32_t FrameDecode::get_sprite_table_address() { 87 | return get_palette_table_address() + frame_table_header.num_palettes * PALETTE_SIZE * 3; 88 | } 89 | -------------------------------------------------------------------------------- /frame_decode.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "pico_stick_frame.hpp" 4 | #include "constants.hpp" 5 | #include "aps6404.hpp" 6 | 7 | class FrameDecode { 8 | public: 9 | static constexpr int MAX_SPRITE_HEIGHT = 64; 10 | 11 | FrameDecode(pimoroni::APS6404& aps6404) 12 | : ram(aps6404) 13 | {} 14 | 15 | // Read the headers from PSRAM. Returns false if PSRAM contents is invalid 16 | bool read_headers(); 17 | 18 | // Fill the frame table from PSRAM, frame_table is an array of at least config.v_length 19 | void get_frame_table(int frame_counter, pico_stick::FrameTableEntry* frame_table); 20 | 21 | // Fill a palette 22 | void get_palette(int idx, int frame_counter, uint8_t palette[PALETTE_SIZE * 3]); 23 | 24 | // Get a sprite header 25 | void get_sprite_header(int idx, pico_stick::SpriteHeader* sprite_header); 26 | 27 | // Fill a sprite into appropriately sized buffer 28 | // Returns the length of the sprite data written, in bytes (always a multiple of 4) 29 | uint32_t get_sprite(int idx, const pico_stick::SpriteHeader& sprite_header, pico_stick::SpriteLine* sprite_line_table, uint32_t* sprite_data, uint32_t buffer_len); 30 | 31 | public: 32 | pico_stick::Config config; 33 | pico_stick::FrameTableHeader frame_table_header; 34 | 35 | private: 36 | uint32_t get_frame_table_address(); 37 | uint32_t get_palette_table_address(); 38 | uint32_t get_sprite_table_address(); 39 | 40 | pimoroni::APS6404& ram; 41 | uint32_t buffer[(MAX_SPRITE_HEIGHT >> 1) + 1]; 42 | }; 43 | -------------------------------------------------------------------------------- /i2c_interface.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "i2c_fifo.h" 4 | #include "i2c_slave.h" 5 | #include "pico/stdlib.h" 6 | #include "hardware/gpio.h" 7 | #include "hardware/structs/usb.h" 8 | 9 | #include "constants.hpp" 10 | #include "pins.hpp" 11 | #include "edid.hpp" 12 | 13 | namespace { 14 | constexpr uint I2C_SLAVE_ADDRESS = 0x0d; 15 | constexpr uint I2C_BAUDRATE = 400000; 16 | 17 | constexpr i2c_inst_t* I2C_INSTANCE = i2c1; 18 | 19 | constexpr uint I2C_SPRITE_REG_BASE = 0; 20 | constexpr uint I2C_SPRITE_DATA_LEN = 7; 21 | 22 | constexpr uint I2C_SCROLL_GROUP_REG_BASE = 0xE0; 23 | constexpr uint I2C_SCROLL_GROUP_DATA_LEN = 13; 24 | 25 | constexpr uint I2C_HIGH_REG_BASE = 0xC0; 26 | constexpr uint I2C_NUM_HIGH_REGS = 0x40; 27 | constexpr uint I2C_GPIO_INPUT_REG = 0xC0; 28 | constexpr uint I2C_GPIO_HI_INPUT_REG = 0xC8; 29 | constexpr uint I2C_EDID_REGISTER = 0xFB; 30 | 31 | // Callback made after an I2C write to high registers is complete. It gives the first register written, 32 | // The last register written, a pointer to the memory representing all high registers (from 0xC0), and a pointer to the scroll group memory 33 | void (*i2c_reg_written_callback)(uint8_t, uint8_t, uint8_t*, uint8_t*) = nullptr; 34 | 35 | // Calback made after an I2C write to sprite memory. It gives the index of the first sprite written, 36 | // number of bytes written (this may go on to further sprites), and a pointer to the memory 37 | // holding all of the sprite info. 38 | void (*i2c_sprite_written_callback)(uint8_t, uint8_t, uint8_t*) = nullptr; 39 | 40 | // To write a series of bytes, the master first 41 | // writes the memory address, followed by the data. The address is automatically incremented 42 | // for each byte transferred, looping back to 0 upon reaching the end. Reading is done 43 | // sequentially from the current memory address. 44 | struct I2CContext 45 | { 46 | uint8_t sprite_mem[MAX_SPRITES * I2C_SPRITE_DATA_LEN]; 47 | uint8_t scroll_group_mem[(NUM_SCROLL_GROUPS - 1) * I2C_SCROLL_GROUP_DATA_LEN]; 48 | alignas(4) uint8_t high_regs[I2C_NUM_HIGH_REGS]; 49 | uint16_t cur_register; 50 | uint8_t first_register; 51 | uint8_t access_idx; 52 | bool got_register; 53 | bool data_written; 54 | } context __attribute__((section(".usb_ram.i2c_context"))); 55 | 56 | // Our handler is called from the I2C ISR, so it must complete quickly. Blocking calls / 57 | // printing to stdio may interfere with interrupt handling. 58 | void i2c_slave_handler(i2c_inst_t *i2c, i2c_slave_event_t event) { 59 | struct I2CContext* cxt = &context; 60 | switch (event) { 61 | case I2C_SLAVE_RECEIVE: // master has written some data 62 | if (!cxt->got_register) { 63 | // writes always start with the memory address 64 | cxt->cur_register = i2c_read_byte(i2c); 65 | cxt->first_register = cxt->cur_register; 66 | cxt->access_idx = 0; 67 | cxt->got_register = true; 68 | cxt->data_written = false; 69 | } else if (cxt->cur_register >= I2C_SPRITE_REG_BASE && cxt->cur_register < I2C_SPRITE_REG_BASE + MAX_SPRITES) { 70 | // save into memory 71 | cxt->sprite_mem[cxt->cur_register * I2C_SPRITE_DATA_LEN + cxt->access_idx] = i2c_read_byte(i2c); 72 | if (++cxt->access_idx == I2C_SPRITE_DATA_LEN) { 73 | cxt->access_idx = 0; 74 | ++cxt->cur_register; 75 | } 76 | cxt->data_written = true; 77 | } else if (cxt->cur_register >= I2C_SCROLL_GROUP_REG_BASE + 1 && cxt->cur_register < I2C_SCROLL_GROUP_REG_BASE + NUM_SCROLL_GROUPS) { 78 | // save into memory 79 | cxt->scroll_group_mem[(cxt->cur_register - I2C_SCROLL_GROUP_REG_BASE - 1) * I2C_SCROLL_GROUP_DATA_LEN + cxt->access_idx] = i2c_read_byte(i2c); 80 | if (++cxt->access_idx == I2C_SCROLL_GROUP_DATA_LEN) { 81 | cxt->access_idx = 0; 82 | ++cxt->cur_register; 83 | } 84 | cxt->data_written = true; 85 | } else if (cxt->cur_register >= I2C_HIGH_REG_BASE && cxt->cur_register < I2C_HIGH_REG_BASE + I2C_NUM_HIGH_REGS) { 86 | cxt->high_regs[cxt->cur_register - I2C_HIGH_REG_BASE] = i2c_read_byte(i2c); 87 | ++cxt->cur_register; 88 | cxt->data_written = true; 89 | } else { 90 | ++cxt->cur_register; 91 | // Read and discard the byte 92 | i2c_read_byte(i2c); 93 | } 94 | break; 95 | case I2C_SLAVE_REQUEST: // master is requesting data 96 | // load from memory 97 | if (cxt->cur_register >= I2C_SPRITE_REG_BASE && cxt->cur_register < I2C_SPRITE_REG_BASE + MAX_SPRITES) { 98 | i2c_write_byte(i2c, cxt->sprite_mem[cxt->cur_register * I2C_SPRITE_DATA_LEN + cxt->access_idx]); 99 | if (++cxt->access_idx == I2C_SPRITE_DATA_LEN) { 100 | cxt->access_idx = 0; 101 | ++cxt->cur_register; 102 | } 103 | } else if (cxt->cur_register >= I2C_SCROLL_GROUP_REG_BASE + 1 && cxt->cur_register < I2C_SCROLL_GROUP_REG_BASE + NUM_SCROLL_GROUPS) { 104 | i2c_write_byte(i2c, cxt->scroll_group_mem[(cxt->cur_register - I2C_SCROLL_GROUP_REG_BASE - 1) * I2C_SCROLL_GROUP_DATA_LEN + cxt->access_idx]); 105 | if (++cxt->access_idx == I2C_SCROLL_GROUP_DATA_LEN) { 106 | cxt->access_idx = 0; 107 | ++cxt->cur_register; 108 | } 109 | } else if (cxt->cur_register == I2C_EDID_REGISTER) { 110 | i2c_write_byte(i2c, get_edid_data()[cxt->access_idx]); 111 | if (++cxt->access_idx == 128) cxt->access_idx = 0; 112 | } else if (cxt->cur_register == I2C_GPIO_INPUT_REG) { 113 | i2c_write_byte(i2c, gpio_get_all() >> 23); 114 | ++cxt->cur_register; 115 | } else if (cxt->cur_register == I2C_GPIO_HI_INPUT_REG) { 116 | i2c_write_byte(i2c, sio_hw->gpio_hi_in | ((usb_hw->phy_direct & 0x60000) >> 11)); 117 | ++cxt->cur_register; 118 | } else if (cxt->cur_register >= I2C_HIGH_REG_BASE && cxt->cur_register < I2C_HIGH_REG_BASE + I2C_NUM_HIGH_REGS) { 119 | i2c_write_byte(i2c, cxt->high_regs[cxt->cur_register - I2C_HIGH_REG_BASE]); 120 | ++cxt->cur_register; 121 | } else { 122 | ++cxt->cur_register; 123 | } 124 | break; 125 | case I2C_SLAVE_FINISH: // master has signalled Stop / Restart 126 | if (cxt->data_written) { 127 | //printf("I2C: W%02hhx-%02hhx\n", cxt->first_register, cxt->cur_register-1); 128 | if (cxt->first_register >= I2C_SPRITE_REG_BASE && cxt->first_register < I2C_SPRITE_REG_BASE + MAX_SPRITES) { 129 | if (i2c_sprite_written_callback) { 130 | if (cxt->access_idx == 0) cxt->cur_register--; 131 | i2c_sprite_written_callback(cxt->first_register, std::min(cxt->cur_register, uint16_t(I2C_SPRITE_REG_BASE + MAX_SPRITES - 1)), cxt->sprite_mem); 132 | } 133 | } else if (cxt->first_register >= I2C_HIGH_REG_BASE && cxt->first_register < I2C_HIGH_REG_BASE + I2C_NUM_HIGH_REGS) { 134 | if (i2c_reg_written_callback) { 135 | i2c_reg_written_callback(cxt->first_register, std::min(cxt->cur_register-1, int(I2C_HIGH_REG_BASE + I2C_NUM_HIGH_REGS - 1)), cxt->high_regs, cxt->scroll_group_mem); 136 | } 137 | } 138 | } 139 | else if (cxt->cur_register > cxt->first_register) { 140 | //printf("I2C: R%02hhx-%02hhx\n", cxt->first_register, cxt->cur_register-1); 141 | } 142 | cxt->got_register = false; 143 | break; 144 | default: 145 | break; 146 | } 147 | } 148 | } 149 | 150 | namespace i2c_slave_if { 151 | uint8_t* init(void (*sprite_callback)(uint8_t, uint8_t, uint8_t*), void (*reg_callback)(uint8_t, uint8_t, uint8_t*, uint8_t*)) { 152 | i2c_reg_written_callback = reg_callback; 153 | i2c_sprite_written_callback = sprite_callback; 154 | 155 | memset(context.sprite_mem, 0xFF, MAX_SPRITES * I2C_SPRITE_DATA_LEN); 156 | memset(context.high_regs, 0, I2C_NUM_HIGH_REGS); 157 | context.got_register = false; 158 | 159 | gpio_init(I2C_SLAVE_SDA_PIN); 160 | gpio_set_function(I2C_SLAVE_SDA_PIN, GPIO_FUNC_I2C); 161 | gpio_pull_up(I2C_SLAVE_SDA_PIN); 162 | 163 | gpio_init(I2C_SLAVE_SCL_PIN); 164 | gpio_set_function(I2C_SLAVE_SCL_PIN, GPIO_FUNC_I2C); 165 | gpio_pull_up(I2C_SLAVE_SCL_PIN); 166 | 167 | i2c_init(I2C_INSTANCE, I2C_BAUDRATE); 168 | i2c_slave_init(I2C_INSTANCE, I2C_SLAVE_ADDRESS, &i2c_slave_handler); 169 | 170 | return context.high_regs; 171 | } 172 | 173 | void deinit() { 174 | i2c_slave_deinit(I2C_INSTANCE); 175 | i2c_deinit(I2C_INSTANCE); 176 | } 177 | 178 | uint8_t get_reg(uint8_t reg) { 179 | return context.high_regs[reg - I2C_HIGH_REG_BASE]; 180 | } 181 | 182 | uint8_t* get_high_reg_table() { 183 | return context.high_regs; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /i2c_interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // I2C slave interface 6 | namespace i2c_slave_if { 7 | // Initialize the I2C slave interface, optionally providing callbacks that are made 8 | // after each I2C write is complete. 9 | // The sprite callback arguments are: 10 | // - First sprite written 11 | // - Last sprite written (same as first if only one sprite written) 12 | // - Pointer start of sprite memory (for all sprites) 13 | // Similarly the register callback arguments are: 14 | // - First register written 15 | // - Last register written (same as first if only one byte written) 16 | // - Pointer start of high register memory (for all registers, the pointer points at register 0xC0) 17 | // The init call returns the pointer to high register memory, so that it can be properly initialized. 18 | uint8_t* init(void (*sprite_callback)(uint8_t, uint8_t, uint8_t*), void (*reg_callback)(uint8_t, uint8_t, uint8_t*, uint8_t*)); 19 | 20 | // Deinitialize before adjusting clocks, then init again. 21 | void deinit(); 22 | 23 | // Get the current value of a high register 24 | uint8_t get_reg(uint8_t reg); 25 | 26 | // Get the high register memory, it is 64 bytes long and is 32-bit aligned 27 | uint8_t* get_high_reg_table(); 28 | } 29 | -------------------------------------------------------------------------------- /i2c_slave/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(i2c_slave INTERFACE) 2 | 3 | target_include_directories(i2c_slave 4 | INTERFACE 5 | ./include) 6 | 7 | target_sources(i2c_slave 8 | INTERFACE 9 | ${CMAKE_CURRENT_LIST_DIR}/i2c_slave.c 10 | ) 11 | 12 | target_link_libraries(i2c_slave 13 | INTERFACE 14 | hardware_i2c 15 | hardware_irq 16 | ) 17 | -------------------------------------------------------------------------------- /i2c_slave/i2c_slave.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Valentin Milea 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | typedef struct i2c_slave_t 11 | { 12 | i2c_inst_t *i2c; 13 | i2c_slave_handler_t handler; 14 | bool transfer_in_progress; 15 | } i2c_slave_t; 16 | 17 | static i2c_slave_t i2c_slaves[2]; 18 | 19 | static inline void finish_transfer(i2c_slave_t *slave) { 20 | if (slave->transfer_in_progress) { 21 | slave->handler(slave->i2c, I2C_SLAVE_FINISH); 22 | slave->transfer_in_progress = false; 23 | } 24 | } 25 | 26 | static void __not_in_flash_func(i2c_slave_irq_handler)(i2c_slave_t *slave) { 27 | i2c_inst_t *i2c = slave->i2c; 28 | i2c_hw_t *hw = i2c_get_hw(i2c); 29 | 30 | uint32_t intr_stat = hw->intr_stat; 31 | if (intr_stat == 0) { 32 | return; 33 | } 34 | if (intr_stat & I2C_IC_INTR_STAT_R_TX_ABRT_BITS) { 35 | hw->clr_tx_abrt; 36 | finish_transfer(slave); 37 | } 38 | if (intr_stat & I2C_IC_INTR_STAT_R_START_DET_BITS) { 39 | hw->clr_start_det; 40 | finish_transfer(slave); 41 | } 42 | if (intr_stat & I2C_IC_INTR_STAT_R_STOP_DET_BITS) { 43 | hw->clr_stop_det; 44 | finish_transfer(slave); 45 | } 46 | if (intr_stat & I2C_IC_INTR_STAT_R_RX_FULL_BITS) { 47 | slave->transfer_in_progress = true; 48 | slave->handler(i2c, I2C_SLAVE_RECEIVE); 49 | } 50 | if (intr_stat & I2C_IC_INTR_STAT_R_RD_REQ_BITS) { 51 | hw->clr_rd_req; 52 | slave->transfer_in_progress = true; 53 | slave->handler(i2c, I2C_SLAVE_REQUEST); 54 | } 55 | } 56 | 57 | static void __not_in_flash_func(i2c0_slave_irq_handler)() { 58 | i2c_slave_irq_handler(&i2c_slaves[0]); 59 | } 60 | 61 | static void __not_in_flash_func(i2c1_slave_irq_handler)() { 62 | i2c_slave_irq_handler(&i2c_slaves[1]); 63 | } 64 | 65 | void i2c_slave_init(i2c_inst_t *i2c, uint8_t address, i2c_slave_handler_t handler) { 66 | assert(i2c == i2c0 || i2c == i2c1); 67 | assert(handler != NULL); 68 | 69 | uint i2c_index = i2c_hw_index(i2c); 70 | i2c_slave_t *slave = &i2c_slaves[i2c_index]; 71 | slave->i2c = i2c; 72 | slave->handler = handler; 73 | 74 | // Note: The I2C slave does clock stretching implicitly after a RD_REQ, while the Tx FIFO is empty. 75 | // There is also an option to enable clock stretching while the Rx FIFO is full, but we leave it 76 | // disabled since the Rx FIFO should never fill up (unless slave->handler() is way too slow). 77 | i2c_set_slave_mode(i2c, true, address); 78 | 79 | i2c_hw_t *hw = i2c_get_hw(i2c); 80 | // unmask necessary interrupts 81 | hw->intr_mask = I2C_IC_INTR_MASK_M_RX_FULL_BITS | I2C_IC_INTR_MASK_M_RD_REQ_BITS | I2C_IC_RAW_INTR_STAT_TX_ABRT_BITS | I2C_IC_INTR_MASK_M_STOP_DET_BITS | I2C_IC_INTR_MASK_M_START_DET_BITS; 82 | 83 | // enable interrupt for current core 84 | uint num = I2C0_IRQ + i2c_index; 85 | irq_set_exclusive_handler(num, i2c_index == 0 ? i2c0_slave_irq_handler : i2c1_slave_irq_handler); 86 | irq_set_enabled(num, true); 87 | } 88 | 89 | void i2c_slave_deinit(i2c_inst_t *i2c) { 90 | assert(i2c == i2c0 || i2c == i2c1); 91 | 92 | uint i2c_index = i2c_hw_index(i2c); 93 | i2c_slave_t *slave = &i2c_slaves[i2c_index]; 94 | assert(slave->i2c == i2c); // should be called after i2c_slave_init() 95 | 96 | slave->i2c = NULL; 97 | slave->handler = NULL; 98 | slave->transfer_in_progress = false; 99 | 100 | uint num = I2C0_IRQ + i2c_index; 101 | irq_set_enabled(num, false); 102 | irq_remove_handler(num, i2c_index == 0 ? i2c0_slave_irq_handler : i2c1_slave_irq_handler); 103 | 104 | i2c_hw_t *hw = i2c_get_hw(i2c); 105 | hw->intr_mask = I2C_IC_INTR_MASK_RESET; 106 | 107 | i2c_set_slave_mode(i2c, false, 0); 108 | } 109 | -------------------------------------------------------------------------------- /i2c_slave/include/i2c_fifo.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Valentin Milea 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #ifndef _I2C_FIFO_H_ 8 | #define _I2C_FIFO_H_ 9 | 10 | #include 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | /** \file i2c_fifo.h 17 | * 18 | * \brief I2C non-blocking r/w. 19 | */ 20 | 21 | /** 22 | * \brief Pop a byte from I2C Rx FIFO. 23 | * 24 | * This function is non-blocking and assumes the Rx FIFO isn't empty. 25 | * 26 | * \param i2c I2C instance. 27 | * \return uint8_t Byte value. 28 | */ 29 | static inline uint8_t i2c_read_byte(i2c_inst_t *i2c) { 30 | i2c_hw_t *hw = i2c_get_hw(i2c); 31 | assert(hw->status & I2C_IC_STATUS_RFNE_BITS); // Rx FIFO must not be empty 32 | return (uint8_t)hw->data_cmd; 33 | } 34 | 35 | /** 36 | * \brief Push a byte into I2C Tx FIFO. 37 | * 38 | * This function is non-blocking and assumes the Tx FIFO isn't full. 39 | * 40 | * \param i2c I2C instance. 41 | * \param value Byte value. 42 | */ 43 | static inline void i2c_write_byte(i2c_inst_t *i2c, uint8_t value) { 44 | i2c_hw_t *hw = i2c_get_hw(i2c); 45 | assert(hw->status & I2C_IC_STATUS_TFNF_BITS); // Tx FIFO must not be full 46 | hw->data_cmd = value; 47 | } 48 | 49 | #ifdef __cplusplus 50 | } 51 | #endif 52 | 53 | #endif // _I2C_FIFO_H_ 54 | -------------------------------------------------------------------------------- /i2c_slave/include/i2c_slave.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Valentin Milea 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #ifndef _I2C_SLAVE_H_ 8 | #define _I2C_SLAVE_H_ 9 | 10 | #include 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | /** \file i2c_slave.h 17 | * 18 | * \brief I2C slave setup. 19 | */ 20 | 21 | /** 22 | * \brief I2C slave event types. 23 | */ 24 | typedef enum i2c_slave_event_t 25 | { 26 | I2C_SLAVE_RECEIVE, /**< Data from master is available for reading. Slave must read from Rx FIFO. */ 27 | I2C_SLAVE_REQUEST, /**< Master is requesting data. Slave must write into Tx FIFO. */ 28 | I2C_SLAVE_FINISH, /**< Master has sent a Stop or Restart signal. Slave may prepare for the next transfer. */ 29 | } i2c_slave_event_t; 30 | 31 | /** 32 | * \brief I2C slave event handler 33 | * 34 | * The event handler will run from the I2C ISR, so it should return quickly (under 25 us at 400 kb/s). 35 | * Avoid blocking inside the handler and split large data transfers across multiple calls for best results. 36 | * When sending data to master, up to `i2c_get_write_available()` bytes can be written without blocking. 37 | * When receiving data from master, up to `i2c_get_read_available()` bytes can be read without blocking. 38 | * 39 | * \param i2c Slave I2C instance. 40 | * \param event Event type. 41 | */ 42 | typedef void (*i2c_slave_handler_t)(i2c_inst_t *i2c, i2c_slave_event_t event); 43 | 44 | /** 45 | * \brief Configure I2C instance for slave mode. 46 | * 47 | * \param i2c I2C instance. 48 | * \param address 7-bit slave address. 49 | * \param handler Called on events from I2C master. It will run from the I2C ISR, on the CPU core 50 | * where the slave was initialized. 51 | */ 52 | void i2c_slave_init(i2c_inst_t *i2c, uint8_t address, i2c_slave_handler_t handler); 53 | 54 | /** 55 | * \brief Restore I2C instance to master mode. 56 | * 57 | * \param i2c I2C instance. 58 | */ 59 | void i2c_slave_deinit(i2c_inst_t *i2c); 60 | 61 | #ifdef __cplusplus 62 | } 63 | #endif 64 | 65 | #endif // _I2C_SLAVE_H_ 66 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "pico/stdlib.h" 5 | #include "hardware/clocks.h" 6 | #include "hardware/vreg.h" 7 | #include "hardware/dma.h" 8 | #include "hardware/gpio.h" 9 | #include "hardware/irq.h" 10 | #include "hardware/pwm.h" 11 | #include "hardware/structs/usb.h" 12 | #include "hardware/watchdog.h" 13 | #include "pico/bootrom.h" 14 | #include "hardware/structs/pads_qspi.h" 15 | #include "hardware/structs/ioqspi.h" 16 | #include "hardware/adc.h" 17 | #include "pico/multicore.h" 18 | 19 | #include "i2c_interface.hpp" 20 | #include "display.hpp" 21 | #include "aps6404.hpp" 22 | #include "edid.hpp" 23 | 24 | #include "pins.hpp" 25 | #include "constants.hpp" 26 | 27 | // These magic value go into the Watchdog registers, allowing the RP2040 to boot 28 | // from the entry point in RAM without trying to boot from flash. 29 | uint32_t __attribute__((section(".wd_data.boot"))) boot_args[4] = {0xb007c0d3, 0x6ff83f2c, 0x15004000, 0x20000001}; 30 | 31 | using namespace pimoroni; 32 | 33 | DisplayDriver display; 34 | 35 | // Return default value in 50mV units 36 | static uint8_t get_default_voltage_for_clock(uint32_t clock_khz) { 37 | if (clock_khz < 300000) { 38 | return 1200 / 50; 39 | } 40 | else if (clock_khz < 380000) { 41 | return 1250 / 50; 42 | } 43 | else { 44 | return 1300 / 50; 45 | } 46 | } 47 | 48 | static uint8_t get_vreg_select_for_voltage(uint8_t voltage_50mv) { 49 | if (voltage_50mv < 1000 / 50) voltage_50mv = 1000 / 50; 50 | if (voltage_50mv > 1300 / 50) voltage_50mv = 1300 / 50; 51 | 52 | return voltage_50mv - (1000 / 50) + VREG_VOLTAGE_1_00; 53 | } 54 | 55 | static int adc_dma_chan = -1; 56 | 57 | static void restart_adc(uint8_t* regs) { 58 | // Set up ADC 59 | adc_run(false); 60 | 61 | adc_fifo_setup( 62 | true, // Write each completed conversion to the sample FIFO 63 | true, // Enable DMA data request (DREQ) 64 | 1, // DREQ (and IRQ) asserted when at least 1 sample present 65 | false, // Disable error bit 66 | false // Full 12-bit readings 67 | ); 68 | 69 | // Go as slow as possible 70 | adc_set_clkdiv(65535); 71 | 72 | if (adc_dma_chan == -1) adc_dma_chan = dma_claim_unused_channel(true); 73 | else { 74 | dma_channel_abort(adc_dma_chan); 75 | adc_fifo_drain(); 76 | } 77 | dma_channel_config cfg = dma_channel_get_default_config(adc_dma_chan); 78 | 79 | // Reading from constant address, writing to I2C register address 80 | channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16); 81 | channel_config_set_read_increment(&cfg, false); 82 | 83 | void *dst; 84 | if (regs[0xC2] == 6) { 85 | // Read from inputs 3 and 4. 86 | adc_select_input(3); 87 | adc_set_round_robin(0b11000); 88 | channel_config_set_write_increment(&cfg, true); 89 | channel_config_set_ring(&cfg, true, 2); 90 | dst = ®s[0xC4]; 91 | } 92 | else { 93 | adc_select_input(4); 94 | channel_config_set_write_increment(&cfg, false); 95 | dst = ®s[0xC6]; 96 | } 97 | 98 | // Pace transfers based on availability of ADC samples 99 | channel_config_set_dreq(&cfg, DREQ_ADC); 100 | 101 | // Transfer "forever" - should work for 2^32 / (48MHz / 65535) = 67.8 days 102 | dma_channel_configure(adc_dma_chan, &cfg, 103 | dst, // dst 104 | &adc_hw->fifo, // src 105 | 0xFFFFFFFF, // transfer count 106 | true // start immediately 107 | ); 108 | 109 | adc_run(true); 110 | } 111 | 112 | void setup_i2c_reg_data(uint8_t* regs); 113 | 114 | void handle_i2c_reg_write(uint8_t reg, uint8_t end_reg, uint8_t* regs, uint8_t* scroll_group_mem) { 115 | // Subtract 0xC0 from regs so that register numbers match addresses 116 | regs -= 0xC0; 117 | 118 | #define REG_WRITTEN(R) (reg <= R && end_reg >= R) 119 | #define REG_WRITTEN2(R_START, R_END) (reg <= R_END && end_reg >= R_START) 120 | 121 | if (REG_WRITTEN(0xC1)) { 122 | display.enable_heartbeat(regs[0xC1] == 2); 123 | 124 | if (regs[0xC1] == 0) pwm_set_gpio_level(PIN_LED, 0); 125 | else if (regs[0xC1] == 1) pwm_set_gpio_level(PIN_LED, 65535); 126 | else if (regs[0xC1] >= 128) { 127 | uint val = (regs[0xC1] - 128) << 1; 128 | pwm_set_gpio_level(PIN_LED, val * val); 129 | } 130 | } 131 | 132 | if (REG_WRITTEN(0xC2)) { 133 | if (dma_hw->ch[adc_dma_chan].write_addr == (uintptr_t)®s[0xC4] && regs[0xC2] != 6) { 134 | // Stop reading ADC from pin 29 135 | restart_adc(regs); 136 | } 137 | 138 | if (regs[0xC2] < 4) { 139 | gpio_set_dir(PIN_ADC, GPIO_IN); 140 | gpio_set_pulls(PIN_ADC, regs[0xC2] & 0x1, regs[0xC2] & 0x2); 141 | gpio_set_input_enabled(PIN_ADC, true); 142 | gpio_set_function(PIN_LED, GPIO_FUNC_SIO); 143 | } else if (regs[0xC2] == 4) { 144 | gpio_put(PIN_ADC, regs[0xC3]); 145 | gpio_set_dir(PIN_ADC, GPIO_OUT); 146 | gpio_set_input_enabled(PIN_ADC, true); 147 | gpio_set_function(PIN_LED, GPIO_FUNC_SIO); 148 | } else if (regs[0xC2] == 5) { 149 | pwm_config config = pwm_get_default_config(); 150 | pwm_config_set_clkdiv(&config, 4.f); 151 | pwm_config_set_wrap(&config, 254); 152 | pwm_init(PIN_ADC_PWM_SLICE_NUM, &config, true); 153 | pwm_set_gpio_level(PIN_ADC, regs[0xC3]); 154 | gpio_set_input_enabled(PIN_ADC, true); 155 | gpio_set_function(PIN_ADC, GPIO_FUNC_PWM); 156 | } else if (regs[0xC2] == 6) { 157 | adc_gpio_init(PIN_ADC); 158 | restart_adc(regs); 159 | } 160 | } 161 | if (REG_WRITTEN(0xC3)) { 162 | if (regs[0xC2] == 4) gpio_put(PIN_ADC, regs[0xC3]); 163 | else if (regs[0xC2] == 5) pwm_set_gpio_level(PIN_ADC, regs[0xC3]); 164 | } 165 | 166 | if (REG_WRITTEN(0xC9)) { 167 | sio_hw->gpio_hi_out = regs[0xC9] & 0x3F; 168 | hw_write_masked(&usb_hw->phy_direct, (regs[0xC9] & 0xC0) << 4, 0xC00); 169 | } 170 | if (REG_WRITTEN(0xCA)) { 171 | sio_hw->gpio_hi_oe = regs[0xCA] & 0x3F; 172 | hw_write_masked(&usb_hw->phy_direct, (regs[0xCA] & 0xC0) << 2, 0x300); 173 | } 174 | if (REG_WRITTEN2(0xCB, 0xCC)) { // Pull up, pull down 175 | constexpr uint8_t gpio_to_pad_map[] = { 0, 2, 3, 4, 5, 1 }; 176 | for (uint i = 0; i < NUM_QSPI_GPIOS; ++i) { 177 | uint32_t val = 0x62; 178 | if (regs[0xCB] & (1 << gpio_to_pad_map[i])) val |= 8; 179 | if (regs[0xCC] & (1 << gpio_to_pad_map[i])) val |= 4; 180 | pads_qspi_hw->io[i] = val; 181 | } 182 | uint32_t usb_pulls = ((regs[0xCB] & 0x80) ? 0x20 : 0) | 183 | ((regs[0xCB] & 0x40) ? 0x02 : 0) | 184 | ((regs[0xCC] & 0x80) ? 0x40 : 0) | 185 | ((regs[0xCC] & 0x40) ? 0x04 : 0); 186 | hw_write_masked(&usb_hw->phy_direct, usb_pulls, 0x66); 187 | } 188 | 189 | if (REG_WRITTEN(0xD3)) { 190 | display.clear_peak_scanline_time(); 191 | } 192 | if (REG_WRITTEN(0xD4)) { 193 | display.clear_late_scanlines(); 194 | } 195 | 196 | for (int i = 1; i < NUM_SCROLL_GROUPS; ++i) { 197 | if (REG_WRITTEN(0xE0 + i)) { 198 | uint8_t* reg_base = &scroll_group_mem[(i-1) * 13]; 199 | int offset = (reg_base[2] << 16) | 200 | (reg_base[1] << 8) | 201 | (reg_base[0]); 202 | uint32_t max_addr = (reg_base[5] << 16) | 203 | (reg_base[4] << 8) | 204 | (reg_base[3]); 205 | int offset2 = (reg_base[8] << 16) | 206 | (reg_base[7] << 8) | 207 | (reg_base[6]); 208 | int16_t wrap_position = (reg_base[10] << 8) | 209 | (reg_base[9]); 210 | int16_t wrap_offset = (reg_base[12] << 8) | 211 | (reg_base[11]); 212 | 213 | display.set_scroll_wrap(i, wrap_position, wrap_offset); 214 | display.set_frame_data_address_offset(i, offset, max_addr, offset2); 215 | } 216 | } 217 | 218 | if (REG_WRITTEN(0xF8)) { 219 | display.set_palette_idx(regs[0xF8]); 220 | } 221 | 222 | if (REG_WRITTEN(0xF9)) { 223 | display.set_frame_counter(regs[0xF9]); 224 | } 225 | 226 | if (REG_WRITTEN(0xFC)) { 227 | if (regs[0xFD] == 0) { // If not started, can change mode 228 | display.set_res((pico_stick::Resolution)(regs[0xFC] & 0x1F)); 229 | display.enable_balanced_luts((regs[0xFC] & 0x80) != 0); 230 | setup_i2c_reg_data(regs + 0xC0); 231 | } 232 | regs[0xFC] = display.get_res(); 233 | } 234 | if (REG_WRITTEN(0xFE)) { 235 | display.set_spi_mode(regs[0xFE] != 0); 236 | } 237 | if (REG_WRITTEN(0xFF)) { 238 | if (regs[0xFF] == 0x01) { 239 | //printf("Stopping\n"); 240 | display.stop(); 241 | } 242 | } 243 | 244 | #undef REG_WRITTEN 245 | #undef REG_WRITTEN2 246 | } 247 | 248 | void handle_i2c_sprite_write(uint8_t sprite, uint8_t end_sprite, uint8_t* sprite_data) { 249 | for (int i = sprite; i <= end_sprite; ++i) { 250 | uint8_t* sprite_ptr = sprite_data + 7 * i; 251 | 252 | int16_t sprite_idx = (int8_t(sprite_ptr[2]) << 8) | sprite_ptr[1]; 253 | int16_t x = (sprite_ptr[4] << 8) | sprite_ptr[3]; 254 | int16_t y = (sprite_ptr[6] << 8) | sprite_ptr[5]; 255 | display.set_sprite(i, sprite_idx, (pico_stick::BlendMode)(sprite_ptr[0] & 0x7), x, y, (sprite_ptr[0] >> 3) + 1); 256 | } 257 | } 258 | 259 | void set_i2c_reg_data_for_frame(uint8_t* regs, const DisplayDriver::Diags& diags) { 260 | regs -= 0xC0; 261 | 262 | // To reduce latency these are now handled directly in the I2C interface 263 | //regs[0xC0] = gpio_get_all() >> 23; 264 | //regs[0xC8] = sio_hw->gpio_hi_in | ((usb_hw->phy_direct & 0x60000) >> 11); 265 | 266 | regs[0xD0] = (diags.vsync_time * 200) / diags.available_vsync_time; 267 | regs[0xD1] = ((diags.scanline_total_prep_time[0] + diags.scanline_total_prep_time[1]) * 100) / diags.available_total_scanline_time; 268 | regs[0xD2] = std::max(diags.scanline_max_prep_time[0], diags.scanline_max_prep_time[1]); 269 | regs[0xD3] = diags.peak_scanline_time; 270 | regs[0xD4] = diags.total_late_scanlines; 271 | regs[0xD5] = (diags.total_late_scanlines) >> 8; 272 | regs[0xD6] = (diags.total_late_scanlines) >> 16; 273 | regs[0xD7] = (diags.total_late_scanlines) >> 24; 274 | regs[0xD8] = std::max(diags.scanline_max_sprites[0], diags.scanline_max_sprites[1]); 275 | } 276 | 277 | void handle_display_diags_callback(const DisplayDriver::Diags& diags) { 278 | set_i2c_reg_data_for_frame(i2c_slave_if::get_high_reg_table(), diags); 279 | } 280 | 281 | void setup_i2c_reg_data(uint8_t* regs) { 282 | set_i2c_reg_data_for_frame(regs, display.get_diags()); 283 | 284 | // Subtract 0xC0 from regs so that register numbers match addresses 285 | regs -= 0xC0; 286 | 287 | regs[0xC1] = 2; // LED defaults to heartbeat 288 | 289 | // System info 290 | uint32_t clock_10khz = display.get_clock_khz() / 10; 291 | regs[0xDC] = clock_10khz & 0xFF; 292 | regs[0xDD] = clock_10khz >> 8; 293 | regs[0xDE] = get_default_voltage_for_clock(display.get_clock_khz()); 294 | 295 | regs[0xFC] = display.get_res(); 296 | } 297 | 298 | void configure_usb_gpio() { 299 | usb_hw->phy_direct_override = 0x11FC; 300 | } 301 | 302 | int main() { 303 | stdio_init_all(); 304 | 305 | // Setup switches B and C 306 | gpio_init(PIN_SW_B); 307 | gpio_init(PIN_SW_C); 308 | gpio_pull_up(PIN_SW_B); 309 | gpio_pull_up(PIN_SW_C); 310 | 311 | // Set up I2S (not used yet, but make sure we don't interfere) 312 | gpio_init(PIN_I2S_DATA); 313 | gpio_init(PIN_I2S_BCLK); 314 | gpio_init(PIN_I2S_LRCLK); 315 | gpio_disable_pulls(PIN_I2S_DATA); 316 | gpio_disable_pulls(PIN_I2S_BCLK); 317 | gpio_disable_pulls(PIN_I2S_LRCLK); 318 | 319 | // Set up GPIO 320 | gpio_init(PIN_ADC); 321 | gpio_disable_pulls(PIN_ADC); 322 | sio_hw->gpio_hi_oe = 0; 323 | for (uint i = 0; i < NUM_QSPI_GPIOS; ++i) { 324 | pads_qspi_hw->io[i] = 0x52; 325 | ioqspi_hw->io[i].ctrl = 5; 326 | } 327 | 328 | // Setup ADC 329 | adc_init(); 330 | adc_set_temp_sensor_enabled(true); 331 | 332 | // Setup heartbeat LED 333 | gpio_set_function(PIN_LED, GPIO_FUNC_PWM); 334 | { 335 | pwm_config config = pwm_get_default_config(); 336 | pwm_config_set_clkdiv(&config, 4.f); 337 | pwm_init(PIN_LED_PWM_SLICE_NUM, &config, true); 338 | pwm_set_gpio_level(PIN_LED, 0); 339 | } 340 | 341 | configure_usb_gpio(); 342 | 343 | uint8_t* regs = i2c_slave_if::init(handle_i2c_sprite_write, handle_i2c_reg_write); 344 | setup_i2c_reg_data(regs); 345 | regs -= 0xC0; 346 | restart_adc(regs); 347 | printf("DV Display Driver I2C Initialised\n"); 348 | 349 | read_edid(); 350 | 351 | while(true) { 352 | // Wait for I2C to indicate we should start 353 | while (regs[0xFD] == 0) __wfe(); 354 | display.init(); 355 | display.diags_callback = handle_display_diags_callback; 356 | printf("DV Display Driver Initialised\n"); 357 | 358 | // Deinit I2C before adjusting clock 359 | i2c_slave_if::deinit(); 360 | 361 | // Set voltage to value from I2C register - in future this might have been altered since boot. 362 | uint vreg_select = get_vreg_select_for_voltage(i2c_slave_if::get_high_reg_table()[0xDE - 0xC0]); 363 | hw_write_masked(&vreg_and_chip_reset_hw->vreg, vreg_select << VREG_AND_CHIP_RESET_VREG_VSEL_LSB, VREG_AND_CHIP_RESET_VREG_VSEL_BITS); 364 | sleep_ms(10); 365 | 366 | set_sys_clock_khz(display.get_clock_khz(), true); 367 | 368 | stdio_init_all(); 369 | display.get_ram().adjust_clock(); 370 | 371 | // Reinit I2C now clock is set. 372 | i2c_slave_if::init(handle_i2c_sprite_write, handle_i2c_reg_write); 373 | 374 | printf("DV Driver: Clock configured\n"); 375 | 376 | display.run(); 377 | 378 | // Set the clock rate back down to 125MHz. 379 | // It appears that weird stuff happens if we try to reset core 1 380 | // while running at 400MHz!! 381 | 382 | // Deinit I2C before adjusting clock 383 | i2c_slave_if::deinit(); 384 | 385 | set_sys_clock_khz(125000, true); 386 | 387 | stdio_init_all(); 388 | multicore_reset_core1(); 389 | 390 | // Reinit I2C now clock is set. 391 | i2c_slave_if::init(handle_i2c_sprite_write, handle_i2c_reg_write); 392 | 393 | printf("DV Driver: Display stopped\n"); 394 | regs[0xFD] = 0; 395 | } 396 | } -------------------------------------------------------------------------------- /memmap.ld: -------------------------------------------------------------------------------- 1 | /* Based on GCC ARM embedded samples. 2 | Defines the following symbols for use by code: 3 | __exidx_start 4 | __exidx_end 5 | __etext 6 | __data_start__ 7 | __preinit_array_start 8 | __preinit_array_end 9 | __init_array_start 10 | __init_array_end 11 | __fini_array_start 12 | __fini_array_end 13 | __data_end__ 14 | __bss_start__ 15 | __bss_end__ 16 | __end__ 17 | end 18 | __HeapLimit 19 | __StackLimit 20 | __StackTop 21 | __stack (== StackTop) 22 | */ 23 | 24 | MEMORY 25 | { 26 | RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k 27 | SCRATCH_X(rwx) : ORIGIN = 0x20040000, LENGTH = 8k 28 | SCRATCH_Y(rwx) : ORIGIN = 0x15000000, LENGTH = 16k 29 | /*SCRATCH_X(rwx) : ORIGIN = 0x2003e000, LENGTH = 8k 30 | SCRATCH_Y(rwx) : ORIGIN = 0x20040000, LENGTH = 8k*/ 31 | USB_RAM(rwx) : ORIGIN = 0x50100000, LENGTH = 4k 32 | WD_SCRATCH(rw) : ORIGIN = 0x4005801c, LENGTH = 16 33 | } 34 | 35 | ENTRY(_entry_point) 36 | 37 | SECTIONS 38 | { 39 | /* Note in NO_FLASH builds the entry point for both the bootrom, and debugger 40 | entry (ELF entry point), are *first* in the image, and the vector table 41 | follows immediately afterward. This is because the bootrom enters RAM 42 | binaries directly at their lowest address (preferring main RAM over XIP 43 | cache-as-SRAM if both are used). 44 | */ 45 | 46 | .text : { 47 | __logical_binary_start = .; 48 | __reset_start = .; 49 | KEEP (*(.reset)) 50 | __reset_end = .; 51 | KEEP (*(.binary_info_header)) 52 | __binary_info_header_end = .; 53 | . = ALIGN(256); 54 | KEEP (*(.vectors)) 55 | *(.time_critical*) 56 | *(.text*) 57 | . = ALIGN(4); 58 | *(.init) 59 | *(.fini) 60 | /* Pull all c'tors into .text */ 61 | *crtbegin.o(.ctors) 62 | *crtbegin?.o(.ctors) 63 | *(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors) 64 | *(SORT(.ctors.*)) 65 | *(.ctors) 66 | /* Followed by destructors */ 67 | *crtbegin.o(.dtors) 68 | *crtbegin?.o(.dtors) 69 | *(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors) 70 | *(SORT(.dtors.*)) 71 | *(.dtors) 72 | 73 | *(.eh_frame*) 74 | } > RAM 75 | 76 | .rodata : { 77 | *(.rodata*) 78 | . = ALIGN(4); 79 | *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.flashdata*))) 80 | . = ALIGN(4); 81 | } > RAM 82 | 83 | .ARM.extab : 84 | { 85 | *(.ARM.extab* .gnu.linkonce.armextab.*) 86 | } > RAM 87 | 88 | __exidx_start = .; 89 | .ARM.exidx : 90 | { 91 | *(.ARM.exidx* .gnu.linkonce.armexidx.*) 92 | } > RAM 93 | __exidx_end = .; 94 | 95 | /* Machine inspectable binary information */ 96 | . = ALIGN(4); 97 | __binary_info_start = .; 98 | .binary_info : 99 | { 100 | KEEP(*(.binary_info.keep.*)) 101 | *(.binary_info.*) 102 | } > RAM 103 | __binary_info_end = .; 104 | . = ALIGN(4); 105 | 106 | .data : { 107 | /* End of .text-like segments */ 108 | __etext = .; 109 | __data_start__ = .; 110 | *(vtable) 111 | *(.data*) 112 | 113 | . = ALIGN(4); 114 | *(.after_data.*) 115 | 116 | . = ALIGN(4); 117 | /* preinit data */ 118 | PROVIDE_HIDDEN (__mutex_array_start = .); 119 | KEEP(*(SORT(.mutex_array.*))) 120 | KEEP(*(.mutex_array)) 121 | PROVIDE_HIDDEN (__mutex_array_end = .); 122 | 123 | . = ALIGN(4); 124 | /* preinit data */ 125 | PROVIDE_HIDDEN (__preinit_array_start = .); 126 | KEEP(*(SORT(.preinit_array.*))) 127 | KEEP(*(.preinit_array)) 128 | PROVIDE_HIDDEN (__preinit_array_end = .); 129 | 130 | . = ALIGN(4); 131 | /* init data */ 132 | PROVIDE_HIDDEN (__init_array_start = .); 133 | KEEP(*(SORT(.init_array.*))) 134 | KEEP(*(.init_array)) 135 | PROVIDE_HIDDEN (__init_array_end = .); 136 | 137 | . = ALIGN(4); 138 | /* finit data */ 139 | PROVIDE_HIDDEN (__fini_array_start = .); 140 | *(SORT(.fini_array.*)) 141 | *(.fini_array) 142 | PROVIDE_HIDDEN (__fini_array_end = .); 143 | 144 | *(.jcr) 145 | . = ALIGN(4); 146 | /* All data end */ 147 | __data_end__ = .; 148 | } > RAM 149 | 150 | .uninitialized_data (COPY): { 151 | . = ALIGN(4); 152 | *(.uninitialized_data*) 153 | } > RAM 154 | 155 | /* Start and end symbols must be word-aligned */ 156 | .scratch_x : { 157 | __scratch_x_start__ = .; 158 | *(.scratch_x.*) 159 | . = ALIGN(4); 160 | __scratch_x_end__ = .; 161 | } > SCRATCH_X 162 | __scratch_x_source__ = LOADADDR(.scratch_x); 163 | 164 | .scratch_y : { 165 | __scratch_y_start__ = .; 166 | *(.scratch_y.*) 167 | . = ALIGN(4); 168 | __scratch_y_end__ = .; 169 | } > SCRATCH_Y 170 | __scratch_y_source__ = LOADADDR(.scratch_y); 171 | 172 | .usb_ram : { 173 | __usb_ram_start__ = .; 174 | *(.usb_ram.*) 175 | . = ALIGN(4); 176 | __usb_ram_end__ = .; 177 | } > USB_RAM 178 | __usb_ram_source__ = LOADADDR(.usb_ram); 179 | 180 | .wd_data : { 181 | __wd_data_start__ = .; 182 | KEEP(*(.wd_data.*)) 183 | . = ALIGN(4); 184 | __wd_data_end__ = .; 185 | } > WD_SCRATCH 186 | __wd_data_source__ = LOADADDR(.wd_data); 187 | 188 | .bss : { 189 | . = ALIGN(4); 190 | __bss_start__ = .; 191 | *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.bss*))) 192 | *(COMMON) 193 | . = ALIGN(4); 194 | __bss_end__ = .; 195 | } > RAM 196 | 197 | .heap (COPY): 198 | { 199 | __end__ = .; 200 | end = __end__; 201 | *(.heap*) 202 | __HeapLimit = .; 203 | } > RAM 204 | 205 | /* .stack*_dummy section doesn't contains any symbols. It is only 206 | * used for linker to calculate size of stack sections, and assign 207 | * values to stack symbols later 208 | * 209 | * stack1 section may be empty/missing if platform_launch_core1 is not used */ 210 | 211 | /* by default we put core 0 stack at the end of scratch Y, so that if core 1 212 | * stack is not used then all of SCRATCH_X is free. 213 | */ 214 | .stack1_dummy (COPY): 215 | { 216 | *(.stack1*) 217 | } > SCRATCH_X 218 | .stack_dummy (COPY): 219 | { 220 | *(.stack*) 221 | } > SCRATCH_Y 222 | 223 | /* stack limit is poorly named, but historically is maximum heap ptr */ 224 | __StackLimit = ORIGIN(RAM) + LENGTH(RAM); 225 | __StackOneTop = ORIGIN(SCRATCH_X) + LENGTH(SCRATCH_X); 226 | __StackTop = ORIGIN(SCRATCH_Y) + LENGTH(SCRATCH_Y); 227 | __StackOneBottom = __StackOneTop - SIZEOF(.stack1_dummy); 228 | __StackBottom = __StackTop - SIZEOF(.stack_dummy); 229 | PROVIDE(__stack = __StackTop); 230 | 231 | /* Check if data + heap + stack exceeds RAM limit */ 232 | ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed") 233 | 234 | ASSERT( __binary_info_header_end - __logical_binary_start <= 256, "Binary info must be in first 256 bytes of the binary") 235 | /* todo assert on extra code */ 236 | } 237 | 238 | -------------------------------------------------------------------------------- /micropython/ram.py: -------------------------------------------------------------------------------- 1 | from machine import SPI, Pin 2 | 3 | sel = Pin(8, mode=Pin.OUT, value=0) 4 | cs = Pin(17, mode=Pin.OUT, value=1) 5 | 6 | # Bit bang leaving QPI mode 7 | def leave_qpi(): 8 | d0 = Pin(19, mode=Pin.OUT, value=1) 9 | d1 = Pin(20, mode=Pin.OUT, value=1) 10 | d2 = Pin(21, mode=Pin.OUT, value=1) 11 | d3 = Pin(22, mode=Pin.OUT, value=1) 12 | clk = Pin(18, mode=Pin.OUT, value=0) 13 | cs(0) 14 | clk(1) 15 | clk(0) 16 | d1(0) 17 | d3(0) 18 | clk(1) 19 | clk(0) 20 | cs(1) 21 | 22 | del d0, d1, d2, d3, clk 23 | 24 | leave_qpi() 25 | sel(1) 26 | leave_qpi() 27 | sel(0) 28 | 29 | spi = SPI(0, baudrate=33_000_000, sck=Pin(18), mosi=Pin(19), miso=Pin(20)) 30 | 31 | def spi_write(data): 32 | try: 33 | cs(0) 34 | spi.write(data) 35 | finally: 36 | cs(1) 37 | 38 | def reset(): 39 | spi_write(bytes([66])) 40 | spi_write(bytes([99])) 41 | 42 | def write(addr, data): 43 | buf = bytes((2, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF)) 44 | buf += bytes(data) 45 | spi_write(buf) 46 | 47 | def read(addr, data_len): 48 | data = bytearray(data_len + 4) 49 | data[0] = 3 50 | data[1] = (addr >> 16) & 0xFF 51 | data[2] = (addr >> 8) & 0xFF 52 | data[3] = addr & 0xFF 53 | try: 54 | cs(0) 55 | spi.write_readinto(data, data) 56 | finally: 57 | cs(1) 58 | 59 | return data[4:] -------------------------------------------------------------------------------- /micropython/test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import struct 3 | 4 | from pimoroni_i2c import PimoroniI2C 5 | from machine import Pin 6 | 7 | import ram 8 | 9 | i2c = PimoroniI2C(sda=6, scl=7) 10 | vsync = Pin(16, mode=Pin.IN) 11 | 12 | time.sleep(0.5) 13 | 14 | while 13 not in i2c.scan(): 15 | time.sleep(0.5) 16 | 17 | # SPI mode 18 | i2c.writeto_mem(0x0D, 0xFE, bytearray((1,))) 19 | 20 | # Init both RAM banks 21 | ram.sel(0) 22 | ram.reset() 23 | ram.sel(1) 24 | ram.reset() 25 | 26 | # Write header 27 | FRAME_WIDTH = 640 28 | FRAME_HEIGHT = 480 29 | def write_header(bank): 30 | ram.sel(bank) 31 | data = b"PICO" + bytes((1,1,1,1, 32 | 0,0,FRAME_WIDTH & 0xFF,FRAME_WIDTH >> 8, 33 | 0,0,FRAME_HEIGHT & 0xFF,FRAME_HEIGHT >> 8, 34 | 1,0,0,0, 35 | FRAME_HEIGHT & 0xFF,FRAME_HEIGHT >> 8,1,bank, 36 | 0,0,0,0)) 37 | ram.write(0, data) 38 | 39 | write_header(0) 40 | write_header(1) 41 | 42 | # Write frame table for simple frame 43 | BASE_DATA_ADDR = 0x10000 44 | def write_frame_table(): 45 | data_addr = BASE_DATA_ADDR 46 | addr = 28 47 | for i in range(FRAME_HEIGHT): 48 | data = 0x91000000 + data_addr 49 | ram.write(addr, struct.pack("/external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of Pico SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download Pico SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "Pico SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) -------------------------------------------------------------------------------- /pico_stick_frame.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace pico_stick { 6 | enum Resolution : uint8_t { 7 | RESOLUTION_640x480 = 0, 8 | RESOLUTION_720x480 = 1, 9 | RESOLUTION_720x400 = 2, 10 | RESOLUTION_720x576 = 3, 11 | 12 | // These modes require the wide mode build 13 | RESOLUTION_800x600 = 0x10, 14 | RESOLUTION_800x480 = 0x11, 15 | RESOLUTION_800x450 = 0x12, 16 | RESOLUTION_960x540 = 0x13, 17 | RESOLUTION_960x540_50 = 0x14, 18 | RESOLUTION_1280x720 = 0x15, 19 | }; 20 | 21 | enum LineMode : uint8_t { 22 | MODE_ARGB1555 = 1, // 2 bytes per pixel: Alpha 15, Red 14-10, Green 9-5, Blue 4-0 23 | MODE_PALETTE = 2, // 1 byte per pixel: Colour 6-2, Alpha 0 (unused bits must be zero), maps to RGB888 palette entry, 32 colour palette (no pixel doubling yet) 24 | MODE_RGB888 = 3, // 3 bytes per pixel R, G, B (pixel doubling mode only) 25 | MODE_PALETTE256 = 0, // 1 bytes per pixel, 256 colours (pixel doubling mode only), if used for sprites low bit of the palette entry is used as alpha 26 | MODE_INVALID = 0xFF 27 | }; 28 | 29 | enum BlendMode : uint8_t { 30 | BLEND_NONE = 0, // Sprite replaces frame 31 | BLEND_DEPTH = 1, // Depth order, back to front: Sprite A0, Frame A0, Sprite A1, Frame A1 32 | BLEND_DEPTH2 = 2, // Depth order, back to front: Sprite A0, Frame A0, Frame A1, Sprite A1 33 | BLEND_BLEND = 3, // Use frame if Sprite A0 or Frame A1, add if Sprite A1 and Frame A0 34 | BLEND_BLEND2 = 4, // Use frame if Sprite A0, add if Sprite A1 35 | }; 36 | 37 | struct Config { 38 | Resolution res; 39 | uint8_t unused; 40 | uint8_t v_repeat; 41 | bool blank; 42 | 43 | uint16_t h_offset; 44 | uint16_t h_length; 45 | uint16_t v_offset; 46 | uint16_t v_length; 47 | }; 48 | 49 | struct FrameTableHeader { 50 | uint16_t num_frames; 51 | uint16_t first_frame; 52 | uint16_t frame_table_length; 53 | uint8_t frame_rate_divider; 54 | uint8_t bank_number; 55 | uint8_t num_palettes; 56 | bool palette_advance; 57 | uint16_t num_sprites; 58 | }; 59 | 60 | struct FrameTableEntry { 61 | uint32_t entry; 62 | 63 | uint32_t frame_offset_idx() const { return (entry >> 29); } 64 | LineMode line_mode() const { return LineMode((entry >> 27) & 0x3); } 65 | uint32_t h_repeat() const { return (entry >> 24) & 0x7; } 66 | uint32_t line_address() const { return entry & 0xFFFFFF; } 67 | }; 68 | 69 | struct SpriteHeader { 70 | uint32_t hdr; 71 | LineMode sprite_mode() const { return LineMode(hdr >> 28); } 72 | uint32_t palette_index() const { return (hdr >> 24) & 0xF; } 73 | uint32_t sprite_address() const { return hdr & 0xFFFFFF; } 74 | 75 | uint8_t width; 76 | uint8_t height; 77 | }; 78 | 79 | struct SpriteLine { 80 | uint8_t offset; 81 | uint8_t width; 82 | uint16_t data_start; // Index into data of start of line 83 | }; 84 | 85 | inline uint32_t get_pixel_data_len(pico_stick::LineMode mode) { 86 | switch (mode) 87 | { 88 | default: 89 | case MODE_ARGB1555: 90 | return 2; 91 | 92 | case MODE_RGB888: 93 | return 3; 94 | 95 | case MODE_PALETTE: 96 | return 1; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /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 (NOT PIMORONI_PICO_PATH) 5 | set(PIMORONI_PICO_PATH "../../pimoroni-pico/") 6 | endif() 7 | 8 | if(NOT IS_ABSOLUTE ${PIMORONI_PICO_PATH}) 9 | get_filename_component( 10 | PIMORONI_PICO_PATH 11 | "${CMAKE_CURRENT_BINARY_DIR}/${PIMORONI_PICO_PATH}" 12 | ABSOLUTE) 13 | endif() 14 | 15 | if (NOT EXISTS ${PIMORONI_PICO_PATH}) 16 | message(FATAL_ERROR "Directory '${PIMORONI_PICO_PATH}' not found") 17 | endif () 18 | 19 | if (NOT EXISTS ${PIMORONI_PICO_PATH}/pimoroni_pico_import.cmake) 20 | message(FATAL_ERROR "Directory '${PIMORONI_PICO_PATH}' does not appear to contain the Pimoroni Pico libraries") 21 | endif () 22 | 23 | message("PIMORONI_PICO_PATH is ${PIMORONI_PICO_PATH}") 24 | 25 | set(PIMORONI_PICO_PATH ${PIMORONI_PICO_PATH} CACHE PATH "Path to the Pimoroni Pico libraries" FORCE) 26 | 27 | include_directories(${PIMORONI_PICO_PATH}) 28 | list(APPEND CMAKE_MODULE_PATH ${PIMORONI_PICO_PATH}) 29 | -------------------------------------------------------------------------------- /pins.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | constexpr int PIN_HDMI_CEC = 2; 4 | constexpr int PIN_HDMI_HPD = 3; 5 | constexpr int PIN_HDMI_I2C_SDA = 4; 6 | constexpr int PIN_HDMI_I2C_SCL = 5; 7 | constexpr int PIN_HDMI_CLK = 6; 8 | constexpr int PIN_HDMI_D0 = 8; 9 | constexpr int PIN_HDMI_D1 = 10; 10 | constexpr int PIN_HDMI_D2 = 12; 11 | constexpr int I2C_SLAVE_SDA_PIN = 14; 12 | constexpr int I2C_SLAVE_SCL_PIN = 15; 13 | constexpr int PIN_VSYNC = 16; 14 | constexpr int PIN_RAM_CS = 17; 15 | constexpr int PIN_RAM_D0 = 19; 16 | constexpr int PIN_SW_B = 23; 17 | constexpr int PIN_SW_C = 24; 18 | constexpr int PIN_LED = 25; 19 | constexpr int PIN_LED_PWM_SLICE_NUM = 4; 20 | constexpr int PIN_I2S_DATA = 26; 21 | constexpr int PIN_I2S_BCLK = 27; 22 | constexpr int PIN_I2S_LRCLK = 28; 23 | constexpr int PIN_ADC = 29; 24 | constexpr int PIN_ADC_PWM_SLICE_NUM = 6; -------------------------------------------------------------------------------- /sprite.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "sprite.hpp" 5 | #include "display.hpp" 6 | 7 | using namespace pico_stick; 8 | 9 | uint8_t sprite_data_buffer[MAX_SPRITE_DATA_BYTES]; 10 | uint8_t* sprite_data_end; 11 | 12 | void Sprite::update_sprite(FrameDecode& frame_data) { 13 | assert(idx >= 0); 14 | 15 | frame_data.get_sprite_header(idx, &header); 16 | 17 | //printf("Setup sprite width %d, height %d\n", header.width, header.height); 18 | uint32_t sprite_data_len = frame_data.get_sprite(idx, header, lines, (uint32_t*)sprite_data_end, (sprite_data_buffer + MAX_SPRITE_DATA_BYTES) - sprite_data_end); 19 | if (sprite_data_len > 0) { 20 | data = sprite_data_end; 21 | sprite_data_end += sprite_data_len; 22 | } 23 | else { 24 | data = nullptr; 25 | } 26 | } 27 | 28 | void Sprite::copy_sprite(const Sprite& other) { 29 | assert(idx >= 0); 30 | assert(idx == other.idx); 31 | 32 | header = other.header; 33 | memcpy(lines, other.lines, header.height * sizeof(pico_stick::SpriteLine)); 34 | data = other.data; 35 | } 36 | 37 | void Sprite::setup_patches(DisplayDriver& disp) { 38 | assert(idx >= 0); 39 | if (data == nullptr) return; 40 | 41 | for (int i = 0; i < header.height; ++i) { 42 | int line_idx = y + i*v_scale; 43 | if (line_idx < 0 || line_idx >= disp.frame_data.config.v_length) continue; 44 | auto& line = lines[i]; 45 | if (line.width == 0) continue; 46 | 47 | int start = x + line.offset; 48 | int end = start + line.width; 49 | int start_offset = 0; 50 | int line_len = disp.frame_data.config.h_length; 51 | if (disp.frame_table[line_idx].h_repeat() == 2) line_len >>= 1; 52 | 53 | if (end <= 0) continue; 54 | if (start >= line_len) continue; 55 | if (end > line_len) end = line_len; 56 | 57 | const int pixel_size = get_pixel_data_len(header.sprite_mode()); 58 | start *= pixel_size; 59 | end *= pixel_size; 60 | if (start < 0) { 61 | start_offset = -start; 62 | start = 0; 63 | } 64 | 65 | int len = end - start; 66 | if (len > 128) { 67 | len = 128; 68 | } 69 | 70 | uint8_t* const sprite_data_ptr = data + line.data_start + start_offset; 71 | 72 | for (uint8_t i = 0; i < v_scale && line_idx < disp.frame_data.config.v_length; ++i) { 73 | auto* patch = disp.patches[line_idx++]; 74 | int j = 0; 75 | for (; patch->data && j < MAX_PATCHES_PER_LINE; ++j) { 76 | ++patch; 77 | } 78 | if (j == MAX_PATCHES_PER_LINE) { 79 | continue; 80 | } 81 | patch->data = sprite_data_ptr; 82 | patch->offset = start; 83 | patch->len = len; 84 | patch->mode = blend_mode; 85 | } 86 | } 87 | } 88 | 89 | __scratch_x("sprite_buffer") int Sprite::dma_channel_x; 90 | __scratch_x("sprite_buffer") uint32_t Sprite::buffer_x[MAX_SPRITE_WIDTH / 2]; 91 | __scratch_y("sprite_buffer") int Sprite::dma_channel_y; 92 | __scratch_y("sprite_buffer") uint32_t Sprite::buffer_y[MAX_SPRITE_WIDTH / 2]; 93 | 94 | __always_inline static void blend_one_555(BlendMode mode, uint16_t* sprite_pixel_ptr, uint16_t* frame_pixel_ptr) { 95 | constexpr uint16_t alpha_mask = 0x8000; 96 | constexpr uint16_t blend_mask = 0x7BDE; 97 | switch (mode) { 98 | case BLEND_DEPTH: 99 | { 100 | if ((*sprite_pixel_ptr & ~*frame_pixel_ptr) & alpha_mask) { 101 | *frame_pixel_ptr = *sprite_pixel_ptr & (alpha_mask - 1); 102 | } 103 | break; 104 | } 105 | case BLEND_DEPTH2: 106 | { 107 | if (*sprite_pixel_ptr & alpha_mask) { 108 | *frame_pixel_ptr = *sprite_pixel_ptr; 109 | } 110 | break; 111 | } 112 | case BLEND_BLEND: 113 | { 114 | if ((*sprite_pixel_ptr & ~*frame_pixel_ptr) & alpha_mask) { 115 | *frame_pixel_ptr = (uint32_t(*frame_pixel_ptr & blend_mask) + uint32_t(*sprite_pixel_ptr & blend_mask)) >> 1; 116 | } 117 | break; 118 | } 119 | case BLEND_BLEND2: 120 | { 121 | if (*sprite_pixel_ptr & alpha_mask) { 122 | *frame_pixel_ptr = ((uint32_t(*frame_pixel_ptr & blend_mask) + uint32_t(*sprite_pixel_ptr & blend_mask)) >> 1) | (*frame_pixel_ptr & alpha_mask); 123 | } 124 | break; 125 | } 126 | default: 127 | { 128 | *frame_pixel_ptr = *sprite_pixel_ptr; 129 | } 130 | } 131 | } 132 | 133 | __always_inline static void apply_blend_patch_555(const Sprite::BlendPatch& patch, uint8_t* frame_pixel_data, uint32_t* sprite_buffer, int dma_channel) { 134 | uint16_t* sprite_pixel_ptr = (uint16_t*)patch.data; 135 | uint16_t* const sprite_end_ptr = (uint16_t*)(patch.data + patch.len); 136 | uint16_t* frame_pixel_ptr = (uint16_t*)((uint8_t*)frame_pixel_data + patch.offset); 137 | 138 | // Align sprite_pixel_ptr 139 | if ((uintptr_t)sprite_pixel_ptr & 3) { 140 | blend_one_555(patch.mode, sprite_pixel_ptr++, frame_pixel_ptr++); 141 | } 142 | 143 | uint32_t* sprite_pixel_ptr32 = (uint32_t*)sprite_pixel_ptr; 144 | uint32_t* const sprite_end_ptr32 = (uint32_t*)((uintptr_t)sprite_end_ptr & ~3); 145 | uint32_t* frame_pixel_ptr32; 146 | bool dma_reqd; 147 | if (((uintptr_t)frame_pixel_ptr & 3) && sprite_end_ptr32 > sprite_pixel_ptr32) { 148 | dma_channel_wait_for_finish_blocking(dma_channel); 149 | frame_pixel_ptr32 = sprite_buffer; 150 | dma_channel_set_read_addr(dma_channel, frame_pixel_ptr, false); 151 | dma_channel_transfer_to_buffer_now(dma_channel, sprite_buffer, (sprite_end_ptr32 - sprite_pixel_ptr32) << 1); 152 | dma_reqd = true; 153 | } 154 | else { 155 | frame_pixel_ptr32 = (uint32_t*)frame_pixel_ptr; 156 | dma_reqd = false; 157 | } 158 | 159 | // Final pixel 160 | if ((uintptr_t)sprite_end_ptr & 3) { 161 | blend_one_555(patch.mode, sprite_end_ptr - 1, (uint16_t*)((uint8_t*)frame_pixel_data + patch.offset) + (sprite_end_ptr - 1 - (uint16_t*)patch.data)); 162 | } 163 | 164 | if (sprite_pixel_ptr32 >= sprite_end_ptr32) { 165 | return; 166 | } 167 | 168 | constexpr uint32_t alpha_mask = 0x80008000; 169 | constexpr uint32_t blend_mask = 0x7BDE7BDE; 170 | switch (patch.mode) { 171 | case BLEND_DEPTH: 172 | { 173 | for (; sprite_pixel_ptr32 < sprite_end_ptr32; ++sprite_pixel_ptr32, ++frame_pixel_ptr32) { 174 | uint32_t mask = (*sprite_pixel_ptr32 & ~*frame_pixel_ptr32) & alpha_mask; 175 | mask = mask - (mask >> 15); 176 | *frame_pixel_ptr32 = (*frame_pixel_ptr32 & ~mask) | (*sprite_pixel_ptr32 & mask); 177 | } 178 | break; 179 | } 180 | case BLEND_DEPTH2: 181 | { 182 | for (; sprite_pixel_ptr32 < sprite_end_ptr32; ++sprite_pixel_ptr32, ++frame_pixel_ptr32) { 183 | uint32_t mask = *sprite_pixel_ptr32 & alpha_mask; 184 | mask = (mask >> 15) * 0xFFFF; 185 | *frame_pixel_ptr32 = (*frame_pixel_ptr32 & ~mask) | (*sprite_pixel_ptr32 & mask); 186 | } 187 | break; 188 | } 189 | case BLEND_BLEND: 190 | { 191 | // This is the most expensive case, and the compiler's asm is fairly poor (at least on gcc 9.2.1) 192 | // so we have some inline assembler. 193 | #if 0 194 | for (; sprite_pixel_ptr32 < sprite_end_ptr32; ++sprite_pixel_ptr32, ++frame_pixel_ptr32) { 195 | uint32_t mask = (*sprite_pixel_ptr32 & ~*frame_pixel_ptr32) & alpha_mask; 196 | mask = mask - (mask >> 15); 197 | uint32_t blended = (((*frame_pixel_ptr32) & blend_mask) + ((*sprite_pixel_ptr32) & blend_mask)) >> 1; 198 | *frame_pixel_ptr32 = (*frame_pixel_ptr32 & ~mask) | (blended & mask); 199 | } 200 | #else 201 | asm ( ".align 2\n\t" 202 | "1: ldmia %[sprite_pixel_ptr]!, {r1}\n\t" 203 | "ldr r2, [%[frame_pixel_ptr]]\n\t" 204 | "movs r3, r1\n\t" 205 | "bic r3, r2\n\t" 206 | "and r1, %[blend_mask]\n\t" 207 | "and r3, %[alpha_mask]\n\t" 208 | "lsr r0, r3, #15\n\t" 209 | "sub r3, r3, r0\n\t" 210 | "movs r0, r2\n\t" 211 | "and r0, %[blend_mask]\n\t" 212 | "add r0, r0, r1\n\t" 213 | "lsr r0, r0, #1\n\t" 214 | "bic r2, r3\n\t" 215 | "and r0, r3\n\t" 216 | "orr r2, r0\n\t" 217 | "stmia %[frame_pixel_ptr]!, {r2}\n\t" 218 | "cmp %[sprite_pixel_ptr], %[sprite_end_ptr]\n\t" 219 | "bcc 1b\n\t" : 220 | [frame_pixel_ptr] "+l" (frame_pixel_ptr32), 221 | [sprite_pixel_ptr] "+l" (sprite_pixel_ptr32) : 222 | [alpha_mask] "l" (alpha_mask), 223 | [blend_mask] "l" (blend_mask), 224 | [sprite_end_ptr] "r" (sprite_end_ptr32) : 225 | "r1", // sprite_pixel 226 | "r2", // frame_pixel 227 | "r0", "r3", "cc" ); 228 | #endif 229 | break; 230 | } 231 | case BLEND_BLEND2: 232 | { 233 | #if 0 234 | for (; sprite_pixel_ptr32 < sprite_end_ptr32; ++sprite_pixel_ptr32, ++frame_pixel_ptr32) { 235 | uint32_t mask = *sprite_pixel_ptr32 & alpha_mask; 236 | mask = mask - (mask >> 15); 237 | uint32_t blended = (((*frame_pixel_ptr32) & blend_mask) + ((*sprite_pixel_ptr32) & blend_mask)) >> 1; 238 | *frame_pixel_ptr32 = (*frame_pixel_ptr32 & ~mask) | (blended & mask); 239 | } 240 | #else 241 | asm ( ".align 2\n\t" 242 | "2: ldmia %[sprite_pixel_ptr]!, {r1}\n\t" 243 | "ldr r2, [%[frame_pixel_ptr]]\n\t" 244 | "movs r3, r1\n\t" 245 | "and r1, %[blend_mask]\n\t" 246 | "and r3, %[alpha_mask]\n\t" 247 | "lsr r0, r3, #15\n\t" 248 | "sub r3, r3, r0\n\t" 249 | "movs r0, r2\n\t" 250 | "and r0, %[blend_mask]\n\t" 251 | "add r0, r0, r1\n\t" 252 | "lsr r0, r0, #1\n\t" 253 | "bic r2, r3\n\t" 254 | "and r0, r3\n\t" 255 | "orr r2, r0\n\t" 256 | "stmia %[frame_pixel_ptr]!, {r2}\n\t" 257 | "cmp %[sprite_pixel_ptr], %[sprite_end_ptr]\n\t" 258 | "bcc 2b\n\t" : 259 | [frame_pixel_ptr] "+l" (frame_pixel_ptr32), 260 | [sprite_pixel_ptr] "+l" (sprite_pixel_ptr32) : 261 | [alpha_mask] "l" (alpha_mask), 262 | [blend_mask] "l" (blend_mask), 263 | [sprite_end_ptr] "r" (sprite_end_ptr32) : 264 | "r1", // sprite_pixel 265 | "r2", // frame_pixel 266 | "r0", "r3", "cc" ); 267 | #endif 268 | break; 269 | } 270 | default: 271 | { 272 | for (; sprite_pixel_ptr32 < sprite_end_ptr32; ++sprite_pixel_ptr32, ++frame_pixel_ptr32) { 273 | *frame_pixel_ptr32 = *sprite_pixel_ptr32; 274 | } 275 | } 276 | } 277 | 278 | if (dma_reqd) { 279 | // DMA doing halfword transfers to fix up the misalignment. 280 | dma_channel_set_read_addr(dma_channel, sprite_buffer, false); 281 | dma_channel_transfer_to_buffer_now(dma_channel, frame_pixel_ptr, (frame_pixel_ptr32 - sprite_buffer) << 1); 282 | } 283 | } 284 | 285 | __always_inline static void apply_blend_patch_byte(const Sprite::BlendPatch& patch, uint8_t* frame_pixel_data, uint32_t* sprite_buffer, int dma_channel) { 286 | uint8_t* sprite_pixel_ptr = (uint8_t*)patch.data; 287 | uint8_t* const sprite_end_ptr = (uint8_t*)(patch.data + patch.len); 288 | uint8_t* frame_pixel_ptr = ((uint8_t*)frame_pixel_data + patch.offset); 289 | constexpr uint8_t alpha_mask = 0x01; 290 | 291 | switch (patch.mode) { 292 | case BLEND_DEPTH: 293 | case BLEND_BLEND: 294 | for (; sprite_pixel_ptr < sprite_end_ptr; ++sprite_pixel_ptr, ++frame_pixel_ptr) { 295 | if ((*sprite_pixel_ptr & ~*frame_pixel_ptr) & alpha_mask) { 296 | *frame_pixel_ptr = *sprite_pixel_ptr & (~alpha_mask); 297 | } 298 | } 299 | break; 300 | case BLEND_DEPTH2: 301 | case BLEND_BLEND2: 302 | for (; sprite_pixel_ptr < sprite_end_ptr; ++sprite_pixel_ptr, ++frame_pixel_ptr) { 303 | if (*sprite_pixel_ptr & alpha_mask) { 304 | *frame_pixel_ptr = *sprite_pixel_ptr; 305 | } 306 | } 307 | break; 308 | default: 309 | while (sprite_pixel_ptr < sprite_end_ptr) { 310 | *frame_pixel_ptr++ = *sprite_pixel_ptr++; 311 | } 312 | break; 313 | } 314 | } 315 | 316 | void __scratch_x("sprite_blend") Sprite::apply_blend_patch_555_x(const BlendPatch& patch, uint8_t* frame_pixel_data) { 317 | apply_blend_patch_555(patch, frame_pixel_data, buffer_x, dma_channel_x); 318 | } 319 | 320 | void __scratch_y("sprite_blend") Sprite::apply_blend_patch_555_y(const BlendPatch& patch, uint8_t* frame_pixel_data) { 321 | apply_blend_patch_555(patch, frame_pixel_data, buffer_y, dma_channel_y); 322 | } 323 | 324 | void __scratch_x("sprite_blend") Sprite::apply_blend_patch_byte_x(const BlendPatch& patch, uint8_t* frame_pixel_data) { 325 | apply_blend_patch_byte(patch, frame_pixel_data, buffer_x, dma_channel_x); 326 | } 327 | 328 | void __scratch_y("sprite_blend") Sprite::apply_blend_patch_byte_y(const BlendPatch& patch, uint8_t* frame_pixel_data) { 329 | apply_blend_patch_byte(patch, frame_pixel_data, buffer_y, dma_channel_y); 330 | } 331 | 332 | void Sprite::init() { 333 | // Claim DMA channels 334 | dma_channel_x = dma_claim_unused_channel(true); 335 | dma_channel_y = dma_claim_unused_channel(true); 336 | 337 | // Setup Sprite copying DMA channels - transfer halfwords from memory to memory 338 | dma_channel_config c; 339 | c = dma_channel_get_default_config(dma_channel_x); 340 | channel_config_set_read_increment(&c, true); 341 | channel_config_set_write_increment(&c, true); 342 | channel_config_set_transfer_data_size(&c, DMA_SIZE_16); 343 | dma_channel_configure( 344 | dma_channel_x, &c, 345 | nullptr, 346 | nullptr, 347 | 0, 348 | false 349 | ); 350 | 351 | c = dma_channel_get_default_config(dma_channel_y); 352 | channel_config_set_read_increment(&c, true); 353 | channel_config_set_write_increment(&c, true); 354 | channel_config_set_transfer_data_size(&c, DMA_SIZE_16); 355 | dma_channel_configure( 356 | dma_channel_y, &c, 357 | nullptr, 358 | nullptr, 359 | 0, 360 | false 361 | ); 362 | 363 | clear_sprite_data(); 364 | } 365 | 366 | void Sprite::clear_sprite_data() { 367 | sprite_data_end = sprite_data_buffer; 368 | } -------------------------------------------------------------------------------- /sprite.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "constants.hpp" 5 | #include "frame_decode.hpp" 6 | 7 | class Sprite { 8 | public: 9 | void set_sprite_table_idx(int16_t table_idx) { 10 | idx = table_idx; 11 | } 12 | 13 | bool is_enabled() const { return idx >= 0; } 14 | 15 | int16_t get_sprite_table_idx() const { return idx; } 16 | 17 | void set_sprite_pos(int16_t new_x, int16_t new_y) { 18 | x = new_x; y = new_y; 19 | } 20 | 21 | void set_blend_mode(pico_stick::BlendMode mode) { 22 | blend_mode = mode; 23 | } 24 | 25 | void set_sprite_v_scale(uint8_t new_v_scale) { 26 | v_scale = new_v_scale; 27 | } 28 | 29 | pico_stick::BlendMode get_blend_mode() const { 30 | return blend_mode; 31 | } 32 | 33 | struct LinePatch { 34 | uint8_t* data; 35 | uint8_t* dest_ptr; 36 | uint32_t len; // in bytes 37 | uint32_t ctrl; // Control word for DMA chain 38 | }; 39 | 40 | struct BlendPatch { 41 | uint8_t* data; 42 | uint16_t offset; // in bytes 43 | uint8_t len; // in bytes 44 | pico_stick::BlendMode mode; 45 | }; 46 | 47 | void update_sprite(FrameDecode& frame_data); 48 | void copy_sprite(const Sprite& other); 49 | void setup_patches(class DisplayDriver& disp); 50 | static void apply_blend_patch_555_x(const BlendPatch& patch, uint8_t* frame_pixel_data); 51 | static void apply_blend_patch_555_y(const BlendPatch& patch, uint8_t* frame_pixel_data); 52 | static void apply_blend_patch_byte_x(const BlendPatch& patch, uint8_t* frame_pixel_data); 53 | static void apply_blend_patch_byte_y(const BlendPatch& patch, uint8_t* frame_pixel_data); 54 | 55 | static void init(); 56 | static void clear_sprite_data(); 57 | 58 | private: 59 | int16_t x; 60 | int16_t y; 61 | int16_t idx = -1; 62 | uint8_t v_scale = 1; 63 | pico_stick::BlendMode blend_mode = pico_stick::BLEND_NONE; 64 | 65 | pico_stick::SpriteHeader header; 66 | pico_stick::SpriteLine lines[MAX_SPRITE_HEIGHT]; 67 | uint8_t* data = nullptr; 68 | 69 | static int dma_channel_x; 70 | static int dma_channel_y; 71 | static uint32_t buffer_x[MAX_SPRITE_WIDTH / 2]; 72 | static uint32_t buffer_y[MAX_SPRITE_WIDTH / 2]; 73 | }; --------------------------------------------------------------------------------